vue-composable-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/deploy.yml +21 -0
  2. package/.github/workflows/docs.yml +62 -0
  3. package/.vscode/extensions.json +7 -0
  4. package/LICENSE +21 -0
  5. package/README.md +3 -0
  6. package/docs/.vitepress/config.mts +40 -0
  7. package/docs/.vitepress/theme/index.js +4 -0
  8. package/docs/.vitepress/theme/style.scss +155 -0
  9. package/docs/components/combobox.md +174 -0
  10. package/docs/components/demos/combobox.vue +142 -0
  11. package/docs/components/demos/combobox_autocomplete.vue +75 -0
  12. package/docs/components/demos/combobox_tags.vue +127 -0
  13. package/docs/components/demos/input.vue +35 -0
  14. package/docs/components/demos/listbox.vue +64 -0
  15. package/docs/components/demos/menu.vue +93 -0
  16. package/docs/components/demos/popover.vue +16 -0
  17. package/docs/components/demos/tabs.vue +59 -0
  18. package/docs/components/demos/tooltip.vue +27 -0
  19. package/docs/components/listbox.md +9 -0
  20. package/docs/components/menu.md +121 -0
  21. package/docs/components/popover.md +82 -0
  22. package/docs/components/tabs.md +9 -0
  23. package/docs/components/tooltip.md +78 -0
  24. package/docs/composables/form-control.md +130 -0
  25. package/docs/composables/list-option.md +47 -0
  26. package/docs/composables/list.md +192 -0
  27. package/docs/composables/popover.md +90 -0
  28. package/docs/index.md +24 -0
  29. package/docs/intro.md +0 -0
  30. package/package.json +35 -0
  31. package/src/components/combobox/combobox.vue +229 -0
  32. package/src/components/combobox/comboboxFormInput.vue +33 -0
  33. package/src/components/combobox/comboboxOption.vue +52 -0
  34. package/src/components/combobox/comboboxOptions.vue +17 -0
  35. package/src/components/injectionKeys.ts +50 -0
  36. package/src/components/listbox/listbox.vue +91 -0
  37. package/src/components/listbox/listboxOption.vue +37 -0
  38. package/src/components/menu/menu.vue +100 -0
  39. package/src/components/menu/menuItem.vue +86 -0
  40. package/src/components/menu/menuItems.vue +51 -0
  41. package/src/components/popover/popover.vue +68 -0
  42. package/src/components/popover/popoverDialog.vue +35 -0
  43. package/src/components/tabs/tab.vue +35 -0
  44. package/src/components/tabs/tabContainer.vue +50 -0
  45. package/src/components/tabs/tabList.vue +52 -0
  46. package/src/components/tabs/tabPanel.vue +36 -0
  47. package/src/components/tooltip.vue +115 -0
  48. package/src/composables/formControl.ts +144 -0
  49. package/src/composables/list.ts +326 -0
  50. package/src/composables/listOption.ts +80 -0
  51. package/src/composables/popover.ts +306 -0
  52. package/src/main.ts +65 -0
  53. package/src/utils/element.ts +19 -0
  54. package/src/utils/id.ts +5 -0
  55. package/src/vite-env.d.ts +1 -0
  56. package/tsconfig.json +27 -0
  57. package/tsconfig.node.json +10 -0
  58. package/vite.config.ts +28 -0
