diff --git a/docs/props.md b/docs/props.md
index 452749e..da6cba8 100644
--- a/docs/props.md
+++ b/docs/props.md
@@ -143,6 +143,14 @@ Whether the select should display a loading state. When `true`, the select will
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.
+## hideSelectedOptions
+
+**Type**: `boolean`
+
+**Default**: `true`
+
+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.
+
## shouldAutofocusOption
**Type**: `boolean`
diff --git a/docs/slots.md b/docs/slots.md
index 5655e93..23cd349 100644
--- a/docs/slots.md
+++ b/docs/slots.md
@@ -12,15 +12,25 @@ If you are not familiar with Vue's slots, you can read more about them [here](ht
## option
-**Type**: `slotProps: { option: Option }`
+**Type**:
+
+```ts
+slotProps: {
+ option: Option;
+ index: number;
+ isFocused: boolean;
+ isSelected: boolean;
+ isDisabled: boolean;
+}
+```
-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.
+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.
```vue
-
- {{ option.label }} - {{ option.value }}
+
+ {{ option.label }} - {{ option.value }} (#{{ index }})
diff --git a/src/Select.spec.ts b/src/Select.spec.ts
index d58fc2a..9cf905d 100644
--- a/src/Select.spec.ts
+++ b/src/Select.spec.ts
@@ -479,3 +479,76 @@ describe("menu closing behavior", () => {
}
});
});
+
+describe("hideSelectedOptions prop", () => {
+ it("should hide selected options from menu when hideSelectedOptions is true", async () => {
+ const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: true } });
+
+ await openMenu(wrapper);
+ await wrapper.get("div[role='option']").trigger("click");
+ await openMenu(wrapper);
+
+ expect(wrapper.findAll("div[role='option']").length).toBe(options.length - 1);
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).not.toContain(options[0].label);
+ });
+
+ it("should show selected options in menu when hideSelectedOptions is false", async () => {
+ const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: false } });
+
+ await openMenu(wrapper);
+ await wrapper.get("div[role='option']").trigger("click");
+ await openMenu(wrapper);
+
+ expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain(options[0].label);
+ });
+
+ it("should show all options when in single-select mode regardless of hideSelectedOptions", async () => {
+ const wrapper = mount(VueSelect, { props: { modelValue: null, options, hideSelectedOptions: true } });
+
+ await openMenu(wrapper);
+ await wrapper.get("div[role='option']").trigger("click");
+ await openMenu(wrapper);
+
+ expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain(options[0].label);
+ });
+
+ it("should correctly restore hidden options when they are deselected", async () => {
+ const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: true } });
+
+ // Select first option
+ await openMenu(wrapper);
+ await wrapper.get("div[role='option']").trigger("click");
+ await openMenu(wrapper);
+
+ // Verify it's hidden from dropdown
+ expect(wrapper.findAll("div[role='option']").length).toBe(options.length - 1);
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).not.toContain(options[0].label);
+
+ // Remove the option
+ await wrapper.get(".multi-value-remove").trigger("click");
+ await openMenu(wrapper);
+
+ // Verify it's back in the dropdown
+ expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain(options[0].label);
+ });
+
+ it("should correctly filter options when searching with hideSelectedOptions enabled", async () => {
+ const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, hideSelectedOptions: true } });
+
+ // Select first option (France)
+ await openMenu(wrapper);
+ await wrapper.get("div[role='option']").trigger("click");
+
+ // Open menu and search for "United"
+ await openMenu(wrapper);
+ await inputSearch(wrapper, "United");
+
+ // Should only show United Kingdom and United States (not France)
+ expect(wrapper.findAll("div[role='option']").length).toBe(2);
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain("United Kingdom");
+ expect(wrapper.findAll("div[role='option']").map((option) => option.text())).toContain("United States");
+ });
+});
diff --git a/src/Select.vue b/src/Select.vue
index 389733c..3e0d492 100644
--- a/src/Select.vue
+++ b/src/Select.vue
@@ -22,6 +22,7 @@ const props = withDefaults(
isTaggable: false,
isLoading: false,
isMenuOpen: undefined,
+ hideSelectedOptions: true,
shouldAutofocusOption: true,
closeOnSelect: true,
teleport: undefined,
@@ -71,7 +72,7 @@ const availableOptions = computed(() => {
// Remove already selected values from the list of options, when in multi-select mode.
// In case an invalid v-model is provided, we return all options since we can't know what options are valid.
const getNonSelectedOptions = (options: GenericOption[]) => options.filter(
- (option) => Array.isArray(selected.value) ? !selected.value.includes(option.value) : true,
+ (option) => props.hideSelectedOptions && Array.isArray(selected.value) ? !selected.value.includes(option.value) : true,
);
if (props.isSearchable && search.value) {
@@ -149,7 +150,14 @@ const setOption = (option: GenericOption) => {
if (props.isMulti) {
if (Array.isArray(selected.value)) {
- selected.value.push(option.value);
+ const isAlreadyPresent = selected.value.find((v) => v === option.value);
+
+ if (!isAlreadyPresent) {
+ selected.value.push(option.value);
+ }
+ else {
+ selected.value = selected.value.filter((v) => v !== option.value);
+ }
}
else {
selected.value = [option.value];
@@ -498,11 +506,18 @@ onBeforeUnmount(() => {
:menu="menuRef"
:index="i"
:is-focused="focusedOption === i"
- :is-selected="option.value === selected"
+ :is-selected="Array.isArray(selected) ? selected.includes(option.value) : option.value === selected"
:is-disabled="option.disabled || false"
@select="setOption(option)"
>
-
+
{{ getOptionLabel(option) }}
diff --git a/src/types.ts b/src/types.ts
index 385bc38..b7ae6a3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -60,6 +60,12 @@ export type Props = {
*/
isMenuOpen?: boolean;
+ /**
+ * When set to true with `isMulti`, selected options won't be displayed in
+ * the menu.
+ */
+ hideSelectedOptions?: boolean;
+
/**
* When set to true, focus the first option when the menu is opened.
* When set to false, no option will be focused.