import { useComposedRefs } from '@tamagui/compose-refs' import { isWeb } from '@tamagui/constants' import type { GestureReponderEvent } from '@tamagui/web' import { composeEventHandlers } from '@tamagui/helpers' import { useLabelContext } from '@tamagui/label' import React, { useMemo } from 'react' import type { PressableProps, View, ViewProps } from 'react-native' import { BubbleInput } from './BubbleInput' import { getState, isIndeterminate } from './utils' export type CheckedState = boolean | 'indeterminate' type CheckboxBaseProps = Omit & Pick export type CheckboxExtraProps = { children?: React.ReactNode id?: string disabled?: boolean checked?: CheckedState defaultChecked?: CheckedState required?: boolean /** * * @param checked Either boolean or "indeterminate" which is meant to allow for a third state that means "neither", usually indicated by a minus sign. */ onCheckedChange?(checked: CheckedState): void labelledBy?: string name?: string value?: string } export type CheckboxProps = CheckboxBaseProps & CheckboxExtraProps export function useCheckbox( props: P, [checked, setChecked]: [ CheckedState, React.Dispatch>, ], ref: React.Ref ) { const { labelledBy: ariaLabelledby, name, required, disabled, value = 'on', onCheckedChange, ...checkboxProps } = props const [button, setButton] = React.useState(null) const composedRefs = useComposedRefs(ref, setButton as any) const hasConsumerStoppedPropagationRef = React.useRef(false) // We set this to true by default so that events bubble to forms without JS (SSR) const isFormControl = isWeb ? (button ? Boolean(button.closest('form')) : true) : false const labelId = useLabelContext(button) const labelledBy = ariaLabelledby || labelId const parentKeyDown = (props as React.HTMLProps).onKeyDown const handleKeyDown = useMemo( () => composeEventHandlers(parentKeyDown, (event) => { // According to WAI ARIA, Checkboxes don't activate on enter keypress if (event.key === 'Enter') event.preventDefault() }), [parentKeyDown] ) const handlePress = useMemo( () => composeEventHandlers(props.onPress as any, (event: GestureReponderEvent) => { setChecked((prevChecked) => (isIndeterminate(prevChecked) ? true : !prevChecked)) if (isFormControl && 'isPropagationStopped' in event) { hasConsumerStoppedPropagationRef.current = event.isPropagationStopped() // if checkbox is in a form, stop propagation from the button so that we only propagate // one click event (from the input). We propagate changes from an input so that native // form validation works and form events reflect checkbox updates. if (!hasConsumerStoppedPropagationRef.current) event.stopPropagation() } }), [isFormControl] ) return { bubbleInput: isWeb && isFormControl ? ( ) : null, checkboxRef: composedRefs, checkboxProps: { role: 'checkbox', 'aria-labelledby': labelledBy, 'aria-checked': isIndeterminate(checked) ? 'mixed' : checked, ...checkboxProps, ...(isWeb && { type: 'button', value, 'data-state': getState(checked), 'data-disabled': disabled ? '' : undefined, disabled: disabled, onKeyDown: disabled ? undefined : handleKeyDown, }), onPress: disabled ? undefined : handlePress, }, } }