@lavalogic/scoria 0.37.42 → 0.37.44
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/dist/Components/HorizontalTabGroup.svelte +5 -0
- package/dist/Components/HorizontalTabGroupProps.d.ts +12 -0
- package/dist/Components/Table/Body/QuickSearchCell.svelte +12 -4
- package/dist/Components/Table/Misc/FilterInput.svelte +73 -9
- package/dist/Components/Table/Table.svelte +0 -2
- package/dist/Components/VerticalTabGroup.svelte +11 -0
- package/dist/Components/VerticalTabGroupProps.d.ts +12 -0
- package/dist/Helpers/dropdownPortal.d.ts +13 -8
- package/dist/Helpers/dropdownPortal.js +132 -8
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
selected,
|
|
16
16
|
noRadius = false,
|
|
17
17
|
grow = false,
|
|
18
|
+
dense = false,
|
|
18
19
|
filterable = false,
|
|
19
20
|
filterFn,
|
|
20
21
|
}: HorizontalTabGroupProps<CommonDenominator> = $props();
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
class="wrapper"
|
|
43
44
|
class:no-radius={noRadius}
|
|
44
45
|
class:grow
|
|
46
|
+
class:dense
|
|
45
47
|
>
|
|
46
48
|
<div class="tabs">
|
|
47
49
|
<div class="sticky">
|
|
@@ -104,6 +106,9 @@
|
|
|
104
106
|
.wrapper.grow {
|
|
105
107
|
flex-grow: 1;
|
|
106
108
|
}
|
|
109
|
+
.wrapper.dense {
|
|
110
|
+
align-items: flex-start;
|
|
111
|
+
}
|
|
107
112
|
.wrapper:not(.grow) .tabs {
|
|
108
113
|
padding-top: 1px;
|
|
109
114
|
}
|
|
@@ -15,6 +15,18 @@ interface HorizontalTabGroupPropsBase<CommonDenominator> {
|
|
|
15
15
|
noRadius?: boolean;
|
|
16
16
|
/** Stretch to fill the parent's flex axis. */
|
|
17
17
|
grow?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Anchor the tab strip to the top of the group rather than letting
|
|
20
|
+
* it stretch to the height of the active tab's content. In the
|
|
21
|
+
* default `align-items: stretch` row layout, the tabs strip and the
|
|
22
|
+
* content pane stretch each other to the tallest natural height -
|
|
23
|
+
* which is fine for full-page tables, but leaves a tall column of
|
|
24
|
+
* empty `.ui-blue-2` background next to a short tab body inside a
|
|
25
|
+
* dialog. With `dense`, the wrapper switches to `align-items:
|
|
26
|
+
* flex-start` so both children sit at their natural heights and
|
|
27
|
+
* the group collapses onto whichever is taller.
|
|
28
|
+
*/
|
|
29
|
+
dense?: boolean;
|
|
18
30
|
/** The tabs to render. Keyed by `option.label`. */
|
|
19
31
|
tabs: ReadonlyArray<TabOption<CommonDenominator>>;
|
|
20
32
|
/** Currently selected tab, matched by reference. */
|
|
@@ -288,23 +288,31 @@
|
|
|
288
288
|
let showFromDateModal = $state(false);
|
|
289
289
|
let showToDateModal = $state(false);
|
|
290
290
|
|
|
291
|
-
function generateOnChangeObject(
|
|
291
|
+
function generateOnChangeObject(_label: string) {
|
|
292
292
|
return (newValue: SelectOption<unknown> | null) => {
|
|
293
293
|
tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
|
|
294
|
-
|
|
294
|
+
// Cache by `def.id` (which equals `customFilter.id`) so the
|
|
295
|
+
// `$effect` lookup `__queryValues.get(customFilter.id)`
|
|
296
|
+
// finds it on the next reactive pass. Previously we keyed
|
|
297
|
+
// the cache by the human-readable `label`, so the effect's
|
|
298
|
+
// lookup returned `undefined` and the QuerySelect was
|
|
299
|
+
// immediately reset to the placeholder.
|
|
300
|
+
tableContext.dataRepo.__queryValues.set(def.id, newValue);
|
|
295
301
|
assertNotPromise(newValue);
|
|
296
302
|
value = newValue;
|
|
297
303
|
};
|
|
298
304
|
}
|
|
299
305
|
|
|
300
|
-
function generateOnChangePrimitive(
|
|
306
|
+
function generateOnChangePrimitive(_label: string) {
|
|
301
307
|
// Phase 7.6: AsPrimitive QuerySelect now hands us the primitive
|
|
302
308
|
// directly (previously typed as `SelectOption` with a hidden
|
|
303
309
|
// `.value` dig). The discriminated typing makes the flow
|
|
304
310
|
// straightforward.
|
|
305
311
|
return (newValue: unknown) => {
|
|
306
312
|
tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
|
|
307
|
-
|
|
313
|
+
// See `generateOnChangeObject` - cache key matches the
|
|
314
|
+
// `$effect` lookup above.
|
|
315
|
+
tableContext.dataRepo.__queryValues.set(def.id, newValue ?? null);
|
|
308
316
|
assertNotPromise(newValue);
|
|
309
317
|
value = newValue ?? null;
|
|
310
318
|
};
|
|
@@ -257,22 +257,68 @@
|
|
|
257
257
|
let showFromDateModal = $state(false);
|
|
258
258
|
let showToDateModal = $state(false);
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Debounce window for typed filter inputs (Text / Number / Between
|
|
262
|
+
* min+max). Discrete selections (Svelecte selects, date pickers,
|
|
263
|
+
* filter-mode dropdowns) bypass this and fire `filterBy` immediately.
|
|
264
|
+
* Combined with the 400 ms debounce in `createRemoteRowSource`, the
|
|
265
|
+
* server-side lookup lands roughly one second after the last keystroke,
|
|
266
|
+
* which matches the "wait a couple of seconds" expectation users have
|
|
267
|
+
* when typing into a free-text filter.
|
|
268
|
+
*/
|
|
269
|
+
const FILTER_INPUT_DEBOUNCE_MS = 500;
|
|
270
|
+
|
|
271
|
+
let typingDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
272
|
+
|
|
273
|
+
/** Fire the table's `filterBy` after the user stops typing for
|
|
274
|
+
* `FILTER_INPUT_DEBOUNCE_MS`. Repeated calls coalesce; the most
|
|
275
|
+
* recent value wins. */
|
|
276
|
+
function debouncedFilterBy(id: string, newValue: unknown): void {
|
|
277
|
+
if (typingDebounceTimer !== undefined) {
|
|
278
|
+
clearTimeout(typingDebounceTimer);
|
|
279
|
+
}
|
|
280
|
+
typingDebounceTimer = setTimeout(() => {
|
|
281
|
+
typingDebounceTimer = undefined;
|
|
282
|
+
tableContext.paginationRepo?.filterBy(id, newValue);
|
|
283
|
+
}, FILTER_INPUT_DEBOUNCE_MS);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Clear any pending debounce when the row unmounts so we do not
|
|
287
|
+
// fire `filterBy` against a torn-down `paginationRepo`.
|
|
288
|
+
$effect(() => {
|
|
289
|
+
return () => {
|
|
290
|
+
if (typingDebounceTimer !== undefined) {
|
|
291
|
+
clearTimeout(typingDebounceTimer);
|
|
292
|
+
typingDebounceTimer = undefined;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
function generateOnChangeObject(_label: string) {
|
|
261
298
|
return (newValue: SelectOption<unknown> | null) => {
|
|
262
299
|
tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
|
|
263
|
-
|
|
300
|
+
// Cache by the column id (which is what the `$effect` above
|
|
301
|
+
// reads back with `__queryValues.get(customFilter.id)`).
|
|
302
|
+
// Previously we cached by the human-readable `label`, so the
|
|
303
|
+
// effect's lookup returned `undefined` and immediately
|
|
304
|
+
// overwrote `value` with `null` - the QuerySelect would
|
|
305
|
+
// briefly flash the chosen option then snap back to the
|
|
306
|
+
// "Please Select an Item" placeholder.
|
|
307
|
+
tableContext.dataRepo.__queryValues.set(def.id, newValue ?? null);
|
|
264
308
|
value = newValue ?? null;
|
|
265
309
|
};
|
|
266
310
|
}
|
|
267
311
|
|
|
268
|
-
function generateOnChangePrimitive(
|
|
312
|
+
function generateOnChangePrimitive(_label: string) {
|
|
269
313
|
// Phase 7.6: AsPrimitive QuerySelect now hands us the primitive
|
|
270
314
|
// directly. The previous code typed this as `SelectOption<unknown>`
|
|
271
315
|
// and read `.value` off it; with the discriminated typing the
|
|
272
316
|
// primitive flows straight through.
|
|
273
317
|
return (newValue: unknown) => {
|
|
274
318
|
tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
|
|
275
|
-
|
|
319
|
+
// See note in `generateOnChangeObject` - keyed by `def.id`
|
|
320
|
+
// to match the `$effect` lookup.
|
|
321
|
+
tableContext.dataRepo.__queryValues.set(def.id, newValue ?? null);
|
|
276
322
|
value = newValue ?? null;
|
|
277
323
|
};
|
|
278
324
|
}
|
|
@@ -406,6 +452,24 @@
|
|
|
406
452
|
/>
|
|
407
453
|
{/if}
|
|
408
454
|
{:else}
|
|
455
|
+
{@const selectedPrimitives = selectedOptions
|
|
456
|
+
.map((it) => it.value)
|
|
457
|
+
.filter(
|
|
458
|
+
(v): v is string | number => typeof v === 'string' || typeof v === 'number'
|
|
459
|
+
)}
|
|
460
|
+
<!--
|
|
461
|
+
MultiSelect with `valueAsObject={false}` matches
|
|
462
|
+
options against the primitive value, not the whole
|
|
463
|
+
SelectOption shape. Previously the remote-select
|
|
464
|
+
branch passed the SelectOption objects directly
|
|
465
|
+
(the `selectedOptions` $state), meaning the inner
|
|
466
|
+
`selectedKeys` derived saw
|
|
467
|
+
`[{label, value}, ...]` rows where it expected
|
|
468
|
+
`[id1, id2, ...]`, so no chips ever rendered even
|
|
469
|
+
though the filter was active server-side. Pull the
|
|
470
|
+
primitive `value` off each option to match the
|
|
471
|
+
local-select branch above.
|
|
472
|
+
-->
|
|
409
473
|
<MultiSelect
|
|
410
474
|
label={displayLabel}
|
|
411
475
|
labelProp="label"
|
|
@@ -418,7 +482,7 @@
|
|
|
418
482
|
// primitives directly.
|
|
419
483
|
tableContext.paginationRepo?.filterBy(def.id, e);
|
|
420
484
|
}}
|
|
421
|
-
selectedValues={
|
|
485
|
+
selectedValues={selectedPrimitives}
|
|
422
486
|
collapseSelection
|
|
423
487
|
/>
|
|
424
488
|
{/if}
|
|
@@ -501,7 +565,7 @@
|
|
|
501
565
|
id={inputId}
|
|
502
566
|
bind:value={valueMin}
|
|
503
567
|
oninput={() => {
|
|
504
|
-
|
|
568
|
+
debouncedFilterBy(def.id, [valueMin, valueMax]);
|
|
505
569
|
}}
|
|
506
570
|
onkeydown={(e: KeyboardEvent) => {
|
|
507
571
|
e.stopPropagation();
|
|
@@ -512,7 +576,7 @@
|
|
|
512
576
|
id={inputId}
|
|
513
577
|
bind:value={valueMax}
|
|
514
578
|
oninput={() => {
|
|
515
|
-
|
|
579
|
+
debouncedFilterBy(def.id, [valueMin, valueMax]);
|
|
516
580
|
}}
|
|
517
581
|
onkeydown={(e: KeyboardEvent) => {
|
|
518
582
|
e.stopPropagation();
|
|
@@ -523,7 +587,7 @@
|
|
|
523
587
|
id={inputId}
|
|
524
588
|
bind:value
|
|
525
589
|
oninput={() => {
|
|
526
|
-
|
|
590
|
+
debouncedFilterBy(def.id, [value]);
|
|
527
591
|
}}
|
|
528
592
|
onkeydown={(e: KeyboardEvent) => {
|
|
529
593
|
e.stopPropagation();
|
|
@@ -561,7 +625,7 @@
|
|
|
561
625
|
id={inputId}
|
|
562
626
|
bind:value
|
|
563
627
|
oninput={() => {
|
|
564
|
-
|
|
628
|
+
debouncedFilterBy(def.id, value);
|
|
565
629
|
}}
|
|
566
630
|
onkeydown={(e: KeyboardEvent) => {
|
|
567
631
|
e.stopPropagation();
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
noscroll,
|
|
17
17
|
noTabMessage,
|
|
18
18
|
nohistory = false,
|
|
19
|
+
dense = false,
|
|
19
20
|
}: VerticalTabGroupProps<CommonDenominator> = $props();
|
|
20
21
|
|
|
21
22
|
const isNonEmptyString = (raw: unknown): raw is string =>
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
<div
|
|
49
50
|
class="wrapper"
|
|
50
51
|
class:noscroll
|
|
52
|
+
class:dense
|
|
51
53
|
{style}
|
|
52
54
|
>
|
|
53
55
|
<div class="tabs">
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
<div
|
|
63
65
|
class="content"
|
|
64
66
|
class:noscroll
|
|
67
|
+
class:dense
|
|
65
68
|
>
|
|
66
69
|
{#if selected}
|
|
67
70
|
{@render selected.content(selected.args)}
|
|
@@ -88,6 +91,10 @@
|
|
|
88
91
|
.wrapper.noscroll {
|
|
89
92
|
overflow: visible;
|
|
90
93
|
}
|
|
94
|
+
.wrapper.dense {
|
|
95
|
+
flex: 0 0 auto;
|
|
96
|
+
overflow: visible;
|
|
97
|
+
}
|
|
91
98
|
.wrapper .tabs {
|
|
92
99
|
border-radius: 0 4px 0 0;
|
|
93
100
|
display: flex;
|
|
@@ -113,4 +120,8 @@
|
|
|
113
120
|
}
|
|
114
121
|
.wrapper .content.noscroll {
|
|
115
122
|
overflow: visible;
|
|
123
|
+
}
|
|
124
|
+
.wrapper .content.dense {
|
|
125
|
+
flex-grow: 0;
|
|
126
|
+
overflow: visible;
|
|
116
127
|
}</style>
|
|
@@ -20,4 +20,16 @@ export interface VerticalTabGroupProps<CommonDenominator> {
|
|
|
20
20
|
noTabMessage?: string;
|
|
21
21
|
/** Suppress on-mount history restore even when `name` is set. */
|
|
22
22
|
nohistory?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Size the group to the height of the selected tab's content rather
|
|
25
|
+
* than filling whatever vertical space the parent offers. Use this
|
|
26
|
+
* inside dialogs / forms where the tab body is a short, content-
|
|
27
|
+
* driven panel and the default `flex-grow: 1` would leave a
|
|
28
|
+
* stretch of empty background below the rendered content.
|
|
29
|
+
*
|
|
30
|
+
* Concretely this disables the `flex: 1` on the wrapper and the
|
|
31
|
+
* `flex-grow: 1` on the inner `.content`, so the group collapses
|
|
32
|
+
* vertically onto whatever the active tab renders.
|
|
33
|
+
*/
|
|
34
|
+
dense?: boolean;
|
|
23
35
|
}
|
|
@@ -32,14 +32,19 @@
|
|
|
32
32
|
* 3. Listens for capture-phase `scroll` events and for viewport
|
|
33
33
|
* `resize` events so the position tracks the trigger while
|
|
34
34
|
* the dropdown is open. Repositioning is rAF-throttled.
|
|
35
|
-
* 4. Applies a very large `z-index` to the dropdown
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
35
|
+
* 4. Applies a very large `z-index` to the dropdown AND
|
|
36
|
+
* temporarily lifts the dropdown's nearest stacking-context
|
|
37
|
+
* ancestor (the sticky / isolated table strip that contains
|
|
38
|
+
* the trigger) so the dropdown's z-index can actually win
|
|
39
|
+
* against other peer stacking contexts in the same parent.
|
|
40
|
+
* Without the lift the dropdown's z-index was trapped inside
|
|
41
|
+
* its ancestor's own context and could be hidden by an
|
|
42
|
+
* adjacent sibling stacking context with even a small
|
|
43
|
+
* z-index - which is exactly what happened with the table's
|
|
44
|
+
* sticky footer covering open cell dropdowns (or, after the
|
|
45
|
+
* initial fix, the table body covering open footer
|
|
46
|
+
* dropdowns). The lift is ref-counted so two dropdowns that
|
|
47
|
+
* share an ancestor coordinate cleanly.
|
|
43
48
|
*/
|
|
44
49
|
export interface DropdownPortalOptions {
|
|
45
50
|
/** The Svelecte root wrapper `<div class="svelecte">`. Used as the
|
|
@@ -32,14 +32,19 @@
|
|
|
32
32
|
* 3. Listens for capture-phase `scroll` events and for viewport
|
|
33
33
|
* `resize` events so the position tracks the trigger while
|
|
34
34
|
* the dropdown is open. Repositioning is rAF-throttled.
|
|
35
|
-
* 4. Applies a very large `z-index` to the dropdown
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
35
|
+
* 4. Applies a very large `z-index` to the dropdown AND
|
|
36
|
+
* temporarily lifts the dropdown's nearest stacking-context
|
|
37
|
+
* ancestor (the sticky / isolated table strip that contains
|
|
38
|
+
* the trigger) so the dropdown's z-index can actually win
|
|
39
|
+
* against other peer stacking contexts in the same parent.
|
|
40
|
+
* Without the lift the dropdown's z-index was trapped inside
|
|
41
|
+
* its ancestor's own context and could be hidden by an
|
|
42
|
+
* adjacent sibling stacking context with even a small
|
|
43
|
+
* z-index - which is exactly what happened with the table's
|
|
44
|
+
* sticky footer covering open cell dropdowns (or, after the
|
|
45
|
+
* initial fix, the table body covering open footer
|
|
46
|
+
* dropdowns). The lift is ref-counted so two dropdowns that
|
|
47
|
+
* share an ancestor coordinate cleanly.
|
|
43
48
|
*/
|
|
44
49
|
/**
|
|
45
50
|
* Z-index applied to the open dropdown. Picked very high so the
|
|
@@ -48,6 +53,108 @@
|
|
|
48
53
|
* stays below modal overlays (`z 99999` in `Action`-based popovers).
|
|
49
54
|
*/
|
|
50
55
|
const PORTAL_Z_INDEX = 99998;
|
|
56
|
+
/**
|
|
57
|
+
* Z-index applied to the dropdown's nearest stacking-context ancestor
|
|
58
|
+
* while the dropdown is open. One below `PORTAL_Z_INDEX` so the
|
|
59
|
+
* dropdown still wins inside its own stacking context, but high
|
|
60
|
+
* enough that the whole ancestor block paints above its peer stacking
|
|
61
|
+
* contexts (e.g. the table footer's `position: sticky; z-index: 10`
|
|
62
|
+
* row, or the table body's `isolation: isolate; z-index: 20`
|
|
63
|
+
* container). Without this lift, the dropdown's `z-index: 99998` was
|
|
64
|
+
* trapped inside the ancestor's own stacking context and could not
|
|
65
|
+
* paint above a sibling stacking context with even a small z-index.
|
|
66
|
+
*
|
|
67
|
+
* The previous workaround pinned `.datatable-grid` at `z-index: 20`
|
|
68
|
+
* so its cell dropdowns sat above the footer; that fixed the cell
|
|
69
|
+
* case but reversed the failure for the footer's `Records-per-Page`
|
|
70
|
+
* select, whose dropdown opens upwards into the rows. Lifting the
|
|
71
|
+
* dropdown's actual ancestor at open time means both cases work
|
|
72
|
+
* without either side needing a hard-coded z-index bias.
|
|
73
|
+
*/
|
|
74
|
+
const STACKING_LIFT_Z_INDEX = 99997;
|
|
75
|
+
/**
|
|
76
|
+
* Walk up the DOM from `el` and return the first ancestor that
|
|
77
|
+
* establishes a new CSS stacking context. The dropdown's z-index
|
|
78
|
+
* only competes inside this ancestor's context, so lifting this
|
|
79
|
+
* element is what determines whether the dropdown can paint above a
|
|
80
|
+
* sibling sticky / isolated block. Stops at `document.body` (which
|
|
81
|
+
* doesn't form a useful pivot for the lift).
|
|
82
|
+
*/
|
|
83
|
+
function findStackingContextAncestor(el) {
|
|
84
|
+
let current = el.parentElement;
|
|
85
|
+
while (current && current !== document.body) {
|
|
86
|
+
if (createsStackingContext(current)) {
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
current = current.parentElement;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True when the computed style of `el` causes the browser to
|
|
95
|
+
* establish a new stacking context. Covers the cases that actually
|
|
96
|
+
* appear in scoria today (sticky / isolated table strips, fixed
|
|
97
|
+
* popover ancestors). Not exhaustive against every CSS feature that
|
|
98
|
+
* triggers a stacking context, but tuned for our tree.
|
|
99
|
+
*/
|
|
100
|
+
function createsStackingContext(el) {
|
|
101
|
+
const cs = getComputedStyle(el);
|
|
102
|
+
if (cs.position === 'fixed' || cs.position === 'sticky') {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if ((cs.position === 'relative' || cs.position === 'absolute') &&
|
|
106
|
+
cs.zIndex !== 'auto') {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (cs.isolation === 'isolate') {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (cs.transform !== 'none') {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (cs.filter !== 'none') {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (cs.perspective !== 'none') {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (cs.mixBlendMode !== 'normal') {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (Number.parseFloat(cs.opacity) < 1) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (cs.willChange && cs.willChange !== 'auto') {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const raiseCounts = new WeakMap();
|
|
133
|
+
function liftStackingContext(el) {
|
|
134
|
+
let info = raiseCounts.get(el);
|
|
135
|
+
if (!info) {
|
|
136
|
+
info = { count: 0, originalInlineZIndex: el.style.zIndex };
|
|
137
|
+
raiseCounts.set(el, info);
|
|
138
|
+
el.style.zIndex = String(STACKING_LIFT_Z_INDEX);
|
|
139
|
+
}
|
|
140
|
+
info.count++;
|
|
141
|
+
let released = false;
|
|
142
|
+
return () => {
|
|
143
|
+
if (released) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
released = true;
|
|
147
|
+
const current = raiseCounts.get(el);
|
|
148
|
+
if (!current) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
current.count--;
|
|
152
|
+
if (current.count <= 0) {
|
|
153
|
+
el.style.zIndex = current.originalInlineZIndex;
|
|
154
|
+
raiseCounts.delete(el);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
51
158
|
/**
|
|
52
159
|
* Minimum free vertical space below the trigger before the dropdown
|
|
53
160
|
* is flipped to open upwards. Picked to fit the default Svelecte
|
|
@@ -109,6 +216,7 @@ export function attachDropdownPortal(options) {
|
|
|
109
216
|
};
|
|
110
217
|
}
|
|
111
218
|
let isOpen = false;
|
|
219
|
+
let releaseStackingLift = null;
|
|
112
220
|
const reposition = () => {
|
|
113
221
|
if (!isOpen) {
|
|
114
222
|
return;
|
|
@@ -175,6 +283,18 @@ export function attachDropdownPortal(options) {
|
|
|
175
283
|
return;
|
|
176
284
|
}
|
|
177
285
|
isOpen = true;
|
|
286
|
+
// Lift the dropdown's nearest stacking-context ancestor so the
|
|
287
|
+
// whole block paints above any sibling stacking contexts that
|
|
288
|
+
// would otherwise hide it. Without this, the dropdown's high
|
|
289
|
+
// z-index is trapped inside the ancestor's own stacking context.
|
|
290
|
+
// Resolved from `trigger` rather than `dropdown` because the
|
|
291
|
+
// dropdown's `position: fixed` makes it its own stacking
|
|
292
|
+
// context; we want to lift the surrounding sticky / isolated
|
|
293
|
+
// table strip, not the dropdown itself.
|
|
294
|
+
const stackingAncestor = findStackingContextAncestor(trigger);
|
|
295
|
+
if (stackingAncestor) {
|
|
296
|
+
releaseStackingLift = liftStackingContext(stackingAncestor);
|
|
297
|
+
}
|
|
178
298
|
dropdown.style.display = '';
|
|
179
299
|
dropdown.setAttribute('data-scoria-portal-dropdown', '');
|
|
180
300
|
reposition();
|
|
@@ -189,6 +309,10 @@ export function attachDropdownPortal(options) {
|
|
|
189
309
|
throttled.cancel();
|
|
190
310
|
window.removeEventListener('scroll', handleScroll, { capture: true });
|
|
191
311
|
window.removeEventListener('resize', handleResize);
|
|
312
|
+
if (releaseStackingLift) {
|
|
313
|
+
releaseStackingLift();
|
|
314
|
+
releaseStackingLift = null;
|
|
315
|
+
}
|
|
192
316
|
dropdown.style.position = '';
|
|
193
317
|
dropdown.style.top = '';
|
|
194
318
|
dropdown.style.left = '';
|