@lavalogic/scoria 0.37.43 → 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.
@@ -288,23 +288,31 @@
288
288
  let showFromDateModal = $state(false);
289
289
  let showToDateModal = $state(false);
290
290
 
291
- function generateOnChangeObject(label: string) {
291
+ function generateOnChangeObject(_label: string) {
292
292
  return (newValue: SelectOption<unknown> | null) => {
293
293
  tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
294
- tableContext.dataRepo.__queryValues.set(label, newValue);
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(label: string) {
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
- tableContext.dataRepo.__queryValues.set(label, newValue ?? null);
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
  };
@@ -294,22 +294,31 @@
294
294
  };
295
295
  });
296
296
 
297
- function generateOnChangeObject(label: string) {
297
+ function generateOnChangeObject(_label: string) {
298
298
  return (newValue: SelectOption<unknown> | null) => {
299
299
  tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
300
- tableContext.dataRepo.__queryValues.set(label, newValue ?? null);
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);
301
308
  value = newValue ?? null;
302
309
  };
303
310
  }
304
311
 
305
- function generateOnChangePrimitive(label: string) {
312
+ function generateOnChangePrimitive(_label: string) {
306
313
  // Phase 7.6: AsPrimitive QuerySelect now hands us the primitive
307
314
  // directly. The previous code typed this as `SelectOption<unknown>`
308
315
  // and read `.value` off it; with the discriminated typing the
309
316
  // primitive flows straight through.
310
317
  return (newValue: unknown) => {
311
318
  tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
312
- tableContext.dataRepo.__queryValues.set(label, newValue ?? null);
319
+ // See note in `generateOnChangeObject` - keyed by `def.id`
320
+ // to match the `$effect` lookup.
321
+ tableContext.dataRepo.__queryValues.set(def.id, newValue ?? null);
313
322
  value = newValue ?? null;
314
323
  };
315
324
  }
@@ -893,6 +893,4 @@ div.sidebar-wrapper .sidebar.toolbar-right {
893
893
  text-align: left;
894
894
  min-width: max-content;
895
895
  isolation: isolate;
896
- position: relative;
897
- z-index: 20;
898
896
  }</style>
@@ -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 so it wins
36
- * against the table's sticky-footer (`z-index: 10`) and
37
- * pinned-column (`z-index: 5-11`) stacking layers. Note that
38
- * `z-index` only competes within a shared stacking context;
39
- * the dropdown's `position: fixed` + `z-index` makes it its
40
- * own stacking context, but ancestor stacking contexts can
41
- * still constrain it. See `_mixins.scss` `table-cell-svelecte`
42
- * for the per-cell rules that avoid creating such ancestors.
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 so it wins
36
- * against the table's sticky-footer (`z-index: 10`) and
37
- * pinned-column (`z-index: 5-11`) stacking layers. Note that
38
- * `z-index` only competes within a shared stacking context;
39
- * the dropdown's `position: fixed` + `z-index` makes it its
40
- * own stacking context, but ancestor stacking contexts can
41
- * still constrain it. See `_mixins.scss` `table-cell-svelecte`
42
- * for the per-cell rules that avoid creating such ancestors.
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 = '';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.37.43",
4
+ "version": "0.37.44",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },