From 7f8fe2734451c922c23b49c43f4d3260f669826f Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 5 Aug 2026 11:35:40 -0700 Subject: [PATCH 1/3] Search users select dropdown by email, not just name The users fieldtype's Select Dropdown mode already sends each user's email to the browser, but only searched the title field client-side, which falls back to email only when a user has no name set. Users with a name couldn't be found by searching their email. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LBwXt1YibnzZCneXjGCqt7 --- .../inputs/relationship/SelectField.vue | 7 ++++++ .../js/components/ui/Combobox/Combobox.vue | 4 +++- .../inputs/relationship/SelectField.test.js | 22 +++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/resources/js/components/inputs/relationship/SelectField.vue b/resources/js/components/inputs/relationship/SelectField.vue index 6bedba8c661..628d716ae12 100644 --- a/resources/js/components/inputs/relationship/SelectField.vue +++ b/resources/js/components/inputs/relationship/SelectField.vue @@ -12,6 +12,7 @@ :read-only="readOnly" :taggable="isTaggable" :close-on-select="isTaggable" + :search-keys="searchKeys" option-label="title" option-value="id" @update:modelValue="itemsSelected" @@ -91,6 +92,12 @@ export default { }; }, + // The `users` fieldtype falls back to displaying a user's email as their title when + // they have no name, but doesn't show it otherwise, so it needs to be searchable too. + searchKeys() { + return this.config.type === 'users' ? ['title', 'email'] : null; + }, + cacheKey() { return JSON.stringify({ ...this.parameters, url: this.url }); }, diff --git a/resources/js/components/ui/Combobox/Combobox.vue b/resources/js/components/ui/Combobox/Combobox.vue index 478da310f21..8e46a26295b 100644 --- a/resources/js/components/ui/Combobox/Combobox.vue +++ b/resources/js/components/ui/Combobox/Combobox.vue @@ -61,6 +61,8 @@ const props = defineProps({ readOnly: { type: Boolean, default: false }, /** When `true`, the options will be searchable. */ searchable: { type: Boolean, default: true }, + /** Keys of the option object to search against. Defaults to just `optionLabel`. */ + searchKeys: { type: Array, default: null }, /** Determines if the dropdown should open */ shouldOpenDropdown: { type: Function, default: () => true }, /** Controls the size of the combobox.

