diff --git a/CHANGELOG.md b/CHANGELOG.md index a5241bb..3fe4979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure links are triggered inside `Popover Panel` components ([#1153](https://github.com/tailwindlabs/headlessui/pull/1153)) - Improve SSR for `Tab` component ([#1155](https://github.com/tailwindlabs/headlessui/pull/1155)) - Fix `hover` scroll ([#1161](https://github.com/tailwindlabs/headlessui/pull/1161)) +- Guarantee DOM sort order when performing actions ([#1168](https://github.com/tailwindlabs/headlessui/pull/1168)) ## [Unreleased - @headlessui/vue] @@ -23,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix Dialog usage in Tabs ([#1149](https://github.com/tailwindlabs/headlessui/pull/1149)) - Ensure links are triggered inside `Popover Panel` components ([#1153](https://github.com/tailwindlabs/headlessui/pull/1153)) - Fix `hover` scroll ([#1161](https://github.com/tailwindlabs/headlessui/pull/1161)) +- Guarantee DOM sort order when performing actions ([#1168](https://github.com/tailwindlabs/headlessui/pull/1168)) ## [@headlessui/react@v1.5.0] - 2022-02-17 diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index be64c95..3e96c5e 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -35,6 +35,7 @@ import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-cl import { useResolveButtonType } from '../../hooks/use-resolve-button-type' import { useLatestValue } from '../../hooks/use-latest-value' import { useTreeWalker } from '../../hooks/use-tree-walker' +import { sortByDomNode } from '../../utils/focus-management' enum ComboboxStates { Open, @@ -50,6 +51,7 @@ type ComboboxOptionDataRef = MutableRefObject<{ textValue?: string disabled: boolean value: unknown + domRef: MutableRefObject }> interface StateDefinition { @@ -129,11 +131,14 @@ let reducers: { state.optionsRef.current && !state.optionsPropsRef.current.static && state.comboboxState === ComboboxStates.Closed - ) + ) { return state + } + + let options = sortByDomNode(state.options, (option) => option.dataRef.current.domRef.current) let activeOptionIndex = calculateActiveIndex(action, { - resolveItems: () => state.options, + resolveItems: () => options, resolveActiveIndex: () => state.activeOptionIndex, resolveId: (item) => item.id, resolveDisabled: (item) => item.dataRef.current.disabled, @@ -142,6 +147,7 @@ let reducers: { if (state.activeOptionIndex === activeOptionIndex) return state return { ...state, + options, // Sorted options activeOptionIndex, activationTrigger: action.trigger ?? ActivationTrigger.Other, } @@ -150,17 +156,7 @@ let reducers: { let currentActiveOption = state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null - let orderMap = Array.from( - state.optionsRef.current?.querySelectorAll('[id^="headlessui-combobox-option-"]')! - ).reduce( - (lookup, element, index) => Object.assign(lookup, { [element.id]: index }), - {} - ) as Record - - let options = [...state.options, { id: action.id, dataRef: action.dataRef }].sort( - (a, z) => orderMap[a.id] - orderMap[z.id] - ) - + let options = [...state.options, { id: action.id, dataRef: action.dataRef }] let nextState = { ...state, options, @@ -859,8 +855,9 @@ let Option = forwardRefWithAs(function Option< let active = state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false let selected = state.comboboxPropsRef.current.value === value - let bag = useRef({ disabled, value }) - let optionRef = useSyncRefs(ref) + let internalOptionRef = useRef(null) + let bag = useRef({ disabled, value, domRef: internalOptionRef }) + let optionRef = useSyncRefs(ref, internalOptionRef) useIsoMorphicEffect(() => { bag.current.disabled = disabled diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index df0da51..1b6994e 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -29,7 +29,7 @@ import { disposables } from '../../utils/disposables' import { Keys } from '../keyboard' import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index' import { isDisabledReactIssue7711 } from '../../utils/bugs' -import { isFocusableElement, FocusableMode } from '../../utils/focus-management' +import { isFocusableElement, FocusableMode, sortByDomNode } from '../../utils/focus-management' import { useWindowEvent } from '../../hooks/use-window-event' import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' @@ -48,6 +48,7 @@ type ListboxOptionDataRef = MutableRefObject<{ textValue?: string disabled: boolean value: unknown + domRef: MutableRefObject }> interface StateDefinition { @@ -126,8 +127,10 @@ let reducers: { if (state.disabled) return state if (state.listboxState === ListboxStates.Closed) return state + let options = sortByDomNode(state.options, (option) => option.dataRef.current.domRef.current) + let activeOptionIndex = calculateActiveIndex(action, { - resolveItems: () => state.options, + resolveItems: () => options, resolveActiveIndex: () => state.activeOptionIndex, resolveId: (item) => item.id, resolveDisabled: (item) => item.dataRef.current.disabled, @@ -136,6 +139,7 @@ let reducers: { if (state.searchQuery === '' && state.activeOptionIndex === activeOptionIndex) return state return { ...state, + options, // Sorted options searchQuery: '', activeOptionIndex, activationTrigger: action.trigger ?? ActivationTrigger.Other, @@ -180,17 +184,7 @@ let reducers: { return { ...state, searchQuery: '' } }, [ActionTypes.RegisterOption]: (state, action) => { - let orderMap = Array.from( - state.optionsRef.current?.querySelectorAll('[id^="headlessui-listbox-option-"]')! - ).reduce( - (lookup, element, index) => Object.assign(lookup, { [element.id]: index }), - {} - ) as Record - - let options = [...state.options, { id: action.id, dataRef: action.dataRef }].sort( - (a, z) => orderMap[a.id] - orderMap[z.id] - ) - + let options = [...state.options, { id: action.id, dataRef: action.dataRef }] return { ...state, options } }, [ActionTypes.UnregisterOption]: (state, action) => { @@ -665,9 +659,10 @@ let Option = forwardRefWithAs(function Option< let active = state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false let selected = state.propsRef.current.value === value - let optionRef = useSyncRefs(ref) + let internalOptionRef = useRef(null) + let optionRef = useSyncRefs(ref, internalOptionRef) - let bag = useRef({ disabled, value }) + let bag = useRef({ disabled, value, domRef: internalOptionRef }) useIsoMorphicEffect(() => { bag.current.disabled = disabled diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index 0b08e91..7cab27d 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -30,7 +30,7 @@ import { useId } from '../../hooks/use-id' import { Keys } from '../keyboard' import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index' import { isDisabledReactIssue7711 } from '../../utils/bugs' -import { isFocusableElement, FocusableMode } from '../../utils/focus-management' +import { isFocusableElement, FocusableMode, sortByDomNode } from '../../utils/focus-management' import { useWindowEvent } from '../../hooks/use-window-event' import { useTreeWalker } from '../../hooks/use-tree-walker' import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed' @@ -46,7 +46,11 @@ enum ActivationTrigger { Other, } -type MenuItemDataRef = MutableRefObject<{ textValue?: string; disabled: boolean }> +type MenuItemDataRef = MutableRefObject<{ + textValue?: string + disabled: boolean + domRef: MutableRefObject +}> interface StateDefinition { menuState: MenuStates @@ -98,8 +102,10 @@ let reducers: { return { ...state, menuState: MenuStates.Open } }, [ActionTypes.GoToItem]: (state, action) => { + let items = sortByDomNode(state.items, (item) => item.dataRef.current.domRef.current) + let activeItemIndex = calculateActiveIndex(action, { - resolveItems: () => state.items, + resolveItems: () => items, resolveActiveIndex: () => state.activeItemIndex, resolveId: (item) => item.id, resolveDisabled: (item) => item.dataRef.current.disabled, @@ -108,6 +114,7 @@ let reducers: { if (state.searchQuery === '' && state.activeItemIndex === activeItemIndex) return state return { ...state, + items, // Sorted items searchQuery: '', activeItemIndex, activationTrigger: action.trigger ?? ActivationTrigger.Other, @@ -144,17 +151,7 @@ let reducers: { return { ...state, searchQuery: '', searchActiveItemIndex: null } }, [ActionTypes.RegisterItem]: (state, action) => { - let orderMap = Array.from( - state.itemsRef.current?.querySelectorAll('[id^="headlessui-menu-item-"]')! - ).reduce( - (lookup, element, index) => Object.assign(lookup, { [element.id]: index }), - {} - ) as Record - - let items = [...state.items, { id: action.id, dataRef: action.dataRef }].sort( - (a, z) => orderMap[a.id] - orderMap[z.id] - ) - + let items = [...state.items, { id: action.id, dataRef: action.dataRef }] return { ...state, items } }, [ActionTypes.UnregisterItem]: (state, action) => { @@ -560,7 +557,8 @@ let Item = forwardRefWithAs(function Item(null) + let itemRef = useSyncRefs(ref, internalItemRef) useIsoMorphicEffect(() => { if (state.menuState !== MenuStates.Open) return @@ -573,7 +571,7 @@ let Item = forwardRefWithAs(function Item({ disabled }) + let bag = useRef({ disabled, domRef: internalItemRef }) useIsoMorphicEffect(() => { bag.current.disabled = disabled diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx index f2d0b66..375868e 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx @@ -20,7 +20,7 @@ import { useId } from '../../hooks/use-id' import { match } from '../../utils/match' import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect' import { Keys } from '../../components/keyboard' -import { focusIn, Focus, FocusResult } from '../../utils/focus-management' +import { focusIn, Focus, FocusResult, sortByDomNode } from '../../utils/focus-management' import { useFlags } from '../../hooks/use-flags' import { Label, useLabels } from '../../components/label/label' import { Description, useDescriptions } from '../../components/description/description' @@ -53,12 +53,14 @@ let reducers: { ) => StateDefinition } = { [ActionTypes.RegisterOption](state, action) { + let nextOptions = [ + ...state.options, + { id: action.id, element: action.element, propsRef: action.propsRef }, + ] + return { ...state, - options: [ - ...state.options, - { id: action.id, element: action.element, propsRef: action.propsRef }, - ], + options: sortByDomNode(nextOptions, (option) => option.element.current), } }, [ActionTypes.UnregisterOption](state, action) { diff --git a/packages/@headlessui-react/src/utils/focus-management.ts b/packages/@headlessui-react/src/utils/focus-management.ts index 703e87d..0c2773f 100644 --- a/packages/@headlessui-react/src/utils/focus-management.ts +++ b/packages/@headlessui-react/src/utils/focus-management.ts @@ -102,15 +102,27 @@ export function focusElement(element: HTMLElement | null) { element?.focus({ preventScroll: true }) } +export function sortByDomNode( + nodes: T[], + resolveKey: (item: T) => HTMLElement | null = (i) => i as unknown as HTMLElement | null +): T[] { + return nodes.slice().sort((aItem, zItem) => { + let a = resolveKey(aItem) + let z = resolveKey(zItem) + + if (a === null || z === null) return 0 + + let position = a.compareDocumentPosition(z) + + if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1 + if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1 + return 0 + }) +} + export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { let elements = Array.isArray(container) - ? container.slice().sort((a, z) => { - let position = a.compareDocumentPosition(z) - - if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1 - if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1 - return 0 - }) + ? sortByDomNode(container) : getFocusableElements(container) let active = document.activeElement as HTMLElement diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index e12d6d6..ffbc021 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -26,6 +26,7 @@ import { useOpenClosed, State, useOpenClosedProvider } from '../../internal/open import { match } from '../../utils/match' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' import { useTreeWalker } from '../../hooks/use-tree-walker' +import { sortByDomNode } from '../../utils/focus-management' enum ComboboxStates { Open, @@ -37,7 +38,11 @@ enum ActivationTrigger { Other, } -type ComboboxOptionDataRef = Ref<{ disabled: boolean; value: unknown }> +type ComboboxOptionDataRef = Ref<{ + disabled: boolean + value: unknown + domRef: Ref +}> type StateDefinition = { // State comboboxState: Ref @@ -140,24 +145,27 @@ export let Combobox = defineComponent({ optionsRef.value && !optionsPropsRef.value.static && comboboxState.value === ComboboxStates.Closed - ) + ) { return + } + + let orderedOptions = sortByDomNode(options.value, (option) => option.dataRef.domRef.value) let nextActiveOptionIndex = calculateActiveIndex( focus === Focus.Specific ? { focus: Focus.Specific, id: id! } : { focus: focus as Exclude }, { - resolveItems: () => options.value, + resolveItems: () => orderedOptions, resolveActiveIndex: () => activeOptionIndex.value, resolveId: (option) => option.id, resolveDisabled: (option) => option.dataRef.disabled, } ) - if (activeOptionIndex.value === nextActiveOptionIndex) return activeOptionIndex.value = nextActiveOptionIndex activationTrigger.value = trigger ?? ActivationTrigger.Other + options.value = orderedOptions }, syncInputValue() { let value = api.value.value @@ -189,17 +197,9 @@ export let Combobox = defineComponent({ registerOption(id: string, dataRef: ComboboxOptionDataRef) { let currentActiveOption = activeOptionIndex.value !== null ? options.value[activeOptionIndex.value] : null - let orderMap = Array.from( - optionsRef.value?.querySelectorAll('[id^="headlessui-combobox-option-"]') ?? [] - ).reduce( - (lookup, element, index) => Object.assign(lookup, { [element.id]: index }), - {} - ) as Record // @ts-expect-error The expected type comes from property 'dataRef' which is declared here on type '{ id: string; dataRef: { textValue: string; disabled: boolean; }; }' - options.value = [...options.value, { id, dataRef }].sort( - (a, z) => orderMap[a.id] - orderMap[z.id] - ) + options.value = [...options.value, { id, dataRef }] // If we inserted an option before the current active option then the // active option index would be wrong. To fix this, we will re-lookup @@ -630,6 +630,7 @@ export let ComboboxOption = defineComponent({ setup(props, { slots, attrs }) { let api = useComboboxContext('ComboboxOption') let id = `headlessui-combobox-option-${useId()}` + let internalOptionRef = ref(null) let active = computed(() => { return api.activeOptionIndex.value !== null @@ -642,6 +643,7 @@ export let ComboboxOption = defineComponent({ let dataRef = computed(() => ({ disabled: props.disabled, value: props.value, + domRef: internalOptionRef, })) onMounted(() => api.registerOption(id, dataRef)) @@ -696,6 +698,7 @@ export let ComboboxOption = defineComponent({ let slot = { active: active.value, selected: selected.value, disabled } let propsWeControl = { id, + ref: internalOptionRef, role: 'option', tabIndex: disabled === true ? undefined : -1, 'aria-disabled': disabled === true ? true : undefined, diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 7e2163a..06e7ab6 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -24,6 +24,7 @@ import { useWindowEvent } from '../../hooks/use-window-event' import { useOpenClosed, State, useOpenClosedProvider } from '../../internal/open-closed' import { match } from '../../utils/match' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { sortByDomNode } from '../../utils/focus-management' enum ListboxStates { Open, @@ -39,7 +40,12 @@ function nextFrame(cb: () => void) { requestAnimationFrame(() => requestAnimationFrame(cb)) } -type ListboxOptionDataRef = Ref<{ textValue: string; disabled: boolean; value: unknown }> +type ListboxOptionDataRef = Ref<{ + textValue: string + disabled: boolean + value: unknown + domRef: Ref +}> type StateDefinition = { // State listboxState: Ref @@ -133,22 +139,24 @@ export let Listbox = defineComponent({ if (props.disabled) return if (listboxState.value === ListboxStates.Closed) return + let orderedOptions = sortByDomNode(options.value, (option) => option.dataRef.domRef.value) + let nextActiveOptionIndex = calculateActiveIndex( focus === Focus.Specific ? { focus: Focus.Specific, id: id! } : { focus: focus as Exclude }, { - resolveItems: () => options.value, + resolveItems: () => orderedOptions, resolveActiveIndex: () => activeOptionIndex.value, resolveId: (option) => option.id, resolveDisabled: (option) => option.dataRef.disabled, } ) - if (searchQuery.value === '' && activeOptionIndex.value === nextActiveOptionIndex) return searchQuery.value = '' activeOptionIndex.value = nextActiveOptionIndex activationTrigger.value = trigger ?? ActivationTrigger.Other + options.value = orderedOptions }, search(value: string) { if (props.disabled) return @@ -185,17 +193,8 @@ export let Listbox = defineComponent({ searchQuery.value = '' }, registerOption(id: string, dataRef: ListboxOptionDataRef) { - let orderMap = Array.from( - optionsRef.value?.querySelectorAll('[id^="headlessui-listbox-option-"]') ?? [] - ).reduce( - (lookup, element, index) => Object.assign(lookup, { [element.id]: index }), - {} - ) as Record - // @ts-expect-error The expected type comes from property 'dataRef' which is declared here on type '{ id: string; dataRef: { textValue: string; disabled: boolean; }; }' - options.value = [...options.value, { id, dataRef }].sort( - (a, z) => orderMap[a.id] - orderMap[z.id] - ) + options.value = [...options.value, { id, dataRef }] }, unregisterOption(id: string) { let nextOptions = options.value.slice() @@ -518,6 +517,7 @@ export let ListboxOption = defineComponent({ setup(props, { slots, attrs }) { let api = useListboxContext('ListboxOption') let id = `headlessui-listbox-option-${useId()}` + let internalOptionRef = ref(null) let active = computed(() => { return api.activeOptionIndex.value !== null @@ -527,11 +527,12 @@ export let ListboxOption = defineComponent({ let selected = computed(() => toRaw(api.value.value) === toRaw(props.value)) - let dataRef = ref({ + let dataRef = computed(() => ({ disabled: props.disabled, value: props.value, textValue: '', - }) + domRef: internalOptionRef, + })) onMounted(() => { let textValue = document.getElementById(id)?.textContent?.toLowerCase().trim() if (textValue !== undefined) dataRef.value.textValue = textValue @@ -589,6 +590,7 @@ export let ListboxOption = defineComponent({ let slot = { active: active.value, selected: selected.value, disabled } let propsWeControl = { id, + ref: internalOptionRef, role: 'option', tabIndex: disabled === true ? undefined : -1, 'aria-disabled': disabled === true ? true : undefined, diff --git a/packages/@headlessui-vue/src/components/menu/menu.test.tsx b/packages/@headlessui-vue/src/components/menu/menu.test.tsx index 3f5b4e7..3116f7c 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-vue/src/components/menu/menu.test.tsx @@ -704,6 +704,7 @@ describe('Rendering', () => { 'However we need to passthrough the following props:', ' - disabled', ' - id', + ' - ref', ' - role', ' - tabIndex', ' - aria-disabled', diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index d4636eb..887d201 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -21,6 +21,7 @@ import { useTreeWalker } from '../../hooks/use-tree-walker' import { useOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed' import { match } from '../../utils/match' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { sortByDomNode } from '../../utils/focus-management' enum MenuStates { Open, @@ -36,7 +37,11 @@ function nextFrame(cb: () => void) { requestAnimationFrame(() => requestAnimationFrame(cb)) } -type MenuItemDataRef = Ref<{ textValue: string; disabled: boolean }> +type MenuItemDataRef = Ref<{ + textValue: string + disabled: boolean + domRef: Ref +}> type StateDefinition = { // State menuState: Ref @@ -99,22 +104,23 @@ export let Menu = defineComponent({ }, openMenu: () => (menuState.value = MenuStates.Open), goToItem(focus: Focus, id?: string, trigger?: ActivationTrigger) { + let orderedItems = sortByDomNode(items.value, (item) => item.dataRef.domRef.value) let nextActiveItemIndex = calculateActiveIndex( focus === Focus.Specific ? { focus: Focus.Specific, id: id! } : { focus: focus as Exclude }, { - resolveItems: () => items.value, + resolveItems: () => orderedItems, resolveActiveIndex: () => activeItemIndex.value, resolveId: (item) => item.id, resolveDisabled: (item) => item.dataRef.disabled, } ) - if (searchQuery.value === '' && activeItemIndex.value === nextActiveItemIndex) return searchQuery.value = '' activeItemIndex.value = nextActiveItemIndex activationTrigger.value = trigger ?? ActivationTrigger.Other + items.value = orderedItems }, search(value: string) { let wasAlreadySearching = searchQuery.value !== '' @@ -142,17 +148,8 @@ export let Menu = defineComponent({ searchQuery.value = '' }, registerItem(id: string, dataRef: MenuItemDataRef) { - let orderMap = Array.from( - itemsRef.value?.querySelectorAll('[id^="headlessui-menu-item-"]') ?? [] - ).reduce( - (lookup, element, index) => Object.assign(lookup, { [element.id]: index }), - {} - ) as Record - // @ts-expect-error The expected type comes from property 'dataRef' which is declared here on type '{ id: string; dataRef: { textValue: string; disabled: boolean; }; }' - items.value = [...items.value, { id, dataRef }].sort( - (a, z) => orderMap[a.id] - orderMap[z.id] - ) + items.value = [...items.value, { id, dataRef }] }, unregisterItem(id: string) { let nextItems = items.value.slice() @@ -449,6 +446,7 @@ export let MenuItem = defineComponent({ setup(props, { slots, attrs }) { let api = useMenuContext('MenuItem') let id = `headlessui-menu-item-${useId()}` + let internalItemRef = ref(null) let active = computed(() => { return api.activeItemIndex.value !== null @@ -456,7 +454,11 @@ export let MenuItem = defineComponent({ : false }) - let dataRef = ref({ disabled: props.disabled, textValue: '' }) + let dataRef = computed(() => ({ + disabled: props.disabled, + textValue: '', + domRef: internalItemRef, + })) onMounted(() => { let textValue = document.getElementById(id)?.textContent?.toLowerCase().trim() if (textValue !== undefined) dataRef.value.textValue = textValue @@ -500,6 +502,7 @@ export let MenuItem = defineComponent({ let slot = { active: active.value, disabled } let propsWeControl = { id, + ref: internalItemRef, role: 'menuitem', tabIndex: disabled === true ? undefined : -1, 'aria-disabled': disabled === true ? true : undefined, diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts index 44481ee..e4a231b 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts @@ -15,7 +15,7 @@ import { } from 'vue' import { dom } from '../../utils/dom' import { Keys } from '../../keyboard' -import { focusIn, Focus, FocusResult } from '../../utils/focus-management' +import { focusIn, Focus, FocusResult, sortByDomNode } from '../../utils/focus-management' import { useId } from '../../hooks/use-id' import { omit, render } from '../../utils/render' import { Label, useLabels } from '../label/label' @@ -98,15 +98,8 @@ export let RadioGroup = defineComponent({ return true }, registerOption(action: UnwrapRef