@joewinke/jatui 0.8.3 → 0.8.5
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.
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
dropup = false,
|
|
33
33
|
appendToBody = false,
|
|
34
34
|
clearable = false,
|
|
35
|
+
clearLabel = 'Clear selection',
|
|
35
36
|
triggerAttrs,
|
|
36
37
|
embedded = false,
|
|
37
38
|
onChange,
|
|
@@ -57,6 +58,8 @@
|
|
|
57
58
|
appendToBody?: boolean;
|
|
58
59
|
/** Show an × button on the trigger when a value is selected */
|
|
59
60
|
clearable?: boolean;
|
|
61
|
+
/** Accessible label for the clear (×) button. */
|
|
62
|
+
clearLabel?: string;
|
|
60
63
|
/** Extra attributes forwarded to the trigger button (e.g. data-rail-id) */
|
|
61
64
|
triggerAttrs?: Record<string, string>;
|
|
62
65
|
/** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
|
|
@@ -194,6 +197,35 @@
|
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
function positionPanel() {
|
|
201
|
+
if (!triggerRef) return;
|
|
202
|
+
const rect = triggerRef.getBoundingClientRect();
|
|
203
|
+
const width = Math.max(rect.width, 192);
|
|
204
|
+
// Viewport-aware flip: if there isn't room below the trigger but there
|
|
205
|
+
// is above, anchor the panel to the trigger's top and grow upward. This
|
|
206
|
+
// keeps the panel on-screen for rows near the bottom of a scroll area
|
|
207
|
+
// (e.g. suggested-task rows in the completed-session overlay).
|
|
208
|
+
const estPanelHeight = 300;
|
|
209
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
210
|
+
const spaceAbove = rect.top;
|
|
211
|
+
const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
|
|
212
|
+
// Clamp left so a right-edge trigger doesn't push the panel off-screen.
|
|
213
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
|
|
214
|
+
// .sd-panel's class rule always sets `top: 100%`, so each branch must
|
|
215
|
+
// explicitly reset the property it isn't using — otherwise the flip-up
|
|
216
|
+
// branch ends up with both top (100% = viewport height) and bottom set
|
|
217
|
+
// while height is auto, which over-constrains to a negative (clamped
|
|
218
|
+
// to 0) height and the whole panel silently renders invisible.
|
|
219
|
+
const vertical = flipUp
|
|
220
|
+
? `bottom: ${window.innerHeight - rect.top + 4}px; top: auto;`
|
|
221
|
+
: `top: ${rect.bottom + 4}px; bottom: auto;`;
|
|
222
|
+
// min-width override is required: .sd-panel's class rule sets
|
|
223
|
+
// `min-width: max(100%, 14rem)`, and for a position:fixed element
|
|
224
|
+
// that 100% resolves against the viewport, not the trigger — without
|
|
225
|
+
// this override the panel balloons to ~100vw wide.
|
|
226
|
+
panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
$effect(() => {
|
|
198
230
|
if (embedded) {
|
|
199
231
|
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
@@ -201,31 +233,26 @@
|
|
|
201
233
|
}
|
|
202
234
|
if (open) {
|
|
203
235
|
if (appendToBody && triggerRef) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// min-width override is required: .sd-panel's class rule sets
|
|
225
|
-
// `min-width: max(100%, 14rem)`, and for a position:fixed element
|
|
226
|
-
// that 100% resolves against the viewport, not the trigger — without
|
|
227
|
-
// this override the panel balloons to ~100vw wide.
|
|
228
|
-
panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
|
|
236
|
+
positionPanel();
|
|
237
|
+
// A position:fixed panel is anchored to the VIEWPORT, so without this it
|
|
238
|
+
// stays put while the page scrolls and appears to follow the reader down
|
|
239
|
+
// the page (Joe UAT 2026-09-25). Re-anchor on every scroll (capture: any
|
|
240
|
+
// scroll container) and resize; close once the trigger leaves the view.
|
|
241
|
+
const reanchor = (e?: Event) => {
|
|
242
|
+
if (e && e.target instanceof Node && containerRef?.contains(e.target)) return;
|
|
243
|
+
const r = triggerRef?.getBoundingClientRect();
|
|
244
|
+
if (!r || r.bottom < 0 || r.top > window.innerHeight) { open = false; searchQuery = ''; return; }
|
|
245
|
+
positionPanel();
|
|
246
|
+
};
|
|
247
|
+
window.addEventListener('scroll', reanchor, { capture: true, passive: true });
|
|
248
|
+
window.addEventListener('resize', reanchor, { passive: true });
|
|
249
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
250
|
+
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
251
|
+
return () => {
|
|
252
|
+
window.removeEventListener('scroll', reanchor, { capture: true });
|
|
253
|
+
window.removeEventListener('resize', reanchor);
|
|
254
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
255
|
+
};
|
|
229
256
|
}
|
|
230
257
|
document.addEventListener('mousedown', handleClickOutside);
|
|
231
258
|
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
@@ -349,6 +376,7 @@
|
|
|
349
376
|
class:sd-disabled={disabled}
|
|
350
377
|
class:sd-chip={variant === 'chip'}
|
|
351
378
|
class:sd-sm={size === 'sm'}
|
|
379
|
+
class:sd-has-clear={clearable && value !== ''}
|
|
352
380
|
style={activeColor
|
|
353
381
|
? variant === 'chip'
|
|
354
382
|
? `--sd-color: ${activeColor};`
|
|
@@ -371,20 +399,23 @@
|
|
|
371
399
|
{/if}
|
|
372
400
|
<span class="truncate">{triggerLabel}</span>
|
|
373
401
|
</span>
|
|
374
|
-
{#if clearable && value !== ''}
|
|
375
|
-
<button
|
|
376
|
-
type="button"
|
|
377
|
-
class="sd-clear"
|
|
378
|
-
aria-label="Clear filter"
|
|
379
|
-
onclick={(e) => { e.stopPropagation(); onChange(''); }}
|
|
380
|
-
>
|
|
381
|
-
<svg class="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
382
|
-
</button>
|
|
383
|
-
{/if}
|
|
384
402
|
<svg class="sd-chevron" class:sd-chevron-open={open} fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
385
403
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
386
404
|
</svg>
|
|
387
405
|
</button>
|
|
406
|
+
<!-- The clear control is a SIBLING of the trigger (a <button> inside a
|
|
407
|
+
<button> is invalid HTML and unreachable by keyboard), pinned to the
|
|
408
|
+
trigger's right edge beside the chevron (mos-hhzd0). -->
|
|
409
|
+
{#if clearable && value !== ''}
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
class="sd-clear"
|
|
413
|
+
aria-label={clearLabel}
|
|
414
|
+
onclick={(e) => { e.stopPropagation(); onChange(''); }}
|
|
415
|
+
>
|
|
416
|
+
<svg class="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
417
|
+
</button>
|
|
418
|
+
{/if}
|
|
388
419
|
|
|
389
420
|
{#if open}
|
|
390
421
|
<div
|
|
@@ -494,6 +525,7 @@
|
|
|
494
525
|
}
|
|
495
526
|
|
|
496
527
|
.sd-trigger-label {
|
|
528
|
+
flex: 1 1 auto;
|
|
497
529
|
display: flex;
|
|
498
530
|
align-items: center;
|
|
499
531
|
gap: 0.375rem;
|
|
@@ -520,7 +552,16 @@
|
|
|
520
552
|
font-size: 0.75rem;
|
|
521
553
|
}
|
|
522
554
|
|
|
555
|
+
/* Room for the absolutely-positioned × between the label and the chevron
|
|
556
|
+
(the chevron stays at the trigger's right edge). */
|
|
557
|
+
.sd-has-clear .sd-trigger-label {
|
|
558
|
+
padding-right: 1.25rem;
|
|
559
|
+
}
|
|
523
560
|
.sd-clear {
|
|
561
|
+
position: absolute;
|
|
562
|
+
top: 50%;
|
|
563
|
+
right: 1.5rem;
|
|
564
|
+
transform: translateY(-50%);
|
|
524
565
|
display: flex;
|
|
525
566
|
align-items: center;
|
|
526
567
|
justify-content: center;
|
|
@@ -29,6 +29,8 @@ type $$ComponentProps = {
|
|
|
29
29
|
appendToBody?: boolean;
|
|
30
30
|
/** Show an × button on the trigger when a value is selected */
|
|
31
31
|
clearable?: boolean;
|
|
32
|
+
/** Accessible label for the clear (×) button. */
|
|
33
|
+
clearLabel?: string;
|
|
32
34
|
/** Extra attributes forwarded to the trigger button (e.g. data-rail-id) */
|
|
33
35
|
triggerAttrs?: Record<string, string>;
|
|
34
36
|
/** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
|