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