diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 48339ad9d5..3c445395bd 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve iOS scroll locking ([#1830](https://github.com/tailwindlabs/headlessui/pull/1830)) - Add `
` check to radio group options in React ([#1835](https://github.com/tailwindlabs/headlessui/pull/1835)) - Ensure `Tab` order stays consistent, and the currently active `Tab` stays active ([#1837](https://github.com/tailwindlabs/headlessui/pull/1837)) +- Ensure `Combobox.Label` is properly linked when rendered after `Combobox.Button` and `Combobox.Input` components ([#1838](https://github.com/tailwindlabs/headlessui/pull/1838)) ## [1.7.0] - 2022-09-06 diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 0ddb12b190..a4df3818a9 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -631,6 +631,22 @@ describe('Rendering', () => { }) ) + it( + 'should be possible to link Input/Button and Label if Label is rendered last', + suppressConsoleLogs(async () => { + render( + + + + Label + + ) + + assertComboboxLabelLinkedWithCombobox() + assertComboboxButtonLinkedWithComboboxLabel() + }) + ) + it( 'should be possible to render a Combobox.Label using a render prop and an `as` prop', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index c2c1338838..276c97a522 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -67,6 +67,7 @@ type ComboboxOptionDataRef = MutableRefObject<{ interface StateDefinition { dataRef: MutableRefObject<_Data> + labelId: string | null comboboxState: ComboboxState @@ -83,6 +84,8 @@ enum ActionTypes { RegisterOption, UnregisterOption, + + RegisterLabel, } function adjustOrderedState( @@ -124,6 +127,7 @@ type Actions = trigger?: ActivationTrigger } | { type: ActionTypes.RegisterOption; id: string; dataRef: ComboboxOptionDataRef } + | { type: ActionTypes.RegisterLabel; id: string | null } | { type: ActionTypes.UnregisterOption; id: string } let reducers: { @@ -227,12 +231,19 @@ let reducers: { activationTrigger: ActivationTrigger.Other, } }, + [ActionTypes.RegisterLabel]: (state, action) => { + return { + ...state, + labelId: action.id, + } + }, } let ComboboxActionsContext = createContext<{ openCombobox(): void closeCombobox(): void registerOption(id: string, dataRef: ComboboxOptionDataRef): () => void + registerLabel(id: string): () => void goToOption(focus: Focus.Specific, id: string, trigger?: ActivationTrigger): void goToOption(focus: Focus, id?: string, trigger?: ActivationTrigger): void selectOption(id: string): void @@ -402,6 +413,7 @@ function ComboboxFn) let defaultToFirstOption = useRef(false) @@ -536,6 +548,11 @@ function ComboboxFn dispatch({ type: ActionTypes.UnregisterOption, id }) }) + let registerLabel = useEvent((id) => { + dispatch({ type: ActionTypes.RegisterLabel, id }) + return () => dispatch({ type: ActionTypes.RegisterLabel, id: null }) + }) + let onChange = useEvent((value: unknown) => { return match(data.mode, { [ValueMode.Single]() { @@ -560,6 +577,7 @@ function ComboboxFn ({ onChange, registerOption, + registerLabel, goToOption, closeCombobox, openCombobox, @@ -775,9 +793,9 @@ let Input = forwardRefWithAs(function Input< // TODO: Verify this. The spec says that, for the input/combobox, the label is the labelling element when present // Otherwise it's the ID of the non-label element let labelledby = useComputed(() => { - if (!data.labelRef.current) return undefined - return [data.labelRef.current.id].join(' ') - }, [data.labelRef.current]) + if (!data.labelId) return undefined + return [data.labelId].join(' ') + }, [data.labelId]) let slot = useMemo( () => ({ open: data.comboboxState === ComboboxState.Open, disabled: data.disabled }), @@ -892,9 +910,9 @@ let Button = forwardRefWithAs(function Button { - if (!data.labelRef.current) return undefined - return [data.labelRef.current.id, id].join(' ') - }, [data.labelRef.current, id]) + if (!data.labelId) return undefined + return [data.labelId, id].join(' ') + }, [data.labelId, id]) let slot = useMemo( () => ({ @@ -943,8 +961,11 @@ let Label = forwardRefWithAs(function Label actions.registerLabel(id), [id]) + let handleClick = useEvent(() => data.inputRef.current?.focus({ preventScroll: true })) let slot = useMemo( @@ -1027,8 +1048,8 @@ let Options = forwardRefWithAs(function Options< }) let labelledby = useComputed( - () => data.labelRef.current?.id ?? data.buttonRef.current?.id, - [data.labelRef.current, data.buttonRef.current] + () => data.labelId ?? data.buttonRef.current?.id, + [data.labelId, data.buttonRef.current] ) let slot = useMemo( diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index 014c15ed55..a7c7b21b30 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -654,6 +654,27 @@ describe('Rendering', () => { }) ) + it( + 'should be possible to link Input/Button and Label if Label is rendered last', + suppressConsoleLogs(async () => { + renderTemplate({ + template: html` + + + + Label + + `, + setup: () => ({ value: ref(null) }), + }) + + await new Promise(nextTick) + + assertComboboxLabelLinkedWithCombobox() + assertComboboxButtonLinkedWithComboboxLabel() + }) + ) + it( 'should be possible to render a ComboboxLabel using a render prop and an `as` prop', suppressConsoleLogs(async () => {