|
- import * as React from 'react';
- import { tailwind } from '@tesseract-design/web-base';
- import { useFallbackId } from '@modal-sh/react-utils';
-
- const { tw } = tailwind;
-
- const ToggleSwitchDerivedElementComponent = 'input' as const;
-
- /**
- * Derived HTML element of the {@link ToggleSwitch} component.
- */
- export type ToggleSwitchDerivedElement = HTMLElementTagNameMap[
- typeof ToggleSwitchDerivedElementComponent
- ];
-
- /**
- * Props of the {@link ToggleSwitch} component.
- */
- export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size' | 'children'> {
- /**
- * Should the component occupy the whole width of its parent?
- */
- block?: boolean;
- /**
- * Complementary content of the component.
- */
- subtext?: React.ReactNode;
- /**
- * Is the component in an indeterminate state?
- */
- indeterminate?: boolean;
- /**
- * Label to display when the component is checked.
- */
- checkedLabel?: React.ReactNode;
- /**
- * Label to display when the component is unchecked.
- */
- uncheckedLabel?: React.ReactNode;
- }
-
- export const toggleSwitchPlugin: tailwind.PluginCreator = ({ addComponents }) => {
- addComponents({
- '.toggle-switch': {
- '& + label + label + label > :first-child': {
- appearance: 'none',
- cursor: 'pointer',
- position: 'relative',
- overflow: 'hidden',
- height: '1.5em',
- width: '3em',
- display: 'block',
- 'box-sizing': 'border-box',
- 'border-radius': '9999px',
- color: 'rgb(var(--color-primary))',
- },
-
- '&:checked + label + label + label > :first-child': {
- color: 'rgb(var(--color-tertiary))',
- },
-
- '&:focus + label + label + label > :first-child': {
- color: 'rgb(var(--color-secondary))',
- },
-
- '& + label:active + label + label > :first-child': {
- color: 'rgb(var(--color-tertiary))',
- },
-
- '& + label + label:active + label > :first-child': {
- color: 'rgb(var(--color-tertiary))',
- },
-
- '& + label + label + label:active > :first-child': {
- color: 'rgb(var(--color-tertiary))',
- },
-
- '& + label + label + label > :first-child > :first-child': {
- width: '100%',
- height: '100%',
- 'background-color': 'rgb(var(--color-primary) / 50%)',
- 'border-radius': '9999px',
- display: 'block',
- 'box-sizing': 'border-box',
- 'background-clip': 'content-box',
- padding: '0.25em',
- appearance: 'none',
- },
-
- '&:checked + label + label + label > :first-child > :first-child': {
- 'background-color': 'rgb(var(--color-tertiary) / 50%)',
- },
-
- '&:focus + label + label + label > :first-child > :first-child': {
- 'background-color': 'rgb(var(--color-secondary) / 50%)',
- },
-
- '& + label:active + label + label > :first-child > :first-child': {
- 'background-color': 'rgb(var(--color-tertiary) / 50%)',
- },
-
- '& + label + label:active + label > :first-child > :first-child': {
- 'background-color': 'rgb(var(--color-tertiary) / 50%)',
- },
-
- '& + label + label + label:active > :first-child > :first-child': {
- 'background-color': 'rgb(var(--color-tertiary) / 50%)',
- },
-
- '& + label + label + label > :first-child > :first-child > :first-child': {
- appearance: 'none',
- 'border-radius': '9999px',
- display: 'block',
- width: '100%',
- height: '100%',
- margin: '-0.25em',
- 'box-sizing': 'border-box',
- 'background-clip': 'content-box',
- },
-
- '& + label + label + label > :first-child > :first-child > :first-child > :first-child': {
- width: '1.5em',
- height: '1.5em',
- margin: '-0.25em 0 0 0',
- display: 'block',
- 'border-radius': '9999px',
- 'background-color': 'currentColor',
- appearance: 'none',
- 'aspect-ratio': '1 / 1',
- 'z-index': '1',
- position: 'relative',
- 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-primary) / 50%)',
- },
-
- '&:checked + label + label + label > :first-child > :first-child > :first-child > :first-child': {
- 'margin-left': 'calc(100% - 1em)',
- 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
- },
-
- '&:indeterminate + label + label + label > :first-child > :first-child > :first-child > :first-child': {
- 'margin-left': 'calc(50% - 0.5em)',
- },
-
- '&:focus + label + label + label > :first-child > :first-child > :first-child > :first-child': {
- 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-secondary) / 50%)',
- },
-
- '& + label:active + label + label > :first-child > :first-child > :first-child > :first-child': {
- 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
- },
-
- '& + label + label:active + label > :first-child > :first-child > :first-child > :first-child': {
- 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
- },
-
- '& + label + label + label:active > :first-child > :first-child > :first-child > :first-child': {
- 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
- },
- },
- });
- };
-
- /**
- * Component for toggling a Boolean value in an appearance of a toggle switch.
- */
- export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>((
- {
- uncheckedLabel,
- checkedLabel,
- block = false,
- id: idProp,
- className,
- subtext,
- style,
- indeterminate = false,
- ...etcProps
- },
- forwardedRef,
- ) => {
- const defaultRef = React.useRef<ToggleSwitchDerivedElement>(null);
- const ref = forwardedRef ?? defaultRef;
- const id = useFallbackId(idProp);
-
- React.useEffect(() => {
- if (typeof ref === 'function') {
- const defaultElement = defaultRef.current as ToggleSwitchDerivedElement;
- defaultElement.indeterminate = indeterminate;
- ref(defaultElement);
- return;
- }
- const element = ref.current as ToggleSwitchDerivedElement;
- element.indeterminate = indeterminate;
- }, [indeterminate, defaultRef, ref]);
-
- return (
- <div
- className={tw(
- 'gap-x-4 flex-wrap',
- block && 'flex',
- !block && 'inline-flex align-center',
- className,
- )}
- style={style}
- data-testid="base"
- >
- <ToggleSwitchDerivedElementComponent
- {...etcProps}
- ref={typeof ref === 'function' ? defaultRef : ref}
- type="checkbox"
- id={id}
- className="sr-only peer/radio toggle-switch"
- />
- <label
- htmlFor={id}
- className="peer/children order-3 cursor-pointer peer-disabled/radio:cursor-not-allowed"
- >
- <span
- data-testid="children"
- >
- {checkedLabel}
- </span>
- </label>
- <label
- htmlFor={id}
- className="peer/children order-1 cursor-pointer peer-disabled/radio:cursor-not-allowed"
- >
- {uncheckedLabel && (
- <>
- <span className="sr-only">
- {' / '}
- </span>
- <span
- data-testid="uncheckedLabel"
- >
- {uncheckedLabel}
- </span>
- </>
- )}
- </label>
- <label
- htmlFor={id}
- className={tw(
- 'order-2 block rounded-full ring-secondary/50 overflow-hidden gap-4 leading-none select-none cursor-pointer',
- 'peer-focus/radio:outline-0 peer-focus/radio:ring-4 peer-focus/radio:ring-secondary/50',
- 'active:ring-tertiary/50 active:ring-4',
- 'peer-active/children:ring-tertiary/50 peer-active/children:ring-4 peer-active/children:text-tertiary',
- 'peer-disabled/radio:opacity-50 peer-disabled/radio:cursor-not-allowed peer-disabled/radio:ring-0',
- 'text-primary peer-disabled/radio:text-primary peer-focus/radio:text-secondary peer-checked/radio:text-tertiary active:text-tertiary',
- !uncheckedLabel && '-ml-4',
- )}
- >
- <span>
- <span>
- <span>
- <span />
- </span>
- </span>
- </span>
- </label>
- {subtext && (
- <div
- className={tw(
- 'block w-full text-xs order-4',
- !uncheckedLabel && 'pl-16',
- uncheckedLabel && 'pt-2',
- )}
- data-testid="subtext"
- >
- {subtext}
- </div>
- )}
- </div>
- );
- });
-
- ToggleSwitch.displayName = 'ToggleSwitch';
-
- ToggleSwitch.defaultProps = {
- block: false,
- subtext: undefined,
- indeterminate: false,
- checkedLabel: undefined,
- uncheckedLabel: undefined,
- };
|