diff --git a/package-lock.json b/package-lock.json index 2745db8226..3f3fcc082e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10724,7 +10724,7 @@ }, "packages/@headlessui-vue": { "name": "@headlessui/vue", - "version": "1.7.16", + "version": "1.7.22", "license": "MIT", "dependencies": { "@tanstack/vue-virtual": "3.0.0-beta.60" diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index eaed7a3a4a..a6c2822372 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Nothing yet! +## [2.0.4] - 2024-05-25 + +### Fixed + +- [internal] Don’t set a focus fallback for Dialog’s in demo mode ([#3194](https://github.com/tailwindlabs/headlessui/pull/3194)) +- Ensure page doesn't scroll down when pressing `Escape` to close the `Dialog` component ([#3218](https://github.com/tailwindlabs/headlessui/pull/3218)) +- Fix crash when toggling between `virtual` and non-virtual mode in `Combobox` component ([#3236](https://github.com/tailwindlabs/headlessui/pull/3236)) +- Ensure tabbing to a portalled `` component moves focus inside (without using ``) ([#3239](https://github.com/tailwindlabs/headlessui/pull/3239)) +- Only handle form reset when `defaultValue` is used ([#3240](https://github.com/tailwindlabs/headlessui/pull/3240)) + +### Deprecated + +- Mark `SwitchGroup` as deprecated, prefer `Field` instead ([#3232](https://github.com/tailwindlabs/headlessui/pull/3232)) + +### Changed + +- Use native `fieldset` instead of `div` by default for `
` component ([#3237](https://github.com/tailwindlabs/headlessui/pull/3237)) + ## [2.0.3] - 2024-05-07 ### Fixed @@ -68,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Require React 18 ([#2887](https://github.com/tailwindlabs/headlessui/pull/2887), [#3092](https://github.com/tailwindlabs/headlessui/pull/3092), [#3131](https://github.com/tailwindlabs/headlessui/pull/3131)) - Always render hidden form input fields for `Checkbox`, `Switch`, and `RadioGroup` components ([#3095](https://github.com/tailwindlabs/headlessui/pull/3095)) - Deprecate the `RadioGroup.Option` component in favor of new `Radio` component ([#2887](https://github.com/tailwindlabs/headlessui/pull/2887)) +- Deprecate the `active` prop in favor of new `focus` prop ([#2887](https://github.com/tailwindlabs/headlessui/pull/2887)) - Dialog is now focused by default instead of the first focusable element ([#2887](https://github.com/tailwindlabs/headlessui/pull/2887)) - Change default tags for `ListboxOptions`, `ListboxOption`, `ComboboxOptions`, `ComboboxOption`, and `TabGroup` components ([#3109](https://github.com/tailwindlabs/headlessui/pull/3109)) - Change default tag from `div` to `Fragment` on `Transition` components ([#3110](https://github.com/tailwindlabs/headlessui/pull/3110), [#3147](https://github.com/tailwindlabs/headlessui/pull/3147)) @@ -678,7 +697,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Everything! -[unreleased]: https://github.com/tailwindlabs/headlessui/compare/@headlessui/react@v2.0.3...HEAD +[unreleased]: https://github.com/tailwindlabs/headlessui/compare/@headlessui/react@v2.0.4...HEAD +[2.0.4]: https://github.com/tailwindlabs/headlessui/compare/@headlessui/react@v2.0.3...@headlessui/react@v2.0.4 [2.0.3]: https://github.com/tailwindlabs/headlessui/compare/@headlessui/react@v2.0.2...@headlessui/react@v2.0.3 [2.0.2]: https://github.com/tailwindlabs/headlessui/compare/@headlessui/react@v2.0.1...@headlessui/react@v2.0.2 [2.0.1]: https://github.com/tailwindlabs/headlessui/compare/@headlessui/react@v2.0.0...@headlessui/react@v2.0.1 diff --git a/packages/@headlessui-react/package.json b/packages/@headlessui-react/package.json index 0adb575372..82504cbfdc 100644 --- a/packages/@headlessui-react/package.json +++ b/packages/@headlessui-react/package.json @@ -1,6 +1,6 @@ { "name": "@headlessui/react", - "version": "2.0.3", + "version": "2.0.4", "description": "A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.", "main": "dist/index.cjs", "typings": "dist/index.d.ts", diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx index a8d743ac07..2c7e22a288 100644 --- a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx +++ b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx @@ -13,6 +13,7 @@ import React, { } 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' @@ -85,7 +86,7 @@ function CheckboxFn { + if (defaultChecked === undefined) return return onChange?.(defaultChecked) - }, [onChange /* Explicitly ignoring `defaultChecked` */]) + }, [onChange, defaultChecked]) return ( <> diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 9f8756b901..b5828b2c09 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -24,6 +24,7 @@ import React, { import { useActivePress } from '../../hooks/use-active-press' import { useByComparator, type ByComparator } from '../../hooks/use-by-comparator' import { useControllable } from '../../hooks/use-controllable' +import { useDefaultValue } from '../../hooks/use-default-value' import { useDisposables } from '../../hooks/use-disposables' import { useElementSize } from '../../hooks/use-element-size' import { useEvent } from '../../hooks/use-event' @@ -125,7 +126,7 @@ enum ActionTypes { SetActivationTrigger, - UpdateVirtualOptions, + UpdateVirtualConfiguration, } function adjustOrderedState( @@ -180,7 +181,11 @@ type Actions = } | { type: ActionTypes.UnregisterOption; id: string } | { type: ActionTypes.SetActivationTrigger; trigger: ActivationTrigger } - | { type: ActionTypes.UpdateVirtualOptions; options: T[] } + | { + type: ActionTypes.UpdateVirtualConfiguration + options: T[] + disabled: ((value: any) => boolean) | null + } let reducers: { [P in ActionTypes]: ( @@ -236,16 +241,15 @@ let reducers: { } if (state.virtual) { + let { options, disabled } = state.virtual let activeOptionIndex = action.focus === Focus.Specific ? action.idx : calculateActiveIndex(action, { - resolveItems: () => state.virtual!.options, + resolveItems: () => options, resolveActiveIndex: () => - state.activeOptionIndex ?? - state.virtual!.options.findIndex((option) => !state.virtual!.disabled(option)) ?? - null, - resolveDisabled: state.virtual!.disabled, + state.activeOptionIndex ?? options.findIndex((option) => !disabled(option)) ?? null, + resolveDisabled: disabled, resolveId() { throw new Error('Function not implemented.') }, @@ -373,14 +377,21 @@ let reducers: { activationTrigger: action.trigger, } }, - [ActionTypes.UpdateVirtualOptions]: (state, action) => { - if (state.virtual?.options === action.options) { + [ActionTypes.UpdateVirtualConfiguration]: (state, action) => { + if (state.virtual === null) { + return { + ...state, + virtual: { options: action.options, disabled: action.disabled ?? (() => false) }, + } + } + + if (state.virtual.options === action.options && state.virtual.disabled === action.disabled) { return state } let adjustedActiveOptionIndex = state.activeOptionIndex if (state.activeOptionIndex !== null) { - let idx = action.options.indexOf(state.virtual!.options[state.activeOptionIndex]) + let idx = action.options.indexOf(state.virtual.options[state.activeOptionIndex]) if (idx !== -1) { adjustedActiveOptionIndex = idx } else { @@ -391,7 +402,7 @@ let reducers: { return { ...state, activeOptionIndex: adjustedActiveOptionIndex, - virtual: Object.assign({}, state.virtual, { options: action.options }), + virtual: { options: action.options, disabled: action.disabled ?? (() => false) }, } }, } @@ -425,6 +436,7 @@ function VirtualProvider(props: { children: (data: { option: unknown; open: boolean }) => React.ReactElement }) { let data = useData('VirtualProvider') + let { options } = data.virtual! let [paddingStart, paddingEnd] = useMemo(() => { let el = data.optionsRef.current @@ -441,7 +453,7 @@ function VirtualProvider(props: { let virtualizer = useVirtualizer({ scrollPaddingStart: paddingStart, scrollPaddingEnd: paddingEnd, - count: data.virtual!.options.length, + count: options.length, estimateSize() { return 40 }, @@ -454,7 +466,7 @@ function VirtualProvider(props: { let [baseKey, setBaseKey] = useState(0) useIsoMorphicEffect(() => { setBaseKey((v) => v + 1) - }, [data.virtual?.options]) + }, [options]) let items = virtualizer.getVirtualItems() @@ -487,10 +499,7 @@ function VirtualProvider(props: { return } - if ( - data.activeOptionIndex !== null && - data.virtual!.options.length > data.activeOptionIndex - ) { + if (data.activeOptionIndex !== null && options.length > data.activeOptionIndex) { virtualizer.scrollToIndex(data.activeOptionIndex) } } @@ -501,13 +510,13 @@ function VirtualProvider(props: { {React.cloneElement( props.children?.({ - option: data.virtual!.options[item.index], + option: options[item.index], open: data.comboboxState === ComboboxState.Open, }), { key: `${baseKey}-${item.key}`, 'data-index': item.index, - 'aria-setsize': data.virtual!.options.length, + 'aria-setsize': options.length, 'aria-posinset': item.index + 1, style: { position: 'absolute', @@ -627,7 +636,7 @@ function ComboboxFn( controlledValue, controlledOnChange, @@ -710,7 +720,7 @@ function ComboboxFn !(virtual?.disabled?.(option) ?? false) + (option) => !(virtual.disabled?.(option) ?? false) ) if (localActiveOptionIndex !== -1) { @@ -748,8 +758,12 @@ function ComboboxFn { if (!virtual) return - dispatch({ type: ActionTypes.UpdateVirtualOptions, options: virtual.options }) - }, [virtual, virtual?.options]) + dispatch({ + type: ActionTypes.UpdateVirtualConfiguration, + options: virtual.options, + disabled: virtual.disabled ?? null, + }) + }, [virtual, virtual?.options, virtual?.disabled]) useIsoMorphicEffect(() => { state.dataRef.current = data @@ -875,8 +889,9 @@ function ComboboxFn { + if (defaultValue === undefined) return return theirOnChange?.(defaultValue) - }, [theirOnChange /* Explicitly ignoring `defaultValue` */]) + }, [theirOnChange, defaultValue]) return ( ( if (event.key !== Keys.Escape) return event.preventDefault() event.stopPropagation() + + // Ensure that we blur the current activeElement to prevent maintaining + // focus and potentially scrolling the page to the end (because the Dialog + // is rendered in a Portal at the end of the document.body and the browser + // tries to keep the focused element in view) + // + // Typically only happens in Safari. + if ( + document.activeElement && + 'blur' in document.activeElement && + typeof document.activeElement.blur === 'function' + ) { + document.activeElement.blur() + } + close() }) @@ -342,7 +357,7 @@ function DialogFn( id, role, tabIndex: -1, - 'aria-modal': dialogState === DialogStates.Open ? true : undefined, + 'aria-modal': __demoMode ? undefined : dialogState === DialogStates.Open ? true : undefined, 'aria-labelledby': state.titleId, 'aria-describedby': describedby, } @@ -393,7 +408,7 @@ function DialogFn( diff --git a/packages/@headlessui-react/src/components/field/field.test.tsx b/packages/@headlessui-react/src/components/field/field.test.tsx index d1c8b880f6..cd7bf632a4 100644 --- a/packages/@headlessui-react/src/components/field/field.test.tsx +++ b/packages/@headlessui-react/src/components/field/field.test.tsx @@ -55,7 +55,7 @@ describe('Rendering', () => { let fieldset = container.firstChild let field = fieldset?.firstChild - expect(fieldset).toHaveAttribute('aria-disabled', 'true') + expect(fieldset).toHaveAttribute('disabled') expect(field).toHaveAttribute('aria-disabled', 'true') }) }) diff --git a/packages/@headlessui-react/src/components/fieldset/fieldset.test.tsx b/packages/@headlessui-react/src/components/fieldset/fieldset.test.tsx index b7501fd477..7fbd5715cb 100644 --- a/packages/@headlessui-react/src/components/fieldset/fieldset.test.tsx +++ b/packages/@headlessui-react/src/components/fieldset/fieldset.test.tsx @@ -22,10 +22,24 @@ describe('Rendering', () => { let fieldset = container.firstChild + expect(fieldset).toBeInstanceOf(HTMLFieldSetElement) + expect(fieldset).not.toHaveAttribute('role', 'group') + }) + + it('should render a `Fieldset` using a custom component', async () => { + let { container } = render( +
+ +
+ ) + + let fieldset = container.firstChild + + expect(fieldset).toBeInstanceOf(HTMLSpanElement) expect(fieldset).toHaveAttribute('role', 'group') }) - it('should add an `aria-disabled` attribute when disabling the `Fieldset`', async () => { + it('should forward the `disabled` attribute when disabling the `Fieldset`', async () => { let { container } = render(
@@ -34,10 +48,33 @@ describe('Rendering', () => { let fieldset = container.firstChild - expect(fieldset).toHaveAttribute('role', 'group') + expect(fieldset).toHaveAttribute('disabled') + }) + + it('should add an `aria-disabled` attribute when disabling the `Fieldset` when using another element via the `as` prop', async () => { + let { container } = render( +
+ +
+ ) + + let fieldset = container.firstChild + expect(fieldset).toHaveAttribute('aria-disabled', 'true') }) + it('should make nested inputs disabled when the fieldset is disabled', async () => { + let { container } = render( +
+ +
+ ) + + let fieldset = container.firstChild + + expect(fieldset?.firstChild).toBeDisabled() + }) + it('should link a `Fieldset` to a nested `Legend`', async () => { let { container } = render(
diff --git a/packages/@headlessui-react/src/components/fieldset/fieldset.tsx b/packages/@headlessui-react/src/components/fieldset/fieldset.tsx index 81f3972509..e23b1572c4 100644 --- a/packages/@headlessui-react/src/components/fieldset/fieldset.tsx +++ b/packages/@headlessui-react/src/components/fieldset/fieldset.tsx @@ -1,15 +1,17 @@ 'use client' import React, { useMemo, type ElementType, type Ref } from 'react' +import { useResolvedTag } from '../../hooks/use-resolved-tag' +import { useSyncRefs } from '../../hooks/use-sync-refs' import { DisabledProvider, useDisabled } from '../../internal/disabled' import type { Props } from '../../types' import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render' import { useLabels } from '../label/label' -let DEFAULT_FIELDSET_TAG = 'div' as const +let DEFAULT_FIELDSET_TAG = 'fieldset' as const type FieldsetRenderPropArg = {} -type FieldsetPropsWeControl = 'aria-controls' +type FieldsetPropsWeControl = 'aria-labelledby' | 'aria-disabled' | 'role' export type FieldsetProps = Props< TTag, @@ -27,17 +29,26 @@ function FieldsetFn( let providedDisabled = useDisabled() let { disabled = providedDisabled || false, ...theirProps } = props + let [tag, resolveTag] = useResolvedTag(props.as ?? DEFAULT_FIELDSET_TAG) + let fieldsetRef = useSyncRefs(ref, resolveTag) + let [labelledBy, LabelProvider] = useLabels() let slot = useMemo(() => ({ disabled }) satisfies FieldsetRenderPropArg, [disabled]) - let ourProps = { - ref, - role: 'group', - - 'aria-labelledby': labelledBy, - 'aria-disabled': disabled || undefined, - } + let ourProps = + tag === 'fieldset' + ? { + ref: fieldsetRef, + 'aria-labelledby': labelledBy, + disabled: disabled || undefined, + } + : { + ref: fieldsetRef, + role: 'group', + 'aria-labelledby': labelledBy, + 'aria-disabled': disabled || undefined, + } return ( diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 1de94eff9e..9f9ac41940 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -24,6 +24,7 @@ import { useActivePress } from '../../hooks/use-active-press' import { useByComparator, type ByComparator } from '../../hooks/use-by-comparator' import { useComputed } from '../../hooks/use-computed' import { useControllable } from '../../hooks/use-controllable' +import { useDefaultValue } from '../../hooks/use-default-value' import { useDidElementMove } from '../../hooks/use-did-element-move' import { useDisposables } from '../../hooks/use-disposables' import { useElementSize } from '../../hooks/use-element-size' @@ -481,7 +482,7 @@ function ListboxFn< let providedDisabled = useDisabled() let { value: controlledValue, - defaultValue, + defaultValue: _defaultValue, form, name, onChange: controlledOnChange, @@ -493,9 +494,11 @@ function ListboxFn< __demoMode = false, ...theirProps } = props + const orientation = horizontal ? 'horizontal' : 'vertical' let listboxRef = useSyncRefs(ref) + let defaultValue = useDefaultValue(_defaultValue) let [value = multiple ? [] : undefined, theirOnChange] = useControllable( controlledValue, controlledOnChange, @@ -660,8 +663,9 @@ function ListboxFn< let ourProps = { ref: listboxRef } let reset = useCallback(() => { + if (defaultValue === undefined) return return theirOnChange?.(defaultValue) - }, [theirOnChange /* Explicitly ignoring `defaultValue` */]) + }, [theirOnChange, defaultValue]) return ( { }) describe('`Tab` key', () => { + it( + 'should be possible to Tab through the panel contents and end up in the Button again (without PopoverGroup)', + suppressConsoleLogs(async () => { + render( + + Trigger + + Link 1 + Link 2 + + + ) + + // Focus the button of the first Popover + getByText('Trigger')?.focus() + + // Open popover + await click(getByText('Trigger')) + + // Verify we are focused on the first link + await press(Keys.Tab) + assertActiveElement(getByText('Link 1')) + + // Verify we are focused on the second link + await press(Keys.Tab) + assertActiveElement(getByText('Link 2')) + + // Let's Tab again + await press(Keys.Tab) + + // Verify that the first Popover is still open + assertPopoverButton({ state: PopoverState.Visible }) + assertPopoverPanel({ state: PopoverState.Visible }) + + // Verify that the button is focused again + assertActiveElement(getByText('Trigger')) + }) + ) + it( 'should be possible to Tab through the panel contents onto the next Popover.Button', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx index 3940555f3d..823b044104 100644 --- a/packages/@headlessui-react/src/components/popover/popover.tsx +++ b/packages/@headlessui-react/src/components/popover/popover.tsx @@ -92,6 +92,7 @@ interface StateDefinition { beforePanelSentinel: MutableRefObject afterPanelSentinel: MutableRefObject + afterButtonSentinel: MutableRefObject __demoMode: boolean } @@ -256,9 +257,19 @@ function PopoverFn( panelId: null, beforePanelSentinel: createRef(), afterPanelSentinel: createRef(), + afterButtonSentinel: createRef(), } as StateDefinition) let [ - { popoverState, button, buttonId, panel, panelId, beforePanelSentinel, afterPanelSentinel }, + { + popoverState, + button, + buttonId, + panel, + panelId, + beforePanelSentinel, + afterPanelSentinel, + afterButtonSentinel, + }, dispatch, ] = reducerBag @@ -346,6 +357,7 @@ function PopoverFn( if (root.contains(event.target)) return if (beforePanelSentinel.current?.contains?.(event.target)) return if (afterPanelSentinel.current?.contains?.(event.target)) return + if (afterButtonSentinel.current?.contains?.(event.target)) return dispatch({ type: ActionTypes.ClosePopover }) }, @@ -700,6 +712,7 @@ function ButtonFn( {visible && !isWithinPanel && isPortalled && ( ) let options = state.options as Option[] @@ -188,6 +188,7 @@ function RadioGroupFn(null) let radioGroupRef = useSyncRefs(internalRadioGroupRef, ref) + let defaultValue = useDefaultValue(_defaultValue) let [value, onChange] = useControllable(controlledValue, controlledOnChange, defaultValue) let firstOption = useMemo( @@ -304,8 +305,9 @@ function RadioGroupFn ({ value }) satisfies RadioGroupRenderPropArg, [value]) let reset = useCallback(() => { - return triggerChange(defaultValue!) - }, [triggerChange /* Explicitly ignoring `defaultValue` */]) + if (defaultValue === undefined) return + return triggerChange(defaultValue) + }, [triggerChange, defaultValue]) return ( diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index a5715dc7b8..5a17ce0525 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -17,6 +17,7 @@ import React, { } 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' @@ -146,7 +147,7 @@ function SwitchFn( id = providedId || `headlessui-switch-${internalId}`, disabled = providedDisabled || false, checked: controlledChecked, - defaultChecked = false, + defaultChecked: _defaultChecked, onChange: controlledOnChange, name, value, @@ -162,7 +163,12 @@ function SwitchFn( groupContext === null ? null : groupContext.setSwitch ) - let [checked, onChange] = useControllable(controlledChecked, controlledOnChange, defaultChecked) + let defaultChecked = useDefaultValue(_defaultChecked) + let [checked, onChange] = useControllable( + controlledChecked, + controlledOnChange, + defaultChecked ?? false + ) let d = useDisposables() let [changing, setChanging] = useState(false) @@ -232,8 +238,9 @@ function SwitchFn( ) let reset = useCallback(() => { + if (defaultChecked === undefined) return return onChange?.(defaultChecked) - }, [onChange /* Explicitly ignoring `defaultChecked` */]) + }, [onChange, defaultChecked]) return ( <> @@ -269,6 +276,7 @@ export interface _internal_ComponentSwitchLabel extends _internal_ComponentLabel export interface _internal_ComponentSwitchDescription extends _internal_ComponentDescription {} let SwitchRoot = forwardRefWithAs(SwitchFn) as _internal_ComponentSwitch +/** @deprecated use `` instead of `` */ export let SwitchGroup = GroupFn as _internal_ComponentSwitchGroup /** @deprecated use `