diff --git a/documentation-site/components/yard/config/checkbox-v2.ts b/documentation-site/components/yard/config/checkbox-v2.ts new file mode 100644 index 0000000000..37298e693b --- /dev/null +++ b/documentation-site/components/yard/config/checkbox-v2.ts @@ -0,0 +1,167 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import pick from "just-pick"; +import { Checkbox, LABEL_PLACEMENT } from "baseui/checkbox-v2"; +import { PropTypes } from "react-view"; +import type { TConfig } from "../types"; +import { changeHandlers } from "./common/common"; + +const CheckboxConfig: TConfig = { + componentName: "Checkbox", + imports: { + "baseui/checkbox-v2": { + named: ["Checkbox"], + }, + }, + scope: { + Checkbox, + LABEL_PLACEMENT, + }, + theme: [ + "contentStateDisabled", + "tagRedContentSecondary", + "borderSelected", + "contentTertiary", + "contentPrimary", + "hoverNegativeAlpha", + "hoverOverlayAlpha", + "pressedNegativeAlpha", + "pressedOverlayAlpha", + "contentInversePrimary", + ], + props: { + checked: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in checked state.", + stateful: true, + }, + children: { + value: `Sign up for the newsletter`, + type: PropTypes.ReactNode, + description: `The React Nodes displayed next to the checkbox.`, + }, + disabled: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in disabled state.", + }, + onChange: { + value: "e => setChecked(e.target.checked)", + type: PropTypes.Function, + description: "Called when checkbox value is changed.", + propHook: { + what: "e.target.checked", + into: "checked", + }, + }, + error: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in error state.", + }, + isIndeterminate: { + value: false, + type: PropTypes.Boolean, + description: + "Indicates indeterminate state for the checkbox. Checked property is ignored.", + }, + labelPlacement: { + value: "LABEL_PLACEMENT.right", + options: LABEL_PLACEMENT, + type: PropTypes.Enum, + enumName: "LABEL_PLACEMENT", + description: + "Determines how to position the label relative to the checkbox.", + imports: { + "baseui/checkbox-v2": { + named: ["LABEL_PLACEMENT"], + }, + }, + }, + required: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in required state.", + hidden: true, + }, + inputRef: { + value: undefined, + type: PropTypes.Ref, + description: "A ref to access an input element.", + hidden: true, + }, + autoFocus: { + value: false, + type: PropTypes.Boolean, + description: "If true the component will be focused on the first mount.", + hidden: true, + }, + containsInteractiveElement: { + value: false, + type: PropTypes.Boolean, + description: + "Indicates the checkbox label contains an interactive element, and the default label behavior should be prevented for child elements.", + hidden: true, + }, + name: { + value: undefined, + type: PropTypes.String, + description: "Name attribute.", + hidden: true, + }, + title: { + value: undefined, + type: PropTypes.String, + description: "Title attribute.", + hidden: true, + }, + "aria-label": { + value: undefined, + type: PropTypes.String, + description: "Aria-label attribute", + hidden: true, + }, + ...pick(changeHandlers, [ + "onBlur", + "onFocus", + "onMouseDown", + "onMouseEnter", + "onMouseLeave", + ]), + overrides: { + value: undefined, + type: PropTypes.Custom, + description: "Lets you customize all aspects of the component.", + custom: { + names: ["Root", "CheckmarkContainer", "Checkmark", "Label", "Input"], + sharedProps: { + $isFocused: { + type: PropTypes.Boolean, + description: "True when the component is focused.", + }, + $isHovered: { + type: PropTypes.Boolean, + description: "True when the component is hovered.", + }, + $isActive: { + type: PropTypes.Boolean, + description: "True when the component is active.", + }, + $error: "error", + $checked: "checked", + $isIndeterminate: "isIndeterminate", + $required: "required", + $disabled: "disabled", + $labelPlacement: "labelPlacement", + }, + }, + }, + }, +}; + +export default CheckboxConfig; diff --git a/documentation-site/components/yard/config/switch.ts b/documentation-site/components/yard/config/switch.ts new file mode 100644 index 0000000000..b3079a22a1 --- /dev/null +++ b/documentation-site/components/yard/config/switch.ts @@ -0,0 +1,174 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import pick from "just-pick"; +import { Switch, LABEL_PLACEMENT, SIZE } from "baseui/switch"; +import { PropTypes } from "react-view"; +import type { TConfig } from "../types"; +import { changeHandlers } from "./common/common"; + +const SwitchConfig: TConfig = { + componentName: "Switch", + imports: { + "baseui/switch": { + named: ["Switch"], + }, + }, + scope: { + Switch, + LABEL_PLACEMENT, + SIZE, + }, + theme: [ + "contentStateDisabled", + "contentTertiary", + "contentPrimary", + "hoverOverlayAlpha", + "contentInversePrimary", + "hoverOverlayInverseAlpha", + "backgroundTertiary", + "backgroundStateDisabled", + "backgroundInversePrimary", + "borderStateDisabled", + ], + props: { + checked: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in checked state.", + stateful: true, + }, + children: { + value: `Sign up for the newsletter`, + type: PropTypes.ReactNode, + description: `The React Nodes displayed next to the switch.`, + }, + disabled: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in disabled state.", + }, + onChange: { + value: "e => setChecked(e.target.checked)", + type: PropTypes.Function, + description: "Called when switch value is changed.", + propHook: { + what: "e.target.checked", + into: "checked", + }, + }, + showIcon: { + value: false, + type: PropTypes.Boolean, + description: "Renders check icon when switch is enabled.", + }, + labelPlacement: { + value: "LABEL_PLACEMENT.right", + options: LABEL_PLACEMENT, + type: PropTypes.Enum, + enumName: "LABEL_PLACEMENT", + description: + "Determines how to position the label relative to the switch.", + imports: { + "baseui/switch": { + named: ["LABEL_PLACEMENT"], + }, + }, + }, + size: { + value: "SIZE.default", + options: SIZE, + type: PropTypes.Enum, + enumName: "SIZE", + description: "Determines the size of the switch(including the label).", + imports: { + "baseui/switch": { + named: ["SIZE"], + }, + }, + }, + required: { + value: false, + type: PropTypes.Boolean, + description: "Renders component in required state.", + hidden: true, + }, + inputRef: { + value: undefined, + type: PropTypes.Ref, + description: "A ref to access an input element.", + hidden: true, + }, + autoFocus: { + value: false, + type: PropTypes.Boolean, + description: "If true the component will be focused on the first mount.", + hidden: true, + }, + containsInteractiveElement: { + value: false, + type: PropTypes.Boolean, + description: + "Indicates the switch label contains an interactive element, and the default label behavior should be prevented for child elements.", + hidden: true, + }, + name: { + value: undefined, + type: PropTypes.String, + description: "Name attribute.", + hidden: true, + }, + title: { + value: undefined, + type: PropTypes.String, + description: "Title attribute.", + hidden: true, + }, + "aria-label": { + value: undefined, + type: PropTypes.String, + description: "Aria-label attribute", + hidden: true, + }, + ...pick(changeHandlers, [ + "onBlur", + "onFocus", + "onMouseDown", + "onMouseEnter", + "onMouseLeave", + ]), + overrides: { + value: undefined, + type: PropTypes.Custom, + description: "Lets you customize all aspects of the component.", + custom: { + names: ["Root", "Toggle", "ToggleTrack", "Label", "Input"], + sharedProps: { + $isFocused: { + type: PropTypes.Boolean, + description: "True when the component is focused.", + }, + $isHovered: { + type: PropTypes.Boolean, + description: "True when the component is hovered.", + }, + $isActive: { + type: PropTypes.Boolean, + description: "True when the component is active.", + }, + $checked: "checked", + $required: "required", + $disabled: "disabled", + $labelPlacement: "labelPlacement", + $size: "size", + $showIcon: "showIcon", + }, + }, + }, + }, +}; + +export default SwitchConfig; diff --git a/documentation-site/examples/checkbox-v2/alignment.tsx b/documentation-site/examples/checkbox-v2/alignment.tsx new file mode 100644 index 0000000000..071aaa4335 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/alignment.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import { Checkbox, LABEL_PLACEMENT } from "baseui/checkbox-v2"; + +export default function Example() { + const [checkboxes, setCheckboxes] = React.useState([false, false]); + return ( +
+ { + const nextCheckboxes = [...checkboxes]; + nextCheckboxes[0] = e.currentTarget.checked; + setCheckboxes(nextCheckboxes); + }} + labelPlacement={LABEL_PLACEMENT.left} + > + Label on the left + + { + const nextCheckboxes = [...checkboxes]; + nextCheckboxes[1] = e.currentTarget.checked; + setCheckboxes(nextCheckboxes); + }} + labelPlacement={LABEL_PLACEMENT.right} + > + Label on the right + +
+ ); +} diff --git a/documentation-site/examples/checkbox-v2/basic-controlled.tsx b/documentation-site/examples/checkbox-v2/basic-controlled.tsx new file mode 100644 index 0000000000..59cc5f3b71 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/basic-controlled.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { Checkbox } from "baseui/checkbox-v2"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)}> + click me + + ); +} diff --git a/documentation-site/examples/checkbox-v2/basic-uncontrolled.tsx b/documentation-site/examples/checkbox-v2/basic-uncontrolled.tsx new file mode 100644 index 0000000000..d651131871 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/basic-uncontrolled.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { StatefulCheckbox } from "baseui/checkbox-v2"; + +export default function Example() { + return click me; +} diff --git a/documentation-site/examples/checkbox-v2/component-overrides.tsx b/documentation-site/examples/checkbox-v2/component-overrides.tsx new file mode 100644 index 0000000000..1b912c6e86 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/component-overrides.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { useStyletron } from "baseui"; +import { StatefulCheckbox } from "baseui/checkbox-v2"; +import { Alert } from "baseui/icon"; + +export default function Example() { + const [css, theme] = useStyletron(); + return ( + ( +
+ +
+ ), + }} + > + With style overrides +
+ ); +} diff --git a/documentation-site/examples/checkbox-v2/disabled.tsx b/documentation-site/examples/checkbox-v2/disabled.tsx new file mode 100644 index 0000000000..2833d28b84 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/disabled.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import { Checkbox } from "baseui/checkbox-v2"; + +export default function Example() { + return ( +
+ Disabled checkbox + + Disabled checkbox (checked) + +
+ ); +} diff --git a/documentation-site/examples/checkbox-v2/error.tsx b/documentation-site/examples/checkbox-v2/error.tsx new file mode 100644 index 0000000000..91449e5877 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/error.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { Checkbox } from "baseui/checkbox-v2"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)} error> + Checkbox with an error + + ); +} diff --git a/documentation-site/examples/checkbox-v2/indeterminate.tsx b/documentation-site/examples/checkbox-v2/indeterminate.tsx new file mode 100644 index 0000000000..68fc4c219e --- /dev/null +++ b/documentation-site/examples/checkbox-v2/indeterminate.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import { useStyletron } from "baseui"; +import { Checkbox } from "baseui/checkbox-v2"; + +function GroupList() { + const [css, theme] = useStyletron(); + const [checkboxes, setCheckboxes] = React.useState([true, false]); + + const allChecked = checkboxes.every(Boolean); + const isIndeterminate = checkboxes.some(Boolean) && !allChecked; + + return ( +
+ { + const target = e.target as HTMLInputElement; + setCheckboxes([target.checked, target.checked]); + }} + isIndeterminate={isIndeterminate} + checked={allChecked} + > + Indeterminate checkbox if not all subcheckboxes are checked + + +
+ { + const target = e.target as HTMLInputElement; + setCheckboxes([target.checked, checkboxes[1]]); + }} + > + First subcheckbox + + { + const target = e.target as HTMLInputElement; + setCheckboxes([checkboxes[0], target.checked]); + }} + > + Second subcheckbox + +
+
+ ); +} + +export default GroupList; diff --git a/documentation-site/examples/checkbox-v2/multiline.tsx b/documentation-site/examples/checkbox-v2/multiline.tsx new file mode 100644 index 0000000000..accb99a5f7 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/multiline.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { Checkbox } from "baseui/checkbox-v2"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)}> + It started as a simple idea: What if you could request a ride from your + phone? More than 5 billion trips later, we’re working to make + transportation safer and more accessible, helping people order food + quickly and affordably, reducing congestion in cities by getting more + people into fewer cars, and creating opportunities for people to work on + their own terms. + + ); +} diff --git a/documentation-site/examples/checkbox-v2/overrides.tsx b/documentation-site/examples/checkbox-v2/overrides.tsx new file mode 100644 index 0000000000..5f32602c75 --- /dev/null +++ b/documentation-site/examples/checkbox-v2/overrides.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { Checkbox } from "baseui/checkbox-v2"; +import { expandBorderStyles } from "baseui/styles"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)} + overrides={{ + Root: { + style: ({ $theme }) => ({ + ...expandBorderStyles($theme.borders.border300), + }), + }, + Label: { + style: ({ $theme }) => ({ + color: $theme.colors.warning, + }), + }, + Checkmark: { + style: ({ $checked, $theme }) => ({ + borderLeftColor: $theme.colors.warning, + borderRightColor: $theme.colors.warning, + borderTopColor: $theme.colors.warning, + borderBottomColor: $theme.colors.warning, + backgroundColor: $checked ? $theme.colors.warning : null, + }), + }, + }} + > + With style overrides + + ); +} diff --git a/documentation-site/examples/switch/alignment.tsx b/documentation-site/examples/switch/alignment.tsx new file mode 100644 index 0000000000..7bcd1e8f3f --- /dev/null +++ b/documentation-site/examples/switch/alignment.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import { Switch, LABEL_PLACEMENT } from "baseui/switch"; + +export default function Example() { + const [switches, setSwitches] = React.useState([false, false]); + return ( +
+ { + const nextSwitches = [...switches]; + nextSwitches[0] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + labelPlacement={LABEL_PLACEMENT.left} + > + Label on the left + + { + const nextSwitches = [...switches]; + nextSwitches[1] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + labelPlacement={LABEL_PLACEMENT.right} + > + Label on the right + +
+ ); +} diff --git a/documentation-site/examples/switch/basic-controlled.tsx b/documentation-site/examples/switch/basic-controlled.tsx new file mode 100644 index 0000000000..7c1ffb87db --- /dev/null +++ b/documentation-site/examples/switch/basic-controlled.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { Switch } from "baseui/switch"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)}> + click me + + ); +} diff --git a/documentation-site/examples/switch/basic-uncontrolled.tsx b/documentation-site/examples/switch/basic-uncontrolled.tsx new file mode 100644 index 0000000000..4c1fd35a84 --- /dev/null +++ b/documentation-site/examples/switch/basic-uncontrolled.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { StatefulSwitch } from "baseui/switch"; + +export default function Example() { + return click me; +} diff --git a/documentation-site/examples/switch/disabled.tsx b/documentation-site/examples/switch/disabled.tsx new file mode 100644 index 0000000000..0d186d1c90 --- /dev/null +++ b/documentation-site/examples/switch/disabled.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { Switch } from "baseui/switch"; + +export default function Example() { + return ( +
+ Disabled switch + + Disabled switch (checked) + +
+ ); +} diff --git a/documentation-site/examples/switch/multiline.tsx b/documentation-site/examples/switch/multiline.tsx new file mode 100644 index 0000000000..67db944834 --- /dev/null +++ b/documentation-site/examples/switch/multiline.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { Switch } from "baseui/switch"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)}> + It started as a simple idea: What if you could request a ride from your + phone? More than 5 billion trips later, we’re working to make + transportation safer and more accessible, helping people order food + quickly and affordably, reducing congestion in cities by getting more + people into fewer cars, and creating opportunities for people to work on + their own terms. + + ); +} diff --git a/documentation-site/examples/switch/overrides.tsx b/documentation-site/examples/switch/overrides.tsx new file mode 100644 index 0000000000..bb95dcefc0 --- /dev/null +++ b/documentation-site/examples/switch/overrides.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import { Switch } from "baseui/switch"; +import { expandBorderStyles } from "baseui/styles"; + +export default function Example() { + const [checked, setChecked] = React.useState(true); + return ( + setChecked(!checked)} + overrides={{ + Root: { + style: ({ $theme }) => ({ + ...expandBorderStyles($theme.borders.border300), + }), + }, + Label: { + style: ({ $theme }) => ({ + color: $theme.colors.warning, + }), + }, + Toggle: { + style: ({ $checked, $theme }) => ({ + borderLeftColor: $theme.colors.warning, + borderRightColor: $theme.colors.warning, + borderTopColor: $theme.colors.warning, + borderBottomColor: $theme.colors.warning, + backgroundColor: $checked + ? $theme.colors.backgroundAccent + : $theme.colors.backgroundWarning, + }), + }, + }} + > + With style overrides + + ); +} diff --git a/documentation-site/examples/switch/showIcon.tsx b/documentation-site/examples/switch/showIcon.tsx new file mode 100644 index 0000000000..a48065b9ca --- /dev/null +++ b/documentation-site/examples/switch/showIcon.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import { Switch, SIZE } from "baseui/switch"; + +export default function Example() { + const [switches, setSwitches] = React.useState([true, true]); + return ( +
+ { + const nextSwitches = [...switches]; + nextSwitches[0] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + showIcon={false} + > + No Icon + + { + const nextSwitches = [...switches]; + nextSwitches[1] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + showIcon={true} + > + Show Icon + +
+ ); +} diff --git a/documentation-site/examples/switch/size.tsx b/documentation-site/examples/switch/size.tsx new file mode 100644 index 0000000000..437ff61ebe --- /dev/null +++ b/documentation-site/examples/switch/size.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import { Switch, SIZE } from "baseui/switch"; + +export default function Example() { + const [switches, setSwitches] = React.useState([false, false]); + return ( +
+ { + const nextSwitches = [...switches]; + nextSwitches[0] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + size={SIZE.default} + > + Default size + + { + const nextSwitches = [...switches]; + nextSwitches[1] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + size={SIZE.small} + > + Small size + +
+ ); +} diff --git a/documentation-site/pages/components/checkbox-v2.mdx b/documentation-site/pages/components/checkbox-v2.mdx new file mode 100644 index 0000000000..96ce8cc095 --- /dev/null +++ b/documentation-site/pages/components/checkbox-v2.mdx @@ -0,0 +1,84 @@ +import Example from "../../components/example"; +import Layout from "../../components/layout"; +import Exports from "../../components/exports"; + +import Basic from "examples/checkbox-v2/basic-controlled.tsx"; +import Uncontrolled from "examples/checkbox-v2/basic-uncontrolled"; +import Multiline from "examples/checkbox-v2/multiline.tsx"; +import Error from "examples/checkbox-v2/error.tsx"; +import Indeterminate from "examples/checkbox-v2/indeterminate.tsx"; +import Disabled from "examples/checkbox-v2/disabled.tsx"; +import Alignment from "examples/checkbox-v2/alignment.tsx"; +import Customization from "examples/checkbox-v2/overrides.tsx"; +import ComponentOverrides from "examples/checkbox-v2/component-overrides.tsx"; + +import { Block } from "baseui/block"; +import * as CheckboxExports from "baseui/checkbox-v2"; + +import Yard from "../../components/yard/index"; +import checkboxYardConfig from "../../components/yard/config/checkbox-v2"; + +export default Layout; + +# Checkbox + + + +Checkboxes are used to provide users with multiple options for selection in a series of options. + +## When to use + +- When a collection of options share context. +- When a user wants to select multiple options. + +## Examples + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +As with many of our components, there is also an [uncontrolled](https://reactjs.org/docs/uncontrolled-components.html) version, `Checkbox`, which manages its own state. + + diff --git a/documentation-site/pages/components/checkbox.mdx b/documentation-site/pages/components/checkbox.mdx index c98eee5dbd..25ebe190ed 100644 --- a/documentation-site/pages/components/checkbox.mdx +++ b/documentation-site/pages/components/checkbox.mdx @@ -21,8 +21,22 @@ import * as CheckboxExports from "baseui/checkbox"; import Yard from "../../components/yard/index"; import checkboxYardConfig from "../../components/yard/config/checkbox"; +import { Notification, KIND as NOTIFICATION_KIND } from "baseui/notification"; + export default Layout; + + This checkbox component is about to be deprecated in favor of 2 separate + components: Checkbox-v2 and Switch. The supported apis keep almost same, so + the migration just requires minimal efforts(most likely only change import + path) but gains much more benefits. We encouraged you to start new + implementations with Checkbox-v2 or Switch depending your use case and design. + Any migrations from Checkbox to Checkbox-v2 or Switch are recommended. + + # Checkbox @@ -91,7 +105,7 @@ options. When engaged (on), Base Web toggles are colored and when disengaged (of -As with many of our components, there is also an [uncontrolled](https://reactjs.org/docs/uncontrolled-components.html) version, `StatefulCheckbox`, which manages its own state. +As with many of our components, there is also an [uncontrolled](https://reactjs.org/docs/uncontrolled-components.html) version, `Checkbox`, which manages its own state. + +Switches are used to allow users to to toggle an option on/off. + +Switch is used as a toggle to allow the user to make a binary choice usually (but not limited) in +the form of a yes/no or on/off suggestion. Switches are often used in product settings or as filter +options. When engaged (on), Base Web switches are colored and when disengaged (off) they’re grey. + +## Examples + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +As with many of our components, there is also an [uncontrolled](https://reactjs.org/docs/uncontrolled-components.html) version, `Switch`, which manages its own state. + + diff --git a/documentation-site/routes.jsx b/documentation-site/routes.jsx index 63c5f0de3d..50c375396e 100644 --- a/documentation-site/routes.jsx +++ b/documentation-site/routes.jsx @@ -87,6 +87,10 @@ const routes = [ title: 'Checkbox', itemId: '/components/checkbox', }, + { + title: 'Checkbox-v2', + itemId: '/components/checkbox-v2', + }, { title: 'Combobox', itemId: '/components/combobox', @@ -127,6 +131,10 @@ const routes = [ title: 'Stepper', itemId: '/components/stepper', }, + { + title: 'Switch', + itemId: '/components/switch', + }, { title: 'Textarea', itemId: '/components/textarea', diff --git a/package-lock.json b/package-lock.json index 4d7a5a0e9d..9cbe76bc31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "baseui", - "version": "15.0.2", + "version": "16.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "baseui", - "version": "15.0.2", + "version": "16.1.0", "license": "MIT", "dependencies": { "@date-io/date-fns": "^2.13.1", @@ -101,7 +101,6 @@ "integrity": "sha512-8iX+E9Cnet2167RdP8wM5PGPoEnw/jZNvHrtTRHs4g53n/Rg45iLmE9qFzxCqXGBmUO9LXKYdcXnettFKFLifg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^1.9.0" } @@ -111,8 +110,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/@babel/cli": { "version": "7.28.3", @@ -175,6 +173,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3020,7 +3019,6 @@ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -3247,6 +3245,7 @@ "integrity": "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" @@ -3455,7 +3454,6 @@ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3467,7 +3465,6 @@ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3479,7 +3476,6 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3491,7 +3487,6 @@ "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3586,7 +3581,6 @@ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3598,7 +3592,6 @@ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -3614,7 +3607,6 @@ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3636,7 +3628,6 @@ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3678,7 +3669,6 @@ "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -3857,7 +3847,6 @@ "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -4729,18 +4718,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", @@ -4812,7 +4789,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4824,7 +4800,6 @@ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -4900,6 +4875,7 @@ "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -4924,6 +4900,7 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -5032,7 +5009,6 @@ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -5043,24 +5019,21 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", @@ -5068,7 +5041,6 @@ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -5080,8 +5052,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", @@ -5089,7 +5060,6 @@ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -5103,7 +5073,6 @@ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -5114,7 +5083,6 @@ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -5124,8 +5092,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", @@ -5133,7 +5100,6 @@ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -5151,7 +5117,6 @@ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -5166,7 +5131,6 @@ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -5180,7 +5144,6 @@ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -5196,7 +5159,6 @@ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -5207,16 +5169,14 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/accepts": { "version": "1.3.8", @@ -5238,6 +5198,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5251,7 +5212,6 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -5275,6 +5235,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5292,7 +5253,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -5311,7 +5271,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5328,8 +5287,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -5532,7 +5490,6 @@ "integrity": "sha512-r+mCJ7zXgXElgR4IRC+fkvNCeoaavWBs6EdCso5Tbcf+iEMKzBU/His60lt34WEZ9vlb8wDkZvQGcVI5GwkfoQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5574,7 +5531,6 @@ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5890,6 +5846,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5909,8 +5866,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bundle-name": { "version": "4.1.0", @@ -6121,7 +6077,6 @@ "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", @@ -6148,7 +6103,6 @@ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", @@ -6192,7 +6146,6 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -6705,7 +6658,6 @@ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -6740,7 +6692,6 @@ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">= 6" }, @@ -7058,7 +7009,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/d3-shape": { "version": "2.1.0", @@ -7198,6 +7150,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -7476,8 +7429,7 @@ "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -7501,7 +7453,6 @@ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -7528,8 +7479,7 @@ "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", @@ -7537,7 +7487,6 @@ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -7554,7 +7503,6 @@ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -7655,7 +7603,6 @@ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" @@ -7670,7 +7617,6 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -7694,7 +7640,6 @@ "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -7709,7 +7654,6 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -7853,8 +7797,7 @@ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-define-property": { "version": "1.0.1", @@ -7881,8 +7824,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8050,7 +7992,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -8065,7 +8006,6 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -8079,7 +8019,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -8090,7 +8029,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -8216,7 +8154,6 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -8305,8 +8242,7 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.20.1", @@ -8736,8 +8672,7 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/global": { "version": "4.4.0", @@ -8859,7 +8794,6 @@ "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -8883,7 +8817,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -9230,7 +9163,6 @@ "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array.prototype.filter": "^1.0.0", "call-bind": "^1.0.2" @@ -9277,7 +9209,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -9291,7 +9222,6 @@ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -9982,8 +9912,7 @@ "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-symbol": { "version": "1.1.1", @@ -10071,7 +10000,6 @@ "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -10083,7 +10011,6 @@ "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -10154,7 +10081,6 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10188,8 +10114,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -10476,7 +10401,6 @@ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -10533,16 +10457,14 @@ "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -10762,6 +10684,7 @@ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/geojson-types": "^1.0.2", @@ -10796,7 +10719,6 @@ "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12082,6 +12004,7 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -12091,8 +12014,7 @@ "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ms": { "version": "2.1.3", @@ -12223,7 +12145,6 @@ "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "commander": "^2.19.0", "moo": "^0.5.0", @@ -12246,8 +12167,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", @@ -12264,8 +12184,7 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/next": { "version": "14.2.35", @@ -12724,7 +12643,6 @@ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" @@ -12739,7 +12657,6 @@ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "parse5": "^7.0.0" }, @@ -13076,6 +12993,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -13226,8 +13144,7 @@ "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", "dev": true, - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/randexp": { "version": "0.4.6", @@ -13235,7 +13152,6 @@ "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "discontinuous-range": "1.0.0", "ret": "~0.1.10" @@ -13250,7 +13166,6 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -13281,6 +13196,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13332,6 +13248,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -14375,7 +14292,6 @@ "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -14406,7 +14322,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14486,7 +14401,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.12" } @@ -14564,7 +14478,6 @@ "integrity": "sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "lodash.flattendeep": "^4.4.0", "nearley": "^2.7.10" @@ -14746,7 +14659,6 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -15035,7 +14947,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15047,7 +14958,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -15082,7 +14992,6 @@ "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -15360,7 +15269,6 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15403,7 +15311,6 @@ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" }, @@ -15438,7 +15345,6 @@ "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -15492,7 +15398,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -15505,8 +15410,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "4.3.3", @@ -15514,7 +15418,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -15534,8 +15437,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tinyqueue": { "version": "2.0.3", @@ -15618,8 +15520,7 @@ "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==", "deprecated": "Use String.prototype.trim() instead", - "dev": true, - "peer": true + "dev": true }, "node_modules/trim-lines": { "version": "3.0.1", @@ -15802,6 +15703,7 @@ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15835,7 +15737,6 @@ "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.18.1" } @@ -15860,7 +15761,6 @@ "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.0", "xtend": "^4.0.0" @@ -16002,7 +15902,6 @@ "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "unist-util-visit": "^1.1.0" }, @@ -16016,8 +15915,7 @@ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/unist-util-remove-position/node_modules/unist-util-visit": { "version": "1.4.1", @@ -16025,7 +15923,6 @@ "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "unist-util-visit-parents": "^2.0.0" } @@ -16036,7 +15933,6 @@ "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "unist-util-is": "^3.0.0" } @@ -16251,6 +16147,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -16470,7 +16367,6 @@ "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16553,7 +16449,6 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -16582,7 +16477,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -16595,8 +16489,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -16604,7 +16497,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -16625,7 +16517,6 @@ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -16639,7 +16530,6 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -16653,7 +16543,6 @@ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -16869,7 +16758,6 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4" } diff --git a/package.json b/package.json index ec98a97240..dae8a58973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "baseui", - "version": "16.0.0", + "version": "16.1.0", "description": "A React Component library implementing the Base design language", "keywords": [ "react", diff --git a/src/checkbox-v2/__tests__/checkbox-v2-auto-focus.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2-auto-focus.scenario.tsx new file mode 100644 index 0000000000..50f266611e --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2-auto-focus.scenario.tsx @@ -0,0 +1,13 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; + +import { StatefulCheckbox } from '../'; + +export function Scenario() { + return click me(auto focus); +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2-indeterminate.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2-indeterminate.scenario.tsx new file mode 100644 index 0000000000..7ad95d98cf --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2-indeterminate.scenario.tsx @@ -0,0 +1,73 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; + +import { Block } from '../../block'; +import { Checkbox } from '../'; + +class GroupList extends React.Component< + {}, + { + checkboxes: Array; + } +> { + state = { checkboxes: [true, false] }; + + render() { + const allChecked = this.state.checkboxes.every(Boolean); + const isIndeterminate = this.state.checkboxes.some(Boolean) && !allChecked; + + return ( + + { + const nextCheckboxes = [e.target.checked, e.target.checked]; + this.setState({ checkboxes: nextCheckboxes }); + }} + isIndeterminate={isIndeterminate} + checked={allChecked} + overrides={{ Root: { props: { 'data-name': 'parent' } } }} + id="parent-checkbox" + aria-controls="child1-checkbox child2-checkbox" + > + Indeterminate checkbox if not all subcheckboxes are checked + + {/* Adding alignItems:flex-start to avoid items stretching */} + + { + const nextCheckboxes = [...this.state.checkboxes]; + nextCheckboxes[0] = e.target.checked; + this.setState({ checkboxes: nextCheckboxes }); + }} + overrides={{ Root: { props: { 'data-name': 'child1' } } }} + id="child1-checkbox" + > + First subcheckbox + + { + const nextCheckboxes = [...this.state.checkboxes]; + nextCheckboxes[1] = e.target.checked; + this.setState({ checkboxes: nextCheckboxes }); + }} + overrides={{ Root: { props: { 'data-name': 'child2' } } }} + id="child2-checkbox" + > + Second subcheckbox + + + + ); + } +} + +export function Scenario() { + return ; +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2-placement.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2-placement.scenario.tsx new file mode 100644 index 0000000000..5e02b5bbcb --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2-placement.scenario.tsx @@ -0,0 +1,44 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +import * as React from 'react'; +import { Checkbox, LABEL_PLACEMENT } from '../'; + +export function Scenario() { + const [checkboxes, setCheckboxes] = React.useState([false, false]); + + return ( +
+ { + const nextCheckboxes = [...checkboxes]; + nextCheckboxes[0] = e.currentTarget.checked; + setCheckboxes(nextCheckboxes); + }} + labelPlacement={LABEL_PLACEMENT.left} + > + Label on the left + + { + const nextCheckboxes = [...checkboxes]; + nextCheckboxes[1] = e.currentTarget.checked; + setCheckboxes(nextCheckboxes); + }} + labelPlacement={LABEL_PLACEMENT.right} + > + Label on the right + +
+ ); +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2-react-hook-form.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2-react-hook-form.scenario.tsx new file mode 100644 index 0000000000..c9ea1dbcb2 --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2-react-hook-form.scenario.tsx @@ -0,0 +1,109 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; + +import { FormProvider, useForm, Controller } from 'react-hook-form'; +import { StatefulCheckbox, Checkbox, LABEL_PLACEMENT } from '../'; +import { Heading, HeadingLevel } from '../../heading'; + +const StatefulCheckboxExample = () => { + const form = useForm({ + defaultValues: { + basewebStatefulCheckbox: true, + nativeCheckbox: true, + }, + }); + + function handleSubmit(data: any) { + console.log(data); + } + const { ref: refA, ...checkboxA } = form.register('basewebStatefulCheckbox'); + + return ( + +
+ ts) type mismatch + inputRef={refA} + labelPlacement={LABEL_PLACEMENT.right} + initialState={{ checked: true, isIndeterminate: false }} + > + Baseweb StatefulCheckbox + + +
+ +
+
+
+ ); +}; + +const CheckboxWithControllerExample = () => { + const form = useForm({ + defaultValues: { + basewebCheckbox: true, + nativeCheckbox: true, + }, + }); + + function handleSubmit(data: any) { + console.log(data); + } + + return ( + +
+ ( + ts) type mismatch + inputRef={ref} + labelPlacement={LABEL_PLACEMENT.right} + > + Baseweb Checkbox + + )} + /> + + +
+ +
+ +
+ ); +}; + +const Example = () => { + return ( + + React-hook-form + + Using StatefulCheckbox + + Using Checkbox with react-hook-form Controller + + + + ); +}; + +export function Scenario() { + return ; +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2-states.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2-states.scenario.tsx new file mode 100644 index 0000000000..05ab8fdf30 --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2-states.scenario.tsx @@ -0,0 +1,39 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { Checkbox } from '../'; + +export function Scenario() { + return ( +
+ Checkbox + Checkbox checked + Checkbox isIndeterminate + Checkbox disabled + + Checkbox disabled checked + + + Checkbox disabled isIndeterminate + + Checkbox error + + Checkbox error checked + + + Checkbox error isIndeterminate + +
+ ); +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2-unlabeled.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2-unlabeled.scenario.tsx new file mode 100644 index 0000000000..87ca3606a5 --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2-unlabeled.scenario.tsx @@ -0,0 +1,14 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; + +import { StatefulCheckbox } from '../'; + +export function Scenario() { + return ; +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2.scenario.tsx b/src/checkbox-v2/__tests__/checkbox-v2.scenario.tsx new file mode 100644 index 0000000000..bae751510a --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2.scenario.tsx @@ -0,0 +1,45 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { useStyletron } from '../../'; +import { StatefulCheckbox } from '../'; +import { HeadingSmall, ParagraphSmall, ParagraphMedium } from '../../typography'; + +export function Scenario() { + const [css] = useStyletron(); + return ( + + click me +
+ + This is a long text. This is a long text. This is a long text. This is a long text. This + is a long text. + +
+ + Checkboxes Group + + Note: checkbox itself does not implement group behavior. Developers need to take care of + Accessibility for checkboxes group. Below is an example with ul and li. + + + Checkboxes group - choose your favorite fruit: + +
    +
  • + Apple +
  • +
  • + Banana +
  • +
  • + Orange +
  • +
+
+ ); +} diff --git a/src/checkbox-v2/__tests__/checkbox-v2.stories.tsx b/src/checkbox-v2/__tests__/checkbox-v2.stories.tsx new file mode 100644 index 0000000000..2c108c2ea7 --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2.stories.tsx @@ -0,0 +1,28 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import React from 'react'; +import { Scenario as CheckboxIndeterminate } from './checkbox-v2-indeterminate.scenario'; +import { Scenario as CheckboxPlacement } from './checkbox-v2-placement.scenario'; +import { Scenario as CheckboxStates } from './checkbox-v2-states.scenario'; +import { Scenario as CheckboxUnlabeled } from './checkbox-v2-unlabeled.scenario'; +import { Scenario as CheckboxDefault } from './checkbox-v2.scenario'; +import { Scenario as CheckboxReactHookForm } from './checkbox-v2-react-hook-form.scenario'; +import { Scenario as CheckboxAutoFocus } from './checkbox-v2-auto-focus.scenario'; + +export const Indeterminate = () => ; +export const Placement = () => ; +export const States = () => ; +export const Unlabeled = () => ; +export const Checkbox = () => ; +export const ReactHookForm = () => ; +export const AutoFocus = () => ; + +export default { + meta: { + runtimeErrorsAllowed: true, + }, +}; diff --git a/src/checkbox-v2/__tests__/checkbox-v2.test.tsx b/src/checkbox-v2/__tests__/checkbox-v2.test.tsx new file mode 100644 index 0000000000..a8b8bd5886 --- /dev/null +++ b/src/checkbox-v2/__tests__/checkbox-v2.test.tsx @@ -0,0 +1,138 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +/* eslint-env node */ + +import * as React from 'react'; +import { render, fireEvent, getByText, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Checkbox } from '..'; +import '@testing-library/jest-dom'; + +/** + * Setting up a typical implementation scenario for Checkbox + */ +const SwitchForm = () => { + const [checkboxes, setCheckboxes] = React.useState([false, false]); + + return ( +
+ ) => { + const isChecked = e.currentTarget.checked; + setCheckboxes((prevCheckboxes) => { + return prevCheckboxes.map((val, idx) => (idx === 0 ? isChecked : val)); + }); + }} + > + Label 1 + + + ) => { + const isChecked = e.currentTarget.checked; + setCheckboxes((prevCheckboxes) => { + return prevCheckboxes.map((val, idx) => (idx === 1 ? isChecked : val)); + }); + }} + > + Label 2 + +
+ ); +}; + +describe('Stateless checkbox', function () { + it('renders provided label', function () { + const { container } = render(label); + getByText(container, 'label'); + }); + + it('calls provided event handlers', async () => { + const user = userEvent.setup(); + const onMouseEnter = jest.fn(); + const onMouseLeave = jest.fn(); + const onMouseUp = jest.fn(); + const onMouseDown = jest.fn(); + const onFocus = jest.fn(); + const onBlur = jest.fn(); + + const { container } = render( + + label + + ); + + const input = container.querySelector('input'); + + if (input) { + // Hover triggers: mouseEnter + await user.hover(input); + expect(onMouseEnter).toHaveBeenCalledTimes(1); + + // Unhover triggers: mouseLeave + await user.unhover(input); + expect(onMouseLeave).toHaveBeenCalledTimes(1); + + // Mouse down / up + await user.pointer({ target: input, keys: '[MouseLeft]' }); + expect(onMouseDown).toHaveBeenCalledTimes(1); + expect(onMouseUp).toHaveBeenCalledTimes(1); + + // Focus (via tab) + await user.tab(); + expect(onFocus).toHaveBeenCalledTimes(1); + + // Blur + await user.tab(); // move focus away + expect(onBlur).toHaveBeenCalledTimes(1); + } + }); + + it('only fires one click event', () => { + const onAncestorClick = jest.fn(); + const { container } = render( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
+ label +
+ ); + const label = container.querySelector('label'); + if (label) { + fireEvent.click(label.parentElement!); + } + expect(onAncestorClick).toHaveBeenCalledTimes(1); + }); + + it('test with a real stateless checkbox use case', async () => { + const user = userEvent.setup(); + render(); + const input = screen.getByLabelText('Label 1'); + + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + + const input2 = screen.getByLabelText('Label 2'); + + expect(input2).not.toBeChecked(); + await user.click(input2); + expect(input2).toBeChecked(); + await user.keyboard(' '); + expect(input2).not.toBeChecked(); + await user.keyboard('{Enter}'); + expect(input2).toBeChecked(); + }); +}); diff --git a/src/checkbox-v2/__tests__/stateful-checkbox-v2-container.test.tsx b/src/checkbox-v2/__tests__/stateful-checkbox-v2-container.test.tsx new file mode 100644 index 0000000000..d08f56b8bc --- /dev/null +++ b/src/checkbox-v2/__tests__/stateful-checkbox-v2-container.test.tsx @@ -0,0 +1,99 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { StatefulContainer, StatefulCheckbox } from '..'; +import '@testing-library/jest-dom'; + +describe('Stateful container', function () { + it('should provide all needed props to children render func', function () { + const children = jest.fn((arg) => null); + // @ts-expect-error - Point of this test is to check a missing prop + render({children}); + const props = children.mock.calls[0][0]; + expect(props.foo).toBe('bar'); + }); + + it('should provide initial state as part of state', function () { + const children = jest.fn((arg) => null); + render({children}); + const props = children.mock.calls[0][0]; + expect(props.checked).toBe(true); + }); + + it('calls provided event handlers', async () => { + const user = userEvent.setup(); + + const onMouseEnter = jest.fn(); + const onMouseLeave = jest.fn(); + const onMouseUp = jest.fn(); + const onMouseDown = jest.fn(); + const onFocus = jest.fn(); + const onBlur = jest.fn(); + + const { container } = render( + + label + + ); + + const input = container.querySelector('input'); + if (input) { + // Hover triggers: mouseEnter + await user.hover(input); + expect(onMouseEnter).toHaveBeenCalledTimes(1); + + // Unhover triggers: mouseLeave + await user.unhover(input); + expect(onMouseLeave).toHaveBeenCalledTimes(1); + + // Mouse down / up + await user.pointer({ target: input, keys: '[MouseLeft]' }); + expect(onMouseDown).toHaveBeenCalledTimes(1); + expect(onMouseUp).toHaveBeenCalledTimes(1); + + // Focus (via tab) + await user.tab(); + expect(onFocus).toHaveBeenCalledTimes(1); + + // Blur + await user.tab(); // move focus away + expect(onBlur).toHaveBeenCalledTimes(1); + } + }); + + it('updates checked state on change', async () => { + const user = userEvent.setup(); + const { container } = render(label); + const input = container.querySelector('input') as HTMLInputElement; + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + }); + + it('updates checked state on change(keyboard)', async () => { + const user = userEvent.setup(); + const { getByRole } = render(label); + const input = getByRole('checkbox'); + + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + await user.keyboard(' '); + expect(input).not.toBeChecked(); + await user.keyboard('{Enter}'); + expect(input).toBeChecked(); + }); +}); diff --git a/src/checkbox-v2/checkbox.tsx b/src/checkbox-v2/checkbox.tsx new file mode 100644 index 0000000000..13daf6df12 --- /dev/null +++ b/src/checkbox-v2/checkbox.tsx @@ -0,0 +1,262 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { getOverride, getOverrideProps } from '../helpers/overrides'; +import type { CheckboxProps } from './types'; +import { + Checkmark as StyledCheckmark, + Input as StyledInput, + Label as StyledLabel, + Root as StyledRoot, + CheckmarkContainer as StyledCheckmarkContainer, +} from './styled-components'; +import { isFocusVisible as isFocusVisibleCheck } from '../utils/focusVisible'; +import { LABEL_PLACEMENT } from './constants'; +import type { ChangeEvent } from 'react'; + +const stopPropagation = (e: ChangeEvent) => e.stopPropagation(); + +const Checkbox = (props: CheckboxProps) => { + const { + overrides = {}, + checked = false, + containsInteractiveElement = false, + disabled = false, + autoFocus = false, + isIndeterminate = false, + error = false, + labelPlacement = LABEL_PLACEMENT.right, + onChange = () => {}, + onMouseEnter = () => {}, + onMouseLeave = () => {}, + onMouseDown = () => {}, + onMouseUp = () => {}, + onFocus = () => {}, + onBlur = () => {}, + onKeyDown, // don't add fallback no-op to allow native keydown behavior if not customized. + onKeyUp, // don't add fallback no-op to allow native keyup behavior if not customized. + value, + id, + name, + children, + required, + title, + inputRef, + } = props; + const [isFocused, setIsFocused] = React.useState(autoFocus); + const [isFocusVisible, setIsFocusVisible] = React.useState(false); + const [isHovered, setIsHovered] = React.useState(false); + const [isActive, setIsActive] = React.useState(false); + const fallbackInputRef = React.useRef(null); + const internalInputRef = inputRef || fallbackInputRef; + + React.useEffect(() => { + if (autoFocus) { + internalInputRef.current?.focus(); + } + }, [autoFocus, internalInputRef]); + + React.useEffect(() => { + if (internalInputRef.current) { + // Have to disable eslint for this case since indeterminate can only be set via JS + // We don't need to explicitly add aria-checked because the checked attribute and this indeterminate property have the same effect + // eslint-disable-next-line react-compiler/react-compiler + internalInputRef.current.indeterminate = isIndeterminate; + } + }, [isIndeterminate, internalInputRef]); + + const onMouseEnterHandler = React.useCallback( + (e: ChangeEvent) => { + setIsHovered(true); + onMouseEnter(e); + }, + [onMouseEnter] + ); + + const onMouseLeaveHandler = React.useCallback( + (e: ChangeEvent) => { + setIsHovered(false); + setIsActive(false); + onMouseLeave(e); + }, + [onMouseLeave] + ); + + const onMouseDownHandler = React.useCallback( + (e: ChangeEvent) => { + setIsActive(true); + onMouseDown(e); + }, + [onMouseDown] + ); + + const onMouseUpHandler = React.useCallback( + (e: ChangeEvent) => { + setIsActive(false); + onMouseUp(e); + }, + [onMouseUp] + ); + + const onFocusHandler = React.useCallback( + (e: ChangeEvent) => { + setIsFocused(true); + onFocus(e); + if (isFocusVisibleCheck(e)) { + setIsFocusVisible(true); + } + }, + [onFocus] + ); + + const onBlurHandler = React.useCallback( + (e: ChangeEvent) => { + setIsFocused(false); + onBlur(e); + if (!isFocusVisibleCheck(e)) { + setIsFocusVisible(false); + } + }, + [onBlur] + ); + + const onKeyUpHandler = React.useCallback( + (event: React.KeyboardEvent) => { + /** + * Handles 'Enter' key press to toggle the checkbox. + */ + + if (event.key === ' ') { + setIsActive(false); + } + if (event.key === 'Enter') { + setIsActive(false); + onChange?.({ + ...event, + currentTarget: { + ...event.currentTarget, + checked: !checked, + }, + target: { + ...event.target, + checked: !checked, + }, + } as unknown as ChangeEvent); + } + onKeyUp?.(event); + }, + [onKeyUp, onChange, checked] + ); + + const onKeyDownHandler = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + setIsActive(true); + } + + onKeyDown?.(event); + }, + [onKeyDown] + ); + + const { + Root: RootOverride, + Checkmark: CheckmarkOverride, + Label: LabelOverride, + Input: InputOverride, + CheckmarkContainer: CheckmarkContainerOverride, + } = overrides; + + const Root = getOverride(RootOverride) || StyledRoot; + const CheckmarkContainer = getOverride(CheckmarkContainerOverride) || StyledCheckmarkContainer; + const Checkmark = getOverride(CheckmarkOverride) || StyledCheckmark; + const Label = getOverride(LabelOverride) || StyledLabel; + const Input = getOverride(InputOverride) || StyledInput; + + const inputEvents = { + onChange, + onFocus: onFocusHandler, + onBlur: onBlurHandler, + onKeyDown: onKeyDownHandler, + onKeyUp: onKeyUpHandler, + }; + const mouseEvents = { + onMouseEnter: onMouseEnterHandler, + onMouseLeave: onMouseLeaveHandler, + onMouseDown: onMouseDownHandler, + onMouseUp: onMouseUpHandler, + }; + const sharedProps = { + $isFocused: isFocused, + $isFocusVisible: isFocusVisible, + $isHovered: isHovered, + $isActive: isActive, + $error: error, + $checked: checked, + $isIndeterminate: isIndeterminate, + $required: required, + $disabled: disabled, + $value: value, + }; + + const labelComp = children && ( + + ); + + return ( + + {labelPlacement === LABEL_PLACEMENT.left && labelComp} + + + + + + + {labelPlacement === LABEL_PLACEMENT.right && labelComp} + + ); +}; + +Checkbox.displayName = 'Checkbox'; + +export default Checkbox; diff --git a/src/checkbox-v2/constants.ts b/src/checkbox-v2/constants.ts new file mode 100644 index 0000000000..e415666bd4 --- /dev/null +++ b/src/checkbox-v2/constants.ts @@ -0,0 +1,17 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +export const STATE_TYPE = { + change: 'CHANGE', +} as const; + +// Note: top and bottom label placements are deprecated from checkbox v2. +// Alternatives: Change design to use left or right placement; use checkbox control only and add your own container and label to realize the top/bottom placements +export const LABEL_PLACEMENT = Object.freeze({ + right: 'right', + left: 'left', +} as const); diff --git a/src/checkbox-v2/index.ts b/src/checkbox-v2/index.ts new file mode 100644 index 0000000000..33e0292621 --- /dev/null +++ b/src/checkbox-v2/index.ts @@ -0,0 +1,23 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +export { default as StatefulCheckbox } from './stateful-checkbox'; +export { default as StatefulContainer } from './stateful-checkbox-container'; +export { default as Checkbox } from './checkbox'; +// Styled elements +export { + Root as StyledRoot, + Checkmark as StyledCheckmark, + CheckmarkContainer as StyledCheckmarkContainer, + Label as StyledLabel, + Input as StyledInput, +} from './styled-components'; + +export { STATE_TYPE, LABEL_PLACEMENT } from './constants'; + +// Flow +export * from './types'; diff --git a/src/checkbox-v2/stateful-checkbox-container.ts b/src/checkbox-v2/stateful-checkbox-container.ts new file mode 100644 index 0000000000..917150e531 --- /dev/null +++ b/src/checkbox-v2/stateful-checkbox-container.ts @@ -0,0 +1,133 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { STATE_TYPE } from './constants'; +import type { StatefulContainerProps, StateReducer } from './types'; + +import type { ChangeEvent } from 'react'; + +const defaultStateReducer: StateReducer = (type, nextState, currentState) => ({ + ...currentState, + ...nextState, +}); + +const StatefulCheckboxContainer = (props: StatefulContainerProps) => { + const { + initialState = { checked: false, isIndeterminate: false }, + stateReducer = defaultStateReducer, + onChange = () => {}, + onMouseEnter = () => {}, + onMouseLeave = () => {}, + onMouseDown = () => {}, + onMouseUp = () => {}, + onFocus = () => {}, + onBlur = () => {}, + onKeyDown = () => {}, + onKeyUp = () => {}, + children = (childProps: {}) => null, + ...restProps + } = props; + const [checked, setChecked] = React.useState(initialState.checked); + const [isIndeterminate, setIsIndeterminate] = React.useState(initialState.isIndeterminate); + + const updateState = React.useCallback( + (type: string, e: ChangeEvent) => { + let nextState = {}; + switch (type) { + case STATE_TYPE.change: + nextState = { checked: e.target.checked }; + break; + } + const newState = stateReducer(type, nextState, { checked, isIndeterminate }, e); + + setChecked(newState.checked); + setIsIndeterminate(newState.isIndeterminate); + }, + [checked, isIndeterminate, stateReducer] + ); + + const onChangeHandler = React.useCallback( + (e: ChangeEvent) => { + updateState(STATE_TYPE.change, e); + onChange && onChange(e); + }, + [updateState, onChange] + ); + + const onMouseEnterHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseEnter && onMouseEnter(e); + }, + [onMouseEnter] + ); + + const onMouseLeaveHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseLeave && onMouseLeave(e); + }, + [onMouseLeave] + ); + + const onFocusHandler = React.useCallback( + (e: ChangeEvent) => { + onFocus && onFocus(e); + }, + [onFocus] + ); + + const onBlurHandler = React.useCallback( + (e: ChangeEvent) => { + onBlur && onBlur(e); + }, + [onBlur] + ); + + const onMouseDownHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseDown && onMouseDown(e); + }, + [onMouseDown] + ); + + const onMouseUpHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseUp && onMouseUp(e); + }, + [onMouseUp] + ); + + const onKeyDownHandler = React.useCallback( + (event: React.KeyboardEvent) => { + onKeyDown && onKeyDown(event); + }, + [onKeyDown] + ); + + const onKeyUpHandler = React.useCallback( + (event: React.KeyboardEvent) => { + onKeyUp && onKeyUp(event); + }, + [onKeyUp] + ); + + return children({ + ...restProps, + checked, + isIndeterminate, + onChange: onChangeHandler, + onMouseEnter: onMouseEnterHandler, + onMouseLeave: onMouseLeaveHandler, + onMouseDown: onMouseDownHandler, + onMouseUp: onMouseUpHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler, + onKeyDown: onKeyDownHandler, + onKeyUp: onKeyUpHandler, + }); +}; + +export default StatefulCheckboxContainer; diff --git a/src/checkbox-v2/stateful-checkbox.tsx b/src/checkbox-v2/stateful-checkbox.tsx new file mode 100644 index 0000000000..40e00dddf6 --- /dev/null +++ b/src/checkbox-v2/stateful-checkbox.tsx @@ -0,0 +1,23 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import StatefulContainer from './stateful-checkbox-container'; +import Checkbox from './checkbox'; +import type { StatefulContainerChildProps, StatefulCheckboxProps } from './types'; +// Styled elements + +const StatefulCheckbox = function (props: StatefulCheckboxProps) { + return ( + + {(childrenProps: StatefulContainerChildProps) => ( + {props.children} + )} + + ); +}; +StatefulCheckbox.displayName = 'StatefulCheckbox'; +export default StatefulCheckbox; diff --git a/src/checkbox-v2/styled-components.ts b/src/checkbox-v2/styled-components.ts new file mode 100644 index 0000000000..02bfee3a57 --- /dev/null +++ b/src/checkbox-v2/styled-components.ts @@ -0,0 +1,223 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +import { styled, type Theme } from '../styles'; + +import type { SharedStyleProps } from './types'; +import { LABEL_PLACEMENT } from './constants'; +import { getFocusOutlineStyle, getOverlayColor } from '../utils/get-shared-styles'; + +type UtilityProps = SharedStyleProps & { $theme: Theme }; + +function getBorderColor(props: UtilityProps) { + const { $disabled, $checked, $error, $isIndeterminate, $theme, $isFocusVisible } = props; + const { colors } = $theme; + if ($disabled) { + return colors.contentStateDisabled; + } + if ($checked || $isIndeterminate) { + return 'transparent'; + } + if ($error) { + return colors.tagRedContentSecondary; + } + if ($isFocusVisible) { + return colors.borderSelected; + } + + return colors.contentTertiary; +} + +function getLabelPadding(props: UtilityProps) { + const { $labelPlacement = '', $theme } = props; + const { sizing } = $theme; + const { scale100 } = sizing; + let paddingDirection; + + switch ($labelPlacement) { + case LABEL_PLACEMENT.left: + paddingDirection = 'Right'; + break; + default: + case LABEL_PLACEMENT.right: + paddingDirection = 'Left'; + break; + } + + if ($theme.direction === 'rtl' && paddingDirection === 'Left') { + paddingDirection = 'Right'; + } else if ($theme.direction === 'rtl' && paddingDirection === 'Right') { + paddingDirection = 'Left'; + } + + return { + [`padding${paddingDirection}`]: scale100, + }; +} + +function getBackgroundColor(props: UtilityProps) { + const { $disabled, $checked, $isIndeterminate, $error, $isHovered, $isActive, $theme } = props; + const { colors } = $theme; + + if ($disabled) { + return $checked || $isIndeterminate ? colors.contentStateDisabled : 'transparent'; + } + if ($checked || $isIndeterminate) { + return $error ? colors.tagRedContentSecondary : colors.contentPrimary; + } + if ($isHovered) { + return $error ? colors.hoverNegativeAlpha : colors.hoverOverlayAlpha; + } + if ($isActive) { + return $error ? colors.pressedNegativeAlpha : colors.pressedOverlayAlpha; + } + return 'transparent'; +} + +function getLabelColor(props: UtilityProps) { + const { $disabled, $theme } = props; + const { colors } = $theme; + return $disabled ? colors.contentStateDisabled : colors.contentPrimary; +} + +export const Root = styled<'label', SharedStyleProps>('label', (props) => { + const { $disabled, $theme } = props; + const { sizing } = $theme; + return { + flexDirection: 'row', + display: 'inline-flex', + verticalAlign: 'middle', + alignItems: 'flex-start', + cursor: $disabled ? 'not-allowed' : 'pointer', + userSelect: 'none', + '@media (pointer: coarse)': { + // Increase target size for touch devices to meet the minimum touch target size of 48x48dp + padding: sizing.scale300, + }, + }; +}); + +Root.displayName = 'Root'; + +// Styled checkmark container as the state layer, backplate container +export const CheckmarkContainer = styled<'span', SharedStyleProps>('span', (props) => { + const { $theme } = props; + const { sizing } = $theme; + const { hoveredColor, pressedColor } = getOverlayColor(props); + + return { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + boxSizing: 'border-box', + minHeight: sizing.scale900, + minWidth: sizing.scale900, + borderRadius: sizing.scale300, + paddingTop: sizing.scale300, + paddingBottom: sizing.scale300, + paddingLeft: sizing.scale300, + paddingRight: sizing.scale300, + '@media (hover: hover)': { + ':hover': { + backgroundColor: hoveredColor, + }, + }, + ':active': { + backgroundColor: pressedColor, + }, + }; +}); + +// @ts-ignore +export const Checkmark = styled<'span', SharedStyleProps>('span', (props) => { + const { $checked, $isIndeterminate, $theme, $isFocusVisible, $isFocused } = props; + const { sizing, animation } = $theme; + + const tickColor = $theme.colors.contentInversePrimary; + + const indeterminate = encodeURIComponent(` + + + + `); + + const check = encodeURIComponent(` + + + + `); + + const borderRadius = sizing.scale100; + const borderColor = getBorderColor(props); + + return { + flex: '0 0 auto', + transitionDuration: animation.timing200, + transitionTimingFunction: animation.easeOutCurve, + transitionProperty: 'background-image, border-color, background-color', + width: '17px', + height: '17px', + boxSizing: 'border-box', + borderLeftStyle: 'solid', + borderRightStyle: 'solid', + borderTopStyle: 'solid', + borderBottomStyle: 'solid', + borderLeftWidth: sizing.scale0, + borderRightWidth: sizing.scale0, + borderTopWidth: sizing.scale0, + borderBottomWidth: sizing.scale0, + borderLeftColor: borderColor, + borderRightColor: borderColor, + borderTopColor: borderColor, + borderBottomColor: borderColor, + borderTopLeftRadius: borderRadius, + borderTopRightRadius: borderRadius, + borderBottomRightRadius: borderRadius, + borderBottomLeftRadius: borderRadius, + // Apply focus outline style if the checkbox is focused and focus is visible(focused by Tab) + ...($isFocusVisible && $isFocused ? getFocusOutlineStyle($theme) : {}), + display: 'inline-block', + verticalAlign: 'middle', + backgroundImage: $isIndeterminate + ? `url('data:image/svg+xml,${indeterminate}');` + : $checked + ? `url('data:image/svg+xml,${check}');` + : null, + backgroundColor: getBackgroundColor(props), + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + }; +}); + +Checkmark.displayName = 'Checkmark'; + +export const Label = styled<'div', SharedStyleProps>('div', (props) => { + const { $theme } = props; + const { typography, sizing } = $theme; + return { + verticalAlign: 'middle', + paddingTop: sizing.scale200, // top padding to make checkbox aligned with first row of the label + ...getLabelPadding(props), + color: getLabelColor(props), + ...typography.ParagraphSmall, + }; +}); + +Label.displayName = 'Label'; + +// tricky style for focus event cause display: none doesn't work +export const Input = styled('input', { + opacity: 0, + width: 0, + height: 0, + overflow: 'hidden', + margin: 0, + padding: 0, + position: 'absolute', +}); + +Input.displayName = 'Input'; diff --git a/src/checkbox-v2/types.ts b/src/checkbox-v2/types.ts new file mode 100644 index 0000000000..c6693075d9 --- /dev/null +++ b/src/checkbox-v2/types.ts @@ -0,0 +1,187 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import type * as React from 'react'; +import type { Override } from '../helpers/overrides'; + +export type LabelPlacement = 'left' | 'right'; + +export type CheckboxOverrides = { + Checkmark?: Override; + CheckmarkContainer?: Override; + Label?: Override; + Root?: Override; + Input?: Override; +}; + +export type DefaultProps = { + overrides?: any; + children?: React.ReactNode; + checked: boolean; + disabled: boolean; + error: boolean; + autoFocus: boolean; + isIndeterminate: boolean; + labelPlacement: LabelPlacement; + onChange: (e: React.ChangeEvent) => unknown; + onMouseEnter: (e: React.ChangeEvent) => unknown; + onMouseLeave: (e: React.ChangeEvent) => unknown; + onMouseDown: (e: React.ChangeEvent) => unknown; + onMouseUp: (e: React.ChangeEvent) => unknown; + onFocus: (e: React.ChangeEvent) => unknown; + onBlur: (e: React.ChangeEvent) => unknown; + containsInteractiveElement?: boolean; +}; + +export type CheckboxProps = { + /** Id of element which contains a related caption */ + 'aria-describedby'?: string; + /** Id of element which contains a related error message */ + 'aria-errormessage'?: string; + /** Passed to the input element aria-label attribute. */ + ariaLabel?: string; + 'aria-label'?: string; + /** Ids of element which this checkbox controls, may be useful when there is a master checkbox controlling multiple child checkboxes - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-controls */ + 'aria-controls'?: string; + /** Component or String value for label of checkbox. */ + children?: React.ReactNode; + /** Indicates if this checkbox children contain an interactive element (prevents the label from moving focus from the child element to the radio button) */ + containsInteractiveElement?: boolean; + overrides?: CheckboxOverrides; + /** Check or uncheck the control. */ + checked?: boolean; + /** Disable the checkbox from being changed. */ + disabled?: boolean; + /** Marks the checkbox as required. */ + required?: boolean; + /** Renders checkbox in errored state. */ + error?: boolean; + /** Used to get a ref to the input element. Useful for programmatically focusing the input */ + inputRef?: React.RefObject; + /** Focus the checkbox on initial render. */ + autoFocus?: boolean; + /** Passed to the input element id attribute */ + id?: string; + /** Passed to the input element name attribute */ + name?: string; + /** Passed to the input element value attribute */ + value?: string; + /** Indicates a 'half' state for the checkmark. In this case, `checked` is ignored. */ + isIndeterminate?: boolean; + /** How to position the label relative to the checkbox itself. */ + labelPlacement?: LabelPlacement; + /** Text to display in native OS tooltip on long hover. */ + title?: string | null; + /** Handler for change events on trigger element. */ + onChange?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseenter events on trigger element. */ + onMouseEnter?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseleave events on trigger element. */ + onMouseLeave?: (e: React.ChangeEvent) => unknown; + /** Handler for mousedown events on trigger element. */ + onMouseDown?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseup events on trigger element. */ + onMouseUp?: (e: React.ChangeEvent) => unknown; + /** handler for focus events on trigger element. */ + onFocus?: (e: React.ChangeEvent) => unknown; + /** handler for blur events on trigger element. */ + onBlur?: (e: React.ChangeEvent) => unknown; + /** Handler for keydown events on trigger element. */ + onKeyDown?: (e: React.KeyboardEvent) => unknown; + /** Handler for keyup events on trigger element. */ + onKeyUp?: (e: React.KeyboardEvent) => unknown; +}; + +export type CheckboxState = { + isFocused: boolean; + isFocusVisible: boolean; + isHovered: boolean; + isActive: boolean; +}; + +export type CheckboxReducerState = { + checked?: boolean; + isIndeterminate?: boolean; +}; + +export type StateReducer = ( + stateType: string, + nextState: CheckboxReducerState, + currentState: CheckboxReducerState, + event: React.ChangeEvent +) => CheckboxReducerState; + +export type StatefulContainerChildProps = { + overrides?: CheckboxOverrides; + /** Handler for change events on trigger element. */ + onChange?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseenter events on trigger element. */ + onMouseEnter?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseleave events on trigger element. */ + onMouseLeave?: (e: React.ChangeEvent) => unknown; + /** Handler for mousedown events on trigger element. */ + onMouseDown?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseup events on trigger element. */ + onMouseUp?: (e: React.ChangeEvent) => unknown; + /** Handler for focus events on trigger element. */ + onFocus?: (e: React.ChangeEvent) => unknown; + /** Handler for blur events on trigger element. */ + onBlur?: (e: React.ChangeEvent) => unknown; + /** Handler for keydown events on trigger element. */ + onKeyDown?: (e: React.KeyboardEvent) => unknown; + /** Handler for keyup events on trigger element. */ + onKeyUp?: (e: React.KeyboardEvent) => unknown; + /** Focus the checkbox on initial render. */ + autoFocus?: boolean; +} & CheckboxReducerState; + +export type StatefulContainerProps = { + overrides?: CheckboxOverrides; + /** Component or String value for label of checkbox. */ + children?: (a: StatefulContainerChildProps) => React.ReactNode; + /** Defines the components initial state value */ + initialState?: CheckboxReducerState; + /** A state change handler. Used to override default state transitions. */ + stateReducer?: StateReducer; + /** Handler for change events on trigger element. */ + onChange?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseenter events on trigger element. */ + onMouseEnter?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseleave events on trigger element. */ + onMouseLeave?: (e: React.ChangeEvent) => unknown; + /** Handler for mousedown events on trigger element. */ + onMouseDown?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseup events on trigger element. */ + onMouseUp?: (e: React.ChangeEvent) => unknown; + /** Handler for focus events on trigger element. */ + onFocus?: (e: React.ChangeEvent) => unknown; + /** Handler for blur events on trigger element. */ + onBlur?: (e: React.ChangeEvent) => unknown; + /** Handler for keydown events on trigger element. */ + onKeyDown?: (e: React.KeyboardEvent) => unknown; + /** Handler for keyup events on trigger element. */ + onKeyUp?: (e: React.KeyboardEvent) => unknown; + /** Focus the checkbox on initial render. */ + autoFocus?: boolean; +}; + +export type StatefulCheckboxProps = Omit & + Omit & + Partial; + +export type SharedStyleProps = { + $isFocused: boolean; + $isFocusVisible: boolean; + $isHovered: boolean; + $isActive: boolean; + $error: boolean; + $checked: boolean; + $isIndeterminate: boolean; + $required: boolean; + $disabled: boolean; + $value: string; + $labelPlacement?: LabelPlacement; +}; diff --git a/src/switch/__tests__/stateful-switch.browser.test.tsx b/src/switch/__tests__/stateful-switch.browser.test.tsx new file mode 100644 index 0000000000..ab3f32f99a --- /dev/null +++ b/src/switch/__tests__/stateful-switch.browser.test.tsx @@ -0,0 +1,84 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { StatefulSwitch } from '..'; + +describe('Switch', function () { + it('calls provided event handlers', async () => { + const user = userEvent.setup(); + + const onMouseEnter = jest.fn(); + const onMouseLeave = jest.fn(); + const onMouseUp = jest.fn(); + const onMouseDown = jest.fn(); + const onFocus = jest.fn(); + const onBlur = jest.fn(); + + const { getByRole } = render( + + label + + ); + + const input = getByRole('switch'); + + // Hover triggers: mouseEnter + await user.hover(input); + expect(onMouseEnter).toHaveBeenCalledTimes(1); + + // Unhover triggers: mouseLeave + await user.unhover(input); + expect(onMouseLeave).toHaveBeenCalledTimes(1); + + // Mouse down / up + await user.pointer({ target: input, keys: '[MouseLeft]' }); + expect(onMouseDown).toHaveBeenCalledTimes(1); + expect(onMouseUp).toHaveBeenCalledTimes(1); + + // Focus (via tab) + await user.tab(); + expect(onFocus).toHaveBeenCalledTimes(1); + + // Blur + await user.tab(); // move focus away + expect(onBlur).toHaveBeenCalledTimes(1); + }); + + it('updates checked state on change', async () => { + const user = userEvent.setup(); + const { getByRole } = render(label); + const input = getByRole('switch'); + + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + }); + + it('updates checked state on change(keyboard)', async () => { + const user = userEvent.setup(); + const { getByRole } = render(label); + const input = getByRole('switch'); + + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + await user.keyboard(' '); + expect(input).not.toBeChecked(); + await user.keyboard('{Enter}'); + expect(input).toBeChecked(); + }); +}); diff --git a/src/switch/__tests__/switch-auto-focus.scenario.tsx b/src/switch/__tests__/switch-auto-focus.scenario.tsx new file mode 100644 index 0000000000..1883f2c9af --- /dev/null +++ b/src/switch/__tests__/switch-auto-focus.scenario.tsx @@ -0,0 +1,24 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; + +import { StatefulSwitch } from '../'; + +export function Scenario() { + return ( +
+ click me(auto focus) +
+ ); +} diff --git a/src/switch/__tests__/switch-disabled.scenario.tsx b/src/switch/__tests__/switch-disabled.scenario.tsx new file mode 100644 index 0000000000..09fb280b32 --- /dev/null +++ b/src/switch/__tests__/switch-disabled.scenario.tsx @@ -0,0 +1,46 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +import * as React from 'react'; +import { Switch } from '../'; + +export function Scenario() { + const [switches, setSwitches] = React.useState([false, false]); + + return ( +
+ ) => { + const nextSwitches = [...switches]; + nextSwitches[0] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + disabled + > + Disabled Switch + + ) => { + const nextSwitches = [...switches]; + nextSwitches[1] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + > + Enabled Switch + +
+ ); +} diff --git a/src/switch/__tests__/switch-placement.scenario.tsx b/src/switch/__tests__/switch-placement.scenario.tsx new file mode 100644 index 0000000000..bf3960feaf --- /dev/null +++ b/src/switch/__tests__/switch-placement.scenario.tsx @@ -0,0 +1,47 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +import * as React from 'react'; +import { Switch, LABEL_PLACEMENT } from '../'; + +export function Scenario() { + const [switches, setSwitches] = React.useState([false, false]); + + return ( +
+ ) => { + const nextSwitches = [...switches]; + nextSwitches[0] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + labelPlacement={LABEL_PLACEMENT.left} + > + Label on the left + + ) => { + const nextSwitches = [...switches]; + nextSwitches[1] = e.currentTarget.checked; + setSwitches(nextSwitches); + }} + labelPlacement={LABEL_PLACEMENT.right} + > + Label on the right + +
+ ); +} diff --git a/src/switch/__tests__/switch-sizes.scenario.tsx b/src/switch/__tests__/switch-sizes.scenario.tsx new file mode 100644 index 0000000000..6c1fb28fe9 --- /dev/null +++ b/src/switch/__tests__/switch-sizes.scenario.tsx @@ -0,0 +1,37 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { StatefulSwitch, SIZE } from '..'; + +export function Scenario() { + return ( +
+ {Object.keys(SIZE).map((size) => ( + + {size} + + ))} + {Object.keys(SIZE).map((size) => ( + + {size} + + ))} + {Object.keys(SIZE).map((size) => ( + + {size} + + ))} +
+ ); +} diff --git a/src/switch/__tests__/switch-states.scenario.tsx b/src/switch/__tests__/switch-states.scenario.tsx new file mode 100644 index 0000000000..018e23782c --- /dev/null +++ b/src/switch/__tests__/switch-states.scenario.tsx @@ -0,0 +1,33 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { Switch } from '..'; + +export function Scenario() { + return ( +
+ Switch + Switch checked + + Switch checked(show checkmark icon) + + + Switch disabled + + Switch disabled checked + +
+ ); +} diff --git a/src/switch/__tests__/switch-unlabeled.scenario.tsx b/src/switch/__tests__/switch-unlabeled.scenario.tsx new file mode 100644 index 0000000000..b15ecce735 --- /dev/null +++ b/src/switch/__tests__/switch-unlabeled.scenario.tsx @@ -0,0 +1,25 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; + +import { StatefulSwitch } from '../'; + +export function Scenario() { + return ( +
+ +
+ ); +} diff --git a/src/switch/__tests__/switch.browser.test.tsx b/src/switch/__tests__/switch.browser.test.tsx new file mode 100644 index 0000000000..6ac70be450 --- /dev/null +++ b/src/switch/__tests__/switch.browser.test.tsx @@ -0,0 +1,73 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { Switch } from '../'; + +/** + * Setting up a typical implementation scenario for Switch + */ +const SwitchForm = () => { + const [switches, setSwitches] = React.useState([false, false]); + + return ( +
+ ) => { + const isChecked = e.currentTarget.checked; + setSwitches((prevSwitches) => { + return prevSwitches.map((val, idx) => (idx === 0 ? isChecked : val)); + }); + }} + > + Label 1 + + + ) => { + const isChecked = e.currentTarget.checked; + setSwitches((prevSwitches) => { + return prevSwitches.map((val, idx) => (idx === 1 ? isChecked : val)); + }); + }} + > + Label 2 + +
+ ); +}; + +describe('Switch', function () { + it('updates checked state on click', async () => { + const user = userEvent.setup(); + render(); + const input = screen.getByLabelText('Label 1'); + + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + }); + + it('updates checked state on change(keyboard)', async () => { + const user = userEvent.setup(); + render(); + const input = screen.getByLabelText('Label 2'); + + expect(input).not.toBeChecked(); + await user.click(input); + expect(input).toBeChecked(); + await user.keyboard(' '); + expect(input).not.toBeChecked(); + await user.keyboard('{Enter}'); + expect(input).toBeChecked(); + }); +}); diff --git a/src/switch/__tests__/switch.scenario.tsx b/src/switch/__tests__/switch.scenario.tsx new file mode 100644 index 0000000000..947aef424f --- /dev/null +++ b/src/switch/__tests__/switch.scenario.tsx @@ -0,0 +1,32 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { useStyletron } from '../..'; +import { StatefulSwitch } from '../'; + +export function Scenario() { + const [css] = useStyletron(); + return ( +
+ click me +
+ + This is a long text. This is a long text. This is a long text. This is a long text. This + is a long text. + +
+
+ ); +} diff --git a/src/switch/__tests__/switch.stories.tsx b/src/switch/__tests__/switch.stories.tsx new file mode 100644 index 0000000000..c7a1ecaf72 --- /dev/null +++ b/src/switch/__tests__/switch.stories.tsx @@ -0,0 +1,29 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import React from 'react'; +import { Scenario as SwitchDisabled } from './switch-disabled.scenario'; +import { Scenario as SwitchPlacement } from './switch-placement.scenario'; +import { Scenario as SwitchStates } from './switch-states.scenario'; +import { Scenario as SwitchUnlabeled } from './switch-unlabeled.scenario'; +import { Scenario as SwitchDefault } from './switch.scenario'; +import { Scenario as SwitchAutoFocus } from './switch-auto-focus.scenario'; +import { Scenario as SwitchSizes } from './switch-sizes.scenario'; + +export const Placement = () => ; +export const States = () => ; +export const Unlabeled = () => ; +export const Switch = () => ; +export const AutoFocus = () => ; +export const Size = () => ; +export const Disabled = () => ; + +export default { + meta: { + runtimeErrorsAllowed: true, + }, +}; diff --git a/src/switch/constants.ts b/src/switch/constants.ts new file mode 100644 index 0000000000..fa80e09231 --- /dev/null +++ b/src/switch/constants.ts @@ -0,0 +1,20 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +export const STATE_TYPE = Object.freeze({ + change: 'CHANGE', +} as const); + +export const LABEL_PLACEMENT = Object.freeze({ + left: 'left', + right: 'right', +} as const); + +export const SIZE = Object.freeze({ + default: 'default', + small: 'small', +} as const); diff --git a/src/switch/index.ts b/src/switch/index.ts new file mode 100644 index 0000000000..f5c4c77f56 --- /dev/null +++ b/src/switch/index.ts @@ -0,0 +1,22 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +export { default as StatefulSwitch } from './stateful-switch'; +export { default as StatefulContainer } from './stateful-switch-container'; +export { default as Switch } from './switch'; +// Styled elements +export { + Root as StyledRoot, + Toggle as StyledToggle, + ToggleTrack as StyledToggleTrack, + Label as StyledLabel, + Input as StyledInput, +} from './styled-components'; + +export { STATE_TYPE, LABEL_PLACEMENT, SIZE } from './constants'; + +export * from './types'; diff --git a/src/switch/stateful-switch-container.ts b/src/switch/stateful-switch-container.ts new file mode 100644 index 0000000000..1a74676b47 --- /dev/null +++ b/src/switch/stateful-switch-container.ts @@ -0,0 +1,131 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { STATE_TYPE } from './constants'; +import type { StatefulContainerProps, StateReducer } from './types'; + +import type { ChangeEvent } from 'react'; + +const defaultStateReducer: StateReducer = (type, nextState, currentState) => ({ + ...currentState, + ...nextState, +}); + +const StatefulSwitchContainer = (props: StatefulContainerProps) => { + const { + initialState = { checked: false }, + stateReducer = defaultStateReducer, + onChange = () => {}, + onMouseEnter = () => {}, + onMouseLeave = () => {}, + onMouseDown = () => {}, + onMouseUp = () => {}, + onFocus = () => {}, + onBlur = () => {}, + onKeyDown = () => {}, + onKeyUp = () => {}, + children = (childProps: {}) => null, + ...restProps + } = props; + const [checked, setChecked] = React.useState(initialState.checked); + + const updateState = React.useCallback( + (type: string, e: ChangeEvent) => { + let nextState = {}; + switch (type) { + case STATE_TYPE.change: + nextState = { checked: e.target.checked }; + break; + } + const newState = stateReducer(type, nextState, { checked }, e); + + setChecked(newState.checked); + }, + [checked, stateReducer] + ); + + const onChangeHandler = React.useCallback( + (e: ChangeEvent) => { + updateState(STATE_TYPE.change, e); + onChange && onChange(e); + }, + [updateState, onChange] + ); + + const onMouseEnterHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseEnter && onMouseEnter(e); + }, + [onMouseEnter] + ); + + const onMouseLeaveHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseLeave && onMouseLeave(e); + }, + [onMouseLeave] + ); + + const onFocusHandler = React.useCallback( + (e: ChangeEvent) => { + onFocus && onFocus(e); + }, + [onFocus] + ); + + const onBlurHandler = React.useCallback( + (e: ChangeEvent) => { + onBlur && onBlur(e); + }, + [onBlur] + ); + + const onMouseDownHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseDown && onMouseDown(e); + }, + [onMouseDown] + ); + + const onMouseUpHandler = React.useCallback( + (e: ChangeEvent) => { + onMouseUp && onMouseUp(e); + }, + [onMouseUp] + ); + + const onKeyDownHandler = React.useCallback( + (event: React.KeyboardEvent) => { + onKeyDown && onKeyDown(event); + }, + [onKeyDown] + ); + + const onKeyUpHandler = React.useCallback( + (event: React.KeyboardEvent) => { + onKeyUp && onKeyUp(event); + }, + [onKeyUp] + ); + + return children({ + ...restProps, + checked, + onChange: onChangeHandler, + onMouseEnter: onMouseEnterHandler, + onMouseLeave: onMouseLeaveHandler, + onMouseDown: onMouseDownHandler, + onMouseUp: onMouseUpHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler, + onKeyDown: onKeyDownHandler, + onKeyUp: onKeyUpHandler, + }); +}; + +export default StatefulSwitchContainer; diff --git a/src/switch/stateful-switch.tsx b/src/switch/stateful-switch.tsx new file mode 100644 index 0000000000..635ba12e0d --- /dev/null +++ b/src/switch/stateful-switch.tsx @@ -0,0 +1,24 @@ + +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import StatefulContainer from './stateful-switch-container'; +import Switch from './switch'; +import type { StatefulContainerChildProps, StatefulSwitchProps } from './types'; +// Styled elements + +const StatefulSwitch = function (props: StatefulSwitchProps) { + return ( + + {(childrenProps: StatefulContainerChildProps) => ( + {props.children} + )} + + ); +}; +StatefulSwitch.displayName = 'StatefulSwitch'; +export default StatefulSwitch; diff --git a/src/switch/styled-components.ts b/src/switch/styled-components.ts new file mode 100644 index 0000000000..3a0d34a6ad --- /dev/null +++ b/src/switch/styled-components.ts @@ -0,0 +1,247 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ + +import { styled, type Theme } from '../styles'; + +import type { SharedStyleProps } from './types'; +import { LABEL_PLACEMENT, SIZE } from './constants'; +import { getFocusOutlineStyle } from '../utils/get-shared-styles'; + +type UtilityProps = SharedStyleProps & { $theme: Theme }; + +function getLabelPadding(props: UtilityProps) { + const { $labelPlacement, $theme, $size } = props; + const { sizing } = $theme; + const { scale0, scale100, scale300 } = sizing; + let paddingDirection; + + switch ($labelPlacement) { + case LABEL_PLACEMENT.left: + paddingDirection = 'Right'; + break; + case LABEL_PLACEMENT.right: + default: + paddingDirection = 'Left'; + break; + } + + if ($theme.direction === 'rtl' && paddingDirection === 'Left') { + paddingDirection = 'Right'; + } else if ($theme.direction === 'rtl' && paddingDirection === 'Right') { + paddingDirection = 'Left'; + } + + return { + [`padding${paddingDirection}`]: scale300, + paddingTop: $size === SIZE.small ? scale0 : scale100, + }; +} + +function getLabelColor(props: UtilityProps) { + const { $disabled, $theme } = props; + const { colors } = $theme; + return $disabled ? colors.contentStateDisabled : colors.contentPrimary; +} + +export const Root = styled<'label', SharedStyleProps>('label', (props) => { + const { $disabled, $theme, $size } = props; + const { sizing } = $theme; + return { + flexDirection: 'row', + display: 'inline-flex', + verticalAlign: 'middle', + alignItems: 'flex-start', + cursor: $disabled ? 'not-allowed' : 'pointer', + userSelect: 'none', + '@media (pointer: coarse)': { + // Increase target size for touch devices to meet the minimum touch target size of 48x48dp + padding: $size === SIZE.small ? sizing.scale500 : sizing.scale300, + }, + }; +}); + +Root.displayName = 'Root'; + +export const Toggle = styled<'div', SharedStyleProps>('div', (props) => { + const { $theme, $checked, $disabled, $size, $isHovered, $showIcon } = props; + const { sizing, colors, direction } = $theme; + let backgroundColor = colors.contentTertiary; + if ($disabled) { + backgroundColor = colors.contentStateDisabled; + } else if ($checked) { + backgroundColor = colors.contentInversePrimary; + } + + let height, width; + switch ($size) { + case SIZE.small: + if ($checked) { + // 16px + width = sizing.scale600; + height = sizing.scale600; + } else { + // 12px + width = sizing.scale500; + height = sizing.scale500; + } + + break; + case SIZE.default: + default: + if ($checked) { + // 24px + width = sizing.scale800; + height = sizing.scale800; + } else { + // 16px + width = sizing.scale600; + height = sizing.scale600; + } + + break; + } + + const translateX = $size === SIZE.small ? sizing.scale600 : sizing.scale700; + const iconSize = $size === SIZE.small ? 12 : 16; + const checkmarkIcon = encodeURIComponent(` + + + + `); + + return { + backgroundColor, + borderTopLeftRadius: '50%', + borderTopRightRadius: '50%', + borderBottomRightRadius: '50%', + borderBottomLeftRadius: '50%', + boxShadow: + $isHovered && !$checked + ? `inset 0 0 0 999px ${colors.hoverOverlayInverseAlpha}` + : $isHovered && $checked + ? `inset 0 0 0 999px ${colors.hoverOverlayAlpha}` + : 'none', + outline: 'none', + height, + width, + transform: $checked + ? `translateX(${direction === 'rtl' ? `-${translateX}` : translateX})` + : undefined, + transition: 'transform 350ms cubic-bezier(0.27, 1.06, 0.18, 1.00)', + backgroundImage: + $showIcon && $checked ? `url('data:image/svg+xml,${checkmarkIcon}');` : undefined, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + }; +}); + +Toggle.displayName = 'Toggle'; + +export const ToggleTrack = styled<'div', SharedStyleProps>('div', (props) => { + const { $size, $theme, $checked, $disabled, $isHovered, $isFocusVisible, $isFocused } = props; + const { sizing, colors } = $theme; + + let height, width; + switch ($size) { + case SIZE.small: + width = sizing.scale1000; //'40px'; + height = sizing.scale800; //'24px'; + break; + case SIZE.default: + default: + width = '52px'; + height = sizing.scale900; //'32px'; + break; + } + + let backgroundColor = colors.backgroundTertiary; + if ($disabled) { + backgroundColor = colors.backgroundStateDisabled; + } else if ($checked) { + backgroundColor = colors.backgroundInversePrimary; + } + + let borderColor = 'transparent'; + let borderStyle = 'solid'; + let borderWidth = sizing.scale0; + let outline = 'none'; + let outlineOffset = '0px'; + if ($disabled) { + borderColor = colors.borderStateDisabled; + } + + if (!$disabled && !$checked) { + borderColor = colors.contentTertiary; + } + + if (!$disabled && $checked) { + borderStyle = 'none'; + borderWidth = '0px'; + } + + if (!$disabled && $isFocusVisible && $isFocused) { + const outlineStyles = getFocusOutlineStyle($theme); + outline = outlineStyles.outline; + outlineOffset = outlineStyles.outlineOffset; + } + + return { + alignItems: 'center', + backgroundColor, + borderTopLeftRadius: '999px', + borderTopRightRadius: '999px', + borderBottomRightRadius: '999px', + borderBottomLeftRadius: '999px', + borderStyle, + borderWidth, + borderColor, + display: 'flex', + flex: '0 0 auto', + height, + width, + outline, + outlineOffset, + paddingTop: sizing.scale100, + paddingBottom: sizing.scale100, + paddingLeft: sizing.scale100, + paddingRight: sizing.scale100, + boxSizing: 'border-box', + boxShadow: + $isHovered && !$checked + ? `inset 0 0 0 999px ${colors.hoverOverlayAlpha}` + : $isHovered && $checked + ? `inset 0 0 0 999px ${colors.hoverOverlayInverseAlpha}` + : 'none', + }; +}); +ToggleTrack.displayName = 'ToggleTrack'; + +export const Label = styled<'div', SharedStyleProps>('div', (props) => { + const { $theme, $size } = props; + const { typography } = $theme; + return { + verticalAlign: 'middle', + ...getLabelPadding(props), + color: getLabelColor(props), + ...($size === SIZE.small ? typography.ParagraphSmall : typography.ParagraphMedium), + }; +}); + +Label.displayName = 'Label'; + +// tricky style for focus event cause display: none doesn't work +export const Input = styled('input', { + opacity: 0, + width: 0, + height: 0, + overflow: 'hidden', + margin: 0, + padding: 0, + position: 'absolute', +}); + +Input.displayName = 'Input'; diff --git a/src/switch/switch.tsx b/src/switch/switch.tsx new file mode 100644 index 0000000000..96134af96c --- /dev/null +++ b/src/switch/switch.tsx @@ -0,0 +1,257 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import * as React from 'react'; +import { getOverride, getOverrideProps } from '../helpers/overrides'; +import type { SwitchProps } from './types'; +import { + Toggle as StyledToggle, + Input as StyledInput, + Label as StyledLabel, + Root as StyledRoot, + ToggleTrack as StyledToggleTrack, +} from './styled-components'; +import { isFocusVisible as isFocusVisibleCheck } from '../utils/focusVisible'; +import { LABEL_PLACEMENT, SIZE } from './constants'; +import type { ChangeEvent } from 'react'; + +const stopPropagation = (e: ChangeEvent) => e.stopPropagation(); + +const Switch = (props: SwitchProps) => { + const { + overrides = {}, + checked = false, + containsInteractiveElement = false, + disabled = false, + autoFocus = false, + showIcon = false, + labelPlacement = LABEL_PLACEMENT.right, + size = SIZE.default, + onChange = () => {}, + onMouseEnter = () => {}, + onMouseLeave = () => {}, + onMouseDown = () => {}, + onMouseUp = () => {}, + onFocus = () => {}, + onBlur = () => {}, + onKeyDown, // don't add fallback no-op to allow native keydown behavior if not customized. + onKeyUp, // don't add fallback no-op to allow native keyup behavior if not customized. + value, + id, + name, + children, + required, + title, + inputRef, + } = props; + const [isFocused, setIsFocused] = React.useState(autoFocus); + const [isFocusVisible, setIsFocusVisible] = React.useState(false); + const [isHovered, setIsHovered] = React.useState(false); + const [isActive, setIsActive] = React.useState(false); + const fallbackInputRef = React.useRef(null); + const internalInputRef = inputRef || fallbackInputRef; + + React.useEffect(() => { + if (autoFocus) { + internalInputRef.current?.focus(); + } + }, [autoFocus, internalInputRef]); + + const onMouseEnterHandler = React.useCallback( + (e: ChangeEvent) => { + setIsHovered(true); + onMouseEnter(e); + }, + [onMouseEnter] + ); + + const onMouseLeaveHandler = React.useCallback( + (e: ChangeEvent) => { + setIsHovered(false); + setIsActive(false); + onMouseLeave(e); + }, + [onMouseLeave] + ); + + const onMouseDownHandler = React.useCallback( + (e: ChangeEvent) => { + setIsActive(true); + onMouseDown(e); + }, + [onMouseDown] + ); + + const onMouseUpHandler = React.useCallback( + (e: ChangeEvent) => { + setIsActive(false); + onMouseUp(e); + }, + [onMouseUp] + ); + + const onFocusHandler = React.useCallback( + (e: ChangeEvent) => { + setIsFocused(true); + onFocus(e); + if (isFocusVisibleCheck(e)) { + setIsFocusVisible(true); + } + }, + [onFocus] + ); + + const onBlurHandler = React.useCallback( + (e: ChangeEvent) => { + setIsFocused(false); + onBlur(e); + if (!isFocusVisibleCheck(e)) { + setIsFocusVisible(false); + } + }, + [onBlur] + ); + + const onKeyUpHandler = React.useCallback( + (event: React.KeyboardEvent) => { + /** + * Handles 'Enter' key press to toggle the switch. + */ + + if (event.key === ' ') { + setIsActive(false); + } + if (event.key === 'Enter') { + setIsActive(false); + onChange?.({ + ...event, + currentTarget: { + ...event.currentTarget, + checked: !checked, + }, + target: { + ...event.target, + checked: !checked, + }, + } as unknown as ChangeEvent); + } + onKeyUp?.(event); + }, + [onKeyUp, onChange, checked] + ); + + const onKeyDownHandler = React.useCallback( + (event: React.KeyboardEvent) => { + /** + * Handles 'Enter' key press to toggle the switch. + */ + if (event.key === 'Enter') { + setIsActive(true); + } + + if (event.key === ' ') { + setIsActive(true); + } + onKeyDown?.(event); + }, + [onKeyDown] + ); + + const { + Root: RootOverride, + Toggle: ToggleOverride, + Label: LabelOverride, + Input: InputOverride, + ToggleTrack: ToggleTrackOverride, + } = overrides; + + const Root = getOverride(RootOverride) || StyledRoot; + const ToggleTrack = getOverride(ToggleTrackOverride) || StyledToggleTrack; + const Toggle = getOverride(ToggleOverride) || StyledToggle; + const Label = getOverride(LabelOverride) || StyledLabel; + const Input = getOverride(InputOverride) || StyledInput; + + const inputEvents = { + onChange, + onFocus: onFocusHandler, + onBlur: onBlurHandler, + onKeyDown: onKeyDownHandler, + onKeyUp: onKeyUpHandler, + }; + const mouseEvents = { + onMouseEnter: onMouseEnterHandler, + onMouseLeave: onMouseLeaveHandler, + onMouseDown: onMouseDownHandler, + onMouseUp: onMouseUpHandler, + }; + const sharedProps = { + $isFocused: isFocused, + $isFocusVisible: isFocusVisible, + $isHovered: isHovered, + $isActive: isActive, + $checked: checked, + $required: required, + $disabled: disabled, + $value: value, + $showIcon: showIcon, + $size: size, + }; + + const labelComponent = children && ( + + ); + + return ( + + {labelPlacement === LABEL_PLACEMENT.left && labelComponent} + + + + + + + {labelPlacement === LABEL_PLACEMENT.right && labelComponent} + + ); +}; + +export default Switch; diff --git a/src/switch/types.ts b/src/switch/types.ts new file mode 100644 index 0000000000..38136262d8 --- /dev/null +++ b/src/switch/types.ts @@ -0,0 +1,166 @@ +/* +Copyright (c) Uber Technologies, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*/ +import type * as React from 'react'; +import type { Override } from '../helpers/overrides'; +import type { SIZE, LABEL_PLACEMENT } from './constants'; + +export type LabelPlacement = keyof typeof LABEL_PLACEMENT; + +export type SwitchOverrides = { + Toggle?: Override; + ToggleTrack?: Override; + Label?: Override; + Root?: Override; + Input?: Override; +}; + +export type SwitchProps = { + /** Id of element which contains a related caption */ + 'aria-describedby'?: string; + /** Passed to the input element aria-label attribute. */ + ariaLabel?: string; + 'aria-label'?: string; + /** Id of element which contains an error message */ + 'aria-errormessage'?: string; + /** Indicates whether the switch is in an error state */ + 'aria-invalid'?: boolean; + /** Component or String value for label of switch. */ + children?: React.ReactNode; + /** Indicates if this switch children contain an interactive element (prevents the label from moving focus from the child element to the radio button) */ + containsInteractiveElement?: boolean; + overrides?: SwitchOverrides; + /** Check or uncheck the control. */ + checked?: boolean; + /** Disable the switch from being changed. */ + disabled?: boolean; + /** Marks the switch as required. */ + required?: boolean; + /** Used to get a ref to the input element. Useful for programmatically focusing the input */ + inputRef?: React.RefObject; + /** Focus the switch on initial render. */ + autoFocus?: boolean; + /** Passed to the input element id attribute */ + id?: string; + /** Passed to the input element name attribute */ + name?: string; + /** Passed to the input element value attribute */ + value?: string; + /** How to position the label relative to the switch itself. */ + labelPlacement?: LabelPlacement; + /** Text to display in native OS tooltip on long hover. */ + title?: string | null; + /** Handler for change events on trigger element. */ + onChange?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseenter events on trigger element. */ + onMouseEnter?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseleave events on trigger element. */ + onMouseLeave?: (e: React.ChangeEvent) => unknown; + /** Handler for mousedown events on trigger element. */ + onMouseDown?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseup events on trigger element. */ + onMouseUp?: (e: React.ChangeEvent) => unknown; + /** handler for focus events on trigger element. */ + onFocus?: (e: React.ChangeEvent) => unknown; + /** handler for blur events on trigger element. */ + onBlur?: (e: React.ChangeEvent) => unknown; + /** handler for keydown events on trigger element. */ + onKeyDown?: (e: React.KeyboardEvent) => unknown; + /** Handler for keyup events on trigger element. */ + onKeyUp?: (e: React.KeyboardEvent) => unknown; + /** size of switch component - both control and label(if existing) */ + size?: keyof typeof SIZE; + /** whether show checkmark icon when switch is on */ + showIcon?: boolean; +}; + +export type SwitchState = { + isFocused: boolean; + isFocusVisible: boolean; + isHovered: boolean; + isActive: boolean; +}; + +export type SwitchReducerState = { + checked?: boolean; +}; + +export type StateReducer = ( + stateType: string, + nextState: SwitchReducerState, + currentState: SwitchReducerState, + event: React.ChangeEvent | React.KeyboardEvent +) => SwitchReducerState; + +export type StatefulContainerChildProps = { + overrides?: SwitchOverrides; + /** Handler for change events on trigger element. */ + onChange?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseenter events on trigger element. */ + onMouseEnter?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseleave events on trigger element. */ + onMouseLeave?: (e: React.ChangeEvent) => unknown; + /** Handler for mousedown events on trigger element. */ + onMouseDown?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseup events on trigger element. */ + onMouseUp?: (e: React.ChangeEvent) => unknown; + /** Handler for focus events on trigger element. */ + onFocus?: (e: React.ChangeEvent) => unknown; + /** Handler for blur events on trigger element. */ + onBlur?: (e: React.ChangeEvent) => unknown; + /** Handler for keydown events on trigger element. */ + onKeyDown?: (e: React.KeyboardEvent) => unknown; + /** Handler for keyup events on trigger element. */ + onKeyUp?: (e: React.KeyboardEvent) => unknown; + /** Focus the switch on initial render. */ + autoFocus?: boolean; +} & SwitchReducerState; + +export type StatefulContainerProps = { + overrides?: SwitchOverrides; + /** Component or String value for label of switch. */ + children?: (a: StatefulContainerChildProps) => React.ReactNode; + /** Defines the components initial state value */ + initialState?: SwitchReducerState; + /** A state change handler. Used to override default state transitions. */ + stateReducer?: StateReducer; + /** Handler for change events on trigger element. */ + onChange?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseenter events on trigger element. */ + onMouseEnter?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseleave events on trigger element. */ + onMouseLeave?: (e: React.ChangeEvent) => unknown; + /** Handler for mousedown events on trigger element. */ + onMouseDown?: (e: React.ChangeEvent) => unknown; + /** Handler for mouseup events on trigger element. */ + onMouseUp?: (e: React.ChangeEvent) => unknown; + /** Handler for focus events on trigger element. */ + onFocus?: (e: React.ChangeEvent) => unknown; + /** Handler for blur events on trigger element. */ + onBlur?: (e: React.ChangeEvent) => unknown; + /** Handler for keydown events on trigger element. */ + onKeyDown?: (e: React.KeyboardEvent) => unknown; + /** Handler for keyup events on trigger element. */ + onKeyUp?: (e: React.KeyboardEvent) => unknown; + /** Focus the switch on initial render. */ + autoFocus?: boolean; +}; + +export type StatefulSwitchProps = Omit & Partial; + +export type SharedStyleProps = { + $isFocused: boolean; + $isFocusVisible: boolean; + $isHovered: boolean; + $isActive: boolean; + $checked: boolean; + $required: boolean; + $disabled: boolean; + $value: string; + $labelPlacement?: LabelPlacement; + $showIcon?: boolean; + $size?: keyof typeof SIZE; +}; diff --git a/src/themes/dark-theme/color-semantic-tokens.ts b/src/themes/dark-theme/color-semantic-tokens.ts index b2bc7749b2..7ab9928792 100644 --- a/src/themes/dark-theme/color-semantic-tokens.ts +++ b/src/themes/dark-theme/color-semantic-tokens.ts @@ -41,6 +41,19 @@ export default (foundation: FoundationColors = defaultFoundationColors): Semanti borderInverseTransparent: hexToRgba(foundation.primaryB, '0.2') || '', borderInverseSelected: foundation.primaryB, + // Brand Default + brandBackgroundPrimary: primitiveDarkColors.brandDefault500Dark, + brandBackgroundSecondary: primitiveDarkColors.brandDefault100Dark, + brandBackgroundTertiary: primitiveDarkColors.white, + brandBackgroundDisabled: primitiveDarkColors.brandDefault100Dark, + brandContentPrimary: primitiveDarkColors.brandDefault600Dark, + brandContentOnPrimary: primitiveDarkColors.white, + brandContentOnSecondary: primitiveDarkColors.brandDefault700Dark, + brandContentOnTertiary: primitiveDarkColors.black, + brandContentOnGradient: primitiveDarkColors.white, + brandContentDisabled: primitiveDarkColors.brandDefault400Dark, + brandBorderAccessible: primitiveDarkColors.brandDefault600Dark, + brandBorderSubtle: primitiveDarkColors.brandDefault400Dark, }; const coreExtensions: CoreExtensionSemanticColors = { diff --git a/src/themes/light-theme/color-semantic-tokens.ts b/src/themes/light-theme/color-semantic-tokens.ts index e26c9ed2ae..610fce53f2 100644 --- a/src/themes/light-theme/color-semantic-tokens.ts +++ b/src/themes/light-theme/color-semantic-tokens.ts @@ -49,6 +49,20 @@ export default ( hexToRgba(defaultFoundationColors.primaryB, '0.2') || '', borderInverseSelected: foundation.primaryB, + + // brand default + brandBackgroundPrimary: primitiveLightColors.brandDefault600, + brandBackgroundSecondary: primitiveLightColors.brandDefault50, + brandBackgroundTertiary: primitiveLightColors.white, + brandBackgroundDisabled: primitiveLightColors.brandDefault50, + brandContentPrimary: primitiveLightColors.brandDefault600, + brandContentOnPrimary: primitiveLightColors.white, + brandContentOnSecondary: primitiveLightColors.brandDefault700, + brandContentOnTertiary: primitiveLightColors.black, + brandContentOnGradient: primitiveLightColors.white, + brandContentDisabled: primitiveLightColors.brandDefault300, + brandBorderAccessible: primitiveLightColors.brandDefault600, + brandBorderSubtle: primitiveLightColors.brandDefault100, }; const coreExtensions: CoreExtensionSemanticColors = { diff --git a/src/themes/types.ts b/src/themes/types.ts index d404a7105e..431b14b074 100644 --- a/src/themes/types.ts +++ b/src/themes/types.ts @@ -169,6 +169,19 @@ export type CoreSemanticColors = { borderInverseOpaque: string; borderInverseTransparent: string; borderInverseSelected: string; + // Brand default + brandBackgroundPrimary: string; + brandBackgroundSecondary: string; + brandBackgroundTertiary: string; + brandBackgroundDisabled: string; + brandContentPrimary: string; + brandContentOnPrimary: string; + brandContentOnSecondary: string; + brandContentOnTertiary: string; + brandContentOnGradient: string; + brandContentDisabled: string; + brandBorderAccessible: string; + brandBorderSubtle: string; }; export type CoreExtensionSemanticColors = { // Backgrounds diff --git a/src/tokens/color-primitive-tokens.ts b/src/tokens/color-primitive-tokens.ts index 31587a2395..707abbb0ed 100644 --- a/src/tokens/color-primitive-tokens.ts +++ b/src/tokens/color-primitive-tokens.ts @@ -184,6 +184,18 @@ const primitiveColors: PrimitiveColors = { /* @deprecated use orange color tokens instead */ brown700: '#3D281E', + // Brand colors + brandDefault50: '#EFF4FE', + brandDefault100: '#DEE9FE', + brandDefault200: '#CDDEFF', + brandDefault300: '#A9C9FF', + brandDefault400: '#6DAAFB', + brandDefault500: '#068BEE', + brandDefault600: '#276EF1', + brandDefault700: '#175BCC', + brandDefault800: '#1948A3', + brandDefault900: '#002661', + /***** dark color tokens *****/ gray50Dark: '#161616', gray100Dark: '#292929', @@ -305,6 +317,18 @@ const primitiveColors: PrimitiveColors = { magenta700Dark: '#E099C9', magenta800Dark: '#EEB6DB', magenta900Dark: '#F1D4E7', + + // Brand colors + brandDefault50Dark: '#09152C', + brandDefault100Dark: '#182946', + brandDefault200Dark: '#22375C', + brandDefault300Dark: '#2D4775', + brandDefault400Dark: '#335BA3', + brandDefault500Dark: '#3F6EC5', + brandDefault600Dark: '#5E8BDB', + brandDefault700Dark: '#93B4EE', + brandDefault800Dark: '#B3CCF6', + brandDefault900Dark: '#D1DFF6', }; const primitiveLightColors = {} as PrimitiveLightColors; diff --git a/src/tokens/types.ts b/src/tokens/types.ts index 9523e08afc..14018a8080 100644 --- a/src/tokens/types.ts +++ b/src/tokens/types.ts @@ -168,6 +168,18 @@ export type PrimitiveColors = { /** @deprecated use blue color tokens instead */ cobalt700: string; + // brand color tokens + brandDefault50: string; + brandDefault100: string; + brandDefault200: string; + brandDefault300: string; + brandDefault400: string; + brandDefault500: string; + brandDefault600: string; + brandDefault700: string; + brandDefault800: string; + brandDefault900: string; + // dark color tokens gray50Dark: string; gray100Dark: string; @@ -289,6 +301,17 @@ export type PrimitiveColors = { magenta700Dark: string; magenta800Dark: string; magenta900Dark: string; + + brandDefault50Dark: string; + brandDefault100Dark: string; + brandDefault200Dark: string; + brandDefault300Dark: string; + brandDefault400Dark: string; + brandDefault500Dark: string; + brandDefault600Dark: string; + brandDefault700Dark: string; + brandDefault800Dark: string; + brandDefault900Dark: string; }; export type PrimitiveLightColors = { diff --git a/src/utils/get-shared-styles.ts b/src/utils/get-shared-styles.ts new file mode 100644 index 0000000000..c6cfda8c11 --- /dev/null +++ b/src/utils/get-shared-styles.ts @@ -0,0 +1,39 @@ +import type { Theme } from '../styles'; + +export const getFocusOutlineStyle = (theme: Theme) => { + const { colors, sizing } = theme; + return { + // 2px for the outline and 2px for the outline offset(Accessible 4px focus ring) + outline: `${sizing.scale0} solid ${colors.brandBorderAccessible}`, + outlineOffset: sizing.scale0, + }; +}; + +// Get overlay (for example, backplate background) color based on disabled and error states +// Used in Checkbox, Radio, and Switch components Or any other component that requires an overlay(for example, backplate) for hovering and pressing states +export const getOverlayColor = ({ + $theme, + $disabled, + $error, +}: { + $theme: Theme; + $disabled: boolean; + $error: boolean; +}) => { + const { colors } = $theme; + const hoveredColor = $disabled + ? 'transparent' + : $error + ? colors.hoverNegativeAlpha + : colors.hoverOverlayAlpha; + const pressedColor = $disabled + ? 'transparent' + : $error + ? colors.pressedNegativeAlpha + : colors.pressedOverlayAlpha; + + return { + hoveredColor, + pressedColor, + }; +};