Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust active {item,option} index #1184

Merged
merged 2 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure that `appear` works regardless of multiple rerenders ([#1179](https://github.com/tailwindlabs/headlessui/pull/1179))
- Reset Combobox Input when the value gets reset ([#1181](https://github.com/tailwindlabs/headlessui/pull/1181))
- Fix double `beforeEnter` due to SSR ([#1183](https://github.com/tailwindlabs/headlessui/pull/1183))
- Adjust active {item,option} index ([#1184](https://github.com/tailwindlabs/headlessui/pull/1184))

## [Unreleased - @headlessui/vue]

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

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

Expand Down
77 changes: 43 additions & 34 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,35 @@ enum ActionTypes {
UnregisterOption,
}

function adjustOrderedState(
state: StateDefinition,
adjustment: (options: StateDefinition['options']) => StateDefinition['options'] = (i) => i
) {
let currentActiveOption =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null

let sortedOptions = sortByDomNode(
adjustment(state.options.slice()),
(option) => option.dataRef.current.domRef.current
)

// 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 the correct index.
let adjustedActiveOptionIndex = currentActiveOption
? sortedOptions.indexOf(currentActiveOption)
: null

// Reset to `null` in case the currentActiveOption was removed.
if (adjustedActiveOptionIndex === -1) {
adjustedActiveOptionIndex = null
}

return {
options: sortedOptions,
activeOptionIndex: adjustedActiveOptionIndex,
}
}

type Actions =
| { type: ActionTypes.CloseCombobox }
| { type: ActionTypes.OpenCombobox }
Expand Down Expand Up @@ -135,39 +164,29 @@ let reducers: {
return state
}

let options = sortByDomNode(state.options, (option) => option.dataRef.current.domRef.current)

let adjustedState = adjustOrderedState(state)
let activeOptionIndex = calculateActiveIndex(action, {
resolveItems: () => options,
resolveActiveIndex: () => state.activeOptionIndex,
resolveItems: () => adjustedState.options,
resolveActiveIndex: () => adjustedState.activeOptionIndex,
resolveId: (item) => item.id,
resolveDisabled: (item) => item.dataRef.current.disabled,
})

if (state.activeOptionIndex === activeOptionIndex) return state
return {
...state,
options, // Sorted options
...adjustedState,
activeOptionIndex,
activationTrigger: action.trigger ?? ActivationTrigger.Other,
}
},
[ActionTypes.RegisterOption]: (state, action) => {
let currentActiveOption =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
let adjustedState = adjustOrderedState(state, (options) => {
return [...options, { id: action.id, dataRef: action.dataRef }]
})

let options = [...state.options, { id: action.id, dataRef: action.dataRef }]
let nextState = {
...state,
options,
activeOptionIndex: (() => {
if (currentActiveOption === null) return null

// 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
// the correct index.
return options.indexOf(currentActiveOption)
})(),
...adjustedState,
activationTrigger: ActivationTrigger.Other,
}

Expand All @@ -181,25 +200,15 @@ let reducers: {
return nextState
},
[ActionTypes.UnregisterOption]: (state, action) => {
let nextOptions = state.options.slice()
let currentActiveOption =
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null

let idx = nextOptions.findIndex((a) => a.id === action.id)

if (idx !== -1) nextOptions.splice(idx, 1)
let adjustedState = adjustOrderedState(state, (options) => {
let idx = options.findIndex((a) => a.id === action.id)
if (idx !== -1) options.splice(idx, 1)
return options
})

return {
...state,
options: nextOptions,
activeOptionIndex: (() => {
if (idx === state.activeOptionIndex) return null
if (currentActiveOption === null) return null

// If we removed the option before the actual active index, then it would be out of sync. To
// fix this, we will find the correct (new) index position.
return nextOptions.indexOf(currentActiveOption)
})(),
...adjustedState,
activationTrigger: ActivationTrigger.Other,
}
},
Expand Down
73 changes: 47 additions & 26 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,35 @@ enum ActionTypes {
UnregisterOption,
}

function adjustOrderedState(
state: StateDefinition,
adjustment: (options: StateDefinition['options']) => StateDefinition['options'] = (i) => i
) {
let currentActiveOption =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null

let sortedOptions = sortByDomNode(
adjustment(state.options.slice()),
(option) => option.dataRef.current.domRef.current
)

// 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 the correct index.
let adjustedActiveOptionIndex = currentActiveOption
? sortedOptions.indexOf(currentActiveOption)
: null

// Reset to `null` in case the currentActiveOption was removed.
if (adjustedActiveOptionIndex === -1) {
adjustedActiveOptionIndex = null
}

return {
options: sortedOptions,
activeOptionIndex: adjustedActiveOptionIndex,
}
}

type Actions =
| { type: ActionTypes.CloseListbox }
| { type: ActionTypes.OpenListbox }
Expand Down Expand Up @@ -127,19 +156,17 @@ 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 adjustedState = adjustOrderedState(state)
let activeOptionIndex = calculateActiveIndex(action, {
resolveItems: () => options,
resolveActiveIndex: () => state.activeOptionIndex,
resolveId: (item) => item.id,
resolveDisabled: (item) => item.dataRef.current.disabled,
resolveItems: () => adjustedState.options,
resolveActiveIndex: () => adjustedState.activeOptionIndex,
resolveId: (option) => option.id,
resolveDisabled: (option) => option.dataRef.current.disabled,
})

if (state.searchQuery === '' && state.activeOptionIndex === activeOptionIndex) return state
return {
...state,
options, // Sorted options
...adjustedState,
searchQuery: '',
activeOptionIndex,
activationTrigger: action.trigger ?? ActivationTrigger.Other,
Expand Down Expand Up @@ -184,29 +211,23 @@ let reducers: {
return { ...state, searchQuery: '' }
},
[ActionTypes.RegisterOption]: (state, action) => {
let options = [...state.options, { id: action.id, dataRef: action.dataRef }]
return { ...state, options }
let adjustedState = adjustOrderedState(state, (options) => [
...options,
{ id: action.id, dataRef: action.dataRef },
])

return { ...state, ...adjustedState }
},
[ActionTypes.UnregisterOption]: (state, action) => {
let nextOptions = state.options.slice()
let currentActiveOption =
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null

let idx = nextOptions.findIndex((a) => a.id === action.id)

if (idx !== -1) nextOptions.splice(idx, 1)
let adjustedState = adjustOrderedState(state, (options) => {
let idx = options.findIndex((a) => a.id === action.id)
if (idx !== -1) options.splice(idx, 1)
return options
})

return {
...state,
options: nextOptions,
activeOptionIndex: (() => {
if (idx === state.activeOptionIndex) return null
if (currentActiveOption === null) return null

// If we removed the option before the actual active index, then it would be out of sync. To
// fix this, we will find the correct (new) index position.
return nextOptions.indexOf(currentActiveOption)
})(),
...adjustedState,
activationTrigger: ActivationTrigger.Other,
}
},
Expand Down
65 changes: 42 additions & 23 deletions packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ enum ActionTypes {
UnregisterItem,
}

function adjustOrderedState(
state: StateDefinition,
adjustment: (items: StateDefinition['items']) => StateDefinition['items'] = (i) => i
) {
let currentActiveItem = state.activeItemIndex !== null ? state.items[state.activeItemIndex] : null

let sortedItems = sortByDomNode(
adjustment(state.items.slice()),
(item) => item.dataRef.current.domRef.current
)

// If we inserted an item before the current active item then the active item index
// would be wrong. To fix this, we will re-lookup the correct index.
let adjustedActiveItemIndex = currentActiveItem ? sortedItems.indexOf(currentActiveItem) : null

// Reset to `null` in case the currentActiveItem was removed.
if (adjustedActiveItemIndex === -1) {
adjustedActiveItemIndex = null
}

return {
items: sortedItems,
activeItemIndex: adjustedActiveItemIndex,
}
}

type Actions =
| { type: ActionTypes.CloseMenu }
| { type: ActionTypes.OpenMenu }
Expand Down Expand Up @@ -102,19 +128,17 @@ let reducers: {
return { ...state, menuState: MenuStates.Open }
},
[ActionTypes.GoToItem]: (state, action) => {
let items = sortByDomNode(state.items, (item) => item.dataRef.current.domRef.current)

let adjustedState = adjustOrderedState(state)
let activeItemIndex = calculateActiveIndex(action, {
resolveItems: () => items,
resolveActiveIndex: () => state.activeItemIndex,
resolveItems: () => adjustedState.items,
resolveActiveIndex: () => adjustedState.activeItemIndex,
resolveId: (item) => item.id,
resolveDisabled: (item) => item.dataRef.current.disabled,
})

if (state.searchQuery === '' && state.activeItemIndex === activeItemIndex) return state
return {
...state,
items, // Sorted items
...adjustedState,
searchQuery: '',
activeItemIndex,
activationTrigger: action.trigger ?? ActivationTrigger.Other,
Expand Down Expand Up @@ -151,28 +175,23 @@ let reducers: {
return { ...state, searchQuery: '', searchActiveItemIndex: null }
},
[ActionTypes.RegisterItem]: (state, action) => {
let items = [...state.items, { id: action.id, dataRef: action.dataRef }]
return { ...state, items }
let adjustedState = adjustOrderedState(state, (items) => [
...items,
{ id: action.id, dataRef: action.dataRef },
])

return { ...state, ...adjustedState }
},
[ActionTypes.UnregisterItem]: (state, action) => {
let nextItems = state.items.slice()
let currentActiveItem = state.activeItemIndex !== null ? nextItems[state.activeItemIndex] : null

let idx = nextItems.findIndex((a) => a.id === action.id)

if (idx !== -1) nextItems.splice(idx, 1)
let adjustedState = adjustOrderedState(state, (items) => {
let idx = items.findIndex((a) => a.id === action.id)
if (idx !== -1) items.splice(idx, 1)
return items
})

return {
...state,
items: nextItems,
activeItemIndex: (() => {
if (idx === state.activeItemIndex) return null
if (currentActiveItem === null) return null

// If we removed the item before the actual active index, then it would be out of sync. To
// fix this, we will find the correct (new) index position.
return nextItems.indexOf(currentActiveItem)
})(),
...adjustedState,
activationTrigger: ActivationTrigger.Other,
}
},
Expand Down
Loading