From d9aa16e2c689b3db275d812b175e4bbe94f22e86 Mon Sep 17 00:00:00 2001
From: Emmad Usmani <emmadusmani@berkeley.edu>
Date: Fri, 6 Oct 2023 10:50:51 -0700
Subject: [PATCH] Mantine Switch (#34005)

* wip

* Add comma and fix spelling error

* Added some padding and things

* switch control styles

* text styles

* add storybook examples

* Add guidance text

* Add top padding for small switches

* change default size from `sm` to `md`

* fix missing default `size` prop

* fix paddings

* remove label padding if empty

* use `getStylesRef`

---------

Co-authored-by: Oisin Coveney <oisin@metabase.com>
Co-authored-by: Maz Ameli <maz@metabase.com>
---
 .../inputs/Switch/Switch.stories.mdx          | 118 +++++++++++++++++
 .../inputs/Switch/Switch.styled.tsx           | 120 ++++++++++++++++++
 .../ui/components/inputs/Switch/index.ts      |   3 +
 .../metabase/ui/components/inputs/index.ts    |   1 +
 frontend/src/metabase/ui/theme.ts             |   2 +
 5 files changed, 244 insertions(+)
 create mode 100644 frontend/src/metabase/ui/components/inputs/Switch/Switch.stories.mdx
 create mode 100644 frontend/src/metabase/ui/components/inputs/Switch/Switch.styled.tsx
 create mode 100644 frontend/src/metabase/ui/components/inputs/Switch/index.ts

diff --git a/frontend/src/metabase/ui/components/inputs/Switch/Switch.stories.mdx b/frontend/src/metabase/ui/components/inputs/Switch/Switch.stories.mdx
new file mode 100644
index 00000000000..b8a3122cc47
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Switch/Switch.stories.mdx
@@ -0,0 +1,118 @@
+import { Canvas, Story, Meta } from "@storybook/addon-docs";
+
+import { Switch, Stack } from "metabase/ui";
+
+export const args = {
+  labelPosition: "right",
+  label: "Eat all the cheese",
+  description: undefined,
+  size: "md",
+  disabled: false,
+};
+
+export const argTypes = {
+  labelPosition: {
+    control: { type: "inline-radio" },
+    options: ["left", "right"],
+  },
+  label: {
+    control: { type: "text" },
+  },
+  description: {
+    control: { type: "text" },
+  },
+  size: {
+    control: { type: "inline-radio" },
+    options: ["xs", "sm", "md"],
+  },
+  disabled: {
+    control: { type: "boolean" },
+  },
+};
+
+<Meta
+  title="Inputs/Switch"
+  component={Switch}
+  args={args}
+  argTypes={argTypes}
+/>
+
+# Switch
+
+Our themed wrapper around [Mantine Switch](https://mantine.dev/core/switch/).
+
+## When to use Switch
+
+Use Switch when you have a setting that can be only either on or off, or enabled/disabled. If there are multiple related options in a list that could be selected or chosen, use a checkbox group instead.
+
+## Docs
+
+- [Figma File](https://www.figma.com/file/xdNKMROC99J6Z4Sqg6V3JI/Input-%2F-Switch?type=design&node-id=1-96&mode=design&t=0K7GSP6rqj8M2muz-0)
+- [Mantine Checkbox Docs](https://mantine.dev/core/switch/)
+
+## Usage guidelines
+
+In most situations you should use the `md` size variant, with the label on the left and the toggle on the right. (This allows users to read what the setting or option is first.)
+
+## Examples
+
+export const DefaultTemplate = args => <Switch {...args} />;
+
+export const StateTemplate = args => (
+  <Stack>
+    <Switch {...args} label="Unchecked switch" checked={false} />
+    <Switch {...args} label="Checked switch" checked />
+    <Switch {...args} label="Disabled unchecked switch" disabled />
+    <Switch {...args} label="Disabled checked switch" disabled checked />
+  </Stack>
+);
+
+export const Default = DefaultTemplate.bind({});
+
+<Canvas>
+  <Story name="Default">{Default}</Story>
+</Canvas>
+
+### Label
+
+export const Label = StateTemplate.bind({});
+
+<Canvas>
+  <Story name="Label">{Label}</Story>
+</Canvas>
+
+### Left label position
+
+export const LabelLeft = StateTemplate.bind({});
+LabelLeft.args = {
+  labelPosition: "left",
+};
+
+<Canvas>
+  <Story name="Label, left position" args={LabelLeft.args}>
+    {LabelLeft}
+  </Story>
+</Canvas>
+
+### Description
+
+export const Description = StateTemplate.bind({});
+Description.args = {
+  description: "Every type of cheese will be consumed, regardless of stink.",
+};
+
+<Canvas>
+  <Story name="Description">{Description}</Story>
+</Canvas>
+
+### Left description position
+
+export const DescriptionLeft = StateTemplate.bind({});
+DescriptionLeft.args = {
+  labelPosition: "left",
+  description: "Every type of cheese will be consumed, regardless of stink.",
+};
+
+<Canvas>
+  <Story name="Description, left position">{DescriptionLeft}</Story>
+</Canvas>
diff --git a/frontend/src/metabase/ui/components/inputs/Switch/Switch.styled.tsx b/frontend/src/metabase/ui/components/inputs/Switch/Switch.styled.tsx
new file mode 100644
index 00000000000..dedf2f604b9
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Switch/Switch.styled.tsx
@@ -0,0 +1,120 @@
+import type { MantineThemeOverride, SwitchStylesParams } from "@mantine/core";
+import { rem, getSize, getStylesRef } from "@mantine/core";
+
+import { color } from "metabase/lib/colors";
+
+const LABEL_FONT_SIZES = {
+  xs: rem(12),
+  sm: rem(14),
+  md: rem(16),
+};
+
+const LABEL_LINE_HEIGHT = {
+  xs: rem(16),
+  sm: rem(24),
+  md: rem(24),
+};
+
+const SWITCH_PADDING = {
+  xs: rem(8),
+  sm: rem(8),
+  md: rem(16),
+};
+
+const TRACK_HEIGHTS = {
+  xs: rem(16),
+  sm: rem(20),
+  md: rem(24),
+};
+
+const TRACK_WIDTHS = {
+  xs: rem(32),
+  sm: rem(40),
+  md: rem(48),
+  lg: rem(64),
+};
+
+const THUMB_SIZES = {
+  xs: rem(12),
+  sm: rem(14),
+  md: rem(18),
+};
+
+const TRACK_PADDING_TOP = {
+  xs: rem(0),
+  sm: rem(2),
+  md: rem(0),
+};
+
+export const getSwitchOverrides = (): MantineThemeOverride["components"] => ({
+  Switch: {
+    defaultProps: {
+      color: "brand",
+      size: "md",
+    },
+    styles: (
+      theme,
+      { error, labelPosition }: SwitchStylesParams,
+      { size = "md" },
+    ) => {
+      return {
+        labelWrapper: {
+          [labelPosition === "left" ? "paddingRight" : "paddingLeft"]: getSize({
+            size,
+            sizes: SWITCH_PADDING,
+          }),
+          "&:empty": {
+            padding: 0,
+          },
+        },
+        label: {
+          padding: 0,
+          fontWeight: 700,
+          fontSize: getSize({ size, sizes: LABEL_FONT_SIZES }),
+          lineHeight: getSize({ size, sizes: LABEL_LINE_HEIGHT }),
+          color: theme.colors.text[2],
+          cursor: "pointer",
+          "&[data-disabled]": {
+            color: theme.colors.text[0],
+            cursor: "default",
+          },
+        },
+        description: {
+          padding: 0,
+          marginTop: rem(8),
+          fontSize: rem(12),
+          color: theme.colors.text[1],
+        },
+        error: {
+          padding: 0,
+          marginTop: rem(8),
+          fontSize: rem(12),
+          color: theme.colors.error[0],
+        },
+        track: {
+          backgroundColor: theme.colors.bg[1],
+          border: error ? `1px solid ${color("accent3")}` : "none",
+          boxSizing: "border-box",
+          borderRadius: rem(24),
+          height: getSize({ size, sizes: TRACK_HEIGHTS }),
+          width: getSize({ size, sizes: TRACK_WIDTHS }),
+          cursor: "pointer",
+          [`${getStylesRef("input")}:disabled + &`]: {
+            backgroundColor: theme.colors.bg[1],
+          },
+          marginTop: getSize({ size, sizes: TRACK_PADDING_TOP }),
+        },
+        thumb: {
+          backgroundColor: theme.white,
+          border: "none",
+          borderRadius: rem(22),
+          height: getSize({ size, sizes: THUMB_SIZES }),
+          width: getSize({ size, sizes: THUMB_SIZES }),
+          [`${getStylesRef("input")}:disabled + * > &`]: {
+            backgroundColor: theme.colors.bg[0],
+          },
+        },
+      };
+    },
+  },
+});
diff --git a/frontend/src/metabase/ui/components/inputs/Switch/index.ts b/frontend/src/metabase/ui/components/inputs/Switch/index.ts
new file mode 100644
index 00000000000..2e040ba4f9d
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Switch/index.ts
@@ -0,0 +1,3 @@
+export { Switch } from "@mantine/core";
+export type { SwitchProps, SwitchGroupProps } from "@mantine/core";
+export { getSwitchOverrides } from "./Switch.styled";
diff --git a/frontend/src/metabase/ui/components/inputs/index.ts b/frontend/src/metabase/ui/components/inputs/index.ts
index b9e0952b33d..c277fe05c32 100644
--- a/frontend/src/metabase/ui/components/inputs/index.ts
+++ b/frontend/src/metabase/ui/components/inputs/index.ts
@@ -5,4 +5,5 @@ export * from "./NumberInput";
 export * from "./Radio";
 export * from "./Select";
 export * from "./Textarea";
+export * from "./Switch";
 export * from "./TextInput";
diff --git a/frontend/src/metabase/ui/theme.ts b/frontend/src/metabase/ui/theme.ts
index 790e56d7917..31463cbed36 100644
--- a/frontend/src/metabase/ui/theme.ts
+++ b/frontend/src/metabase/ui/theme.ts
@@ -15,6 +15,7 @@ import {
   getPaperOverrides,
   getSelectOverrides,
   getTextareaOverrides,
+  getSwitchOverrides,
   getTextInputOverrides,
   getTextOverrides,
   getTitleOverrides,
@@ -115,6 +116,7 @@ export const getThemeOverrides = (): MantineThemeOverride => ({
     ...getPaperOverrides(),
     ...getSelectOverrides(),
     ...getTextareaOverrides(),
+    ...getSwitchOverrides(),
     ...getTextInputOverrides(),
     ...getTextOverrides(),
     ...getTitleOverrides(),
-- 
GitLab