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