Skip to content

Commit

Permalink
Fix initial anchor="selection" state (#3324)
Browse files Browse the repository at this point in the history
* compute `selectedOptionIndex` when using `anchor="selection"`

Instead of relying on the DOM directly, we can compute the
`selectedOptionIndex` and rely on the data directly.

We will also freeze the value while closing to prevent UI changes.

* update changelog
  • Loading branch information
RobinMalfait committed Jun 25, 2024
1 parent a593d19 commit ff41b27
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 33 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fix issues spreading omitted props onto components ([#3313](https://github.com/tailwindlabs/headlessui/pull/3313))
- Fix initial `anchor="selection"` positioning ([#3324](https://github.com/tailwindlabs/headlessui/pull/3324))

## [2.1.0] - 2024-06-21

Expand Down
57 changes: 24 additions & 33 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -944,26 +944,6 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
allowed: useEvent(() => [data.buttonRef.current, data.optionsRef.current]),
})

let initialOption = useRef<number | null>(null)

useEffect(() => {
if (!anchor?.to?.includes('selection')) return

if (!visible) {
initialOption.current = null
return
}

let elements = Array.from(data.listRef.current.values())
// TODO: Do not rely on DOM elements here
initialOption.current = elements.findIndex((el) => el?.dataset.selected === '')
// Default to first option if nothing is selected
if (initialOption.current === -1) {
initialOption.current = elements.findIndex((el) => el?.dataset.disabled === undefined)
actions.goToOption(Focus.First)
}
}, [visible, data.listRef])

// We keep track whether the button moved or not, we only check this when the menu state becomes
// closed. If the button moved, then we want to cancel pending transitions to prevent that the
// attached `MenuItems` is still transitioning while the button moved away.
Expand All @@ -980,17 +960,38 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
// its transitions, or rely on the `visible` state to hide the panel whenever necessary.
let panelEnabled = didButtonMove ? false : visible

// We should freeze when the listbox is visible but "closed". This means that
// a transition is currently happening and the component is still visible (for
// the transition) but closed from a functionality perspective.
let shouldFreeze = visible && data.listboxState === ListboxStates.Closed

// Frozen state, the selected value will only update visually when the user re-opens the <Listbox />
let frozenValue = useFrozenData(shouldFreeze, data.value)

let isSelected = useEvent((compareValue: unknown) => data.compare(frozenValue, compareValue))

let selectedOptionIndex = useMemo(() => {
if (anchor == null) return null
if (!anchor?.to?.includes('selection')) return null

// Only compute the selected option index when using `selection` in the
// `anchor` prop.
let idx = data.options.findIndex((option) => isSelected(option.dataRef.current.value))
// Ensure that if no data is selected, we default to the first item.
if (idx === -1) idx = 0
return idx
}, [anchor, data.options])

let anchorOptions = (() => {
if (anchor == null) return undefined
if (data.listRef.current.size <= 0) return { ...anchor, inner: undefined }
if (selectedOptionIndex === null) return { ...anchor, inner: undefined }

let elements = Array.from(data.listRef.current.values())

return {
...anchor,
inner: {
listRef: { current: elements },
index: initialOption.current!,
index: selectedOptionIndex,
},
}
})()
Expand Down Expand Up @@ -1115,16 +1116,6 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
...transitionDataAttributes(transitionData),
})

// We should freeze when the listbox is visible but "closed". This means that
// a transition is currently happening and the component is still visible (for
// the transition) but closed from a functionality perspective.
let shouldFreeze = visible && data.listboxState === ListboxStates.Closed

// Frozen state, the selected value will only update visually when the user re-opens the <Listbox />
let frozenValue = useFrozenData(shouldFreeze, data.value)

let isSelected = useEvent((compareValue: unknown) => data.compare(frozenValue, compareValue))

return (
<Portal enabled={portal ? props.static || visible : false}>
<ListboxDataContext.Provider
Expand Down

0 comments on commit ff41b27

Please sign in to comment.