@r2digisolutions/components 0.14.11 → 0.14.13
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,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { on } from 'svelte/events';
|
|
3
4
|
import ComboboxItem from '../ComboboxItem/ComboboxItem.svelte';
|
|
4
5
|
import {
|
|
5
6
|
COMBOBOX_CREATE_KEY,
|
|
@@ -65,8 +66,11 @@
|
|
|
65
66
|
}: ComboboxProps = $props();
|
|
66
67
|
|
|
67
68
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
69
|
+
let triggerEl = $state<HTMLDivElement | null>(null);
|
|
70
|
+
let popoverEl = $state<HTMLDivElement | null>(null);
|
|
68
71
|
let inputEl = $state<HTMLInputElement | null>(null);
|
|
69
72
|
let highlighted = $state(0);
|
|
73
|
+
let openingGesture = $state(false);
|
|
70
74
|
let childEntries = $state<{ id: number; value: string; disabled: boolean; label: string }[]>([]);
|
|
71
75
|
let nextChildId = 0;
|
|
72
76
|
const uid = $props.id();
|
|
@@ -117,8 +121,96 @@
|
|
|
117
121
|
const highlightedKey = $derived(navKeys[activeIndex] ?? null);
|
|
118
122
|
const hasRows = $derived(items.length > 0 || childEntries.length > 0 || Boolean(children));
|
|
119
123
|
|
|
124
|
+
function positionPopover(opts: { measure?: boolean } = {}) {
|
|
125
|
+
if (!triggerEl || !popoverEl) return;
|
|
126
|
+
if (!popoverEl.matches(':popover-open') && opts.measure !== false) return;
|
|
127
|
+
|
|
128
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
129
|
+
if (rect.width < 2 && rect.height < 2) return;
|
|
130
|
+
|
|
131
|
+
const gap = 8;
|
|
132
|
+
const margin = 8;
|
|
133
|
+
const maxH = 240;
|
|
134
|
+
const spaceBelow = window.innerHeight - rect.bottom - gap - margin;
|
|
135
|
+
const spaceAbove = rect.top - gap - margin;
|
|
136
|
+
const openUp = spaceBelow < 120 && spaceAbove > spaceBelow;
|
|
137
|
+
const height = Math.min(maxH, Math.max(80, openUp ? spaceAbove : spaceBelow));
|
|
138
|
+
|
|
139
|
+
const top = openUp ? undefined : rect.bottom + gap;
|
|
140
|
+
const bottom = openUp ? window.innerHeight - rect.top + gap : undefined;
|
|
141
|
+
|
|
142
|
+
const style = [
|
|
143
|
+
'position:fixed',
|
|
144
|
+
'margin:0',
|
|
145
|
+
'inset:auto',
|
|
146
|
+
`left:${Math.max(margin, Math.min(rect.left, window.innerWidth - rect.width - margin))}px`,
|
|
147
|
+
`width:${rect.width}px`,
|
|
148
|
+
top !== undefined ? `top:${top}px` : 'top:auto',
|
|
149
|
+
bottom !== undefined ? `bottom:${bottom}px` : 'bottom:auto',
|
|
150
|
+
`max-height:${height}px`
|
|
151
|
+
].join(';');
|
|
152
|
+
|
|
153
|
+
popoverEl.style.cssText = style;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function syncNative() {
|
|
157
|
+
if (!popoverEl) return;
|
|
158
|
+
const isOpen = popoverEl.matches(':popover-open');
|
|
159
|
+
try {
|
|
160
|
+
if (open && !isOpen) popoverEl.showPopover();
|
|
161
|
+
else if (!open && isOpen) popoverEl.hidePopover();
|
|
162
|
+
} catch {
|
|
163
|
+
/* ignore */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onDocPointerDown(event: PointerEvent) {
|
|
168
|
+
if (!open || openingGesture) return;
|
|
169
|
+
const target = event.target;
|
|
170
|
+
if (!(target instanceof Node)) return;
|
|
171
|
+
if (rootEl?.contains(target) || popoverEl?.contains(target)) return;
|
|
172
|
+
setOpen(false);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function markOpeningGesture() {
|
|
176
|
+
openingGesture = true;
|
|
177
|
+
queueMicrotask(() => {
|
|
178
|
+
openingGesture = false;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function onPopoverClose() {
|
|
183
|
+
if (selected) query = selected.label;
|
|
184
|
+
else if (!creatable) query = '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleBeforeToggle(event: ToggleEvent) {
|
|
188
|
+
if (event.newState === 'open' && disabled) {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (event.newState === 'open') {
|
|
193
|
+
positionPopover({ measure: false });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleToggle(event: ToggleEvent) {
|
|
198
|
+
const next = event.newState === 'open';
|
|
199
|
+
open = next;
|
|
200
|
+
if (next) {
|
|
201
|
+
highlighted = 0;
|
|
202
|
+
queueMicrotask(() => {
|
|
203
|
+
positionPopover();
|
|
204
|
+
requestAnimationFrame(() => positionPopover());
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
onPopoverClose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
function setOpen(next: boolean) {
|
|
121
212
|
if (disabled) return;
|
|
213
|
+
if (next) markOpeningGesture();
|
|
122
214
|
open = next;
|
|
123
215
|
if (next) {
|
|
124
216
|
highlighted = 0;
|
|
@@ -207,21 +299,45 @@
|
|
|
207
299
|
}
|
|
208
300
|
}
|
|
209
301
|
|
|
210
|
-
function onDocPointerDown(e: PointerEvent) {
|
|
211
|
-
if (!open || !rootEl) return;
|
|
212
|
-
const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
|
|
213
|
-
if (path.includes(rootEl) || rootEl.contains(e.target as Node)) return;
|
|
214
|
-
setOpen(false);
|
|
215
|
-
if (selected) query = selected.label;
|
|
216
|
-
else if (!creatable) query = '';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
302
|
$effect(() => {
|
|
220
303
|
if (selected && !open && query !== selected.label) {
|
|
221
304
|
query = selected.label;
|
|
222
305
|
}
|
|
223
306
|
});
|
|
224
307
|
|
|
308
|
+
$effect(() => {
|
|
309
|
+
open;
|
|
310
|
+
queueMicrotask(() => {
|
|
311
|
+
syncNative();
|
|
312
|
+
if (open) positionPopover();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
$effect(() => {
|
|
317
|
+
if (!open) return;
|
|
318
|
+
|
|
319
|
+
let frame = 0;
|
|
320
|
+
const reposition = () => {
|
|
321
|
+
cancelAnimationFrame(frame);
|
|
322
|
+
frame = requestAnimationFrame(() => positionPopover());
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const offScroll = on(window, 'scroll', reposition, { capture: true, passive: true });
|
|
326
|
+
const offResize = on(window, 'resize', reposition);
|
|
327
|
+
// Defer past the opening click — same gesture must not count as outside.
|
|
328
|
+
const timer = window.setTimeout(() => {
|
|
329
|
+
document.addEventListener('pointerdown', onDocPointerDown, true);
|
|
330
|
+
}, 0);
|
|
331
|
+
|
|
332
|
+
return () => {
|
|
333
|
+
cancelAnimationFrame(frame);
|
|
334
|
+
offScroll();
|
|
335
|
+
offResize();
|
|
336
|
+
window.clearTimeout(timer);
|
|
337
|
+
document.removeEventListener('pointerdown', onDocPointerDown, true);
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
|
|
225
341
|
const ctx: ComboboxContext = {
|
|
226
342
|
getValue: () => value,
|
|
227
343
|
getQuery: () => query,
|
|
@@ -243,14 +359,13 @@
|
|
|
243
359
|
setComboboxContext(ctx);
|
|
244
360
|
</script>
|
|
245
361
|
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
<div class={['max-w-sm relative w-full', className]} bind:this={rootEl}>
|
|
362
|
+
<div class={['max-w-sm w-full', className]} bind:this={rootEl}>
|
|
249
363
|
{#if label}
|
|
250
364
|
<span class="mb-1.5 text-sm font-medium text-primary block">{label}</span>
|
|
251
365
|
{/if}
|
|
252
366
|
|
|
253
367
|
<div
|
|
368
|
+
bind:this={triggerEl}
|
|
254
369
|
class={[
|
|
255
370
|
'h-10 gap-2 rounded-xl border-border bg-surface-elevated px-3 flex items-center border transition-colors',
|
|
256
371
|
open && 'border-brand-500 ring-brand-500/20 ring-2',
|
|
@@ -312,16 +427,19 @@
|
|
|
312
427
|
{/if}
|
|
313
428
|
</div>
|
|
314
429
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
430
|
+
<div
|
|
431
|
+
bind:this={popoverEl}
|
|
432
|
+
id={listboxId}
|
|
433
|
+
popover="manual"
|
|
434
|
+
role="listbox"
|
|
435
|
+
onbeforetoggle={handleBeforeToggle}
|
|
436
|
+
ontoggle={handleToggle}
|
|
437
|
+
class="combobox-popover inset-auto m-0 overflow-y-auto rounded-xl border border-border bg-surface-elevated p-1.5 shadow-xl outline-none"
|
|
438
|
+
>
|
|
439
|
+
{#if !hasRows}
|
|
440
|
+
<div class="px-3 py-2.5 text-xs text-muted text-center">{emptyText}</div>
|
|
441
|
+
{:else}
|
|
442
|
+
{#each items as item, index (itemKey(item))}
|
|
325
443
|
{#if item.kind === 'create'}
|
|
326
444
|
<ComboboxItem
|
|
327
445
|
value={COMBOBOX_CREATE_KEY}
|
|
@@ -369,6 +487,19 @@
|
|
|
369
487
|
{/each}
|
|
370
488
|
{@render children?.()}
|
|
371
489
|
{/if}
|
|
372
|
-
|
|
373
|
-
{/if}
|
|
490
|
+
</div>
|
|
374
491
|
</div>
|
|
492
|
+
|
|
493
|
+
<style>
|
|
494
|
+
.combobox-popover {
|
|
495
|
+
position: fixed;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.combobox-popover:popover-open {
|
|
499
|
+
display: block;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.combobox-popover:not(:popover-open) {
|
|
503
|
+
display: none;
|
|
504
|
+
}
|
|
505
|
+
</style>
|
|
@@ -166,13 +166,14 @@
|
|
|
166
166
|
class={[
|
|
167
167
|
useSplit && 'lg:hidden',
|
|
168
168
|
isCard && 'px-4 pt-4 sm:px-5 sm:pt-5',
|
|
169
|
+
isCard && !divided && 'pb-1',
|
|
169
170
|
!isCard && 'pb-0'
|
|
170
171
|
]}
|
|
171
172
|
>
|
|
172
173
|
<div class="flex items-start justify-between gap-3">
|
|
173
174
|
<div class="flex min-w-0 flex-1 items-start gap-3">
|
|
174
175
|
{@render iconEl('md')}
|
|
175
|
-
<div class="min-w-0 space-y-
|
|
176
|
+
<div class="min-w-0 space-y-0.5">
|
|
176
177
|
{#if eyebrow}
|
|
177
178
|
<p class="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted">
|
|
178
179
|
{eyebrow}
|
|
@@ -192,7 +193,7 @@
|
|
|
192
193
|
</div>
|
|
193
194
|
|
|
194
195
|
{#if divided && layout === 'stack'}
|
|
195
|
-
<div class={['mt-
|
|
196
|
+
<div class={['mt-3 border-b border-border', isCard && '-mx-4 sm:-mx-5']}></div>
|
|
196
197
|
{/if}
|
|
197
198
|
</div>
|
|
198
199
|
{/if}
|
|
@@ -200,7 +201,8 @@
|
|
|
200
201
|
{#if open}
|
|
201
202
|
<div
|
|
202
203
|
class={[
|
|
203
|
-
isCard && 'px-4 py-4 sm:px-5 sm:py-5',
|
|
204
|
+
isCard && divided && 'px-4 py-4 sm:px-5 sm:py-5',
|
|
205
|
+
isCard && !divided && 'px-4 pt-2 pb-4 sm:px-5 sm:pb-5',
|
|
204
206
|
!isCard && (collapsible || divided) && layout === 'stack' && 'pt-4',
|
|
205
207
|
useSplit &&
|
|
206
208
|
'lg:grid lg:grid-cols-[minmax(12rem,17rem)_minmax(0,1fr)] lg:items-start lg:gap-10'
|