@r2digisolutions/components 0.14.11 → 0.14.12
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,6 +66,8 @@
|
|
|
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);
|
|
70
73
|
let childEntries = $state<{ id: number; value: string; disabled: boolean; label: string }[]>([]);
|
|
@@ -117,6 +120,78 @@
|
|
|
117
120
|
const highlightedKey = $derived(navKeys[activeIndex] ?? null);
|
|
118
121
|
const hasRows = $derived(items.length > 0 || childEntries.length > 0 || Boolean(children));
|
|
119
122
|
|
|
123
|
+
function positionPopover(opts: { measure?: boolean } = {}) {
|
|
124
|
+
if (!triggerEl || !popoverEl) return;
|
|
125
|
+
if (!popoverEl.matches(':popover-open') && opts.measure !== false) return;
|
|
126
|
+
|
|
127
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
128
|
+
if (rect.width < 2 && rect.height < 2) return;
|
|
129
|
+
|
|
130
|
+
const gap = 8;
|
|
131
|
+
const margin = 8;
|
|
132
|
+
const maxH = 240;
|
|
133
|
+
const spaceBelow = window.innerHeight - rect.bottom - gap - margin;
|
|
134
|
+
const spaceAbove = rect.top - gap - margin;
|
|
135
|
+
const openUp = spaceBelow < 120 && spaceAbove > spaceBelow;
|
|
136
|
+
const height = Math.min(maxH, Math.max(80, openUp ? spaceAbove : spaceBelow));
|
|
137
|
+
|
|
138
|
+
const top = openUp ? undefined : rect.bottom + gap;
|
|
139
|
+
const bottom = openUp ? window.innerHeight - rect.top + gap : undefined;
|
|
140
|
+
|
|
141
|
+
const style = [
|
|
142
|
+
'position:fixed',
|
|
143
|
+
'margin:0',
|
|
144
|
+
'inset:auto',
|
|
145
|
+
`left:${Math.max(margin, Math.min(rect.left, window.innerWidth - rect.width - margin))}px`,
|
|
146
|
+
`width:${rect.width}px`,
|
|
147
|
+
top !== undefined ? `top:${top}px` : 'top:auto',
|
|
148
|
+
bottom !== undefined ? `bottom:${bottom}px` : 'bottom:auto',
|
|
149
|
+
`max-height:${height}px`
|
|
150
|
+
].join(';');
|
|
151
|
+
|
|
152
|
+
popoverEl.style.cssText = style;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function syncNative() {
|
|
156
|
+
if (!popoverEl) return;
|
|
157
|
+
const isOpen = popoverEl.matches(':popover-open');
|
|
158
|
+
try {
|
|
159
|
+
if (open && !isOpen) popoverEl.showPopover();
|
|
160
|
+
else if (!open && isOpen) popoverEl.hidePopover();
|
|
161
|
+
} catch {
|
|
162
|
+
/* ignore */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function onPopoverClose() {
|
|
167
|
+
if (selected) query = selected.label;
|
|
168
|
+
else if (!creatable) query = '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleBeforeToggle(event: ToggleEvent) {
|
|
172
|
+
if (event.newState === 'open' && disabled) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (event.newState === 'open') {
|
|
177
|
+
positionPopover({ measure: false });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleToggle(event: ToggleEvent) {
|
|
182
|
+
const next = event.newState === 'open';
|
|
183
|
+
open = next;
|
|
184
|
+
if (next) {
|
|
185
|
+
highlighted = 0;
|
|
186
|
+
queueMicrotask(() => {
|
|
187
|
+
positionPopover();
|
|
188
|
+
requestAnimationFrame(() => positionPopover());
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
onPopoverClose();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
120
195
|
function setOpen(next: boolean) {
|
|
121
196
|
if (disabled) return;
|
|
122
197
|
open = next;
|
|
@@ -207,21 +282,39 @@
|
|
|
207
282
|
}
|
|
208
283
|
}
|
|
209
284
|
|
|
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
285
|
$effect(() => {
|
|
220
286
|
if (selected && !open && query !== selected.label) {
|
|
221
287
|
query = selected.label;
|
|
222
288
|
}
|
|
223
289
|
});
|
|
224
290
|
|
|
291
|
+
$effect(() => {
|
|
292
|
+
open;
|
|
293
|
+
queueMicrotask(() => {
|
|
294
|
+
syncNative();
|
|
295
|
+
if (open) positionPopover();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
$effect(() => {
|
|
300
|
+
if (!open) return;
|
|
301
|
+
|
|
302
|
+
let frame = 0;
|
|
303
|
+
const reposition = () => {
|
|
304
|
+
cancelAnimationFrame(frame);
|
|
305
|
+
frame = requestAnimationFrame(() => positionPopover());
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const offScroll = on(window, 'scroll', reposition, { capture: true, passive: true });
|
|
309
|
+
const offResize = on(window, 'resize', reposition);
|
|
310
|
+
|
|
311
|
+
return () => {
|
|
312
|
+
cancelAnimationFrame(frame);
|
|
313
|
+
offScroll();
|
|
314
|
+
offResize();
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
|
|
225
318
|
const ctx: ComboboxContext = {
|
|
226
319
|
getValue: () => value,
|
|
227
320
|
getQuery: () => query,
|
|
@@ -243,14 +336,13 @@
|
|
|
243
336
|
setComboboxContext(ctx);
|
|
244
337
|
</script>
|
|
245
338
|
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
<div class={['max-w-sm relative w-full', className]} bind:this={rootEl}>
|
|
339
|
+
<div class={['max-w-sm w-full', className]} bind:this={rootEl}>
|
|
249
340
|
{#if label}
|
|
250
341
|
<span class="mb-1.5 text-sm font-medium text-primary block">{label}</span>
|
|
251
342
|
{/if}
|
|
252
343
|
|
|
253
344
|
<div
|
|
345
|
+
bind:this={triggerEl}
|
|
254
346
|
class={[
|
|
255
347
|
'h-10 gap-2 rounded-xl border-border bg-surface-elevated px-3 flex items-center border transition-colors',
|
|
256
348
|
open && 'border-brand-500 ring-brand-500/20 ring-2',
|
|
@@ -312,12 +404,16 @@
|
|
|
312
404
|
{/if}
|
|
313
405
|
</div>
|
|
314
406
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
407
|
+
<div
|
|
408
|
+
bind:this={popoverEl}
|
|
409
|
+
id={listboxId}
|
|
410
|
+
popover="auto"
|
|
411
|
+
role="listbox"
|
|
412
|
+
onbeforetoggle={handleBeforeToggle}
|
|
413
|
+
ontoggle={handleToggle}
|
|
414
|
+
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"
|
|
415
|
+
>
|
|
416
|
+
{#if open}
|
|
321
417
|
{#if !hasRows}
|
|
322
418
|
<div class="px-3 py-2.5 text-xs text-muted text-center">{emptyText}</div>
|
|
323
419
|
{:else}
|
|
@@ -369,6 +465,20 @@
|
|
|
369
465
|
{/each}
|
|
370
466
|
{@render children?.()}
|
|
371
467
|
{/if}
|
|
372
|
-
|
|
373
|
-
|
|
468
|
+
{/if}
|
|
469
|
+
</div>
|
|
374
470
|
</div>
|
|
471
|
+
|
|
472
|
+
<style>
|
|
473
|
+
.combobox-popover {
|
|
474
|
+
position: fixed;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.combobox-popover:popover-open {
|
|
478
|
+
display: block;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.combobox-popover:not(:popover-open) {
|
|
482
|
+
display: none;
|
|
483
|
+
}
|
|
484
|
+
</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'
|