Options: `xs`, `sm`, `base`, `lg`, `xl` */ @@ -236,7 +238,7 @@ const filteredOptions = computed(() => { fuzzysort .go(searchQuery.value, props.options, { all: true, - key: props.optionLabel, + ...(props.searchKeys?.length ? { keys: props.searchKeys } : { key: props.optionLabel }), }) .map((result) => result.obj) ); diff --git a/resources/js/tests/components/inputs/relationship/SelectField.test.js b/resources/js/tests/components/inputs/relationship/SelectField.test.js index 322f3f5216b..856ab21a57c 100644 --- a/resources/js/tests/components/inputs/relationship/SelectField.test.js +++ b/resources/js/tests/components/inputs/relationship/SelectField.test.js @@ -17,12 +17,12 @@ const stubs = { StatusIndicator: true, }; -function mountSelectField({ items = [] } = {}) { +function mountSelectField({ items = [], config = {} } = {}) { return mount(SelectField, { props: { items, url: '/test/select-field', - config: {}, + config, }, global: { mocks: { @@ -51,3 +51,21 @@ describe('SelectField comboboxOptions', () => { wrapper.unmount(); }); }); + +describe('SelectField searchKeys', () => { + test('searches by title and email for the users fieldtype', () => { + const wrapper = mountSelectField({ config: { type: 'users' } }); + + expect(wrapper.vm.searchKeys).toEqual(['title', 'email']); + + wrapper.unmount(); + }); + + test('is null for other relationship fieldtypes', () => { + const wrapper = mountSelectField({ config: { type: 'entries' } }); + + expect(wrapper.vm.searchKeys).toBeNull(); + + wrapper.unmount(); + }); +}); From 6e887d657c1fe607f2fd42c0d16a23390890feff Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 6 Aug 2026 15:48:32 -0400 Subject: [PATCH 2/3] Add searchKeys section to Combobox story --- resources/js/stories/Combobox.stories.ts | 78 ++++++++++++++++++++++++ resources/js/stories/docs/Combobox.mdx | 4 ++ 2 files changed, 82 insertions(+) diff --git a/resources/js/stories/Combobox.stories.ts b/resources/js/stories/Combobox.stories.ts index ea53ab5a6cd..3b2e73b2aef 100644 --- a/resources/js/stories/Combobox.stories.ts +++ b/resources/js/stories/Combobox.stories.ts @@ -383,6 +383,50 @@ export const _IgnoreFilter: Story = { }), }; +const searchKeysCode = ` + +`; + +export const _SearchKeys: Story = { + tags: ['!dev'], + parameters: { + docs: { + source: { code: searchKeysCode }, + description: { + story: 'By default, search only matches against `optionLabel`. Use `searchKeys` to also match against other keys on the option object — useful when an option has a value that isn\'t displayed but should still be searchable, such as an email address. Try searching "nightowl" below — it only appears in Tim McEwan\'s email, not his label.', + }, + }, + }, + render: () => ({ + components: { Combobox }, + setup() { + const value = ref(null); + const options = [ + { label: 'Tyler Lyle', email: 'workhorse92@example.com', value: 'tyler' }, + { label: 'Tim McEwan', email: 'nightowl47@example.com', value: 'tim' }, + { label: 'Nikki Flores', email: 'skyline08@example.com', value: 'nikki' }, + ]; + return { value, options }; + }, + template: ` + + `, + }), +}; + const optionSlotsCode = ` ({ + components: { Combobox }, + setup() { + const value = ref(null); + const options = [ + { label: 'Tyler Lyle', email: 'workhorse92@example.com', value: 'tyler' }, + { label: 'Tim McEwan', email: 'nightowl47@example.com', value: 'tim' }, + { label: 'Nikki Flores', email: 'skyline08@example.com', value: 'nikki' }, + ]; + return { value, options }; + }, + template: ``, + }), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const trigger = canvas.getByRole('combobox'); + + await userEvent.click(trigger); + + const input = document.querySelector('input[type="search"]') as HTMLInputElement; + + // "nightowl" only appears in Tim McEwan's email, not in any label. + await userEvent.type(input, 'nightowl'); + + await new Promise((r) => setTimeout(r, 100)); + + const options = document.querySelectorAll('[data-ui-combobox-item]'); + expect(options.length).toBe(1); + expect(options[0].getAttribute('data-ui-combobox-item')).toBe('tim'); + }, +}; + export const TestSearchDisabledWhenNotSearchable: Story = { tags: ['!dev', 'test'], render: () => ({ diff --git a/resources/js/stories/docs/Combobox.mdx b/resources/js/stories/docs/Combobox.mdx index fba290cc178..eaf5d845f17 100644 --- a/resources/js/stories/docs/Combobox.mdx +++ b/resources/js/stories/docs/Combobox.mdx @@ -42,6 +42,10 @@ By default, the combobox is searchable. You may disable search by setting the `s When `ignoreFilter` is `true`, the combobox won't filter options locally. Use this with the `search` event to implement server-side searching. +## Search by Multiple Keys +By default, search only matches against the `optionLabel` key. Use the `searchKeys` prop to also match against other keys on the option object — useful when an option has a value that isn't displayed but should still be searchable, such as an email address. + + ## Customize Options You may customize how options are rendered using the `selected-option` and `option` slots. From ef96ab476a5feb536574d50f6bcef2a9fb30c6ee Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 6 Aug 2026 20:24:48 -0400 Subject: [PATCH 3/3] Empty commit to retrigger CI