Skip to content

Commit 8208c07

Browse files
authored
Adjust active {item,option} index (#1184)
* adjust active {item,option} index We had various ordering issues, and now we properly sort all the notes which is awesome. However, there is this case where we still use the `activeOptionIndex` / `activeItemIndex` from _before_ the sort happens. Now we will ensure that this is properly adjusted when performing the sort of the items. In addition, we will also properly adjust these values when `registering` and `unregistering` items, not only when performing actions. * update changelog
1 parent 8e7478d commit 8208c07

File tree

7 files changed

+299
-174
lines changed

7 files changed

+299
-174
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Ensure that `appear` works regardless of multiple rerenders ([#1179](https://github.com/tailwindlabs/headlessui/pull/1179))
2020
- Reset Combobox Input when the value gets reset ([#1181](https://github.com/tailwindlabs/headlessui/pull/1181))
2121
- Fix double `beforeEnter` due to SSR ([#1183](https://github.com/tailwindlabs/headlessui/pull/1183))
22+
- Adjust active {item,option} index ([#1184](https://github.com/tailwindlabs/headlessui/pull/1184))
2223

2324
## [Unreleased - @headlessui/vue]
2425

@@ -32,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3233
- Guarantee DOM sort order when performing actions ([#1168](https://github.com/tailwindlabs/headlessui/pull/1168))
3334
- Improve outside click support ([#1175](https://github.com/tailwindlabs/headlessui/pull/1175))
3435
- Reset Combobox Input when the value gets reset ([#1181](https://github.com/tailwindlabs/headlessui/pull/1181))
36+
- Adjust active {item,option} index ([#1184](https://github.com/tailwindlabs/headlessui/pull/1184))
3537

3638
## [@headlessui/react@v1.5.0] - 2022-02-17
3739

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

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,35 @@ enum ActionTypes {
9292
UnregisterOption,
9393
}
9494

95+
function adjustOrderedState(
96+
state: StateDefinition,
97+
adjustment: (options: StateDefinition['options']) => StateDefinition['options'] = (i) => i
98+
) {
99+
let currentActiveOption =
100+
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
101+
102+
let sortedOptions = sortByDomNode(
103+
adjustment(state.options.slice()),
104+
(option) => option.dataRef.current.domRef.current
105+
)
106+
107+
// If we inserted an option before the current active option then the active option index
108+
// would be wrong. To fix this, we will re-lookup the correct index.
109+
let adjustedActiveOptionIndex = currentActiveOption
110+
? sortedOptions.indexOf(currentActiveOption)
111+
: null
112+
113+
// Reset to `null` in case the currentActiveOption was removed.
114+
if (adjustedActiveOptionIndex === -1) {
115+
adjustedActiveOptionIndex = null
116+
}
117+
118+
return {
119+
options: sortedOptions,
120+
activeOptionIndex: adjustedActiveOptionIndex,
121+
}
122+
}
123+
95124
type Actions =
96125
| { type: ActionTypes.CloseCombobox }
97126
| { type: ActionTypes.OpenCombobox }
@@ -135,39 +164,29 @@ let reducers: {
135164
return state
136165
}
137166

138-
let options = sortByDomNode(state.options, (option) => option.dataRef.current.domRef.current)
139-
167+
let adjustedState = adjustOrderedState(state)
140168
let activeOptionIndex = calculateActiveIndex(action, {
141-
resolveItems: () => options,
142-
resolveActiveIndex: () => state.activeOptionIndex,
169+
resolveItems: () => adjustedState.options,
170+
resolveActiveIndex: () => adjustedState.activeOptionIndex,
143171
resolveId: (item) => item.id,
144172
resolveDisabled: (item) => item.dataRef.current.disabled,
145173
})
146174

147-
if (state.activeOptionIndex === activeOptionIndex) return state
148175
return {
149176
...state,
150-
options, // Sorted options
177+
...adjustedState,
151178
activeOptionIndex,
152179
activationTrigger: action.trigger ?? ActivationTrigger.Other,
153180
}
154181
},
155182
[ActionTypes.RegisterOption]: (state, action) => {
156-
let currentActiveOption =
157-
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
183+
let adjustedState = adjustOrderedState(state, (options) => {
184+
return [...options, { id: action.id, dataRef: action.dataRef }]
185+
})
158186

159-
let options = [...state.options, { id: action.id, dataRef: action.dataRef }]
160187
let nextState = {
161188
...state,
162-
options,
163-
activeOptionIndex: (() => {
164-
if (currentActiveOption === null) return null
165-
166-
// If we inserted an option before the current active option then the
167-
// active option index would be wrong. To fix this, we will re-lookup
168-
// the correct index.
169-
return options.indexOf(currentActiveOption)
170-
})(),
189+
...adjustedState,
171190
activationTrigger: ActivationTrigger.Other,
172191
}
173192

@@ -181,25 +200,15 @@ let reducers: {
181200
return nextState
182201
},
183202
[ActionTypes.UnregisterOption]: (state, action) => {
184-
let nextOptions = state.options.slice()
185-
let currentActiveOption =
186-
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null
187-
188-
let idx = nextOptions.findIndex((a) => a.id === action.id)
189-
190-
if (idx !== -1) nextOptions.splice(idx, 1)
203+
let adjustedState = adjustOrderedState(state, (options) => {
204+
let idx = options.findIndex((a) => a.id === action.id)
205+
if (idx !== -1) options.splice(idx, 1)
206+
return options
207+
})
191208

192209
return {
193210
...state,
194-
options: nextOptions,
195-
activeOptionIndex: (() => {
196-
if (idx === state.activeOptionIndex) return null
197-
if (currentActiveOption === null) return null
198-
199-
// If we removed the option before the actual active index, then it would be out of sync. To
200-
// fix this, we will find the correct (new) index position.
201-
return nextOptions.indexOf(currentActiveOption)
202-
})(),
211+
...adjustedState,
203212
activationTrigger: ActivationTrigger.Other,
204213
}
205214
},

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

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,35 @@ enum ActionTypes {
8383
UnregisterOption,
8484
}
8585

86+
function adjustOrderedState(
87+
state: StateDefinition,
88+
adjustment: (options: StateDefinition['options']) => StateDefinition['options'] = (i) => i
89+
) {
90+
let currentActiveOption =
91+
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
92+
93+
let sortedOptions = sortByDomNode(
94+
adjustment(state.options.slice()),
95+
(option) => option.dataRef.current.domRef.current
96+
)
97+
98+
// If we inserted an option before the current active option then the active option index
99+
// would be wrong. To fix this, we will re-lookup the correct index.
100+
let adjustedActiveOptionIndex = currentActiveOption
101+
? sortedOptions.indexOf(currentActiveOption)
102+
: null
103+
104+
// Reset to `null` in case the currentActiveOption was removed.
105+
if (adjustedActiveOptionIndex === -1) {
106+
adjustedActiveOptionIndex = null
107+
}
108+
109+
return {
110+
options: sortedOptions,
111+
activeOptionIndex: adjustedActiveOptionIndex,
112+
}
113+
}
114+
86115
type Actions =
87116
| { type: ActionTypes.CloseListbox }
88117
| { type: ActionTypes.OpenListbox }
@@ -127,19 +156,17 @@ let reducers: {
127156
if (state.disabled) return state
128157
if (state.listboxState === ListboxStates.Closed) return state
129158

130-
let options = sortByDomNode(state.options, (option) => option.dataRef.current.domRef.current)
131-
159+
let adjustedState = adjustOrderedState(state)
132160
let activeOptionIndex = calculateActiveIndex(action, {
133-
resolveItems: () => options,
134-
resolveActiveIndex: () => state.activeOptionIndex,
135-
resolveId: (item) => item.id,
136-
resolveDisabled: (item) => item.dataRef.current.disabled,
161+
resolveItems: () => adjustedState.options,
162+
resolveActiveIndex: () => adjustedState.activeOptionIndex,
163+
resolveId: (option) => option.id,
164+
resolveDisabled: (option) => option.dataRef.current.disabled,
137165
})
138166

139-
if (state.searchQuery === '' && state.activeOptionIndex === activeOptionIndex) return state
140167
return {
141168
...state,
142-
options, // Sorted options
169+
...adjustedState,
143170
searchQuery: '',
144171
activeOptionIndex,
145172
activationTrigger: action.trigger ?? ActivationTrigger.Other,
@@ -184,29 +211,23 @@ let reducers: {
184211
return { ...state, searchQuery: '' }
185212
},
186213
[ActionTypes.RegisterOption]: (state, action) => {
187-
let options = [...state.options, { id: action.id, dataRef: action.dataRef }]
188-
return { ...state, options }
214+
let adjustedState = adjustOrderedState(state, (options) => [
215+
...options,
216+
{ id: action.id, dataRef: action.dataRef },
217+
])
218+
219+
return { ...state, ...adjustedState }
189220
},
190221
[ActionTypes.UnregisterOption]: (state, action) => {
191-
let nextOptions = state.options.slice()
192-
let currentActiveOption =
193-
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null
194-
195-
let idx = nextOptions.findIndex((a) => a.id === action.id)
196-
197-
if (idx !== -1) nextOptions.splice(idx, 1)
222+
let adjustedState = adjustOrderedState(state, (options) => {
223+
let idx = options.findIndex((a) => a.id === action.id)
224+
if (idx !== -1) options.splice(idx, 1)
225+
return options
226+
})
198227

199228
return {
200229
...state,
201-
options: nextOptions,
202-
activeOptionIndex: (() => {
203-
if (idx === state.activeOptionIndex) return null
204-
if (currentActiveOption === null) return null
205-
206-
// If we removed the option before the actual active index, then it would be out of sync. To
207-
// fix this, we will find the correct (new) index position.
208-
return nextOptions.indexOf(currentActiveOption)
209-
})(),
230+
...adjustedState,
210231
activationTrigger: ActivationTrigger.Other,
211232
}
212233
},

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

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,32 @@ enum ActionTypes {
7373
UnregisterItem,
7474
}
7575

76+
function adjustOrderedState(
77+
state: StateDefinition,
78+
adjustment: (items: StateDefinition['items']) => StateDefinition['items'] = (i) => i
79+
) {
80+
let currentActiveItem = state.activeItemIndex !== null ? state.items[state.activeItemIndex] : null
81+
82+
let sortedItems = sortByDomNode(
83+
adjustment(state.items.slice()),
84+
(item) => item.dataRef.current.domRef.current
85+
)
86+
87+
// If we inserted an item before the current active item then the active item index
88+
// would be wrong. To fix this, we will re-lookup the correct index.
89+
let adjustedActiveItemIndex = currentActiveItem ? sortedItems.indexOf(currentActiveItem) : null
90+
91+
// Reset to `null` in case the currentActiveItem was removed.
92+
if (adjustedActiveItemIndex === -1) {
93+
adjustedActiveItemIndex = null
94+
}
95+
96+
return {
97+
items: sortedItems,
98+
activeItemIndex: adjustedActiveItemIndex,
99+
}
100+
}
101+
76102
type Actions =
77103
| { type: ActionTypes.CloseMenu }
78104
| { type: ActionTypes.OpenMenu }
@@ -102,19 +128,17 @@ let reducers: {
102128
return { ...state, menuState: MenuStates.Open }
103129
},
104130
[ActionTypes.GoToItem]: (state, action) => {
105-
let items = sortByDomNode(state.items, (item) => item.dataRef.current.domRef.current)
106-
131+
let adjustedState = adjustOrderedState(state)
107132
let activeItemIndex = calculateActiveIndex(action, {
108-
resolveItems: () => items,
109-
resolveActiveIndex: () => state.activeItemIndex,
133+
resolveItems: () => adjustedState.items,
134+
resolveActiveIndex: () => adjustedState.activeItemIndex,
110135
resolveId: (item) => item.id,
111136
resolveDisabled: (item) => item.dataRef.current.disabled,
112137
})
113138

114-
if (state.searchQuery === '' && state.activeItemIndex === activeItemIndex) return state
115139
return {
116140
...state,
117-
items, // Sorted items
141+
...adjustedState,
118142
searchQuery: '',
119143
activeItemIndex,
120144
activationTrigger: action.trigger ?? ActivationTrigger.Other,
@@ -151,28 +175,23 @@ let reducers: {
151175
return { ...state, searchQuery: '', searchActiveItemIndex: null }
152176
},
153177
[ActionTypes.RegisterItem]: (state, action) => {
154-
let items = [...state.items, { id: action.id, dataRef: action.dataRef }]
155-
return { ...state, items }
178+
let adjustedState = adjustOrderedState(state, (items) => [
179+
...items,
180+
{ id: action.id, dataRef: action.dataRef },
181+
])
182+
183+
return { ...state, ...adjustedState }
156184
},
157185
[ActionTypes.UnregisterItem]: (state, action) => {
158-
let nextItems = state.items.slice()
159-
let currentActiveItem = state.activeItemIndex !== null ? nextItems[state.activeItemIndex] : null
160-
161-
let idx = nextItems.findIndex((a) => a.id === action.id)
162-
163-
if (idx !== -1) nextItems.splice(idx, 1)
186+
let adjustedState = adjustOrderedState(state, (items) => {
187+
let idx = items.findIndex((a) => a.id === action.id)
188+
if (idx !== -1) items.splice(idx, 1)
189+
return items
190+
})
164191

165192
return {
166193
...state,
167-
items: nextItems,
168-
activeItemIndex: (() => {
169-
if (idx === state.activeItemIndex) return null
170-
if (currentActiveItem === null) return null
171-
172-
// If we removed the item before the actual active index, then it would be out of sync. To
173-
// fix this, we will find the correct (new) index position.
174-
return nextItems.indexOf(currentActiveItem)
175-
})(),
194+
...adjustedState,
176195
activationTrigger: ActivationTrigger.Other,
177196
}
178197
},

0 commit comments

Comments
 (0)