@@ -0,0 +1,229 @@
1
+ <template>
2
+ <template v-if="name">
3
+ <ComboboxFormInput
4
+ v-if="isMultiselectable"
5
+ v-for="(value, id) in formInputs"
6
+ :name="`${name}[${id}]`"
7
+ :value="value"
8
+ />
9
+ <ComboboxFormInput v-else :name="name" :value="formInputs" />
10
+ </template>
11
+
12
+ <slot
13
+ name="input"
14
+ :popoverId="popoverId"
15
+ :attrs="inputAttrs"
16
+ :isOpen="isOpen"
17
+ :toggle="toggle"
18
+ >
19
+ <input v-bind="{ ...$attrs, ...inputAttrs }" autocomplete="off" />
20
+ </slot>
21
+
22
+ <slot />
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { ref, computed, provide, watch, nextTick } from "vue";
27
+ import { useSequentialId } from "../../utils/id";
28
+ import { usePopover } from "../../composables/popover";
29
+ import { useList } from "../../composables/list";
30
+ import { useFormControl, FormValidator } from "../../composables/formControl";
31
+ import ComboboxFormInput from "./comboboxFormInput.vue";
32
+ import { injectCombobox } from "../injectionKeys";
33
+
34
+ defineOptions({ inheritAttrs: false });
35
+
36
+ const props = withDefaults(
37
+ defineProps<{
38
+ displayValue?: (value: any) => string;
39
+
40
+ id?: string;
41
+ activatorId?: string;
42
+
43
+ name?: string;
44
+ formValue?: (item: any) => Record<string, string> | string;
45
+
46
+ validators?: FormValidator[];
47
+ showValidationError?: boolean;
48
+ }>(),
49
+ {
50
+ displayValue: (value: any) =>
51
+ Array.isArray(value)
52
+ ? value.map((i) => i.toString()).join(", ")
53
+ : value.toString(),
54
+ formValue: (item: any) => item,
55
+ showValidationError: true,
56
+ }
57
+ );
58
+
59
+ const formInputs = computed(() => {
60
+ if (isMultiselectable.value) {
61
+ return (model.value as any[]).map((i) => props.formValue(i));
62
+ } else {
63
+ return props.formValue(model.value);
64
+ }
65
+ });
66
+
67
+ const model = defineModel();
68
+ const modelIsValid = defineModel("isValid");
69
+ const modelValidationMessage = defineModel("validationMessage");
70
+
71
+ const comboboxId = props.id ?? useSequentialId("combobox");
72
+ const activatorId = props.activatorId ?? comboboxId;
73
+ const popoverId = `${comboboxId}-popover`;
74
+
75
+ // Popover
76
+ const { isOpen, toggle, open, close } = usePopover(
77
+ `#${activatorId}`,
78
+ `#${popoverId}`,
79
+ {
80
+ direction: "down",
81
+ align: "stretch",
82
+ position: "fixed",
83
+ closeOnOutsideClick: true,
84
+ closeOnInsideClick: false,
85
+ }
86
+ );
87
+
88
+ // List
89
+ const {
90
+ items,
91
+ activeItem,
92
+ isMultiselectable,
93
+ selectActiveItem,
94
+ activateSelectedItem,
95
+ activateFirstItem,
96
+ activateLastItem,
97
+ activateNextItem,
98
+ activatePrevItem,
99
+ } = useList(model);
100
+
101
+ // Form Control
102
+ const { focus, isValid, validationMessage } = useFormControl(
103
+ `#${comboboxId}`,
104
+ props.validators,
105
+ props.showValidationError
106
+ );
107
+
108
+ watch(isValid, () => (modelIsValid.value = isValid.value));
109
+ watch(
110
+ validationMessage,
111
+ () => (modelValidationMessage.value = validationMessage.value)
112
+ );
113
+
114
+ watch(items, () => {
115
+ if (!activeItem.value && items.value.length) {
116
+ if (
117
+ (isMultiselectable.value && (model.value as any[]).length) ||
118
+ (!isMultiselectable.value && model.value)
119
+ ) {
120
+ activateSelectedItem({ focus: false });
121
+ } else {
122
+ activateFirstItem({ focus: false });
123
+ }
124
+ }
125
+ });
126
+
127
+ const inputValue = ref("");
128
+
129
+ function updateInputValue() {
130
+ inputValue.value = props.displayValue(model.value);
131
+ }
132
+
133
+ watch(model, updateInputValue, { deep: true, immediate: true });
134
+
135
+ function onChange() {
136
+ updateInputValue();
137
+ }
138
+
139
+ function onInput(evt: KeyboardEvent) {
140
+ inputValue.value = (evt.target as HTMLInputElement).value;
141
+ open({ focus: false });
142
+ }
143
+
144
+ function onKeyDown(evt: KeyboardEvent) {
145
+ if (!isOpen.value) {
146
+ switch (evt.code) {
147
+ case "Space":
148
+ case "Enter":
149
+ open({ focus: false });
150
+ break;
151
+
152
+ case "End":
153
+ case "ArrowUp":
154
+ open({ focus: false });
155
+ nextTick(() => activateLastItem({ focus: false }));
156
+ break;
157
+
158
+ case "Home":
159
+ case "ArrowDown":
160
+ open({ focus: false });
161
+ nextTick(() => activateFirstItem({ focus: false }));
162
+ break;
163
+
164
+ default:
165
+ return;
166
+ }
167
+ } else {
168
+ switch (evt.code) {
169
+ case "Space":
170
+ selectActiveItem();
171
+ if (!isMultiselectable.value) {
172
+ close();
173
+ }
174
+ break;
175
+ case "Enter":
176
+ selectActiveItem();
177
+ close();
178
+ break;
179
+ case "Tab":
180
+ selectActiveItem();
181
+ close();
182
+ return;
183
+ case "ArrowUp":
184
+ activatePrevItem({ focus: false, loop: false });
185
+ break;
186
+ case "ArrowDown":
187
+ activateNextItem({ focus: false, loop: false });
188
+ break;
189
+ case "Home":
190
+ activateFirstItem({ focus: false });
191
+ break;
192
+ case "End":
193
+ activateLastItem({ focus: false });
194
+ break;
195
+ case "Escape":
196
+ close();
197
+ break;
198
+ default:
199
+ return;
200
+ }
201
+ }
202
+
203
+ evt.preventDefault();
204
+ }
205
+
206
+ const inputAttrs = computed(() => ({
207
+ value: inputValue.value,
208
+ oninput: onInput,
209
+ onchange: onChange,
210
+ onkeydown: onKeyDown,
211
+ onclick: () => open({ focus: false }),
212
+ id: comboboxId,
213
+ "aria-controls": isOpen.value ? popoverId : undefined,
214
+ "aria-expanded": isOpen.value,
215
+ "aria-activedescendant": activeItem.value?.element.value?.id,
216
+ "aria-haspopup": "listbox" as "listbox",
217
+ role: "combobox",
218
+ }));
219
+
220
+ provide(injectCombobox, {
221
+ popoverId,
222
+ isOpen,
223
+ isMultiselectable,
224
+ close,
225
+ focus,
226
+ });
227
+ </script>
228
+
229
+ <style scoped></style>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <input
3
+ v-for="(value, name) in inputs"
4
+ :name="name"
5
+ :value="value"
6
+ type="hidden"
7
+ />
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { computed } from "vue";
12
+
13
+ const props = defineProps<{
14
+ name: string;
15
+ value: any;
16
+ }>();
17
+
18
+ const inputs = computed(() => {
19
+ if (typeof props.value === "object") {
20
+ const fields: Record<string, any> = {};
21
+ for (const [key, value] of Object.entries(props.value)) {
22
+ fields[`${props.name}[${key}]`] = value;
23
+ }
24
+ return fields;
25
+ } else {
26
+ return {
27
+ [props.name]: props.value,
28
+ };
29
+ }
30
+ });
31
+ </script>
32
+
33
+ <style scoped></style>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <div
3
+ ref="itemEl"
4
+ :id="itemId"
5
+ @click="onClick"
6
+ @mouseenter="activate({ focus: false })"
7
+ :data-active="isActive"
8
+ :aria-selected="isSelected"
9
+ :aria-disabled="disabled || undefined"
10
+ role="option"
11
+ >
12
+ <slot :isSelected="isSelected" :isActive="isActive" />
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { ref, inject } from "vue";
18
+ import { useSequentialId } from "../../utils/id";
19
+ import { useListOption } from "../../composables/listOption";
20
+ import { injectCombobox } from "../injectionKeys";
21
+
22
+ const props = defineProps<{
23
+ id?: string;
24
+ value: any;
25
+ disabled?: boolean;
26
+ }>();
27
+
28
+ const itemId = props.id ?? useSequentialId("combobox-option");
29
+
30
+ const itemEl = ref<HTMLElement>();
31
+
32
+ const { isSelected, isActive, activate, select } = useListOption(
33
+ itemEl,
34
+ props.value,
35
+ props.disabled
36
+ );
37
+
38
+ const combobox = inject(injectCombobox);
39
+ if (!combobox) {
40
+ throw new Error("Combobox injectable context is not provided");
41
+ }
42
+
43
+ function onClick() {
44
+ select();
45
+ if (!combobox!.isMultiselectable.value) {
46
+ combobox!.close({ focus: false });
47
+ }
48
+ combobox!.focus();
49
+ }
50
+ </script>
51
+
52
+ <style scoped></style>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <div v-if="combobox!.isOpen.value" :id="combobox!.popoverId" role="listbox">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { inject } from "vue";
9
+ import { injectCombobox } from "../injectionKeys";
10
+
11
+ const combobox = inject(injectCombobox);
12
+ if (!combobox) {
13
+ throw new Error("Combobox injectable context is not provided");
14
+ }
15
+ </script>
16
+
17
+ <style scoped></style>
@@ -0,0 +1,50 @@
1
+ import type { ComputedRef, InjectionKey } from "vue";
2
+
3
+ import type { UseListReturn } from "../composables/list";
4
+ import type { UsePopoverReturn } from "../composables/popover";
5
+ import type { useFormControlReturn } from "../composables/formControl";
6
+
7
+ export const injectTabs: InjectionKey<{
8
+ getIds: (value: any) => ComputedRef<{
9
+ tabId: string | undefined;
10
+ panelId: string | undefined;
11
+ }>;
12
+ selectActiveItem: UseListReturn["selectActiveItem"];
13
+ activateFirstItem: UseListReturn["activateFirstItem"];
14
+ activateLastItem: UseListReturn["activateLastItem"];
15
+ activateNextItem: UseListReturn["activateNextItem"];
16
+ activatePrevItem: UseListReturn["activatePrevItem"];
17
+ orientation: "horizontal" | "vertical";
18
+ }> = Symbol();
19
+
20
+ export const injectPopover: InjectionKey<{
21
+ popoverId: string;
22
+ activatorId: string;
23
+ role: "dialog" | "listbox" | "menu" | "tree" | "grid";
24
+ isOpen: UsePopoverReturn["isOpen"];
25
+ close: UsePopoverReturn["close"];
26
+ }> = Symbol();
27
+
28
+ export const injectMenu: InjectionKey<{
29
+ menuId: string;
30
+ activatorId: string;
31
+ isOpen: UsePopoverReturn["isOpen"];
32
+ close: UsePopoverReturn["close"];
33
+
34
+ activeItemId: ComputedRef<string | undefined>;
35
+ isMultiselectable: UseListReturn["isMultiselectable"];
36
+ activateFirstItem: UseListReturn["activateFirstItem"];
37
+ activateLastItem: UseListReturn["activateLastItem"];
38
+ activateNextItem: UseListReturn["activateNextItem"];
39
+ activatePrevItem: UseListReturn["activatePrevItem"];
40
+ }> = Symbol();
41
+
42
+ export const injectCombobox: InjectionKey<{
43
+ popoverId: string;
44
+ isOpen: UsePopoverReturn["isOpen"];
45
+ close: UsePopoverReturn["close"];
46
+
47
+ isMultiselectable: UseListReturn["isMultiselectable"];
48
+
49
+ focus: useFormControlReturn["focus"];
50
+ }> = Symbol();
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <div
3
+ @keydown="onKeyDown"
4
+ :aria-activedescendant="activeItemId"
5
+ :aria-orientation="orientation"
6
+ :aria-multiselectable="multiselectable"
7
+ role="listbox"
8
+ tabindex="0"
9
+ >
10
+ <slot />
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from "vue";
16
+ import { useList } from "../../composables/list";
17
+
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ orientation?: "horizontal" | "vertical";
21
+ multiselectable?: boolean;
22
+ loop?: boolean;
23
+ }>(),
24
+ {
25
+ orientation: "vertical",
26
+ multiselectable: false,
27
+ loop: false,
28
+ }
29
+ );
30
+
31
+ const model = defineModel();
32
+
33
+ const {
34
+ isMultiselectable,
35
+ activeItem,
36
+ selectActiveItem,
37
+ activateFirstItem,
38
+ activateLastItem,
39
+ activateNextItem,
40
+ activatePrevItem,
41
+ } = useList(model);
42
+
43
+ const activeItemId = computed(() => activeItem.value?.element.value?.id);
44
+
45
+ function onKeyDown(evt: KeyboardEvent) {
46
+ switch (evt.code) {
47
+ case "Space":
48
+ selectActiveItem();
49
+ break;
50
+ case "ArrowUp":
51
+ if (props.orientation === "horizontal") return;
52
+ activatePrevItem({ focus: false, loop: props.loop });
53
+ if (!isMultiselectable.value) {
54
+ selectActiveItem();
55
+ }
56
+ break;
57
+ case "ArrowDown":
58
+ if (props.orientation === "horizontal") return;
59
+ activateNextItem({ focus: false, loop: props.loop });
60
+ if (!isMultiselectable.value) {
61
+ selectActiveItem();
62
+ }
63
+ break;
64
+ case "ArrowLeft":
65
+ if (props.orientation === "vertical") return;
66
+ activatePrevItem({ focus: false, loop: props.loop });
67
+ if (!isMultiselectable.value) {
68
+ selectActiveItem();
69
+ }
70
+ break;
71
+ case "ArrowRight":
72
+ if (props.orientation === "vertical") return;
73
+ activateNextItem({ focus: false, loop: props.loop });
74
+ if (!isMultiselectable.value) {
75
+ selectActiveItem();
76
+ }
77
+ break;
78
+ case "Home":
79
+ activateFirstItem({ focus: false });
80
+ break;
81
+ case "End":
82
+ activateLastItem({ focus: false });
83
+ break;
84
+ default:
85
+ return;
86
+ }
87
+ evt.preventDefault();
88
+ }
89
+ </script>
90
+
91
+ <style scoped></style>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div
3
+ ref="itemEl"
4
+ :id="itemId"
5
+ @click="select"
6
+ :aria-disabled="disabled"
7
+ :aria-selected="isSelected"
8
+ :data-active="isActive"
9
+ role="option"
10
+ tabindex="-1"
11
+ >
12
+ <slot :isSelected="isSelected" :isActive="isActive" />
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { ref } from "vue";
18
+ import { useSequentialId } from "../../utils/id";
19
+ import { useListOption } from "../../composables/listOption";
20
+
21
+ const props = defineProps<{
22
+ id?: string;
23
+ value?: any;
24
+ disabled?: boolean;
25
+ }>();
26
+
27
+ const itemId = props.id ?? useSequentialId("list-option");
28
+
29
+ const itemEl = ref<HTMLElement>();
30
+ const { isSelected, isActive, select } = useListOption(
31
+ itemEl,
32
+ props.value,
33
+ props.disabled
34
+ );
35
+ </script>
36
+
37
+ <style scoped></style>
@@ -0,0 +1,100 @@
1
+ <template>
2
+ <slot :attrs="activatorAttrs"></slot>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed, provide, nextTick } from "vue";
7
+ import { useSequentialId } from "../../utils/id";
8
+ import { usePopover } from "../../composables/popover";
9
+ import { useList } from "../../composables/list";
10
+ import { injectMenu } from "../injectionKeys";
11
+
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ id?: string;
15
+ activatorId?: string;
16
+ direction?: "up" | "down" | "left" | "right";
17
+ align?: "top" | "bottom" | "left" | "right" | "center" | "stretch";
18
+ position?: "fixed" | "absolute";
19
+ }>(),
20
+ {
21
+ direction: "down",
22
+ align: "left",
23
+ position: "fixed",
24
+ }
25
+ );
26
+
27
+ const menuId = props.id ?? useSequentialId("menu");
28
+ const activatorId = props.activatorId ?? useSequentialId("menu-activator");
29
+
30
+ const model = defineModel();
31
+
32
+ const {
33
+ isMultiselectable,
34
+ activeItem,
35
+ activateFirstItem,
36
+ activateLastItem,
37
+ activateNextItem,
38
+ activatePrevItem,
39
+ } = useList(model);
40
+
41
+ const { isOpen, toggle, open, close } = usePopover(
42
+ `#${activatorId}`,
43
+ `#${menuId}`,
44
+ {
45
+ direction: props.direction,
46
+ align: props.align,
47
+ position: props.position,
48
+ closeOnOutsideClick: true,
49
+ closeOnInsideClick: false,
50
+ }
51
+ );
52
+
53
+ function onActivatorKeyDown(evt: KeyboardEvent) {
54
+ switch (evt.code) {
55
+ case "Space":
56
+ case "Enter":
57
+ toggle();
58
+ if (isOpen.value) {
59
+ nextTick(activateFirstItem);
60
+ }
61
+ break;
62
+ case "ArrowUp":
63
+ open();
64
+ nextTick(activateLastItem);
65
+ break;
66
+ case "ArrowDown":
67
+ open();
68
+ nextTick(activateFirstItem);
69
+ break;
70
+ default:
71
+ return;
72
+ }
73
+ evt.preventDefault();
74
+ }
75
+
76
+ const activatorAttrs = computed(() => ({
77
+ id: activatorId,
78
+ "aria-controls": isOpen.value ? menuId : undefined,
79
+ "aria-haspopup": "menu" as "menu",
80
+ "aria-expanded": isOpen.value,
81
+ onclick: toggle,
82
+ onkeydown: onActivatorKeyDown,
83
+ tabindex: 0,
84
+ }));
85
+
86
+ provide(injectMenu, {
87
+ menuId: menuId,
88
+ activatorId: activatorId,
89
+ isOpen,
90
+ close,
91
+ isMultiselectable,
92
+ activeItemId: computed(() => activeItem.value?.element.value?.id),
93
+ activateFirstItem,
94
+ activateLastItem,
95
+ activateNextItem,
96
+ activatePrevItem,
97
+ });
98
+ </script>
99
+
100
+ <style scoped></style>
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div
3
+ ref="itemEl"
4
+ :id="itemId"
5
+ @click="onClick"
6
+ @keydown="onKeyDown"
7
+ @mouseenter="activate({ focus: false })"
8
+ :data-active="isActive"
9
+ :role="role"
10
+ :aria-checked="value !== undefined ? isSelected : undefined"
11
+ :aria-disabled="disabled || undefined"
12
+ tabindex="-1"
13
+ >
14
+ <slot :isSelected="isSelected" :isActive="isActive" />
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { ref, inject, computed } from "vue";
20
+ import { useSequentialId } from "../../utils/id";
21
+ import { useListOption } from "../../composables/listOption";
22
+ import { injectMenu } from "../injectionKeys";
23
+
24
+ const props = defineProps<{
25
+ id?: string;
26
+ value?: any;
27
+ disabled?: boolean;
28
+ }>();
29
+
30
+ const emit = defineEmits<{
31
+ select: [value: any];
32
+ }>();
33
+
34
+ const itemId = props.id ?? useSequentialId("menu-item");
35
+
36
+ const itemEl = ref<HTMLElement>();
37
+ const { isSelected, isActive, select, activate } = useListOption(
38
+ itemEl,
39
+ props.value,
40
+ props.disabled
41
+ );
42
+
43
+ const menu = inject(injectMenu);
44
+ if (!menu) {
45
+ throw new Error("Menu injectable context is not provided");
46
+ }
47
+
48
+ const role = computed(() => {
49
+ if (props.value !== undefined) {
50
+ return menu.isMultiselectable.value ? "menuitemcheckbox" : "menuitemradio";
51
+ }
52
+ return "menuitem";
53
+ });
54
+
55
+ function selectAndEmit() {
56
+ select();
57
+ emit("select", props.value);
58
+ }
59
+
60
+ function onClick() {
61
+ selectAndEmit();
62
+ if (props.value === undefined || !menu?.isMultiselectable.value) {
63
+ menu!.close();
64
+ }
65
+ }
66
+
67
+ function onKeyDown(evt: KeyboardEvent) {
68
+ switch (evt.code) {
69
+ case "Space":
70
+ selectAndEmit();
71
+ if (props.value === undefined) {
72
+ menu!.close();
73
+ }
74
+ break;
75
+ case "Enter":
76
+ selectAndEmit();
77
+ menu!.close();
78
+ break;
79
+ default:
80
+ return;
81
+ }
82
+ evt.preventDefault();
83
+ }
84
+ </script>
85
+
86
+ <style scoped></style>