import * as stylex from "@stylexjs/stylex"; import { type ChangeEvent, memo, useCallback, useState } from "react"; import { sliderConfig, switchConfig } from "./ToggleSliderInput.vars.stylex"; import { interaction } from "./mixins"; import { controlColor } from "./theme.stylex"; import { color, duration, timingFunction } from "./tokens.stylex"; export interface ToggleSliderInputProps { name?: string; disabled?: boolean; title?: string; /** `value` and `onChange` are optional because we want to be able to use the control in an uncontrolled manner as well (for example, in forms) */ value?: boolean; onChange?: (value: boolean) => void; defaultChecked?: boolean; } // Box around the slider // Loosely based on: // https://www.w3schools.com/howto/howto_css_switch.asp // https://kittygiraudel.com/2021/04/05/an-accessible-toggle/#focused-styles const switchStyle = stylex.create({ base: { position: "relative", display: "inline-block", width: switchConfig.width, height: switchConfig.height, }, }); const sliderStyle = stylex.create({ base: { position: "absolute", inset: 0, borderRadius: switchConfig.width, transition: `background-color ${duration.default} ${timingFunction.fast}`, "::before": { position: "absolute", content: "''", height: sliderConfig.size, width: sliderConfig.size, left: sliderConfig.offset, bottom: sliderConfig.offset, borderRadius: "50%", transition: ` transform ${duration.default} ${timingFunction.fast}, background-color ${duration.default} ${timingFunction.fast} `, }, }, off: { backgroundColor: color.gray400, "::before": { backgroundColor: color.gray000, }, }, on: { backgroundColor: controlColor.toggleCheckedBackground, "::before": { transform: `translateX(${sliderConfig.translationOffset})`, backgroundColor: controlColor.toggleCheckedPillBackground, }, }, disabledOff: { backgroundColor: color.gray400, "::before": { backgroundColor: color.gray100, }, }, disabledOn: { backgroundColor: controlColor.toggleCheckedDisabledBackground, "::before": { transform: `translateX(${sliderConfig.translationOffset})`, backgroundColor: controlColor.toggleCheckedDisabledPillBackground, }, }, }); /** @remarks Exported for use in Drawer */ export default memo(function ToggleSliderInput(props: ToggleSliderInputProps) { return props.value !== undefined && props.onChange !== undefined ? ( ) : ( ); }); interface FakeUncontrolledToggleSliderInputProps extends ToggleSliderInputProps { value?: undefined; onChange?: undefined; } const FakeUncontrolledToggleSliderInput = memo( function FakeUncontrolledToggleSliderInput( props: FakeUncontrolledToggleSliderInputProps, ) { const [value, setValue] = useState(props.defaultChecked || false); return ( ); }, ); interface InternalStateUncontrolledToggleSliderInputProps extends ToggleSliderInputProps { value: boolean; onChange: (value: boolean) => void; } // We need to split this up into a fake uncontrolled component because StyleX does not support styling using the input's `checked` state. // See: https://github.com/facebook/stylex/issues/536 const InternalStateUncontrolledToggleSliderInput = memo( function FakeUncontrolledToggleSliderInput({ value, onChange, defaultChecked, disabled, name, title, }: InternalStateUncontrolledToggleSliderInputProps) { const onChangeEffective = useCallback( (e: ChangeEvent) => { onChange(e.target.checked); }, [onChange], ); return ( ); }, );