import React, { Fragment, createContext, useContext, useMemo, useRef, useState, // Types ElementType, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, Ref, useEffect, } from 'react' import { Props } from '../../types' import { forwardRefWithAs, render, compact, HasDisplayName, RefProp } from '../../utils/render' import { useId } from '../../hooks/use-id' import { Keys } from '../keyboard' import { isDisabledReactIssue7711 } from '../../utils/bugs' import { _internal_ComponentLabel, Label, useLabels } from '../label/label' import { _internal_ComponentDescription, Description, useDescriptions, } from '../description/description' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' import { useSyncRefs } from '../../hooks/use-sync-refs' import { Hidden, Features as HiddenFeatures } from '../../internal/hidden' import { attemptSubmit } from '../../utils/form' import { useEvent } from '../../hooks/use-event' import { useControllable } from '../../hooks/use-controllable' import { useDisposables } from '../../hooks/use-disposables' interface StateDefinition { switch: HTMLButtonElement | null setSwitch(element: HTMLButtonElement): void labelledby: string | undefined describedby: string | undefined } let GroupContext = createContext(null) GroupContext.displayName = 'GroupContext' // --- let DEFAULT_GROUP_TAG = Fragment export type SwitchGroupProps = Props function GroupFn( props: SwitchGroupProps ) { let [switchElement, setSwitchElement] = useState(null) let [labelledby, LabelProvider] = useLabels() let [describedby, DescriptionProvider] = useDescriptions() let context = useMemo( () => ({ switch: switchElement, setSwitch: setSwitchElement, labelledby, describedby }), [switchElement, setSwitchElement, labelledby, describedby] ) let ourProps = {} let theirProps = props return ( ) { if (!switchElement) return if (event.currentTarget.tagName === 'LABEL') { event.preventDefault() } switchElement.click() switchElement.focus({ preventScroll: true }) }, }} > {render({ ourProps, theirProps, defaultTag: DEFAULT_GROUP_TAG, name: 'Switch.Group', })} ) } // --- let DEFAULT_SWITCH_TAG = 'button' as const interface SwitchRenderPropArg { checked: boolean } type SwitchPropsWeControl = | 'aria-checked' | 'aria-describedby' | 'aria-labelledby' | 'role' | 'tabIndex' export type SwitchProps = Props< TTag, SwitchRenderPropArg, SwitchPropsWeControl, { checked?: boolean defaultChecked?: boolean onChange?(checked: boolean): void name?: string value?: string form?: string } > function SwitchFn( props: SwitchProps, ref: Ref ) { let internalId = useId() let { id = `headlessui-switch-${internalId}`, checked: controlledChecked, defaultChecked = false, onChange: controlledOnChange, name, value, form, ...theirProps } = props let groupContext = useContext(GroupContext) let internalSwitchRef = useRef(null) let switchRef = useSyncRefs( internalSwitchRef, ref, groupContext === null ? null : groupContext.setSwitch ) let [checked, onChange] = useControllable(controlledChecked, controlledOnChange, defaultChecked) let toggle = useEvent(() => onChange?.(!checked)) let handleClick = useEvent((event: ReactMouseEvent) => { if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() event.preventDefault() toggle() }) let handleKeyUp = useEvent((event: ReactKeyboardEvent) => { if (event.key === Keys.Space) { event.preventDefault() toggle() } else if (event.key === Keys.Enter) { attemptSubmit(event.currentTarget) } }) // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. let handleKeyPress = useEvent((event: ReactKeyboardEvent) => event.preventDefault()) let slot = useMemo(() => ({ checked }), [checked]) let ourProps = { id, ref: switchRef, role: 'switch', type: useResolveButtonType(props, internalSwitchRef), tabIndex: 0, 'aria-checked': checked, 'aria-labelledby': groupContext?.labelledby, 'aria-describedby': groupContext?.describedby, onClick: handleClick, onKeyUp: handleKeyUp, onKeyPress: handleKeyPress, } let d = useDisposables() useEffect(() => { let form = internalSwitchRef.current?.closest('form') if (!form) return if (defaultChecked === undefined) return d.addEventListener(form, 'reset', () => { onChange(defaultChecked) }) }, [internalSwitchRef, onChange /* Explicitly ignoring `defaultValue` */]) return ( <> {name != null && checked && ( )} {render({ ourProps, theirProps, slot, defaultTag: DEFAULT_SWITCH_TAG, name: 'Switch' })} ) } // --- export interface _internal_ComponentSwitch extends HasDisplayName { ( props: SwitchProps & RefProp ): JSX.Element } export interface _internal_ComponentSwitchGroup extends HasDisplayName { ( props: SwitchGroupProps & RefProp ): JSX.Element } export interface _internal_ComponentSwitchLabel extends _internal_ComponentLabel {} export interface _internal_ComponentSwitchDescription extends _internal_ComponentDescription {} let SwitchRoot = forwardRefWithAs(SwitchFn) as unknown as _internal_ComponentSwitch let Group = GroupFn as unknown as _internal_ComponentSwitchGroup export let Switch = Object.assign(SwitchRoot, { Group, Label: Label as _internal_ComponentSwitchLabel, Description: Description as _internal_ComponentSwitchDescription, })