Skip to content

Commit ff41b27

Browse files
authored
Fix initial anchor="selection" state (#3324)
* 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
1 parent a593d19 commit ff41b27

File tree

2 files changed

+25
-33
lines changed

2 files changed

+25
-33
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

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

1415
## [2.1.0] - 2024-06-21
1516

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -944,26 +944,6 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
944944
allowed: useEvent(() => [data.buttonRef.current, data.optionsRef.current]),
945945
})
946946

947-
let initialOption = useRef<number | null>(null)
948-
949-
useEffect(() => {
950-
if (!anchor?.to?.includes('selection')) return
951-
952-
if (!visible) {
953-
initialOption.current = null
954-
return
955-
}
956-
957-
let elements = Array.from(data.listRef.current.values())
958-
// TODO: Do not rely on DOM elements here
959-
initialOption.current = elements.findIndex((el) => el?.dataset.selected === '')
960-
// Default to first option if nothing is selected
961-
if (initialOption.current === -1) {
962-
initialOption.current = elements.findIndex((el) => el?.dataset.disabled === undefined)
963-
actions.goToOption(Focus.First)
964-
}
965-
}, [visible, data.listRef])
966-
967947
// We keep track whether the button moved or not, we only check this when the menu state becomes
968948
// closed. If the button moved, then we want to cancel pending transitions to prevent that the
969949
// attached `MenuItems` is still transitioning while the button moved away.
@@ -980,17 +960,38 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
980960
// its transitions, or rely on the `visible` state to hide the panel whenever necessary.
981961
let panelEnabled = didButtonMove ? false : visible
982962

963+
// We should freeze when the listbox is visible but "closed". This means that
964+
// a transition is currently happening and the component is still visible (for
965+
// the transition) but closed from a functionality perspective.
966+
let shouldFreeze = visible && data.listboxState === ListboxStates.Closed
967+
968+
// Frozen state, the selected value will only update visually when the user re-opens the <Listbox />
969+
let frozenValue = useFrozenData(shouldFreeze, data.value)
970+
971+
let isSelected = useEvent((compareValue: unknown) => data.compare(frozenValue, compareValue))
972+
973+
let selectedOptionIndex = useMemo(() => {
974+
if (anchor == null) return null
975+
if (!anchor?.to?.includes('selection')) return null
976+
977+
// Only compute the selected option index when using `selection` in the
978+
// `anchor` prop.
979+
let idx = data.options.findIndex((option) => isSelected(option.dataRef.current.value))
980+
// Ensure that if no data is selected, we default to the first item.
981+
if (idx === -1) idx = 0
982+
return idx
983+
}, [anchor, data.options])
984+
983985
let anchorOptions = (() => {
984-
if (anchor == null) return undefined
985-
if (data.listRef.current.size <= 0) return { ...anchor, inner: undefined }
986+
if (selectedOptionIndex === null) return { ...anchor, inner: undefined }
986987

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

989990
return {
990991
...anchor,
991992
inner: {
992993
listRef: { current: elements },
993-
index: initialOption.current!,
994+
index: selectedOptionIndex,
994995
},
995996
}
996997
})()
@@ -1115,16 +1116,6 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
11151116
...transitionDataAttributes(transitionData),
11161117
})
11171118

1118-
// We should freeze when the listbox is visible but "closed". This means that
1119-
// a transition is currently happening and the component is still visible (for
1120-
// the transition) but closed from a functionality perspective.
1121-
let shouldFreeze = visible && data.listboxState === ListboxStates.Closed
1122-
1123-
// Frozen state, the selected value will only update visually when the user re-opens the <Listbox />
1124-
let frozenValue = useFrozenData(shouldFreeze, data.value)
1125-
1126-
let isSelected = useEvent((compareValue: unknown) => data.compare(frozenValue, compareValue))
1127-
11281119
return (
11291120
<Portal enabled={portal ? props.static || visible : false}>
11301121
<ListboxDataContext.Provider

0 commit comments

Comments
 (0)