+ );
+}
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 (
+
+ );
+};
+
+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 (
+
+ );
+}
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 (
+
+ );
+}
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 (
+
+ );
+}
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 (
+
+ );
+};
+
+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,
+ };
+};