@revojs/ui 0.2.4 → 0.2.6
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="is"
|
|
4
|
-
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-ui border border-transparent font-[inherit] font-medium leading-
|
|
4
|
+
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-ui border border-transparent font-[inherit] font-medium leading-normal no-underline outline-ring transition-[background-color,border-color,color,opacity] duration-150 hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:hover:opacity-50"
|
|
5
5
|
:class="[fonts[font], layouts[layout], sizes[size], texts[text], variants[variant]]"
|
|
6
6
|
:type="attributes.type ?? 'button'"
|
|
7
7
|
:disabled="disabled"
|
|
@@ -6,62 +6,91 @@
|
|
|
6
6
|
|
|
7
7
|
<input v-if="name" type="hidden" :name="name" :value="modelValue" :required="required" />
|
|
8
8
|
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
<div ref="container">
|
|
10
|
+
<div
|
|
11
|
+
ref="trigger"
|
|
12
|
+
class="flex min-h-10 w-full cursor-text items-center gap-2 rounded-ui border border-input px-3 text-sm ring-ring focus-within:ring-2"
|
|
13
|
+
:class="disabled ? 'cursor-not-allowed opacity-50' : ''"
|
|
14
|
+
@click="search?.focus()"
|
|
15
|
+
>
|
|
16
|
+
<input
|
|
17
|
+
ref="search"
|
|
18
|
+
v-model="query"
|
|
19
|
+
role="combobox"
|
|
20
|
+
class="w-full min-w-0 bg-transparent py-2 outline-none placeholder:opacity-50"
|
|
21
|
+
:placeholder="placeholder ?? 'Select'"
|
|
22
|
+
:aria-expanded="open"
|
|
23
|
+
:aria-controls="listboxId"
|
|
24
|
+
aria-autocomplete="list"
|
|
25
|
+
:disabled="disabled"
|
|
26
|
+
@focus="open = true"
|
|
27
|
+
@input="open = true"
|
|
28
|
+
@keydown.enter.prevent="open ? filtered.length > 0 && select(filtered[0]) : (open = true)"
|
|
29
|
+
@keydown.escape="open = false"
|
|
30
|
+
@keydown.arrow-down.prevent="open = true"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
tabindex="-1"
|
|
36
|
+
class="shrink-0 opacity-60 transition-transform"
|
|
37
|
+
:class="open ? 'rotate-180' : ''"
|
|
38
|
+
:disabled="disabled"
|
|
39
|
+
@click.stop="open = !open"
|
|
40
|
+
>
|
|
41
|
+
<Icon name="arrow-down-01" />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
ref="dropdown"
|
|
48
|
+
popover="manual"
|
|
49
|
+
:id="listboxId"
|
|
50
|
+
role="listbox"
|
|
51
|
+
:aria-label="label"
|
|
52
|
+
class="m-0 max-h-[min(16rem,50vh)] overflow-y-auto rounded-ui border border-border bg-popover p-1 text-popover-foreground shadow-xl"
|
|
53
|
+
:style="panel"
|
|
17
54
|
>
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
<Button
|
|
56
|
+
v-for="option in filtered"
|
|
57
|
+
:key="option.value"
|
|
58
|
+
class="w-full"
|
|
59
|
+
variant="ghost"
|
|
60
|
+
layout="left"
|
|
61
|
+
:disabled="option.disabled"
|
|
62
|
+
role="option"
|
|
63
|
+
:aria-selected="option.value === modelValue"
|
|
64
|
+
@click="select(option)"
|
|
65
|
+
>
|
|
66
|
+
<span class="flex w-full items-center justify-between gap-4 text-left">
|
|
67
|
+
<slot name="option" :option="option" :selected="option.value === modelValue">
|
|
68
|
+
{{ option.label }}
|
|
22
69
|
</slot>
|
|
70
|
+
|
|
71
|
+
<Icon v-if="option.value === modelValue" name="tick-02" />
|
|
23
72
|
</span>
|
|
73
|
+
</Button>
|
|
24
74
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<Button
|
|
30
|
-
v-for="option in options"
|
|
31
|
-
:key="option.value"
|
|
32
|
-
class="w-full"
|
|
33
|
-
variant="ghost"
|
|
34
|
-
layout="left"
|
|
35
|
-
:disabled="option.disabled"
|
|
36
|
-
role="option"
|
|
37
|
-
:aria-selected="option.value === modelValue"
|
|
38
|
-
@click="select(option)"
|
|
39
|
-
>
|
|
40
|
-
<span class="flex w-full items-center justify-between gap-4 text-left">
|
|
41
|
-
<slot name="option" :option="option" :selected="option.value === modelValue">
|
|
42
|
-
{{ option.label }}
|
|
43
|
-
</slot>
|
|
44
|
-
|
|
45
|
-
<Icon v-if="option.value === modelValue" name="tick-02" />
|
|
46
|
-
</span>
|
|
47
|
-
</Button>
|
|
48
|
-
</div>
|
|
49
|
-
</Popover>
|
|
75
|
+
<p v-if="filtered.length === 0" class="px-2 py-6 text-center text-sm opacity-60">
|
|
76
|
+
{{ emptyLabel }}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
50
79
|
</div>
|
|
51
80
|
</template>
|
|
52
81
|
|
|
53
82
|
<script setup lang="ts">
|
|
54
|
-
import { computed, ref } from "vue";
|
|
55
|
-
import type { SelectOption, SelectValue
|
|
83
|
+
import { computed, nextTick, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
|
|
84
|
+
import type { SelectOption, SelectValue } from "../../components/select.ts";
|
|
56
85
|
import Button from "./button.vue";
|
|
57
86
|
import Icon from "./icon.vue";
|
|
58
|
-
import Popover from "./popover.vue";
|
|
59
87
|
|
|
60
88
|
const {
|
|
61
89
|
modelValue = undefined,
|
|
62
90
|
options,
|
|
63
91
|
label = undefined,
|
|
64
92
|
placeholder = undefined,
|
|
93
|
+
emptyLabel = "No results found",
|
|
65
94
|
name = undefined,
|
|
66
95
|
required = false,
|
|
67
96
|
disabled = false,
|
|
@@ -70,6 +99,7 @@ const {
|
|
|
70
99
|
options: Array<SelectOption>;
|
|
71
100
|
label?: string;
|
|
72
101
|
placeholder?: string;
|
|
102
|
+
emptyLabel?: string;
|
|
73
103
|
name?: string;
|
|
74
104
|
required?: boolean;
|
|
75
105
|
disabled?: boolean;
|
|
@@ -81,14 +111,35 @@ const emit = defineEmits<{
|
|
|
81
111
|
|
|
82
112
|
const open = ref(false);
|
|
83
113
|
|
|
114
|
+
const container = useTemplateRef("container");
|
|
115
|
+
|
|
116
|
+
const trigger = useTemplateRef("trigger");
|
|
117
|
+
|
|
118
|
+
const search = useTemplateRef("search");
|
|
119
|
+
|
|
120
|
+
const dropdown = useTemplateRef("dropdown");
|
|
121
|
+
|
|
122
|
+
const listboxId = `select-${useId()}`;
|
|
123
|
+
|
|
84
124
|
const selected = computed(() => options.find((option) => option.value === modelValue));
|
|
85
125
|
|
|
86
|
-
const
|
|
126
|
+
const query = ref(selected.value?.label ?? "");
|
|
87
127
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
const panel = ref<Record<string, string>>({});
|
|
129
|
+
|
|
130
|
+
const filtered = computed(() => {
|
|
131
|
+
const term = query.value.trim().toLowerCase();
|
|
132
|
+
|
|
133
|
+
return term === "" ? options : options.filter((option) => option.label.toLowerCase().includes(term));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function handlePointerDown(event: PointerEvent) {
|
|
137
|
+
const target = event.target as Node;
|
|
138
|
+
|
|
139
|
+
if (!container.value?.contains(target) && !dropdown.value?.contains(target)) {
|
|
140
|
+
open.value = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
92
143
|
|
|
93
144
|
function select(option: SelectOption) {
|
|
94
145
|
if (!option.disabled) {
|
|
@@ -96,4 +147,79 @@ function select(option: SelectOption) {
|
|
|
96
147
|
open.value = false;
|
|
97
148
|
}
|
|
98
149
|
}
|
|
150
|
+
|
|
151
|
+
function updatePanel() {
|
|
152
|
+
const anchor = trigger.value;
|
|
153
|
+
const list = dropdown.value;
|
|
154
|
+
|
|
155
|
+
if (!anchor || !list) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const bounds = anchor.getBoundingClientRect();
|
|
160
|
+
const height = list.offsetHeight;
|
|
161
|
+
const width = bounds.width;
|
|
162
|
+
const left = Math.min(Math.max(8, bounds.left), window.innerWidth - width - 8);
|
|
163
|
+
const top =
|
|
164
|
+
bounds.bottom + height + 8 <= window.innerHeight ? bounds.bottom + 8 : Math.max(8, bounds.top - height - 8);
|
|
165
|
+
|
|
166
|
+
panel.value = {
|
|
167
|
+
bottom: "auto",
|
|
168
|
+
left: `${left}px`,
|
|
169
|
+
position: "fixed",
|
|
170
|
+
right: "auto",
|
|
171
|
+
top: `${top}px`,
|
|
172
|
+
width: `${width}px`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
watch(selected, (value) => {
|
|
177
|
+
if (!open.value) {
|
|
178
|
+
query.value = value?.label ?? "";
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
watch(open, async (value) => {
|
|
183
|
+
const list = dropdown.value;
|
|
184
|
+
|
|
185
|
+
if (!list) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value && !list.matches(":popover-open")) {
|
|
190
|
+
query.value = "";
|
|
191
|
+
|
|
192
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
193
|
+
|
|
194
|
+
window.addEventListener("resize", updatePanel);
|
|
195
|
+
|
|
196
|
+
window.addEventListener("scroll", updatePanel, true);
|
|
197
|
+
|
|
198
|
+
list.showPopover();
|
|
199
|
+
|
|
200
|
+
await nextTick();
|
|
201
|
+
|
|
202
|
+
updatePanel();
|
|
203
|
+
|
|
204
|
+
search.value?.focus();
|
|
205
|
+
} else if (!value && list.matches(":popover-open")) {
|
|
206
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
207
|
+
|
|
208
|
+
window.removeEventListener("resize", updatePanel);
|
|
209
|
+
|
|
210
|
+
window.removeEventListener("scroll", updatePanel, true);
|
|
211
|
+
|
|
212
|
+
list.hidePopover();
|
|
213
|
+
|
|
214
|
+
query.value = selected.value?.label ?? "";
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
onUnmounted(() => {
|
|
219
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
220
|
+
|
|
221
|
+
window.removeEventListener("resize", updatePanel);
|
|
222
|
+
|
|
223
|
+
window.removeEventListener("scroll", updatePanel, true);
|
|
224
|
+
});
|
|
99
225
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revojs/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"@vitejs/plugin-vue": "^6.0.9"
|
|
46
46
|
},
|
|
47
47
|
"optionalDependencies": {
|
|
48
|
-
"vue": "^3.5.
|
|
48
|
+
"vue": "^3.5.43"
|
|
49
49
|
}
|
|
50
50
|
}
|