Files
headlessui/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
T
2024-04-11 11:26:19 +02:00

202 lines
5.3 KiB
TypeScript

'use client'
import { useFocusRing } from '@react-aria/focus'
import { useHover } from '@react-aria/interactions'
import React, {
useCallback,
useMemo,
useState,
type ElementType,
type KeyboardEvent as ReactKeyboardEvent,
type MouseEvent as ReactMouseEvent,
type Ref,
} from 'react'
import { useActivePress } from '../../hooks/use-active-press'
import { useControllable } from '../../hooks/use-controllable'
import { useDisposables } from '../../hooks/use-disposables'
import { useEvent } from '../../hooks/use-event'
import { useId } from '../../hooks/use-id'
import { useDisabled } from '../../internal/disabled'
import { FormFields } from '../../internal/form-fields'
import { useProvidedId } from '../../internal/id'
import type { Props } from '../../types'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
import { attemptSubmit } from '../../utils/form'
import {
forwardRefWithAs,
mergeProps,
render,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
import { useDescribedBy } from '../description/description'
import { Keys } from '../keyboard'
import { useLabelledBy } from '../label/label'
let DEFAULT_CHECKBOX_TAG = 'span' as const
type CheckboxRenderPropArg = {
checked: boolean
changing: boolean
focus: boolean
active: boolean
hover: boolean
autofocus: boolean
disabled: boolean
indeterminate: boolean
}
type CheckboxPropsWeControl =
| 'aria-checked'
| 'aria-describedby'
| 'aria-disabled'
| 'aria-labelledby'
| 'role'
| 'tabIndex'
export type CheckboxProps<
TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG,
TType = string,
> = Props<
TTag,
CheckboxRenderPropArg,
CheckboxPropsWeControl,
{
value?: TType
disabled?: boolean
indeterminate?: boolean
checked?: boolean
defaultChecked?: boolean
autoFocus?: boolean
form?: string
name?: string
onChange?: (checked: boolean) => void
}
>
function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TType = any>(
props: CheckboxProps<TTag, TType>,
ref: Ref<HTMLElement>
) {
let internalId = useId()
let providedId = useProvidedId()
let providedDisabled = useDisabled()
let {
id = providedId || `headlessui-checkbox-${internalId}`,
disabled = providedDisabled || false,
autoFocus = false,
checked: controlledChecked,
defaultChecked = false,
onChange: controlledOnChange,
name,
value,
form,
indeterminate = false,
...theirProps
} = props
let [checked, onChange] = useControllable(controlledChecked, controlledOnChange, defaultChecked)
let labelledBy = useLabelledBy()
let describedBy = useDescribedBy()
let d = useDisposables()
let [changing, setChanging] = useState(false)
let toggle = useEvent(() => {
setChanging(true)
onChange?.(!checked)
d.nextFrame(() => {
setChanging(false)
})
})
let handleClick = useEvent((event: ReactMouseEvent) => {
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
event.preventDefault()
toggle()
})
let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
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<HTMLElement>) => event.preventDefault())
let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
let { pressed: active, pressProps } = useActivePress({ disabled })
let ourProps = mergeProps(
{
ref,
id,
role: 'checkbox',
'aria-checked': indeterminate ? 'mixed' : checked ? 'true' : 'false',
'aria-labelledby': labelledBy,
'aria-describedby': describedBy,
'aria-disabled': disabled ? true : undefined,
indeterminate: indeterminate ? 'true' : undefined,
tabIndex: 0,
onKeyUp: disabled ? undefined : handleKeyUp,
onKeyPress: disabled ? undefined : handleKeyPress,
onClick: disabled ? undefined : handleClick,
},
focusProps,
hoverProps,
pressProps
)
let slot = useMemo(() => {
return {
checked,
disabled,
hover,
focus,
active,
indeterminate,
changing,
autofocus: autoFocus,
} satisfies CheckboxRenderPropArg
}, [checked, indeterminate, disabled, hover, focus, active, changing, autoFocus])
let reset = useCallback(() => {
return onChange?.(defaultChecked)
}, [onChange /* Explicitly ignoring `defaultChecked` */])
return (
<>
{name != null && (
<FormFields
disabled={disabled}
data={checked ? { [name]: value || 'on' } : {}}
form={form}
onReset={reset}
/>
)}
{render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_CHECKBOX_TAG,
name: 'Checkbox',
})}
</>
)
}
// ---
export interface _internal_ComponentCheckbox extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TType = string>(
props: CheckboxProps<TTag, TType> & RefProp<typeof CheckboxFn>
): JSX.Element
}
export let Checkbox = forwardRefWithAs(CheckboxFn) as _internal_ComponentCheckbox