Skip to content

Commit f6689f6

Browse files
authored
fix(select): improve logic to open/close menu when clicking on input or control (#352)
* fix(select): improve logic to open/close menu when clicking on the input or select control * fix(playground): properly handle taggable by creating a new option
1 parent 678871c commit f6689f6

File tree

4 files changed

+67
-3
lines changed

4 files changed

+67
-3
lines changed

playground/demos/SingleSelect.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import VueSelect from "../../src";
55
66
const selected = ref<string | null>(null);
77
8-
const options: Option<string>[] = [
8+
const options = ref<Option<string>[]>([
99
{ label: "Alice's Adventures in Wonderland", value: "alice" },
1010
{ label: "A Wizard of Earthsea", value: "wizard" },
1111
{ label: "Harry Potter and the Philosopher's Stone", value: "harry_potter_1" },
1212
{ label: "Harry Potter and the Chamber of Secrets", value: "harry_potter_2" },
13-
];
13+
]);
14+
15+
const handleCreateOption = (value: string) => {
16+
options.value.push({ label: value, value });
17+
selected.value = value;
18+
};
1419
</script>
1520

1621
<template>
@@ -20,6 +25,7 @@ const options: Option<string>[] = [
2025
:is-multi="false"
2126
:is-taggable="true"
2227
placeholder="Pick a book"
28+
@option-created="(value) => handleCreateOption(value)"
2329
>
2430
<template #taggable-no-options="{ option }">
2531
<div class="custom-taggable-no-options">

src/Select.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@ const options = [
1313
];
1414

1515
async function openMenu(wrapper: ReturnType<typeof mount>, method: "mousedown" | "focus-space" | "single-value" = "mousedown") {
16+
// Check if menu is already closed, if so proceed normally
17+
// If menu is open, use the dropdown toggle button instead to avoid the new toggle logic
18+
const isMenuOpen = wrapper.findAll("div[role='option']").length > 0;
19+
1620
if (method === "mousedown") {
21+
if (isMenuOpen) {
22+
// Menu is open, close it first then reopen
23+
await wrapper.get(".dropdown-icon").trigger("click");
24+
}
25+
1726
await wrapper.get("input").trigger("mousedown");
1827
}
1928
else if (method === "focus-space") {
@@ -640,6 +649,37 @@ describe("menu closing behavior", () => {
640649
expect(wrapper.findAll("div[role='option']").length).toBe(0);
641650
}
642651
});
652+
653+
it("should toggle menu when clicking input while menu is open and search is empty", async () => {
654+
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
655+
656+
// First click should open menu
657+
await wrapper.get("input").trigger("mousedown");
658+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
659+
660+
// Second click should close menu (when search is empty)
661+
await wrapper.get("input").trigger("mousedown");
662+
expect(wrapper.findAll("div[role='option']").length).toBe(0);
663+
664+
// Third click should open menu again
665+
await wrapper.get("input").trigger("mousedown");
666+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
667+
});
668+
669+
it("should keep menu open when clicking input while typing", async () => {
670+
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
671+
672+
// Open menu and start typing
673+
await wrapper.get("input").trigger("mousedown");
674+
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
675+
676+
await inputSearch(wrapper, "United");
677+
expect(wrapper.findAll("div[role='option']").length).toBe(2);
678+
679+
// Clicking input while typing should keep menu open
680+
await wrapper.get("input").trigger("mousedown");
681+
expect(wrapper.findAll("div[role='option']").length).toBe(2);
682+
});
643683
});
644684

645685
describe("hideSelectedOptions prop", () => {

src/Select.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ function toggleMenu() {
167167
168168
function handleControlClick(event: MouseEvent) {
169169
if (indicatorsRef.value?.containerRef && !indicatorsRef.value.containerRef.contains(event.target as Node)) {
170+
// If menu is open and search is empty, close it; otherwise open it
171+
if (menuOpen.value && search.value.length === 0) {
172+
closeMenu();
173+
}
174+
else {
175+
openMenu();
176+
}
177+
}
178+
};
179+
180+
function handleInputMousedown() {
181+
// If menu is open and search is empty, close it; otherwise open it
182+
if (menuOpen.value && search.value.length === 0) {
183+
closeMenu();
184+
}
185+
else {
170186
openMenu();
171187
}
172188
};
@@ -280,6 +296,7 @@ provide(DATA_KEY, {
280296
closeMenu,
281297
toggleMenu,
282298
handleControlClick,
299+
handleInputMousedown,
283300
setOption,
284301
removeOption,
285302
createOption,
@@ -424,7 +441,7 @@ watch(
424441
:aria-labelledby="`vue-select-${uid}-combobox`"
425442
:disabled="isDisabled"
426443
placeholder=""
427-
@mousedown="openMenu()"
444+
@mousedown="handleInputMousedown"
428445
@keydown="handleInputKeydown"
429446
>
430447
</div>

src/lib/provide-inject.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type DataInjection<GenericOption extends Option<OptionValue>, OptionValue
2929
closeMenu: () => void;
3030
toggleMenu: () => void;
3131
handleControlClick: (event: MouseEvent) => void;
32+
handleInputMousedown: () => void;
3233
setOption: (option: GenericOption) => void;
3334
removeOption: (option: GenericOption) => void;
3435
createOption: () => void;

0 commit comments

Comments
 (0)