Skip to content

Commit a2241b6

Browse files
authored
feat(select): add hideSelectedOptions prop (#243)
See #233
1 parent ff1dc37 commit a2241b6

File tree

5 files changed

+120
-8
lines changed

5 files changed

+120
-8
lines changed

docs/props.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ Whether the select should display a loading state. When `true`, the select will
143143

144144
A prop to control the menu open state programmatically. When set to `true`, the menu will be open. When set to `false`, the menu will be closed.
145145

146+
## hideSelectedOptions
147+
148+
**Type**: `boolean`
149+
150+
**Default**: `true`
151+
152+
When set to `true` with `isMulti`, selected options won't appear in the options menu. Set it to `false` to show selected options in the menu.
153+
146154
## shouldAutofocusOption
147155

148156
**Type**: `boolean`

docs/slots.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,25 @@ If you are not familiar with Vue's slots, you can read more about them [here](ht
1212

1313
## option
1414

15-
**Type**: `slotProps: { option: Option }`
15+
**Type**:
16+
17+
```ts
18+
slotProps: {
19+
option: Option;
20+
index: number;
21+
isFocused: boolean;
22+
isSelected: boolean;
23+
isDisabled: boolean;
24+
}
25+
```
1626

17-
Customize the rendered template of an option inside the menu. You can use the slot props to retrieve the current menu option that will be rendered.
27+
Customize the rendered template of an option inside the menu. You can use the slot props to retrieve the current menu option that will be rendered in order to have more context and flexbility.
1828

1929
```vue
2030
<template>
2131
<VueSelect v-model="option" :options="options">
22-
<template #option="{ option }">
23-
{{ option.label }} - {{ option.value }}
32+
<template #option="{ option, index }">
33+
{{ option.label }} - {{ option.value }} (#{{ index }})
2434
</template>
2535
</VueSelect>
2636
</template>

src/Select.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,76 @@ describe("menu closing behavior", () => {
479479
}
480480
});
481481
});
482+
483+
describe("hideSelectedOptions prop", () => {
484+
it("should hide selected options from menu when hideSelectedOptions is true", async () => {
485+
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: true } });
486+
487+
await openMenu(wrapper);
488+
await wrapper.get("div[role='option']").trigger("click");
489+
await openMenu(wrapper);
490+
491+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length - 1);
492+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).not.toContain(options[0].label);
493+
});
494+
495+
it("should show selected options in menu when hideSelectedOptions is false", async () => {
496+
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: false } });
497+
498+
await openMenu(wrapper);
499+
await wrapper.get("div[role='option']").trigger("click");
500+
await openMenu(wrapper);
501+
502+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
503+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain(options[0].label);
504+
});
505+
506+
it("should show all options when in single-select mode regardless of hideSelectedOptions", async () => {
507+
const wrapper = mount(VueSelect, { props: { modelValue: null, options, hideSelectedOptions: true } });
508+
509+
await openMenu(wrapper);
510+
await wrapper.get("div[role='option']").trigger("click");
511+
await openMenu(wrapper);
512+
513+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
514+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain(options[0].label);
515+
});
516+
517+
it("should correctly restore hidden options when they are deselected", async () => {
518+
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: true } });
519+
520+
// Select first option
521+
await openMenu(wrapper);
522+
await wrapper.get("div[role='option']").trigger("click");
523+
await openMenu(wrapper);
524+
525+
// Verify it's hidden from dropdown
526+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length - 1);
527+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).not.toContain(options[0].label);
528+
529+
// Remove the option
530+
await wrapper.get(".multi-value-remove").trigger("click");
531+
await openMenu(wrapper);
532+
533+
// Verify it's back in the dropdown
534+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
535+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain(options[0].label);
536+
});
537+
538+
it("should correctly filter options when searching with hideSelectedOptions enabled", async () => {
539+
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: true } });
540+
541+
// Select first option (France)
542+
await openMenu(wrapper);
543+
await wrapper.get("div[role='option']").trigger("click");
544+
545+
// Open menu and search for "United"
546+
await openMenu(wrapper);
547+
await inputSearch(wrapper, "United");
548+
549+
// Should only show United Kingdom and United States (not France)
550+
expect(wrapper.findAll("div[role='option']").length).toBe(2);
551+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain("United Kingdom");
552+
expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain("United States");
553+
});
554+
});

src/Select.vue

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const props = withDefaults(
2222
isTaggable: false,
2323
isLoading: false,
2424
isMenuOpen: undefined,
25+
hideSelectedOptions: true,
2526
shouldAutofocusOption: true,
2627
closeOnSelect: true,
2728
teleport: undefined,
@@ -71,7 +72,7 @@ const availableOptions = computed<GenericOption[]>(() => {
7172
// Remove already selected values from the list of options, when in multi-select mode.
7273
// In case an invalid v-model is provided, we return all options since we can't know what options are valid.
7374
const getNonSelectedOptions = (options: GenericOption[]) => options.filter(
74-
(option) => Array.isArray(selected.value) ? !selected.value.includes(option.value) : true,
75+
(option) => props.hideSelectedOptions && Array.isArray(selected.value) ? !selected.value.includes(option.value) : true,
7576
);
7677
7778
if (props.isSearchable && search.value) {
@@ -149,7 +150,14 @@ const setOption = (option: GenericOption) => {
149150
150151
if (props.isMulti) {
151152
if (Array.isArray(selected.value)) {
152-
selected.value.push(option.value);
153+
const isAlreadyPresent = selected.value.find((v) => v === option.value);
154+
155+
if (!isAlreadyPresent) {
156+
selected.value.push(option.value);
157+
}
158+
else {
159+
selected.value = selected.value.filter((v) => v !== option.value);
160+
}
153161
}
154162
else {
155163
selected.value = [option.value];
@@ -498,11 +506,18 @@ onBeforeUnmount(() => {
498506
:menu="menuRef"
499507
:index="i"
500508
:is-focused="focusedOption === i"
501-
:is-selected="option.value === selected"
509+
:is-selected="Array.isArray(selected) ? selected.includes(option.value) : option.value === selected"
502510
:is-disabled="option.disabled || false"
503511
@select="setOption(option)"
504512
>
505-
<slot name="option" :option="option">
513+
<slot
514+
name="option"
515+
:option="option"
516+
:index="i"
517+
:is-focused="focusedOption === i"
518+
:is-selected="Array.isArray(selected) ? selected.includes(option.value) : option.value === selected"
519+
:is-disabled="option.disabled || false"
520+
>
506521
{{ getOptionLabel(option) }}
507522
</slot>
508523
</MenuOption>

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export type Props<GenericOption, OptionValue> = {
6060
*/
6161
isMenuOpen?: boolean;
6262

63+
/**
64+
* When set to true with `isMulti`, selected options won't be displayed in
65+
* the menu.
66+
*/
67+
hideSelectedOptions?: boolean;
68+
6369
/**
6470
* When set to true, focus the first option when the menu is opened.
6571
* When set to false, no option will be focused.

0 commit comments

Comments
 (0)