Skip to content

Commit 65a8369

Browse files
committed
feat: major improvements, restructure DOM, split components, improve styles
1 parent 0d2e4fa commit 65a8369

File tree

6 files changed

+458
-263
lines changed

6 files changed

+458
-263
lines changed

src/Indicators.vue

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<script setup lang="ts">
2+
import { defineExpose, useTemplateRef } from "vue";
3+
4+
import ChevronDownIcon from "./icons/ChevronDownIcon.vue";
5+
import XMarkIcon from "./icons/XMarkIcon.vue";
6+
import Spinner from "./Spinner.vue";
7+
8+
defineProps<{
9+
hasSelectedOption: boolean;
10+
isClearable: boolean;
11+
isLoading: boolean;
12+
isDisabled: boolean;
13+
}>();
14+
15+
const emit = defineEmits<{
16+
(e: "clear"): void;
17+
(e: "toggle"): void;
18+
}>();
19+
20+
const container = useTemplateRef("container");
21+
const clearButton = useTemplateRef("clearButton");
22+
const dropdownButton = useTemplateRef("dropdownButton");
23+
24+
defineExpose({ container, clearButton, dropdownButton });
25+
</script>
26+
27+
<template>
28+
<div ref="container" class="indicators-container">
29+
<button
30+
v-if="hasSelectedOption && isClearable && !isLoading"
31+
ref="clearButton"
32+
type="button"
33+
class="clear-button"
34+
tabindex="-1"
35+
:disabled="isDisabled"
36+
@click.stop="emit('clear')"
37+
>
38+
<slot name="clear">
39+
<XMarkIcon />
40+
</slot>
41+
</button>
42+
43+
<button
44+
v-if="!isLoading"
45+
ref="dropdownButton"
46+
type="button"
47+
class="dropdown-icon"
48+
tabindex="-1"
49+
:disabled="isDisabled"
50+
@click.stop="emit('toggle')"
51+
>
52+
<slot name="dropdown">
53+
<ChevronDownIcon />
54+
</slot>
55+
</button>
56+
57+
<slot name="loading">
58+
<Spinner v-if="isLoading" />
59+
</slot>
60+
</div>
61+
</template>
62+
63+
<style lang="scss" scoped>
64+
.indicators-container {
65+
display: flex;
66+
align-items: center;
67+
align-self: stretch;
68+
flex-shrink: 0;
69+
gap: var(--vs-indicators-gap);
70+
padding: var(--vs-padding);
71+
}
72+
73+
.clear-button {
74+
appearance: none;
75+
display: inline-block;
76+
padding: 0;
77+
margin: 0;
78+
border: 0;
79+
width: var(--vs-icon-size);
80+
height: var(--vs-icon-size);
81+
color: var(--vs-icon-color);
82+
background: none;
83+
outline: none;
84+
cursor: pointer;
85+
}
86+
87+
.dropdown-icon {
88+
appearance: none;
89+
display: inline-block;
90+
padding: 0;
91+
margin: 0;
92+
border: 0;
93+
width: var(--vs-icon-size);
94+
height: var(--vs-icon-size);
95+
color: var(--vs-icon-color);
96+
background: none;
97+
outline: none;
98+
cursor: pointer;
99+
transition: var(--vs-dropdown-transition);
100+
}
101+
</style>

src/MultiValue.vue

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<script setup lang="ts">
2+
import XMarkIcon from "./icons/XMarkIcon.vue";
3+
4+
defineProps<{
5+
label: string;
6+
}>();
7+
8+
const emit = defineEmits<{
9+
(e: "remove"): void;
10+
}>();
11+
</script>
12+
13+
<template>
14+
<div
15+
class="multi-value"
16+
>
17+
<div class="multi-value-label">
18+
{{ label }}
19+
</div>
20+
21+
<button
22+
type="button"
23+
class="multi-value-remove"
24+
:aria-label="`Remove ${label}`"
25+
@click="emit('remove')"
26+
>
27+
<XMarkIcon />
28+
</button>
29+
</div>
30+
</template>
31+
32+
<style lang="css" scoped>
33+
.multi-value {
34+
display: flex;
35+
min-width: 0px;
36+
margin: 2px;
37+
border-radius: var(--vs-multi-value-border-radius);
38+
background: var(--vs-multi-value-bg);
39+
}
40+
41+
.multi-value-label {
42+
border-radius: var(--vs-multi-value-border-radius);
43+
padding: 3px 3px 3px 6px;
44+
overflow: hidden;
45+
text-overflow: ellipsis;
46+
white-space: nowrap;
47+
font-size: 85%;
48+
color: var(--vs-multi-value-text-color);
49+
}
50+
51+
.multi-value-remove {
52+
border-radius: var(--vs-multi-value-border-radius);
53+
appearance: none;
54+
display: flex;
55+
align-items: center;
56+
padding: 0 4px;
57+
border: none;
58+
outline: none;
59+
cursor: pointer;
60+
background-color: var(--vs-multi-value-bg);
61+
}
62+
63+
.multi-value-remove svg {
64+
width: var(--vs-multi-value-xmark-size);
65+
height: var(--vs-multi-value-xmark-size);
66+
fill: var(--vs-multi-value-xmark-color);
67+
}
68+
</style>

src/Placeholder.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
text: string;
4+
}>();
5+
</script>
6+
7+
<template>
8+
<div class="placeholder">
9+
{{ text }}
10+
</div>
11+
</template>
12+
13+
<style lang="css" scoped>
14+
.placeholder {
15+
grid-area: 1 / 1 / 2 / 3;
16+
color: var(--vs-placeholder-color);
17+
}
18+
</style>

src/Select.spec.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ it("should render the component", () => {
4040
});
4141

4242
describe("input + menu interactions behavior", () => {
43-
it("should display the placeholder in the input when no option is selected", () => {
44-
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
43+
it("should display the placeholder when no option is selected", () => {
44+
const wrapper = mount(VueSelect, { props: { modelValue: null, options, placeholder: "Select an option" } });
4545

46-
expect(wrapper.find("input").attributes("placeholder"));
46+
expect(wrapper.find(".placeholder").text()).toBe("Select an option");
4747
});
4848

4949
it("should not open the menu when focusing the input", async () => {
@@ -355,7 +355,7 @@ describe("multi-select options", () => {
355355

356356
expect(wrapper.findAll(".menu-option").length).toBe(options.length - 1);
357357

358-
await wrapper.get(".multi-value").trigger("click");
358+
await wrapper.get(".multi-value-remove").trigger("click");
359359
await openMenu(wrapper);
360360

361361
expect(wrapper.findAll(".menu-option").length).toBe(options.length);
@@ -469,12 +469,6 @@ describe("component props", () => {
469469
expect(wrapper.get(".single-value").text()).toBe("Admin");
470470
});
471471

472-
it("should display the placeholder in the input when no option is selected", () => {
473-
const wrapper = mount(VueSelect, { props: { modelValue: null, options, placeholder: "Pick an option" } });
474-
475-
expect(wrapper.find("input").attributes("placeholder")).toBe("Pick an option");
476-
});
477-
478472
it("should disable the input when passing the isDisabled prop", () => {
479473
const wrapper = mount(VueSelect, { props: { modelValue: null, options, isDisabled: true } });
480474

0 commit comments

Comments
 (0)