8c3499cc8d
* add `useDefaultValue` hook This allows us to have a guaranteed `default value` that never changes unless the component re-mounts. Since the hook returns a stable value, we can safely include it in dependency arrays of certain hooks. Before this change, including this is in the dependency arrays it would cause a trigger or change of the hook when the `defaultValue` changes but we never want that. * do not handle `reset` when no `defaultValue` or `defaultChecked` was provided If a `defaultValue` is provided, then the reset will be handled and the `onChange` will be called with this value. If no `defaultValue` was provided, we won't handle the `reset`, otherwise we would call the `onChange` with `undefined` which is incorrect. * update changelog
210 lines
5.6 KiB
TypeScript
210 lines
5.6 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 { useDefaultValue } from '../../hooks/use-default-value'
|
|
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: _defaultChecked,
|
|
onChange: controlledOnChange,
|
|
name,
|
|
value,
|
|
form,
|
|
indeterminate = false,
|
|
...theirProps
|
|
} = props
|
|
|
|
let defaultChecked = useDefaultValue(_defaultChecked)
|
|
let [checked, onChange] = useControllable(
|
|
controlledChecked,
|
|
controlledOnChange,
|
|
defaultChecked ?? false
|
|
)
|
|
|
|
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(() => {
|
|
if (defaultChecked === undefined) return
|
|
return onChange?.(defaultChecked)
|
|
}, [onChange, defaultChecked])
|
|
|
|
return (
|
|
<>
|
|
{name != null && (
|
|
<FormFields
|
|
disabled={disabled}
|
|
data={{ [name]: value || 'on' }}
|
|
overrides={{ type: 'checkbox', checked }}
|
|
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
|