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
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
InjectionKey,
|
||||
PropType,
|
||||
Ref,
|
||||
UnwrapNestedRefs,
|
||||
} from 'vue'
|
||||
|
||||
import { Features, render, omit } from '../../utils/render'
|
||||
@@ -38,11 +39,11 @@ enum ActivationTrigger {
|
||||
Other,
|
||||
}
|
||||
|
||||
type ComboboxOptionDataRef = Ref<{
|
||||
type ComboboxOptionData = {
|
||||
disabled: boolean
|
||||
value: unknown
|
||||
domRef: Ref<HTMLElement | null>
|
||||
}>
|
||||
}
|
||||
type StateDefinition = {
|
||||
// State
|
||||
comboboxState: Ref<ComboboxStates>
|
||||
@@ -57,7 +58,7 @@ type StateDefinition = {
|
||||
optionsRef: Ref<HTMLDivElement | null>
|
||||
|
||||
disabled: Ref<boolean>
|
||||
options: Ref<{ id: string; dataRef: ComboboxOptionDataRef }[]>
|
||||
options: Ref<{ id: string; dataRef: ComputedRef<ComboboxOptionData> }[]>
|
||||
activeOptionIndex: Ref<number | null>
|
||||
activationTrigger: Ref<ActivationTrigger>
|
||||
|
||||
@@ -67,7 +68,7 @@ type StateDefinition = {
|
||||
goToOption(focus: Focus, id?: string, trigger?: ActivationTrigger): void
|
||||
selectOption(id: string): void
|
||||
selectActiveOption(): void
|
||||
registerOption(id: string, dataRef: ComboboxOptionDataRef): void
|
||||
registerOption(id: string, dataRef: ComputedRef<ComboboxOptionData>): void
|
||||
unregisterOption(id: string): void
|
||||
select(value: unknown): void
|
||||
}
|
||||
@@ -113,6 +114,36 @@ export let Combobox = defineComponent({
|
||||
let activationTrigger = ref<StateDefinition['activationTrigger']['value']>(
|
||||
ActivationTrigger.Other
|
||||
)
|
||||
|
||||
function adjustOrderedState(
|
||||
adjustment: (
|
||||
options: UnwrapNestedRefs<StateDefinition['options']['value']>
|
||||
) => UnwrapNestedRefs<StateDefinition['options']['value']> = (i) => i
|
||||
) {
|
||||
let currentActiveOption =
|
||||
activeOptionIndex.value !== null ? options.value[activeOptionIndex.value] : null
|
||||
|
||||
let sortedOptions = sortByDomNode(adjustment(options.value.slice()), (option) =>
|
||||
dom(option.dataRef.domRef)
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
let value = computed(() => props.modelValue)
|
||||
|
||||
let api = {
|
||||
@@ -149,15 +180,14 @@ export let Combobox = defineComponent({
|
||||
return
|
||||
}
|
||||
|
||||
let orderedOptions = sortByDomNode(options.value, (option) => option.dataRef.domRef.value)
|
||||
|
||||
let adjustedState = adjustOrderedState()
|
||||
let nextActiveOptionIndex = calculateActiveIndex(
|
||||
focus === Focus.Specific
|
||||
? { focus: Focus.Specific, id: id! }
|
||||
: { focus: focus as Exclude<Focus, Focus.Specific> },
|
||||
{
|
||||
resolveItems: () => orderedOptions,
|
||||
resolveActiveIndex: () => activeOptionIndex.value,
|
||||
resolveItems: () => adjustedState.options,
|
||||
resolveActiveIndex: () => adjustedState.activeOptionIndex,
|
||||
resolveId: (option) => option.id,
|
||||
resolveDisabled: (option) => option.dataRef.disabled,
|
||||
}
|
||||
@@ -165,7 +195,7 @@ export let Combobox = defineComponent({
|
||||
|
||||
activeOptionIndex.value = nextActiveOptionIndex
|
||||
activationTrigger.value = trigger ?? ActivationTrigger.Other
|
||||
options.value = orderedOptions
|
||||
options.value = adjustedState.options
|
||||
},
|
||||
syncInputValue() {
|
||||
let value = api.value.value
|
||||
@@ -196,37 +226,24 @@ export let Combobox = defineComponent({
|
||||
emit('update:modelValue', dataRef.value)
|
||||
api.syncInputValue()
|
||||
},
|
||||
registerOption(id: string, dataRef: ComboboxOptionDataRef) {
|
||||
let currentActiveOption =
|
||||
activeOptionIndex.value !== null ? options.value[activeOptionIndex.value] : null
|
||||
registerOption(id: string, dataRef: ComboboxOptionData) {
|
||||
let adjustedState = adjustOrderedState((options) => {
|
||||
return [...options, { id, dataRef }]
|
||||
})
|
||||
|
||||
// @ts-expect-error The expected type comes from property 'dataRef' which is declared here on type '{ id: string; dataRef: { textValue: string; disabled: boolean; }; }'
|
||||
options.value = [...options.value, { id, dataRef }]
|
||||
|
||||
// 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.
|
||||
activeOptionIndex.value = (() => {
|
||||
if (currentActiveOption === null) return null
|
||||
return options.value.indexOf(currentActiveOption)
|
||||
})()
|
||||
options.value = adjustedState.options
|
||||
activeOptionIndex.value = adjustedState.activeOptionIndex
|
||||
activationTrigger.value = ActivationTrigger.Other
|
||||
},
|
||||
unregisterOption(id: string) {
|
||||
let nextOptions = options.value.slice()
|
||||
let currentActiveOption =
|
||||
activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null
|
||||
let idx = nextOptions.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) nextOptions.splice(idx, 1)
|
||||
options.value = nextOptions
|
||||
activeOptionIndex.value = (() => {
|
||||
if (idx === activeOptionIndex.value) return null
|
||||
if (currentActiveOption === null) return null
|
||||
let adjustedState = adjustOrderedState((options) => {
|
||||
let idx = options.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) options.splice(idx, 1)
|
||||
return options
|
||||
})
|
||||
|
||||
// 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)
|
||||
})()
|
||||
options.value = adjustedState.options
|
||||
activeOptionIndex.value = adjustedState.activeOptionIndex
|
||||
activationTrigger.value = ActivationTrigger.Other
|
||||
},
|
||||
}
|
||||
@@ -636,7 +653,7 @@ export let ComboboxOption = defineComponent({
|
||||
|
||||
let selected = computed(() => toRaw(api.value.value) === toRaw(props.value))
|
||||
|
||||
let dataRef = computed<ComboboxOptionDataRef['value']>(() => ({
|
||||
let dataRef = computed<ComboboxOptionData>(() => ({
|
||||
disabled: props.disabled,
|
||||
value: props.value,
|
||||
domRef: internalOptionRef,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
watchEffect,
|
||||
toRaw,
|
||||
watch,
|
||||
UnwrapNestedRefs,
|
||||
} from 'vue'
|
||||
|
||||
import { Features, render, omit } from '../../utils/render'
|
||||
@@ -40,12 +41,12 @@ function nextFrame(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb))
|
||||
}
|
||||
|
||||
type ListboxOptionDataRef = Ref<{
|
||||
type ListboxOptionData = {
|
||||
textValue: string
|
||||
disabled: boolean
|
||||
value: unknown
|
||||
domRef: Ref<HTMLElement | null>
|
||||
}>
|
||||
}
|
||||
type StateDefinition = {
|
||||
// State
|
||||
listboxState: Ref<ListboxStates>
|
||||
@@ -57,7 +58,7 @@ type StateDefinition = {
|
||||
optionsRef: Ref<HTMLDivElement | null>
|
||||
|
||||
disabled: Ref<boolean>
|
||||
options: Ref<{ id: string; dataRef: ListboxOptionDataRef }[]>
|
||||
options: Ref<{ id: string; dataRef: ComputedRef<ListboxOptionData> }[]>
|
||||
searchQuery: Ref<string>
|
||||
activeOptionIndex: Ref<number | null>
|
||||
activationTrigger: Ref<ActivationTrigger>
|
||||
@@ -68,7 +69,7 @@ type StateDefinition = {
|
||||
goToOption(focus: Focus, id?: string, trigger?: ActivationTrigger): void
|
||||
search(value: string): void
|
||||
clearSearch(): void
|
||||
registerOption(id: string, dataRef: ListboxOptionDataRef): void
|
||||
registerOption(id: string, dataRef: ComputedRef<ListboxOptionData>): void
|
||||
unregisterOption(id: string): void
|
||||
select(value: unknown): void
|
||||
}
|
||||
@@ -110,6 +111,35 @@ export let Listbox = defineComponent({
|
||||
ActivationTrigger.Other
|
||||
)
|
||||
|
||||
function adjustOrderedState(
|
||||
adjustment: (
|
||||
options: UnwrapNestedRefs<StateDefinition['options']['value']>
|
||||
) => UnwrapNestedRefs<StateDefinition['options']['value']> = (i) => i
|
||||
) {
|
||||
let currentActiveOption =
|
||||
activeOptionIndex.value !== null ? options.value[activeOptionIndex.value] : null
|
||||
|
||||
let sortedOptions = sortByDomNode(adjustment(options.value.slice()), (option) =>
|
||||
dom(option.dataRef.domRef)
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
let value = computed(() => props.modelValue)
|
||||
|
||||
let api = {
|
||||
@@ -139,15 +169,14 @@ export let Listbox = defineComponent({
|
||||
if (props.disabled) return
|
||||
if (listboxState.value === ListboxStates.Closed) return
|
||||
|
||||
let orderedOptions = sortByDomNode(options.value, (option) => option.dataRef.domRef.value)
|
||||
|
||||
let adjustedState = adjustOrderedState()
|
||||
let nextActiveOptionIndex = calculateActiveIndex(
|
||||
focus === Focus.Specific
|
||||
? { focus: Focus.Specific, id: id! }
|
||||
: { focus: focus as Exclude<Focus, Focus.Specific> },
|
||||
{
|
||||
resolveItems: () => orderedOptions,
|
||||
resolveActiveIndex: () => activeOptionIndex.value,
|
||||
resolveItems: () => adjustedState.options,
|
||||
resolveActiveIndex: () => adjustedState.activeOptionIndex,
|
||||
resolveId: (option) => option.id,
|
||||
resolveDisabled: (option) => option.dataRef.disabled,
|
||||
}
|
||||
@@ -156,7 +185,7 @@ export let Listbox = defineComponent({
|
||||
searchQuery.value = ''
|
||||
activeOptionIndex.value = nextActiveOptionIndex
|
||||
activationTrigger.value = trigger ?? ActivationTrigger.Other
|
||||
options.value = orderedOptions
|
||||
options.value = adjustedState.options
|
||||
},
|
||||
search(value: string) {
|
||||
if (props.disabled) return
|
||||
@@ -192,25 +221,23 @@ export let Listbox = defineComponent({
|
||||
|
||||
searchQuery.value = ''
|
||||
},
|
||||
registerOption(id: string, dataRef: ListboxOptionDataRef) {
|
||||
// @ts-expect-error The expected type comes from property 'dataRef' which is declared here on type '{ id: string; dataRef: { textValue: string; disabled: boolean; }; }'
|
||||
options.value = [...options.value, { id, dataRef }]
|
||||
registerOption(id: string, dataRef: ListboxOptionData) {
|
||||
let adjustedState = adjustOrderedState((options) => {
|
||||
return [...options, { id, dataRef }]
|
||||
})
|
||||
|
||||
options.value = adjustedState.options
|
||||
activeOptionIndex.value = adjustedState.activeOptionIndex
|
||||
},
|
||||
unregisterOption(id: string) {
|
||||
let nextOptions = options.value.slice()
|
||||
let currentActiveOption =
|
||||
activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null
|
||||
let idx = nextOptions.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) nextOptions.splice(idx, 1)
|
||||
options.value = nextOptions
|
||||
activeOptionIndex.value = (() => {
|
||||
if (idx === activeOptionIndex.value) return null
|
||||
if (currentActiveOption === null) return null
|
||||
let adjustedState = adjustOrderedState((options) => {
|
||||
let idx = options.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) options.splice(idx, 1)
|
||||
return options
|
||||
})
|
||||
|
||||
// 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)
|
||||
})()
|
||||
options.value = adjustedState.options
|
||||
activeOptionIndex.value = adjustedState.activeOptionIndex
|
||||
activationTrigger.value = ActivationTrigger.Other
|
||||
},
|
||||
select(value: unknown) {
|
||||
@@ -526,7 +553,7 @@ export let ListboxOption = defineComponent({
|
||||
|
||||
let selected = computed(() => toRaw(api.value.value) === toRaw(props.value))
|
||||
|
||||
let dataRef = computed<ListboxOptionDataRef['value']>(() => ({
|
||||
let dataRef = computed<ListboxOptionData>(() => ({
|
||||
disabled: props.disabled,
|
||||
value: props.value,
|
||||
textValue: '',
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
InjectionKey,
|
||||
Ref,
|
||||
watchEffect,
|
||||
ComputedRef,
|
||||
UnwrapNestedRefs,
|
||||
} from 'vue'
|
||||
import { Features, render } from '../../utils/render'
|
||||
import { useId } from '../../hooks/use-id'
|
||||
@@ -37,17 +39,17 @@ function nextFrame(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb))
|
||||
}
|
||||
|
||||
type MenuItemDataRef = Ref<{
|
||||
type MenuItemData = {
|
||||
textValue: string
|
||||
disabled: boolean
|
||||
domRef: Ref<HTMLElement | null>
|
||||
}>
|
||||
}
|
||||
type StateDefinition = {
|
||||
// State
|
||||
menuState: Ref<MenuStates>
|
||||
buttonRef: Ref<HTMLButtonElement | null>
|
||||
itemsRef: Ref<HTMLDivElement | null>
|
||||
items: Ref<{ id: string; dataRef: MenuItemDataRef }[]>
|
||||
items: Ref<{ id: string; dataRef: ComputedRef<MenuItemData> }[]>
|
||||
searchQuery: Ref<string>
|
||||
activeItemIndex: Ref<number | null>
|
||||
activationTrigger: Ref<ActivationTrigger>
|
||||
@@ -58,7 +60,7 @@ type StateDefinition = {
|
||||
goToItem(focus: Focus, id?: string, trigger?: ActivationTrigger): void
|
||||
search(value: string): void
|
||||
clearSearch(): void
|
||||
registerItem(id: string, dataRef: MenuItemDataRef): void
|
||||
registerItem(id: string, dataRef: ComputedRef<MenuItemData>): void
|
||||
unregisterItem(id: string): void
|
||||
}
|
||||
|
||||
@@ -90,6 +92,35 @@ export let Menu = defineComponent({
|
||||
ActivationTrigger.Other
|
||||
)
|
||||
|
||||
function adjustOrderedState(
|
||||
adjustment: (
|
||||
items: UnwrapNestedRefs<StateDefinition['items']['value']>
|
||||
) => UnwrapNestedRefs<StateDefinition['items']['value']> = (i) => i
|
||||
) {
|
||||
let currentActiveItem =
|
||||
activeItemIndex.value !== null ? items.value[activeItemIndex.value] : null
|
||||
|
||||
let sortedItems = sortByDomNode(adjustment(items.value.slice()), (item) =>
|
||||
dom(item.dataRef.domRef)
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
let api = {
|
||||
menuState,
|
||||
buttonRef,
|
||||
@@ -104,14 +135,14 @@ export let Menu = defineComponent({
|
||||
},
|
||||
openMenu: () => (menuState.value = MenuStates.Open),
|
||||
goToItem(focus: Focus, id?: string, trigger?: ActivationTrigger) {
|
||||
let orderedItems = sortByDomNode(items.value, (item) => item.dataRef.domRef.value)
|
||||
let adjustedState = adjustOrderedState()
|
||||
let nextActiveItemIndex = calculateActiveIndex(
|
||||
focus === Focus.Specific
|
||||
? { focus: Focus.Specific, id: id! }
|
||||
: { focus: focus as Exclude<Focus, Focus.Specific> },
|
||||
{
|
||||
resolveItems: () => orderedItems,
|
||||
resolveActiveIndex: () => activeItemIndex.value,
|
||||
resolveItems: () => adjustedState.items,
|
||||
resolveActiveIndex: () => adjustedState.activeItemIndex,
|
||||
resolveId: (item) => item.id,
|
||||
resolveDisabled: (item) => item.dataRef.disabled,
|
||||
}
|
||||
@@ -120,7 +151,7 @@ export let Menu = defineComponent({
|
||||
searchQuery.value = ''
|
||||
activeItemIndex.value = nextActiveItemIndex
|
||||
activationTrigger.value = trigger ?? ActivationTrigger.Other
|
||||
items.value = orderedItems
|
||||
items.value = adjustedState.items
|
||||
},
|
||||
search(value: string) {
|
||||
let wasAlreadySearching = searchQuery.value !== ''
|
||||
@@ -147,25 +178,24 @@ export let Menu = defineComponent({
|
||||
clearSearch() {
|
||||
searchQuery.value = ''
|
||||
},
|
||||
registerItem(id: string, dataRef: MenuItemDataRef) {
|
||||
// @ts-expect-error The expected type comes from property 'dataRef' which is declared here on type '{ id: string; dataRef: { textValue: string; disabled: boolean; }; }'
|
||||
items.value = [...items.value, { id, dataRef }]
|
||||
registerItem(id: string, dataRef: MenuItemData) {
|
||||
let adjustedState = adjustOrderedState((items) => {
|
||||
return [...items, { id, dataRef }]
|
||||
})
|
||||
|
||||
items.value = adjustedState.items
|
||||
activeItemIndex.value = adjustedState.activeItemIndex
|
||||
activationTrigger.value = ActivationTrigger.Other
|
||||
},
|
||||
unregisterItem(id: string) {
|
||||
let nextItems = items.value.slice()
|
||||
let currentActiveItem =
|
||||
activeItemIndex.value !== null ? nextItems[activeItemIndex.value] : null
|
||||
let idx = nextItems.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) nextItems.splice(idx, 1)
|
||||
items.value = nextItems
|
||||
activeItemIndex.value = (() => {
|
||||
if (idx === activeItemIndex.value) return null
|
||||
if (currentActiveItem === null) return null
|
||||
let adjustedState = adjustOrderedState((items) => {
|
||||
let idx = items.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) items.splice(idx, 1)
|
||||
return items
|
||||
})
|
||||
|
||||
// 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)
|
||||
})()
|
||||
items.value = adjustedState.items
|
||||
activeItemIndex.value = adjustedState.activeItemIndex
|
||||
activationTrigger.value = ActivationTrigger.Other
|
||||
},
|
||||
}
|
||||
@@ -453,7 +483,7 @@ export let MenuItem = defineComponent({
|
||||
: false
|
||||
})
|
||||
|
||||
let dataRef = computed<MenuItemDataRef['value']>(() => ({
|
||||
let dataRef = computed<MenuItemData>(() => ({
|
||||
disabled: props.disabled,
|
||||
textValue: '',
|
||||
domRef: internalItemRef,
|
||||
|
||||
Reference in New Issue
Block a user