Ensure typeahead stays on same item if it still matches (#1098)
* ensure typeahead stays on same item if it still matches Fixes: #1090 * update changelog
This commit is contained in:
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improve typeahead search logic ([#1051](https://github.com/tailwindlabs/headlessui/pull/1051))
|
||||
- Improve overal codebase, use modern tech like `esbuild` and TypeScript 4! ([#1055](https://github.com/tailwindlabs/headlessui/pull/1055))
|
||||
- Improve build files ([#1078](https://github.com/tailwindlabs/headlessui/pull/1078))
|
||||
- Ensure typeahead stays on same item if it still matches ([#1098](https://github.com/tailwindlabs/headlessui/pull/1098))
|
||||
|
||||
### Added
|
||||
|
||||
@@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improve typeahead search logic ([#1051](https://github.com/tailwindlabs/headlessui/pull/1051))
|
||||
- Improve overal codebase, use modern tech like `esbuild` and TypeScript 4! ([#1055](https://github.com/tailwindlabs/headlessui/pull/1055))
|
||||
- Improve build files ([#1078](https://github.com/tailwindlabs/headlessui/pull/1078))
|
||||
- Ensure typeahead stays on same item if it still matches ([#1098](https://github.com/tailwindlabs/headlessui/pull/1098))
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -3108,6 +3108,79 @@ describe('Keyboard interactions', () => {
|
||||
assertActiveListboxOption(options[3])
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should stay on the same item while keystrokes still match',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Listbox value={undefined} onChange={console.log}>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
<Listbox.Option value="a">alice</Listbox.Option>
|
||||
<Listbox.Option value="b">bob</Listbox.Option>
|
||||
<Listbox.Option value="c">charlie</Listbox.Option>
|
||||
<Listbox.Option value="d">bob</Listbox.Option>
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
)
|
||||
|
||||
// Open listbox
|
||||
await click(getListboxButton())
|
||||
|
||||
let options = getListboxOptions()
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first option
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "b" in "bob"
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertActiveListboxOption(options[1])
|
||||
|
||||
// Search for "b" in "bob" again
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertActiveListboxOption(options[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first option
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bo" in "bob"
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertActiveListboxOption(options[1])
|
||||
|
||||
// Search for "bo" in "bob" again
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertActiveListboxOption(options[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first option
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bob" in "bob"
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertActiveListboxOption(options[1])
|
||||
|
||||
// Search for "bob" in "bob" again
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertActiveListboxOption(options[3])
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -130,13 +130,16 @@ let reducers: {
|
||||
if (state.disabled) return state
|
||||
if (state.listboxState === ListboxStates.Closed) return state
|
||||
|
||||
let wasAlreadySearching = state.searchQuery !== ''
|
||||
let offset = wasAlreadySearching ? 0 : 1
|
||||
|
||||
let searchQuery = state.searchQuery + action.value.toLowerCase()
|
||||
|
||||
let reOrderedOptions =
|
||||
state.activeOptionIndex !== null
|
||||
? state.options
|
||||
.slice(state.activeOptionIndex + 1)
|
||||
.concat(state.options.slice(0, state.activeOptionIndex + 1))
|
||||
.slice(state.activeOptionIndex + offset)
|
||||
.concat(state.options.slice(0, state.activeOptionIndex + offset))
|
||||
: state.options
|
||||
|
||||
let matchingOption = reOrderedOptions.find(
|
||||
|
||||
@@ -2698,6 +2698,79 @@ describe('Keyboard interactions', () => {
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should stay on the same item while keystrokes still match',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Menu>
|
||||
<Menu.Button>Trigger</Menu.Button>
|
||||
<Menu.Items>
|
||||
<Menu.Item as="a">alice</Menu.Item>
|
||||
<Menu.Item as="a">bob</Menu.Item>
|
||||
<Menu.Item as="a">charlie</Menu.Item>
|
||||
<Menu.Item as="a">bob</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
)
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
let items = getMenuItems()
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first item
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "b" in "bob"
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertMenuLinkedWithMenuItem(items[1])
|
||||
|
||||
// Search for "b" in "bob" again
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first item
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bo" in "bob"
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertMenuLinkedWithMenuItem(items[1])
|
||||
|
||||
// Search for "bo" in "bob" again
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first item
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bob" in "bob"
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertMenuLinkedWithMenuItem(items[1])
|
||||
|
||||
// Search for "bob" in "bob" again
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -99,13 +99,15 @@ let reducers: {
|
||||
return { ...state, searchQuery: '', activeItemIndex }
|
||||
},
|
||||
[ActionTypes.Search]: (state, action) => {
|
||||
let wasAlreadySearching = state.searchQuery !== ''
|
||||
let offset = wasAlreadySearching ? 0 : 1
|
||||
let searchQuery = state.searchQuery + action.value.toLowerCase()
|
||||
|
||||
let reOrderedItems =
|
||||
state.activeItemIndex !== null
|
||||
? state.items
|
||||
.slice(state.activeItemIndex + 1)
|
||||
.concat(state.items.slice(0, state.activeItemIndex + 1))
|
||||
.slice(state.activeItemIndex + offset)
|
||||
.concat(state.items.slice(0, state.activeItemIndex + offset))
|
||||
: state.items
|
||||
|
||||
let matchingItem = reOrderedItems.find(
|
||||
|
||||
@@ -3231,6 +3231,82 @@ describe('Keyboard interactions', () => {
|
||||
assertActiveListboxOption(options[3])
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should stay on the same item while keystrokes still match',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption value="a">alice</ListboxOption>
|
||||
<ListboxOption value="b">bob</ListboxOption>
|
||||
<ListboxOption value="c">charlie</ListboxOption>
|
||||
<ListboxOption value="b">bob</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
`,
|
||||
setup: () => ({ value: ref(null) }),
|
||||
})
|
||||
|
||||
// Open listbox
|
||||
await click(getListboxButton())
|
||||
|
||||
let options = getListboxOptions()
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first option
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "b" in "bob"
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertActiveListboxOption(options[1])
|
||||
|
||||
// Search for "b" in "bob" again
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertActiveListboxOption(options[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first option
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bo" in "bob"
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertActiveListboxOption(options[1])
|
||||
|
||||
// Search for "bo" in "bob" again
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertActiveListboxOption(options[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first option
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bob" in "bob"
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertActiveListboxOption(options[1])
|
||||
|
||||
// Search for "bob" in "bob" again
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertActiveListboxOption(options[3])
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -143,13 +143,16 @@ export let Listbox = defineComponent({
|
||||
if (props.disabled) return
|
||||
if (listboxState.value === ListboxStates.Closed) return
|
||||
|
||||
let wasAlreadySearching = searchQuery.value !== ''
|
||||
let offset = wasAlreadySearching ? 0 : 1
|
||||
|
||||
searchQuery.value += value.toLowerCase()
|
||||
|
||||
let reOrderedOptions =
|
||||
activeOptionIndex.value !== null
|
||||
? options.value
|
||||
.slice(activeOptionIndex.value + 1)
|
||||
.concat(options.value.slice(0, activeOptionIndex.value + 1))
|
||||
.slice(activeOptionIndex.value + offset)
|
||||
.concat(options.value.slice(0, activeOptionIndex.value + offset))
|
||||
: options.value
|
||||
|
||||
let matchingOption = reOrderedOptions.find(
|
||||
|
||||
@@ -2792,6 +2792,79 @@ describe('Keyboard interactions', () => {
|
||||
// We should be on the second `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
})
|
||||
|
||||
it(
|
||||
'should stay on the same item while keystrokes still match',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate(jsx`
|
||||
<Menu>
|
||||
<MenuButton>Trigger</MenuButton>
|
||||
<MenuItems>
|
||||
<MenuItem as="a">alice</MenuItem>
|
||||
<MenuItem as="a">bob</MenuItem>
|
||||
<MenuItem as="a">charlie</MenuItem>
|
||||
<MenuItem as="a">bob</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
`)
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
let items = getMenuItems()
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first item
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "b" in "bob"
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertMenuLinkedWithMenuItem(items[1])
|
||||
|
||||
// Search for "b" in "bob" again
|
||||
await type(word('b'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first item
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bo" in "bob"
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertMenuLinkedWithMenuItem(items[1])
|
||||
|
||||
// Search for "bo" in "bob" again
|
||||
await type(word('bo'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
|
||||
// ---
|
||||
|
||||
// Reset: Go to first item
|
||||
await press(Keys.Home)
|
||||
|
||||
// Search for "bob" in "bob"
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the first `bob`
|
||||
assertMenuLinkedWithMenuItem(items[1])
|
||||
|
||||
// Search for "bob" in "bob" again
|
||||
await type(word('bob'))
|
||||
|
||||
// We should be on the next `bob`
|
||||
assertMenuLinkedWithMenuItem(items[3])
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -106,13 +106,15 @@ export let Menu = defineComponent({
|
||||
activeItemIndex.value = nextActiveItemIndex
|
||||
},
|
||||
search(value: string) {
|
||||
let wasAlreadySearching = searchQuery.value !== ''
|
||||
let offset = wasAlreadySearching ? 0 : 1
|
||||
searchQuery.value += value.toLowerCase()
|
||||
|
||||
let reOrderedItems =
|
||||
activeItemIndex.value !== null
|
||||
? items.value
|
||||
.slice(activeItemIndex.value + 1)
|
||||
.concat(items.value.slice(0, activeItemIndex.value + 1))
|
||||
.slice(activeItemIndex.value + offset)
|
||||
.concat(items.value.slice(0, activeItemIndex.value + offset))
|
||||
: items.value
|
||||
|
||||
let matchingItem = reOrderedItems.find(
|
||||
|
||||
Reference in New Issue
Block a user