@lavalogic/scoria 0.37.36 → 0.37.38
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/Table/Body/Rows/Columns/TableQuerySelect.svelte +0 -1
- package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +0 -1
- package/dist/Helpers/dropdownPortal.d.ts +31 -22
- package/dist/Helpers/dropdownPortal.js +47 -161
- package/dist/scss/_mixins.scss +10 -4
- package/package.json +1 -1
|
@@ -8,33 +8,42 @@
|
|
|
8
8
|
* `transform` / `filter` / `will-change: transform` on the way up to
|
|
9
9
|
* the body.
|
|
10
10
|
*
|
|
11
|
-
* This helper
|
|
11
|
+
* This helper does NOT move the dropdown in the DOM. Moving the
|
|
12
|
+
* `.sv_dropdown` element to `document.body` (the original strategy)
|
|
13
|
+
* broke Svelte 5's per-component event delegation: inline
|
|
14
|
+
* `onmousedown` / `onclick` handlers declared in Svelecte's template
|
|
15
|
+
* are dispatched by the app's root-level delegated listener, which
|
|
16
|
+
* walks the live `parentNode` chain at dispatch time. Once the
|
|
17
|
+
* dropdown was a sibling of the app root rather than a descendant,
|
|
18
|
+
* the chain no longer reached the selection handler, so mouse clicks
|
|
19
|
+
* silently did nothing (keyboard Enter still worked because that
|
|
20
|
+
* path is wired on the input, not the dropdown).
|
|
12
21
|
*
|
|
13
|
-
*
|
|
14
|
-
* the original parent + next-sibling so it can be restored on
|
|
15
|
-
* close).
|
|
16
|
-
* 2. It switches the dropdown to `position: fixed` and sets
|
|
17
|
-
* `top` / `left` / `width` / `bottom` from the trigger's
|
|
18
|
-
* `getBoundingClientRect()`.
|
|
19
|
-
* 3. It picks `top` vs `bottom` based on available space below vs
|
|
20
|
-
* above the trigger, so the dropdown auto-flips upwards when the
|
|
21
|
-
* trigger sits near the bottom of the viewport.
|
|
22
|
-
* 4. It listens for capture-phase `scroll` events and for viewport
|
|
23
|
-
* `resize` events so the position tracks the trigger while the
|
|
24
|
-
* dropdown is open. Repositioning is rAF-throttled to keep the
|
|
25
|
-
* cost off the scroll-event hot path.
|
|
26
|
-
* 5. On close, the dropdown is moved back to its original DOM slot
|
|
27
|
-
* and the inline positioning styles are cleared, so Svelecte's
|
|
28
|
-
* next open re-runs the same flow on a clean element.
|
|
22
|
+
* Instead, the helper:
|
|
29
23
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
24
|
+
* 1. Switches the dropdown to `position: fixed` and sets
|
|
25
|
+
* `top` / `left` / `width` / `bottom` from the trigger's
|
|
26
|
+
* `getBoundingClientRect()`, so it escapes ancestor
|
|
27
|
+
* `overflow: hidden` / `overflow: auto` clipping (which was
|
|
28
|
+
* the original motivation for portaling).
|
|
29
|
+
* 2. Picks `top` vs `bottom` based on available space below vs
|
|
30
|
+
* above the trigger, so the dropdown auto-flips upwards when
|
|
31
|
+
* the trigger sits near the bottom of the viewport.
|
|
32
|
+
* 3. Listens for capture-phase `scroll` events and for viewport
|
|
33
|
+
* `resize` events so the position tracks the trigger while
|
|
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.
|
|
34
43
|
*/
|
|
35
44
|
export interface DropdownPortalOptions {
|
|
36
45
|
/** The Svelecte root wrapper `<div class="svelecte">`. Used as the
|
|
37
|
-
* anchor for positioning the
|
|
46
|
+
* anchor for positioning the dropdown. */
|
|
38
47
|
getTrigger: () => HTMLElement | null | undefined;
|
|
39
48
|
/** The `<div class="sv_dropdown">` Svelecte renders. The helper
|
|
40
49
|
* watches this element's `class` attribute for `is-open` /
|
|
@@ -8,37 +8,46 @@
|
|
|
8
8
|
* `transform` / `filter` / `will-change: transform` on the way up to
|
|
9
9
|
* the body.
|
|
10
10
|
*
|
|
11
|
-
* This helper
|
|
11
|
+
* This helper does NOT move the dropdown in the DOM. Moving the
|
|
12
|
+
* `.sv_dropdown` element to `document.body` (the original strategy)
|
|
13
|
+
* broke Svelte 5's per-component event delegation: inline
|
|
14
|
+
* `onmousedown` / `onclick` handlers declared in Svelecte's template
|
|
15
|
+
* are dispatched by the app's root-level delegated listener, which
|
|
16
|
+
* walks the live `parentNode` chain at dispatch time. Once the
|
|
17
|
+
* dropdown was a sibling of the app root rather than a descendant,
|
|
18
|
+
* the chain no longer reached the selection handler, so mouse clicks
|
|
19
|
+
* silently did nothing (keyboard Enter still worked because that
|
|
20
|
+
* path is wired on the input, not the dropdown).
|
|
12
21
|
*
|
|
13
|
-
*
|
|
14
|
-
* the original parent + next-sibling so it can be restored on
|
|
15
|
-
* close).
|
|
16
|
-
* 2. It switches the dropdown to `position: fixed` and sets
|
|
17
|
-
* `top` / `left` / `width` / `bottom` from the trigger's
|
|
18
|
-
* `getBoundingClientRect()`.
|
|
19
|
-
* 3. It picks `top` vs `bottom` based on available space below vs
|
|
20
|
-
* above the trigger, so the dropdown auto-flips upwards when the
|
|
21
|
-
* trigger sits near the bottom of the viewport.
|
|
22
|
-
* 4. It listens for capture-phase `scroll` events and for viewport
|
|
23
|
-
* `resize` events so the position tracks the trigger while the
|
|
24
|
-
* dropdown is open. Repositioning is rAF-throttled to keep the
|
|
25
|
-
* cost off the scroll-event hot path.
|
|
26
|
-
* 5. On close, the dropdown is moved back to its original DOM slot
|
|
27
|
-
* and the inline positioning styles are cleared, so Svelecte's
|
|
28
|
-
* next open re-runs the same flow on a clean element.
|
|
22
|
+
* Instead, the helper:
|
|
29
23
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
24
|
+
* 1. Switches the dropdown to `position: fixed` and sets
|
|
25
|
+
* `top` / `left` / `width` / `bottom` from the trigger's
|
|
26
|
+
* `getBoundingClientRect()`, so it escapes ancestor
|
|
27
|
+
* `overflow: hidden` / `overflow: auto` clipping (which was
|
|
28
|
+
* the original motivation for portaling).
|
|
29
|
+
* 2. Picks `top` vs `bottom` based on available space below vs
|
|
30
|
+
* above the trigger, so the dropdown auto-flips upwards when
|
|
31
|
+
* the trigger sits near the bottom of the viewport.
|
|
32
|
+
* 3. Listens for capture-phase `scroll` events and for viewport
|
|
33
|
+
* `resize` events so the position tracks the trigger while
|
|
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.
|
|
34
43
|
*/
|
|
35
44
|
/**
|
|
36
|
-
* Z-index applied to the
|
|
37
|
-
* sits above
|
|
38
|
-
* the side-panel chrome, but
|
|
39
|
-
* in `Action`-based popovers).
|
|
45
|
+
* Z-index applied to the open dropdown. Picked very high so the
|
|
46
|
+
* dropdown sits above sticky table strips (`z 5-11` in the table
|
|
47
|
+
* grid), the table footer (`z 10`), and the side-panel chrome, but
|
|
48
|
+
* stays below modal overlays (`z 99999` in `Action`-based popovers).
|
|
40
49
|
*/
|
|
41
|
-
const PORTAL_Z_INDEX =
|
|
50
|
+
const PORTAL_Z_INDEX = 99998;
|
|
42
51
|
/**
|
|
43
52
|
* Minimum free vertical space below the trigger before the dropdown
|
|
44
53
|
* is flipped to open upwards. Picked to fit the default Svelecte
|
|
@@ -90,16 +99,12 @@ export function attachDropdownPortal(options) {
|
|
|
90
99
|
/* nothing wired up */
|
|
91
100
|
};
|
|
92
101
|
}
|
|
93
|
-
let
|
|
102
|
+
let isOpen = false;
|
|
94
103
|
const reposition = () => {
|
|
95
|
-
if (!
|
|
104
|
+
if (!isOpen) {
|
|
96
105
|
return;
|
|
97
106
|
}
|
|
98
107
|
const tr = trigger.getBoundingClientRect();
|
|
99
|
-
// `offsetHeight` only reflects the actual rendered height once
|
|
100
|
-
// Svelecte has laid out the option list, so the first frame
|
|
101
|
-
// after open may read 0; fall back to the flip threshold so
|
|
102
|
-
// we still pick a sensible orientation in that case.
|
|
103
108
|
const dropdownHeight = dropdown.offsetHeight || FLIP_THRESHOLD_PX;
|
|
104
109
|
const viewportHeight = window.innerHeight;
|
|
105
110
|
const spaceBelow = viewportHeight - tr.bottom;
|
|
@@ -108,18 +113,9 @@ export function attachDropdownPortal(options) {
|
|
|
108
113
|
dropdown.style.position = 'fixed';
|
|
109
114
|
dropdown.style.left = `${tr.left}px`;
|
|
110
115
|
dropdown.style.width = `${tr.width}px`;
|
|
111
|
-
// Override Svelecte's own `min-width: 100%` on `.sv_dropdown`.
|
|
112
|
-
// With `position: fixed` the percentage resolves against the
|
|
113
|
-
// initial containing block (the viewport), which would otherwise
|
|
114
|
-
// stretch the dropdown across the whole page. Pin the
|
|
115
|
-
// minimum to the trigger width so the dropdown stays the same
|
|
116
|
-
// width as the select that opened it.
|
|
117
116
|
dropdown.style.minWidth = `${tr.width}px`;
|
|
118
117
|
dropdown.style.maxWidth = `${tr.width}px`;
|
|
119
118
|
dropdown.style.zIndex = String(PORTAL_Z_INDEX);
|
|
120
|
-
// Bound the dropdown's max height to the available space so
|
|
121
|
-
// long option lists scroll internally rather than disappear
|
|
122
|
-
// past the viewport edge.
|
|
123
119
|
if (openUp) {
|
|
124
120
|
dropdown.style.top = '';
|
|
125
121
|
dropdown.style.bottom = `${viewportHeight - tr.top}px`;
|
|
@@ -138,77 +134,25 @@ export function attachDropdownPortal(options) {
|
|
|
138
134
|
const handleResize = () => {
|
|
139
135
|
throttled.schedule();
|
|
140
136
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// lifetime of the SingleSelect, regardless of open / close. This
|
|
144
|
-
// avoids two bugs the older "move on every open, move back on every
|
|
145
|
-
// close" implementation suffered from:
|
|
146
|
-
// 1. Svelte 5's per-component event delegation walks the live
|
|
147
|
-
// DOM parentNode chain at dispatch time; moving the dropdown
|
|
148
|
-
// mid-click left the chain pointing at the old parent and
|
|
149
|
-
// Svelecte's `on_click` selection handler never ran, so mouse
|
|
150
|
-
// clicks did not select an option (keyboard Enter did,
|
|
151
|
-
// because keydown is wired on the input, not the dropdown).
|
|
152
|
-
// 2. The MutationObserver that toggled the move based on the
|
|
153
|
-
// `is-open` class fired between `mousedown` and `click`. The
|
|
154
|
-
// observer's microtask moved the dropdown back to its
|
|
155
|
-
// original parent before the `click` event reached an option,
|
|
156
|
-
// so the click resolved against `body` instead of the
|
|
157
|
-
// dropdown item.
|
|
158
|
-
// Keeping the element in the portal target the whole time also
|
|
159
|
-
// puts the dropdown in the body's stacking context unconditionally,
|
|
160
|
-
// which means a `z-index: 10000` actually wins against the table's
|
|
161
|
-
// own sticky-footer / pinned-column stacking contexts (the
|
|
162
|
-
// `position: fixed`-only variant suffered from that footer being
|
|
163
|
-
// painted ABOVE the open option list).
|
|
164
|
-
let didMoveToPortalTarget = false;
|
|
165
|
-
const moveToPortalTargetOnce = () => {
|
|
166
|
-
if (didMoveToPortalTarget) {
|
|
137
|
+
const onOpen = () => {
|
|
138
|
+
if (isOpen) {
|
|
167
139
|
return;
|
|
168
140
|
}
|
|
169
|
-
|
|
170
|
-
// (so the dropdown ends up in the dialog's top-layer stacking
|
|
171
|
-
// context and is not painted behind the dialog's backdrop /
|
|
172
|
-
// content), otherwise to `document.body`.
|
|
173
|
-
const dialogHost = trigger.closest('dialog');
|
|
174
|
-
const portalTarget = dialogHost && dialogHost.open ? dialogHost : document.body;
|
|
175
|
-
portalTarget.appendChild(dropdown);
|
|
176
|
-
didMoveToPortalTarget = true;
|
|
177
|
-
};
|
|
178
|
-
const portalOut = () => {
|
|
179
|
-
if (isPortalled) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
moveToPortalTargetOnce();
|
|
183
|
-
// Clear the defensive `display: none` that `portalIn` may have
|
|
184
|
-
// applied; the dropdown is opening again now.
|
|
141
|
+
isOpen = true;
|
|
185
142
|
dropdown.style.display = '';
|
|
186
|
-
// Marker so consumers (e.g. the TableSidebar "click-outside"
|
|
187
|
-
// handler) can recognise clicks landing inside the dropdown as
|
|
188
|
-
// part of the trigger, not as outside-clicks.
|
|
189
143
|
dropdown.setAttribute('data-scoria-portal-dropdown', '');
|
|
190
|
-
isPortalled = true;
|
|
191
|
-
// First-frame measurement: lay out, then reposition again on
|
|
192
|
-
// the next frame so we measure the dropdown's real
|
|
193
|
-
// `offsetHeight` once it has options rendered.
|
|
194
144
|
reposition();
|
|
195
145
|
requestAnimationFrame(reposition);
|
|
196
|
-
// `capture: true` so scroll events on any ancestor (the
|
|
197
|
-
// filter-panel side pane, the page itself, the .datatable
|
|
198
|
-
// scroll container) reach this listener; without capture the
|
|
199
|
-
// listener would only fire when the document element scrolls.
|
|
200
146
|
window.addEventListener('scroll', handleScroll, { capture: true, passive: true });
|
|
201
147
|
window.addEventListener('resize', handleResize, { passive: true });
|
|
202
148
|
};
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
149
|
+
const onClose = () => {
|
|
150
|
+
if (!isOpen) {
|
|
205
151
|
return;
|
|
206
152
|
}
|
|
207
153
|
throttled.cancel();
|
|
208
154
|
window.removeEventListener('scroll', handleScroll, { capture: true });
|
|
209
155
|
window.removeEventListener('resize', handleResize);
|
|
210
|
-
// Strip the inline overrides so Svelecte's next open starts
|
|
211
|
-
// from a clean slate.
|
|
212
156
|
dropdown.style.position = '';
|
|
213
157
|
dropdown.style.top = '';
|
|
214
158
|
dropdown.style.left = '';
|
|
@@ -219,88 +163,30 @@ export function attachDropdownPortal(options) {
|
|
|
219
163
|
dropdown.style.maxHeight = '';
|
|
220
164
|
dropdown.style.zIndex = '';
|
|
221
165
|
dropdown.removeAttribute('data-scoria-portal-dropdown');
|
|
222
|
-
// Defensive: if Svelecte left the dropdown without its own
|
|
223
|
-
// `display: none` (transition-cancel race paths where the
|
|
224
|
-
// open/close class flip and the inline style update arrive
|
|
225
|
-
// out of order), the dropdown would otherwise stay `position:
|
|
226
|
-
// static; display: block` in `document.body` and contribute
|
|
227
|
-
// its option-list height to the body scroll size, surfacing
|
|
228
|
-
// as a stray page-level scrollbar after toggling a select.
|
|
229
166
|
if (!dropdown.classList.contains('is-open')) {
|
|
230
167
|
dropdown.style.display = 'none';
|
|
231
168
|
}
|
|
232
|
-
|
|
233
|
-
// parent. Moving mid-click races with the click event (see the
|
|
234
|
-
// `moveToPortalTargetOnce` comment above). The dropdown stays
|
|
235
|
-
// in the portal target for the rest of its lifetime and is
|
|
236
|
-
// removed on component unmount.
|
|
237
|
-
isPortalled = false;
|
|
169
|
+
isOpen = false;
|
|
238
170
|
};
|
|
239
|
-
// Apply / unapply the portal whenever Svelecte toggles the
|
|
240
|
-
// `is-open` class on the dropdown. We watch class only because
|
|
241
|
-
// Svelecte's open state is exposed purely as a class flip.
|
|
242
171
|
const observer = new MutationObserver((mutations) => {
|
|
243
172
|
for (const mutation of mutations) {
|
|
244
173
|
if (mutation.attributeName !== 'class') {
|
|
245
174
|
continue;
|
|
246
175
|
}
|
|
247
176
|
if (dropdown.classList.contains('is-open')) {
|
|
248
|
-
|
|
177
|
+
onOpen();
|
|
249
178
|
}
|
|
250
179
|
else {
|
|
251
|
-
|
|
180
|
+
onClose();
|
|
252
181
|
}
|
|
253
182
|
}
|
|
254
183
|
});
|
|
255
184
|
observer.observe(dropdown, { attributes: true, attributeFilter: ['class'] });
|
|
256
|
-
// Honour the dropdown's initial state in case it mounted already
|
|
257
|
-
// open (e.g. `focusOnLoad`).
|
|
258
185
|
if (dropdown.classList.contains('is-open')) {
|
|
259
|
-
|
|
186
|
+
onOpen();
|
|
260
187
|
}
|
|
261
188
|
return () => {
|
|
262
189
|
observer.disconnect();
|
|
263
|
-
|
|
264
|
-
// Final cleanup: the dropdown was moved to the portal target
|
|
265
|
-
// at first open and stayed there for the component's lifetime
|
|
266
|
-
// (see `moveToPortalTargetOnce`). When the SingleSelect
|
|
267
|
-
// unmounts, take the dropdown out of the portal target so it
|
|
268
|
-
// is not orphaned in the document.
|
|
269
|
-
if (didMoveToPortalTarget && dropdown.parentNode) {
|
|
270
|
-
dropdown.parentNode.removeChild(dropdown);
|
|
271
|
-
}
|
|
190
|
+
onClose();
|
|
272
191
|
};
|
|
273
192
|
}
|
|
274
|
-
tion;
|
|
275
|
-
of;
|
|
276
|
-
mutations;
|
|
277
|
-
{
|
|
278
|
-
if (mutation.attributeName !== 'class') {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (dropdown.classList.contains('is-open')) {
|
|
282
|
-
portalOut();
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
portalIn();
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
;
|
|
289
|
-
observer.observe(dropdown, { attributes: true, attributeFilter: ['class'] });
|
|
290
|
-
// Honour the dropdown's initial state in case it mounted already
|
|
291
|
-
// open (e.g. `focusOnLoad`).
|
|
292
|
-
if (dropdown.classList.contains('is-open')) {
|
|
293
|
-
portalOut();
|
|
294
|
-
}
|
|
295
|
-
return () => {
|
|
296
|
-
observer.disconnect();
|
|
297
|
-
portalIn();
|
|
298
|
-
// Final cleanup: the dropdown was moved to the portal target
|
|
299
|
-
// at first open and stayed there for the component's lifetime
|
|
300
|
-
// (see `moveToPortalTargetOnce`). When the SingleSelect
|
|
301
|
-
// unmounts, take the dropdown out of the portal target so it
|
|
302
|
-
// is not orphaned in the document.
|
|
303
|
-
if (didMoveToPortalTarget && dropdown.parentNode) {
|
|
304
|
-
dropdown.parentNode.removeChild(dropdown);
|
|
305
|
-
}
|
|
306
|
-
};
|
package/dist/scss/_mixins.scss
CHANGED
|
@@ -1757,10 +1757,16 @@
|
|
|
1757
1757
|
// row that already has a value" symptom). `min-width: 0` makes
|
|
1758
1758
|
// the cell honour the column's declared minimum instead.
|
|
1759
1759
|
min-width: 0;
|
|
1760
|
-
//
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
|
|
1760
|
+
// `overflow: hidden` is intentionally NOT applied here:
|
|
1761
|
+
// it would clip the open dropdown (the dropdown lives inside
|
|
1762
|
+
// the cell as a Svelecte-owned sibling of the control, and
|
|
1763
|
+
// `position: fixed` should escape it, but a containing-block
|
|
1764
|
+
// ancestor with `transform` / `filter` would still let
|
|
1765
|
+
// `overflow: hidden` clip it). The leaf `.focusable` /
|
|
1766
|
+
// `.select-cell` rules already include `overflow-x: clip`
|
|
1767
|
+
// from `table-cell-basic` for the cell's text content; that
|
|
1768
|
+
// is enough to ellipsise the in-input label when the cell is
|
|
1769
|
+
// forced narrower than its rendered text.
|
|
1764
1770
|
|
|
1765
1771
|
:global(.select-wrapper) {
|
|
1766
1772
|
--sv-min-height: 0px;
|