Enable search in v-select for string items (#19736)

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Pascal Jufer
2023-09-26 15:40:20 +02:00
committed by GitHub
parent c67775a9a0
commit 74ba9f261f
4 changed files with 311 additions and 55 deletions

View File

@@ -0,0 +1,5 @@
---
"@directus/app": patch
---
Enabled search in `v-select` component for string items

View File

@@ -1,3 +1,169 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Mount component 1`] = `"<v-menu-stub data-v-cdcb6889=\\"\\" class=\\"v-select\\" disabled=\\"false\\" attached=\\"true\\" is-same-width=\\"true\\" show-arrow=\\"false\\" close-on-content-click=\\"true\\" placement=\\"bottom\\"></v-menu-stub>"`;
exports[`should hide items not matching search value > object items 1`] = `
"<div data-v-cdcb6889=\\"\\" id=\\"v-menu-stub\\" class=\\"v-select\\" disabled=\\"false\\" attached=\\"true\\" is-same-width=\\"true\\" show-arrow=\\"false\\" close-on-content-click=\\"true\\" placement=\\"bottom\\">
<div data-v-cdcb6889=\\"\\" id=\\"v-input-stub\\" full-width=\\"true\\" readonly=\\"\\" clickable=\\"\\" disabled=\\"false\\"></div>
<ul data-v-ff20e609=\\"\\" data-v-cdcb6889=\\"\\" class=\\"v-list list\\">
<!--v-if-->
<div data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\">
<div data-v-cdcb6889=\\"\\" id=\\"v-list-item-content-stub\\">
<div data-v-cdcb6889=\\"\\" id=\\"v-input-stub\\" autofocus=\\"\\" small=\\"\\" placeholder=\\"search\\" modelvalue=\\"Item 1\\"></div>
</div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item1\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 1</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item2\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 2</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item3\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 3</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item4\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 4</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item5\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 5</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item6\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 6</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item7\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 7</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item8\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 8</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item9\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 9</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item10\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 10</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item11\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 11</span></div>
</div>
<!--v-if-->
<!--v-if-->
</ul>
</div>"
`;
exports[`should hide items not matching search value > string items 1`] = `
"<div data-v-cdcb6889=\\"\\" id=\\"v-menu-stub\\" class=\\"v-select\\" disabled=\\"false\\" attached=\\"true\\" is-same-width=\\"true\\" show-arrow=\\"false\\" close-on-content-click=\\"true\\" placement=\\"bottom\\">
<div data-v-cdcb6889=\\"\\" id=\\"v-input-stub\\" full-width=\\"true\\" readonly=\\"\\" clickable=\\"\\" disabled=\\"false\\"></div>
<ul data-v-ff20e609=\\"\\" data-v-cdcb6889=\\"\\" class=\\"v-list list\\">
<!--v-if-->
<div data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\">
<div data-v-cdcb6889=\\"\\" id=\\"v-list-item-content-stub\\">
<div data-v-cdcb6889=\\"\\" id=\\"v-input-stub\\" autofocus=\\"\\" small=\\"\\" placeholder=\\"search\\" modelvalue=\\"Item 1\\"></div>
</div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 1\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 1</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 2\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 2</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 3\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 3</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 4\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 4</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 5\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 5</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 6\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 6</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 7\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 7</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 8\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 8</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 9\\" style=\\"display: none;\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 9</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 10\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 10</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 11\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 11</span></div>
</div>
<!--v-if-->
<!--v-if-->
</ul>
</div>"
`;
exports[`should render with object items 1`] = `
"<div data-v-cdcb6889=\\"\\" id=\\"v-menu-stub\\" class=\\"v-select\\" disabled=\\"false\\" attached=\\"true\\" is-same-width=\\"true\\" show-arrow=\\"false\\" close-on-content-click=\\"true\\" placement=\\"bottom\\">
<v-input-stub data-v-cdcb6889=\\"\\" full-width=\\"true\\" readonly=\\"\\" clickable=\\"\\" disabled=\\"false\\"></v-input-stub>
<ul data-v-ff20e609=\\"\\" data-v-cdcb6889=\\"\\" class=\\"v-list list\\">
<!--v-if-->
<!--v-if-->
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item1\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 1</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item2\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 2</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"item3\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 3</span></div>
</div>
<!--v-if-->
<!--v-if-->
</ul>
</div>"
`;
exports[`should render with string items 1`] = `
"<div data-v-cdcb6889=\\"\\" id=\\"v-menu-stub\\" class=\\"v-select\\" disabled=\\"false\\" attached=\\"true\\" is-same-width=\\"true\\" show-arrow=\\"false\\" close-on-content-click=\\"true\\" placement=\\"bottom\\">
<v-input-stub data-v-cdcb6889=\\"\\" full-width=\\"true\\" readonly=\\"\\" clickable=\\"\\" disabled=\\"false\\"></v-input-stub>
<ul data-v-ff20e609=\\"\\" data-v-cdcb6889=\\"\\" class=\\"v-list list\\">
<!--v-if-->
<!--v-if-->
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 1\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 1</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 2\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 2</span></div>
</div>
<div data-v-e8b3360c=\\"\\" data-v-cdcb6889=\\"\\" data-v-ff20e609-s=\\"\\" id=\\"v-list-item-stub\\" active=\\"false\\" clickable=\\"\\" value=\\"Item 3\\">
<!--v-if-->
<div data-v-e8b3360c=\\"\\" id=\\"v-list-item-content-stub\\"><span data-v-e8b3360c=\\"\\" class=\\"item-text\\">Item 3</span></div>
</div>
<!--v-if-->
<!--v-if-->
</ul>
</div>"
`;

View File

@@ -1,47 +1,79 @@
import { test, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import VSelect from './v-select.vue';
import { GlobalMountOptions } from '@vue/test-utils/dist/types';
import { createI18n } from 'vue-i18n';
import { Focus } from '@/__utils__/focus';
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
import { createI18n } from 'vue-i18n';
import VList from '../v-list.vue';
import VSelect from './v-select.vue';
const i18n = createI18n({ legacy: false });
const i18n = createI18n({
legacy: false,
messages: {
'en-US': {
search: 'search',
},
},
});
const global: GlobalMountOptions = {
stubs: [
'v-list',
'v-list-item',
'v-list-item-icon',
'v-list-item-content',
'v-divider',
'v-checkbox',
'v-menu',
'v-icon',
'v-input',
],
const VMenu = {
template: `
<div id="v-menu-stub">
<slot name="activator" />
<slot
v-bind="{
active: true,
}"
/>
</div>
`,
};
const VInput = {
template: `<div id="v-input-stub" />`,
setup(_props: any, { emit }: any) {
emit('update:modelValue', 'Item 1');
},
};
const VListItem = {
template: `
<div id="v-list-item-stub">
<slot />
</div>
`,
};
const VListItemContent = {
template: `
<div id="v-list-item-content-stub">
<slot />
</div>
`,
};
const global = {
stubs: {
'v-menu': VMenu,
'v-input': true,
'v-list': VList,
'v-list-item': VListItem,
'v-list-item-content': VListItemContent,
'v-list-item-icon': true,
'v-divider': true,
'v-checkbox': true,
'v-icon': true,
},
plugins: [i18n],
directives: {
Focus,
},
};
const items = [
{
text: 'Item 1',
value: 'item1',
},
{
text: 'Item 2',
value: 'item2',
},
{
text: 'Item 3',
value: 'item3',
},
];
test('should render with object items', () => {
const items = Array.from({ length: 3 }, (_, index) => {
const number = index + 1;
return { text: `Item ${number}`, value: `item${number}` };
});
test('Mount component', () => {
expect(VSelect).toBeTruthy();
const wrapper = mount(VSelect, {
@@ -53,3 +85,48 @@ test('Mount component', () => {
expect(wrapper.html()).toMatchSnapshot();
});
test('should render with string items', () => {
const items = Array.from({ length: 3 }, (_, index) => `Item ${index + 1}`);
expect(VSelect).toBeTruthy();
const wrapper = mount(VSelect, {
props: {
items,
},
global,
});
expect(wrapper.html()).toMatchSnapshot();
});
describe('should hide items not matching search value', () => {
// There have to be >10 items to enable search
const objectItems = Array.from({ length: 11 }, (_, index) => {
const number = index + 1;
return { text: `Item ${number}`, value: `item${number}` };
});
const stringItems = Array.from({ length: 11 }, (_, index) => `Item ${index + 1}`);
test.each([
['object items', objectItems],
['string items', stringItems],
])('%s', async (_, items) => {
expect(VSelect).toBeTruthy();
const wrapper = mount(VSelect, {
props: {
items,
},
global: { ...global, stubs: { ...global.stubs, 'v-input': VInput } },
});
// Wait for search debounce
await new Promise((r) => setTimeout(r, 300));
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -204,17 +204,10 @@ const props = withDefaults(defineProps<Props>(), {
itemSelectable: 'selectable',
itemChildren: 'children',
modelValue: null,
multiple: false,
groupSelectable: false,
mandatory: true,
placeholder: null,
fullWidth: true,
disabled: false,
showDeselect: false,
allowOther: false,
closeOnContentClick: true,
inline: false,
label: false,
multiplePreviewThreshold: 3,
placement: 'bottom',
isMenuSameWidth: true,
@@ -251,42 +244,57 @@ function useItems() {
const internalSearch = ref<string | null>(null);
const internalItems = computed(() => {
const parseItem = (item: Record<string, any>): Option => {
const parseItem = (item: Record<string, any> | string): Option => {
if (typeof item === 'string') {
return {
text: item,
value: item,
hidden: internalSearch.value ? !filterItem(item) : false,
};
}
if (item.divider === true) return { value: null, divider: true };
const text = get(item, props.itemText);
const value = get(item, props.itemValue);
const children = get(item, props.itemChildren) ? get(item, props.itemChildren).map(parseItem) : null;
return {
text: get(item, props.itemText),
value: get(item, props.itemValue),
text,
value,
icon: props.itemIcon ? get(item, props.itemIcon) : undefined,
disabled: get(item, props.itemDisabled),
selectable: get(item, props.itemSelectable),
children: children ? children.filter(filterItem) : children,
hidden: internalSearch.value ? !filterItem(item) : false,
children: children
? children.filter((childItem: Record<string, any>) =>
filterItem(get(childItem, props.itemText), get(childItem, props.itemValue), childItem.children)
)
: children,
hidden: internalSearch.value ? !filterItem(text, value, item.children) : false,
};
};
const filterItem = (item: Record<string, any>): boolean => {
const filterItem = (
text: string | undefined,
value?: string | number | null,
children?: Record<string, any>[] | null
): boolean => {
if (!internalSearch.value) return true;
const searchValue = internalSearch.value.toLowerCase();
return item?.children
? isMatchingCurrentItem(item, searchValue) ||
item.children.some((item: Record<string, any>) => filterItem(item))
: isMatchingCurrentItem(item, searchValue);
return children
? isMatchingCurrentItem(text, value, searchValue) ||
children.some((childItem: Record<string, any>) =>
filterItem(get(childItem, props.itemText), get(childItem, props.itemValue), childItem.children)
)
: isMatchingCurrentItem(text, value, searchValue);
function isMatchingCurrentItem(item: Record<string, any>, searchValue: string): boolean {
const text = get(item, props.itemText);
const value = get(item, props.itemValue);
function isMatchingCurrentItem(
text: string | undefined,
value: string | number | null | undefined,
searchValue: string
): boolean {
return (
(text ? String(text).toLowerCase().includes(searchValue) : false) ||
(value ? String(value).toLowerCase().includes(searchValue) : false)