@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
- <svelte:document onpointerdown={onDocPointerDown} />
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
- {#if open}
316
- <div
317
- id={listboxId}
318
- role="listbox"
319
- class="left-0 right-0 mt-2 max-h-60 rounded-xl border-border bg-surface-elevated p-1.5 shadow-xl absolute z-50 overflow-y-auto border"
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
- </div>
373
- {/if}
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-1">
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-4 border-b border-border', isCard && '-mx-4 sm:-mx-5']}></div>
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'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.14.11",
3
+ "version": "0.14.12",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",