From c4ac12073878c9cc11f779f7ffcb862863e3217d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 24 Apr 2024 07:38:50 -0400 Subject: [PATCH 1/4] Make sure data-disabled is available on virtualized options --- .../src/components/combobox/combobox.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 0e515da98a..b858a7937b 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -1802,7 +1802,13 @@ function OptionFn< }) let slot = useMemo( - () => ({ active, focus: active, selected, disabled }) satisfies OptionRenderPropArg, + () => + ({ + active, + focus: active, + selected, + disabled: Boolean(disabled || data.virtual?.disabled(value)), + }) satisfies OptionRenderPropArg, [active, selected, disabled] ) From 0d72278b24a39dda7c03d9cc0ebfc78ba3b734c6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 24 Apr 2024 07:57:44 -0400 Subject: [PATCH 2/4] Add test --- .../src/components/combobox/combobox.test.tsx | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index fed87bd563..912964d43a 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -1769,6 +1769,101 @@ describe('Rendering', () => { expect(handleChange).toHaveBeenNthCalledWith(2, 'bob') }) }) + + describe.each([{ virtual: true }, { virtual: false }])('Data attributes', ({ virtual }) => { + let data = ['Option A', 'Option B', 'Option C'] + function MyCombobox({ + options = data.slice() as T[], + useComboboxOptions = true, + comboboxProps = {}, + inputProps = {}, + buttonProps = {}, + optionProps = {}, + }: { + options?: T[] + useComboboxOptions?: boolean + comboboxProps?: Record + inputProps?: Record + buttonProps?: Record + optionProps?: Record + }) { + function isDisabled(option: T): boolean { + return typeof option === 'string' + ? false + : typeof option === 'object' && + option !== null && + 'disabled' in option && + typeof option.disabled === 'boolean' + ? option?.disabled ?? false + : false + } + if (virtual) { + return ( + + + Trigger + {useComboboxOptions && ( + + {({ option }) => { + return + }} + + )} + + ) + } + + return ( + + + Trigger + {useComboboxOptions && ( + + {options.map((option, idx) => { + return ( + + ) + })} + + )} + + ) + } + + it('Disabled options should get a data-disabled attribute', async () => { + render( + + ) + + // Open the Combobox + await click(getByText('Trigger')) + + let options = getComboboxOptions() + + expect(options[0]).not.toHaveAttribute('data-disabled') + expect(options[1]).toHaveAttribute('data-disabled', '') + expect(options[2]).not.toHaveAttribute('data-disabled') + }) + }) }) describe('Rendering composition', () => { From b84128cb20b19ecc51bb4f99271cff1c7d7231a3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 24 Apr 2024 08:10:34 -0400 Subject: [PATCH 3/4] Update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 2d620e5fed..00fb3ffaab 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move focus to `ListboxOptions` and `MenuItems` when they are rendered later ([#3112](https://github.com/tailwindlabs/headlessui/pull/3112)) - Ensure anchored components are always rendered in a stacking context ([#3115](https://github.com/tailwindlabs/headlessui/pull/3115)) - Add optional `onClose` callback to `Combobox` component ([#3122](https://github.com/tailwindlabs/headlessui/pull/3122)) +- Make sure `data-disabled` is available on virtualized options in the `Combobox` component ([#3128](https://github.com/tailwindlabs/headlessui/pull/3128)) ### Changed From ef850847b33e4ae5b69939914ca5e4078065b5a0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Apr 2024 15:25:27 +0200 Subject: [PATCH 4/4] calculate `disabled` state once This way we only have to calculate it once and we can re-use that information throughout the component. If the `value` changes of the option, then the component has to re-render anyway which will re-compute the `disabled` state. --- .../src/components/combobox/combobox.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index b858a7937b..15f13b2246 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -1315,7 +1315,7 @@ function InputFn< : data.virtual ? data.options.find( (option) => - !data.virtual?.disabled(option.dataRef.current.value) && + !option.dataRef.current.disabled && data.compare( option.dataRef.current.value, data.virtual!.options[data.activeOptionIndex!] @@ -1666,18 +1666,18 @@ function OptionFn< // But today is not that day.. TType = Parameters[0]['value'], >(props: ComboboxOptionProps, ref: Ref) { + let data = useData('Combobox.Option') + let actions = useActions('Combobox.Option') + let internalId = useId() let { id = `headlessui-combobox-option-${internalId}`, - disabled = false, value, + disabled = data.virtual?.disabled(value) ?? false, order = null, ...theirProps } = props - let data = useData('Combobox.Option') - let actions = useActions('Combobox.Option') - let refocusInput = useRefocusableInput(data.inputRef) let active = data.virtual @@ -1749,7 +1749,7 @@ function OptionFn< return } - if (disabled || data.virtual?.disabled(value)) return + if (disabled) return select() // We want to make sure that we don't accidentally trigger the virtual keyboard. @@ -1774,7 +1774,7 @@ function OptionFn< }) let handleFocus = useEvent(() => { - if (disabled || data.virtual?.disabled(value)) { + if (disabled) { return actions.goToOption(Focus.Nothing) } let idx = data.calculateIndex(value) @@ -1787,7 +1787,7 @@ function OptionFn< let handleMove = useEvent((evt) => { if (!pointer.wasMoved(evt)) return - if (disabled || data.virtual?.disabled(value)) return + if (disabled) return if (active) return let idx = data.calculateIndex(value) actions.goToOption(Focus.Specific, idx, ActivationTrigger.Pointer) @@ -1795,22 +1795,20 @@ function OptionFn< let handleLeave = useEvent((evt) => { if (!pointer.wasMoved(evt)) return - if (disabled || data.virtual?.disabled(value)) return + if (disabled) return if (!active) return if (data.optionsPropsRef.current.hold) return actions.goToOption(Focus.Nothing) }) - let slot = useMemo( - () => - ({ - active, - focus: active, - selected, - disabled: Boolean(disabled || data.virtual?.disabled(value)), - }) satisfies OptionRenderPropArg, - [active, selected, disabled] - ) + let slot = useMemo(() => { + return { + active, + focus: active, + selected, + disabled, + } satisfies OptionRenderPropArg + }, [active, selected, disabled]) let ourProps = { id,