From 3fc29afef1d0e4c2d7878fc98fced57d5d77a28a Mon Sep 17 00:00:00 2001 From: Anton Kulyk <kuliks.anton@gmail.com> Date: Tue, 6 Jul 2021 12:00:17 +0300 Subject: [PATCH] Add SegmentedControl component (#16885) * Add basic segmented control component * Specify segmented control's selected option color * Update SegmentedControl's padding * Use `PropTypes.node` instead of a bunch of types * Remove `aria-selected` from CSS --- frontend/src/metabase/components/Radio.jsx | 1 + .../src/metabase/components/Radio.styled.js | 1 - .../components/SegmentedControl.info.js | 48 ++++++++++++++ .../metabase/components/SegmentedControl.jsx | 66 +++++++++++++++++++ .../components/SegmentedControl.styled.js | 28 ++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 frontend/src/metabase/components/SegmentedControl.info.js create mode 100644 frontend/src/metabase/components/SegmentedControl.jsx create mode 100644 frontend/src/metabase/components/SegmentedControl.styled.js diff --git a/frontend/src/metabase/components/Radio.jsx b/frontend/src/metabase/components/Radio.jsx index 40edd787816..56041536f5f 100644 --- a/frontend/src/metabase/components/Radio.jsx +++ b/frontend/src/metabase/components/Radio.jsx @@ -92,6 +92,7 @@ function Radio({ xspace={xspace} yspace={yspace} onClick={e => onChange(optionValueFn(option))} + aria-selected={selected} > {option.icon && <Icon name={option.icon} mr={1} />} <input diff --git a/frontend/src/metabase/components/Radio.styled.js b/frontend/src/metabase/components/Radio.styled.js index 84fba215b71..aaabf848a0e 100644 --- a/frontend/src/metabase/components/Radio.styled.js +++ b/frontend/src/metabase/components/Radio.styled.js @@ -13,7 +13,6 @@ const BaseList = styled.ul` const BaseItem = styled.li.attrs({ mr: props => (!props.vertical && !props.last ? props.xspace : null), mb: props => (props.vertical && !props.last ? props.yspace : null), - "aria-selected": props => props.selected, })` ${space} display: flex; diff --git a/frontend/src/metabase/components/SegmentedControl.info.js b/frontend/src/metabase/components/SegmentedControl.info.js new file mode 100644 index 00000000000..23635465955 --- /dev/null +++ b/frontend/src/metabase/components/SegmentedControl.info.js @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import { SegmentedControl } from "metabase/components/SegmentedControl"; + +export const component = SegmentedControl; +export const category = "input"; + +export const description = ` +Radio-like segmented control input +`; + +const SIMPLE_OPTIONS = [ + { name: "Gadget", value: 0 }, + { name: "Gizmo", value: 1 }, +]; + +const OPTIONS_WITH_ICONS = [ + { name: "Gadget", value: 0, icon: "lightbulb" }, + { name: "Gizmo", value: 1, icon: "folder" }, + { name: "Doohickey", value: 2, icon: "insight" }, +]; + +const OPTIONS_WITH_COLORS = [ + { + name: "Gadget", + value: 0, + icon: "lightbulb", + selectedColor: "accent1", + }, + { name: "Gizmo", value: 1, icon: "folder", selectedColor: "accent2" }, + { name: "Doohickey", value: 2, icon: "insight" }, +]; + +function SegmentedControlDemo(props) { + const [value, setValue] = useState(0); + return ( + <SegmentedControl + {...props} + value={value} + onChange={val => setValue(val)} + /> + ); +} + +export const examples = { + default: <SegmentedControlDemo options={SIMPLE_OPTIONS} />, + icons: <SegmentedControlDemo options={OPTIONS_WITH_ICONS} />, + colored: <SegmentedControlDemo options={OPTIONS_WITH_COLORS} />, +}; diff --git a/frontend/src/metabase/components/SegmentedControl.jsx b/frontend/src/metabase/components/SegmentedControl.jsx new file mode 100644 index 00000000000..61ac026eb51 --- /dev/null +++ b/frontend/src/metabase/components/SegmentedControl.jsx @@ -0,0 +1,66 @@ +import React, { useMemo } from "react"; +import PropTypes from "prop-types"; +import _ from "underscore"; +import Icon from "metabase/components/Icon"; +import { SegmentedList, SegmentedItem } from "./SegmentedControl.styled"; + +const optionShape = PropTypes.shape({ + name: PropTypes.node.isRequired, + value: PropTypes.any.isRequired, + icon: PropTypes.string, + + // Expects a color alias, not a color code + // Example: brand, accent1, success + // Won't work: red, #000, rgb(0, 0, 0) + selectedColor: PropTypes.string, +}); + +const propTypes = { + name: PropTypes.string, + value: PropTypes.any, + options: PropTypes.arrayOf(optionShape).isRequired, + onChange: PropTypes.func, +}; + +export function SegmentedControl({ + name: nameFromProps, + value, + options, + onChange, + ...props +}) { + const id = useMemo(() => _.uniqueId("radio-"), []); + const name = nameFromProps || id; + return ( + <SegmentedList {...props}> + {options.map((option, index) => { + const isSelected = option.value === value; + const isFirst = index === 0; + const isLast = index === options.length - 1; + return ( + <SegmentedItem + key={option.value} + isSelected={isSelected} + isFirst={isFirst} + isLast={isLast} + onClick={e => onChange(option.value)} + selectedColor={option.selectedColor || "brand"} + > + {option.icon && <Icon name={option.icon} mr={1} />} + <input + id={`${name}-${option.value}`} + className="Form-radio" + type="radio" + name={name} + value={option.value} + checked={isSelected} + /> + <span>{option.name}</span> + </SegmentedItem> + ); + })} + </SegmentedList> + ); +} + +SegmentedControl.propTypes = propTypes; diff --git a/frontend/src/metabase/components/SegmentedControl.styled.js b/frontend/src/metabase/components/SegmentedControl.styled.js new file mode 100644 index 00000000000..09e4651e126 --- /dev/null +++ b/frontend/src/metabase/components/SegmentedControl.styled.js @@ -0,0 +1,28 @@ +import styled from "styled-components"; +import { color } from "metabase/lib/colors"; + +const BORDER_RADIUS = "8px"; + +export const SegmentedList = styled.ul` + display: flex; +`; + +export const SegmentedItem = styled.li` + display: flex; + align-items: center; + font-weight: bold; + cursor: pointer; + color: ${props => (props.isSelected ? color(props.selectedColor) : null)}; + padding: 6px 12px; + + border: 1px solid ${color("border")}; + border-right-width: ${props => (props.isLast ? "1px" : 0)}; + border-top-left-radius: ${props => (props.isFirst ? BORDER_RADIUS : 0)}; + border-bottom-left-radius: ${props => (props.isFirst ? BORDER_RADIUS : 0)}; + border-top-right-radius: ${props => (props.isLast ? BORDER_RADIUS : 0)}; + border-bottom-right-radius: ${props => (props.isLast ? BORDER_RADIUS : 0)}; + + :hover { + color: ${props => (!props.isSelected ? color(props.selectedColor) : null)}; + } +`; -- GitLab