ketekny-ui-kit 1.0.18 → 1.0.19
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.
- package/package.json +1 -1
- package/src/ui/kSelect.vue +102 -108
package/package.json
CHANGED
package/src/ui/kSelect.vue
CHANGED
|
@@ -17,86 +17,75 @@
|
|
|
17
17
|
<button
|
|
18
18
|
v-if="clearable && isSelectionSet && !disabled"
|
|
19
19
|
type="button"
|
|
20
|
-
class="absolute text-gray-400 -translate-y-1/2 right-8 top-1/2 hover:text-red-500"
|
|
20
|
+
class="absolute text-gray-400 -translate-y-1/2 right-8 top-1/2 hover:text-red-500 transition-colors"
|
|
21
21
|
aria-label="Clear selection"
|
|
22
22
|
@pointerdown.stop.prevent="clearSelection"
|
|
23
23
|
@click.stop.prevent="clearSelection"
|
|
24
24
|
>
|
|
25
|
-
<X class="w-
|
|
25
|
+
<X class="w-4 h-4" />
|
|
26
26
|
</button>
|
|
27
27
|
|
|
28
28
|
<span class="absolute text-gray-400 -translate-y-1/2 pointer-events-none right-3 top-1/2">
|
|
29
|
-
<ChevronDown class="w-
|
|
30
|
-
<ChevronUp class="w-5 h-5" v-else />
|
|
29
|
+
<ChevronDown class="w-4 h-4 transition-transform duration-200" :class="{ 'rotate-180': open }" />
|
|
31
30
|
</span>
|
|
32
31
|
</div>
|
|
33
32
|
</SelectTrigger>
|
|
34
33
|
|
|
35
34
|
<SelectPortal>
|
|
36
35
|
<SelectContent
|
|
37
|
-
:id="dropdownId"
|
|
38
36
|
position="popper"
|
|
39
37
|
:side-offset="6"
|
|
40
|
-
class="z-[9999]
|
|
38
|
+
class="select-content z-[9999] bg-white border border-gray-200 rounded-lg shadow-[0_10px_38px_-10px_rgba(22,23,24,0.35),0_10px_20px_-15px_rgba(22,23,24,0.2)] overflow-hidden"
|
|
41
39
|
>
|
|
42
|
-
<div v-if="searchable"
|
|
40
|
+
<div v-if="searchable" class="sticky top-0 z-10 p-1.5 border-b border-gray-100 bg-white">
|
|
43
41
|
<input
|
|
44
42
|
ref="searchInput"
|
|
45
43
|
type="text"
|
|
46
44
|
v-model="searchQuery"
|
|
47
45
|
placeholder="Αναζήτηση..."
|
|
48
|
-
class="w-full px-
|
|
46
|
+
class="w-full px-2.5 py-1.5 text-sm border border-gray-200 rounded-md focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/30 transition-colors placeholder-gray-400"
|
|
49
47
|
role="searchbox"
|
|
50
48
|
aria-label="Search options"
|
|
51
|
-
@keydown
|
|
49
|
+
@keydown="handleSearchKeydown"
|
|
50
|
+
@keyup.stop
|
|
52
51
|
/>
|
|
53
52
|
</div>
|
|
54
53
|
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
<SelectViewport
|
|
55
|
+
ref="viewportRef"
|
|
56
|
+
class="p-1 overflow-y-auto"
|
|
57
|
+
:style="{ maxHeight: dropdownHeight }"
|
|
58
|
+
>
|
|
59
|
+
<SelectItem
|
|
60
|
+
v-for="(option, index) in filteredOptions"
|
|
61
|
+
:key="`${option[optionValue]}-${index}`"
|
|
62
|
+
:value="option[optionValue]"
|
|
63
|
+
class="select-item relative flex items-center gap-1.5 rounded-[4px] py-1.5 pl-6 pr-3 text-sm cursor-pointer outline-none select-none text-slate-700
|
|
64
|
+
data-[highlighted]:bg-primary data-[highlighted]:text-white
|
|
65
|
+
data-[state=checked]:text-primary
|
|
66
|
+
data-[state=checked]:data-[highlighted]:text-white"
|
|
61
67
|
>
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<div class="flex items-center justify-between gap-2">
|
|
70
|
-
<SelectItemText class="truncate">{{ option[optionLabel] }}</SelectItemText>
|
|
71
|
-
<span class="inline-flex items-center justify-center w-4 h-4 shrink-0">
|
|
72
|
-
<SelectItemIndicator>
|
|
73
|
-
<Check class="w-4 h-4" />
|
|
74
|
-
</SelectItemIndicator>
|
|
75
|
-
</span>
|
|
76
|
-
</div>
|
|
77
|
-
</SelectItem>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<div v-if="filteredOptions.length === 0" class="px-3 py-2 text-gray-400">Δεν βρέθηκαν επιλογές</div>
|
|
81
|
-
</SelectViewport>
|
|
82
|
-
|
|
83
|
-
<SelectScrollUpButton class="absolute top-0 left-0 right-0 z-10 flex items-center justify-center h-6 text-gray-500 bg-white/95">
|
|
84
|
-
<ChevronUp class="w-4 h-4" />
|
|
85
|
-
</SelectScrollUpButton>
|
|
68
|
+
<span class="absolute left-1.5 flex items-center justify-center w-4 h-4 shrink-0">
|
|
69
|
+
<SelectItemIndicator>
|
|
70
|
+
<Check class="w-3.5 h-3.5" />
|
|
71
|
+
</SelectItemIndicator>
|
|
72
|
+
</span>
|
|
73
|
+
<SelectItemText class="truncate">{{ option[optionLabel] }}</SelectItemText>
|
|
74
|
+
</SelectItem>
|
|
86
75
|
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
</
|
|
90
|
-
</
|
|
76
|
+
<div v-if="filteredOptions.length === 0" class="px-3 py-2 text-sm text-gray-400 text-center">
|
|
77
|
+
Δεν βρέθηκαν επιλογές
|
|
78
|
+
</div>
|
|
79
|
+
</SelectViewport>
|
|
91
80
|
</SelectContent>
|
|
92
81
|
</SelectPortal>
|
|
93
82
|
</SelectRoot>
|
|
94
83
|
|
|
95
|
-
<div class="mt-1 text-red-500" v-if="hasError && error !== true && error !== ''">
|
|
84
|
+
<div class="mt-1 text-sm text-red-500" v-if="hasError && error !== true && error !== ''">
|
|
96
85
|
{{ error }}
|
|
97
86
|
</div>
|
|
98
87
|
|
|
99
|
-
<div class="mt-1 text-gray-500" v-if="info">
|
|
88
|
+
<div class="mt-1 text-sm text-gray-500" v-if="info">
|
|
100
89
|
{{ info }}
|
|
101
90
|
</div>
|
|
102
91
|
</div>
|
|
@@ -111,13 +100,11 @@ import {
|
|
|
111
100
|
SelectItemText,
|
|
112
101
|
SelectPortal,
|
|
113
102
|
SelectRoot,
|
|
114
|
-
SelectScrollDownButton,
|
|
115
|
-
SelectScrollUpButton,
|
|
116
103
|
SelectTrigger,
|
|
117
104
|
SelectValue,
|
|
118
105
|
SelectViewport,
|
|
119
106
|
} from "reka-ui";
|
|
120
|
-
import { X, ChevronDown,
|
|
107
|
+
import { X, ChevronDown, Check } from "lucide-vue-next";
|
|
121
108
|
|
|
122
109
|
const props = defineProps({
|
|
123
110
|
options: { type: Array, required: true },
|
|
@@ -130,6 +117,7 @@ const props = defineProps({
|
|
|
130
117
|
disabled: { type: Boolean, default: false },
|
|
131
118
|
placeholder: { type: String, default: "Επιλέξτε μία τιμή" },
|
|
132
119
|
id: { type: String, default: null },
|
|
120
|
+
dropdownHeight: { type: String, default: "min(400px, 40vh)" },
|
|
133
121
|
clearable: { type: Boolean, default: true },
|
|
134
122
|
searchable: { type: Boolean, default: true },
|
|
135
123
|
});
|
|
@@ -139,16 +127,13 @@ const emit = defineEmits(["update:modelValue"]);
|
|
|
139
127
|
const open = ref(false);
|
|
140
128
|
const searchQuery = ref("");
|
|
141
129
|
const searchInput = ref(null);
|
|
142
|
-
const
|
|
130
|
+
const viewportRef = ref(null);
|
|
143
131
|
const generatedId = `select-${Math.random().toString(36).substr(2, 9)}`;
|
|
144
|
-
const viewportMaxHeight = "400px";
|
|
145
132
|
const defaultStyle = "w-full px-3 py-2 border rounded-lg transition shadow-sm focus:outline-none text-gray-700 focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white placeholder-gray-400";
|
|
146
133
|
const errorStyle = "border-red-500 focus:ring focus:ring-red-300";
|
|
147
134
|
const disabledStyle = "!bg-gray-100 !text-gray-400 !cursor-not-allowed";
|
|
148
135
|
|
|
149
136
|
const computedId = computed(() => (props.id != null && props.id !== "" ? props.id : generatedId));
|
|
150
|
-
const dropdownId = computed(() => `${computedId.value}-listbox`);
|
|
151
|
-
|
|
152
137
|
const hasError = computed(() => props.error != null && props.error !== false);
|
|
153
138
|
const isSelectionSet = computed(() => props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== "");
|
|
154
139
|
|
|
@@ -172,17 +157,10 @@ watch(open, (val) => {
|
|
|
172
157
|
if (val) {
|
|
173
158
|
nextTick(() => {
|
|
174
159
|
if (props.searchable) searchInput.value?.focus();
|
|
175
|
-
updateScrollState();
|
|
176
160
|
});
|
|
177
161
|
return;
|
|
178
162
|
}
|
|
179
163
|
searchQuery.value = "";
|
|
180
|
-
canScrollAny.value = false;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
watch(filteredOptions, () => {
|
|
184
|
-
if (!open.value) return;
|
|
185
|
-
nextTick(() => updateScrollState());
|
|
186
164
|
});
|
|
187
165
|
|
|
188
166
|
function clearSelection() {
|
|
@@ -190,81 +168,97 @@ function clearSelection() {
|
|
|
190
168
|
open.value = false;
|
|
191
169
|
}
|
|
192
170
|
|
|
193
|
-
function
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
196
|
-
|
|
197
|
-
if (!nodes.length) return;
|
|
198
|
-
const target = last ? nodes[nodes.length - 1] : nodes[0];
|
|
199
|
-
target.focus();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function moveOptionFocus(step) {
|
|
203
|
-
const container = document.getElementById(dropdownId.value);
|
|
204
|
-
if (!container) return;
|
|
205
|
-
const nodes = Array.from(container.querySelectorAll('[role="option"]:not([data-disabled])'));
|
|
206
|
-
if (!nodes.length) return;
|
|
207
|
-
const activeIndex = nodes.findIndex((node) => node === document.activeElement);
|
|
208
|
-
const baseIndex = activeIndex >= 0 ? activeIndex : step > 0 ? -1 : 0;
|
|
209
|
-
const nextIndex = Math.min(Math.max(baseIndex + step, 0), nodes.length - 1);
|
|
210
|
-
nodes[nextIndex].focus();
|
|
171
|
+
function getOptionNodes() {
|
|
172
|
+
const el = viewportRef.value?.$el ?? viewportRef.value;
|
|
173
|
+
if (!el) return [];
|
|
174
|
+
return Array.from(el.querySelectorAll('[role="option"]:not([data-disabled])'));
|
|
211
175
|
}
|
|
212
176
|
|
|
213
177
|
function handleSearchKeydown(event) {
|
|
178
|
+
// Always stop propagation to prevent reka-ui typeahead on character keys
|
|
179
|
+
event.stopPropagation();
|
|
180
|
+
|
|
214
181
|
if (event.key === "Escape") {
|
|
215
182
|
event.preventDefault();
|
|
216
|
-
event.stopPropagation();
|
|
217
183
|
open.value = false;
|
|
218
184
|
return;
|
|
219
185
|
}
|
|
220
186
|
|
|
221
|
-
if (event.key === "ArrowDown") {
|
|
222
|
-
event.preventDefault();
|
|
223
|
-
event.stopPropagation();
|
|
224
|
-
moveOptionFocus(1);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (event.key === "ArrowUp") {
|
|
187
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
229
188
|
event.preventDefault();
|
|
230
|
-
|
|
231
|
-
|
|
189
|
+
const nodes = getOptionNodes();
|
|
190
|
+
if (!nodes.length) return;
|
|
191
|
+
const activeIndex = nodes.findIndex((n) => n === document.activeElement);
|
|
192
|
+
if (event.key === "ArrowDown") {
|
|
193
|
+
const next = activeIndex < nodes.length - 1 ? nodes[activeIndex + 1] : nodes[0];
|
|
194
|
+
next.focus();
|
|
195
|
+
} else {
|
|
196
|
+
const prev = activeIndex > 0 ? nodes[activeIndex - 1] : nodes[nodes.length - 1];
|
|
197
|
+
prev.focus();
|
|
198
|
+
}
|
|
232
199
|
return;
|
|
233
200
|
}
|
|
234
201
|
|
|
235
202
|
if (event.key === "Home") {
|
|
236
203
|
event.preventDefault();
|
|
237
|
-
|
|
238
|
-
|
|
204
|
+
const nodes = getOptionNodes();
|
|
205
|
+
nodes[0]?.focus();
|
|
239
206
|
return;
|
|
240
207
|
}
|
|
241
208
|
|
|
242
209
|
if (event.key === "End") {
|
|
243
210
|
event.preventDefault();
|
|
244
|
-
|
|
245
|
-
|
|
211
|
+
const nodes = getOptionNodes();
|
|
212
|
+
nodes[nodes.length - 1]?.focus();
|
|
246
213
|
return;
|
|
247
214
|
}
|
|
248
215
|
|
|
249
216
|
if (event.key === "Enter") {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
event.preventDefault();
|
|
255
|
-
event.stopPropagation();
|
|
256
|
-
active.click();
|
|
257
|
-
}
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
const nodes = getOptionNodes();
|
|
219
|
+
const focused = nodes.find((n) => n === document.activeElement);
|
|
220
|
+
if (focused) focused.click();
|
|
258
221
|
}
|
|
259
222
|
}
|
|
223
|
+
</script>
|
|
260
224
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
225
|
+
<style scoped>
|
|
226
|
+
.select-content {
|
|
227
|
+
transform-origin: var(--reka-select-content-transform-origin);
|
|
228
|
+
width: var(--reka-popper-anchor-width);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.select-content[data-state="open"] {
|
|
232
|
+
animation: selectOpen 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.select-content[data-state="closed"] {
|
|
236
|
+
animation: selectClose 0.1s ease-in;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@keyframes selectOpen {
|
|
240
|
+
from {
|
|
241
|
+
opacity: 0;
|
|
242
|
+
transform: translateY(-6px) scale(0.97);
|
|
243
|
+
}
|
|
244
|
+
to {
|
|
245
|
+
opacity: 1;
|
|
246
|
+
transform: translateY(0) scale(1);
|
|
267
247
|
}
|
|
268
|
-
canScrollAny.value = viewport.scrollHeight > viewport.clientHeight + 1;
|
|
269
248
|
}
|
|
270
|
-
|
|
249
|
+
|
|
250
|
+
@keyframes selectClose {
|
|
251
|
+
from {
|
|
252
|
+
opacity: 1;
|
|
253
|
+
transform: translateY(0) scale(1);
|
|
254
|
+
}
|
|
255
|
+
to {
|
|
256
|
+
opacity: 0;
|
|
257
|
+
transform: translateY(-6px) scale(0.97);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.select-item {
|
|
262
|
+
transition: background-color 80ms, color 80ms;
|
|
263
|
+
}
|
|
264
|
+
</style>
|