Skip to content

Commit b822c8a

Browse files
authored
Fix crash when toggling between virtual and non-virtual mode in Combobox component (#3236)
* ensure we correctly merge the `virtual` configuration * use more generic `UpdateVirtualOptions` This way we can passthrough the `disabled` function as well. * properly handle re-use of `disabled` function * use same order in objects * cleanup `!` and `?` if we already know we are in `virtual` mode * directly enable virtual mode in state if previously we weren't using virtual mode * update changelog
1 parent b478189 commit b822c8a

File tree

2 files changed

+37
-24
lines changed

2 files changed

+37
-24
lines changed

packages/@headlessui-react/CHANGELOG.md

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

1212
- [internal] Don’t set a focus fallback for Dialog’s in demo mode ([#3194](https://github.com/tailwindlabs/headlessui/pull/3194))
1313
- Ensure page doesn't scroll down when pressing `Escape` to close the `Dialog` component ([#3218](https://github.com/tailwindlabs/headlessui/pull/3218))
14+
- Fix crash when toggling between `virtual` and non-virtual mode in `Combobox` component ([#3236](https://github.com/tailwindlabs/headlessui/pull/3236))
1415

1516
### Deprecated
1617

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

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ enum ActionTypes {
125125

126126
SetActivationTrigger,
127127

128-
UpdateVirtualOptions,
128+
UpdateVirtualConfiguration,
129129
}
130130

131131
function adjustOrderedState<T>(
@@ -180,7 +180,11 @@ type Actions<T> =
180180
}
181181
| { type: ActionTypes.UnregisterOption; id: string }
182182
| { type: ActionTypes.SetActivationTrigger; trigger: ActivationTrigger }
183-
| { type: ActionTypes.UpdateVirtualOptions; options: T[] }
183+
| {
184+
type: ActionTypes.UpdateVirtualConfiguration
185+
options: T[]
186+
disabled: ((value: any) => boolean) | null
187+
}
184188

185189
let reducers: {
186190
[P in ActionTypes]: <T>(
@@ -236,16 +240,15 @@ let reducers: {
236240
}
237241

238242
if (state.virtual) {
243+
let { options, disabled } = state.virtual
239244
let activeOptionIndex =
240245
action.focus === Focus.Specific
241246
? action.idx
242247
: calculateActiveIndex(action, {
243-
resolveItems: () => state.virtual!.options,
248+
resolveItems: () => options,
244249
resolveActiveIndex: () =>
245-
state.activeOptionIndex ??
246-
state.virtual!.options.findIndex((option) => !state.virtual!.disabled(option)) ??
247-
null,
248-
resolveDisabled: state.virtual!.disabled,
250+
state.activeOptionIndex ?? options.findIndex((option) => !disabled(option)) ?? null,
251+
resolveDisabled: disabled,
249252
resolveId() {
250253
throw new Error('Function not implemented.')
251254
},
@@ -373,14 +376,21 @@ let reducers: {
373376
activationTrigger: action.trigger,
374377
}
375378
},
376-
[ActionTypes.UpdateVirtualOptions]: (state, action) => {
377-
if (state.virtual?.options === action.options) {
379+
[ActionTypes.UpdateVirtualConfiguration]: (state, action) => {
380+
if (state.virtual === null) {
381+
return {
382+
...state,
383+
virtual: { options: action.options, disabled: action.disabled ?? (() => false) },
384+
}
385+
}
386+
387+
if (state.virtual.options === action.options && state.virtual.disabled === action.disabled) {
378388
return state
379389
}
380390

381391
let adjustedActiveOptionIndex = state.activeOptionIndex
382392
if (state.activeOptionIndex !== null) {
383-
let idx = action.options.indexOf(state.virtual!.options[state.activeOptionIndex])
393+
let idx = action.options.indexOf(state.virtual.options[state.activeOptionIndex])
384394
if (idx !== -1) {
385395
adjustedActiveOptionIndex = idx
386396
} else {
@@ -391,7 +401,7 @@ let reducers: {
391401
return {
392402
...state,
393403
activeOptionIndex: adjustedActiveOptionIndex,
394-
virtual: Object.assign({}, state.virtual, { options: action.options }),
404+
virtual: { options: action.options, disabled: action.disabled ?? (() => false) },
395405
}
396406
},
397407
}
@@ -425,6 +435,7 @@ function VirtualProvider(props: {
425435
children: (data: { option: unknown; open: boolean }) => React.ReactElement
426436
}) {
427437
let data = useData('VirtualProvider')
438+
let { options } = data.virtual!
428439

429440
let [paddingStart, paddingEnd] = useMemo(() => {
430441
let el = data.optionsRef.current
@@ -441,7 +452,7 @@ function VirtualProvider(props: {
441452
let virtualizer = useVirtualizer({
442453
scrollPaddingStart: paddingStart,
443454
scrollPaddingEnd: paddingEnd,
444-
count: data.virtual!.options.length,
455+
count: options.length,
445456
estimateSize() {
446457
return 40
447458
},
@@ -454,7 +465,7 @@ function VirtualProvider(props: {
454465
let [baseKey, setBaseKey] = useState(0)
455466
useIsoMorphicEffect(() => {
456467
setBaseKey((v) => v + 1)
457-
}, [data.virtual?.options])
468+
}, [options])
458469

459470
let items = virtualizer.getVirtualItems()
460471

@@ -487,10 +498,7 @@ function VirtualProvider(props: {
487498
return
488499
}
489500

490-
if (
491-
data.activeOptionIndex !== null &&
492-
data.virtual!.options.length > data.activeOptionIndex
493-
) {
501+
if (data.activeOptionIndex !== null && options.length > data.activeOptionIndex) {
494502
virtualizer.scrollToIndex(data.activeOptionIndex)
495503
}
496504
}
@@ -501,13 +509,13 @@ function VirtualProvider(props: {
501509
<Fragment key={item.key}>
502510
{React.cloneElement(
503511
props.children?.({
504-
option: data.virtual!.options[item.index],
512+
option: options[item.index],
505513
open: data.comboboxState === ComboboxState.Open,
506514
}),
507515
{
508516
key: `${baseKey}-${item.key}`,
509517
'data-index': item.index,
510-
'aria-setsize': data.virtual!.options.length,
518+
'aria-setsize': options.length,
511519
'aria-posinset': item.index + 1,
512520
style: {
513521
position: 'absolute',
@@ -710,7 +718,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
710718
defaultValue,
711719
disabled,
712720
mode: multiple ? ValueMode.Multi : ValueMode.Single,
713-
virtual: state.virtual,
721+
virtual: virtual ? state.virtual : null,
714722
get activeOptionIndex() {
715723
if (
716724
defaultToFirstOption.current &&
@@ -719,7 +727,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
719727
) {
720728
if (virtual) {
721729
let localActiveOptionIndex = virtual.options.findIndex(
722-
(option) => !(virtual?.disabled?.(option) ?? false)
730+
(option) => !(virtual.disabled?.(option) ?? false)
723731
)
724732

725733
if (localActiveOptionIndex !== -1) {
@@ -748,8 +756,12 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
748756

749757
useIsoMorphicEffect(() => {
750758
if (!virtual) return
751-
dispatch({ type: ActionTypes.UpdateVirtualOptions, options: virtual.options })
752-
}, [virtual, virtual?.options])
759+
dispatch({
760+
type: ActionTypes.UpdateVirtualConfiguration,
761+
options: virtual.options,
762+
disabled: virtual.disabled ?? null,
763+
})
764+
}, [virtual, virtual?.options, virtual?.disabled])
753765

754766
useIsoMorphicEffect(() => {
755767
state.dataRef.current = data
@@ -1757,7 +1769,7 @@ function OptionFn<
17571769
let {
17581770
id = `headlessui-combobox-option-${internalId}`,
17591771
value,
1760-
disabled = data.virtual?.disabled(value) ?? false,
1772+
disabled = data.virtual?.disabled?.(value) ?? false,
17611773
order = null,
17621774
...theirProps
17631775
} = props

0 commit comments

Comments
 (0)