@lavalogic/scoria 0.37.32 → 0.37.35

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.
@@ -322,10 +322,13 @@
322
322
  box-sizing: border-box;
323
323
  text-align: center;
324
324
  --sv-border: none;
325
+ min-width: 0;
326
+ overflow: hidden;
325
327
  }
326
328
  .select-cell :global(.select-wrapper) {
327
329
  --sv-min-height: 0px;
328
330
  --sv-general-padding: 0px;
329
331
  --sv-item-wrap-padding: 0px 0.75rem;
330
332
  --input-height: var(--height);
333
+ min-width: 0;
331
334
  }</style>
@@ -294,10 +294,13 @@
294
294
  box-sizing: border-box;
295
295
  text-align: center;
296
296
  --sv-border: none;
297
+ min-width: 0;
298
+ overflow: hidden;
297
299
  }
298
300
  .select-cell :global(.select-wrapper) {
299
301
  --sv-min-height: 0px;
300
302
  --sv-general-padding: 0px;
301
303
  --sv-item-wrap-padding: 0px 0.75rem;
302
304
  --input-height: var(--height);
305
+ min-width: 0;
303
306
  }</style>
@@ -90,8 +90,6 @@ export function attachDropdownPortal(options) {
90
90
  /* nothing wired up */
91
91
  };
92
92
  }
93
- let originalParent = null;
94
- let originalNextSibling = null;
95
93
  let isPortalled = false;
96
94
  const reposition = () => {
97
95
  if (!isPortalled) {
@@ -140,68 +138,55 @@ export function attachDropdownPortal(options) {
140
138
  const handleResize = () => {
141
139
  throttled.schedule();
142
140
  };
143
- /**
144
- * Direct mousedown listener attached to the portaled dropdown that
145
- * calls `preventDefault()` to keep focus on the Svelecte input while
146
- * the user clicks an option.
147
- *
148
- * Svelecte already attaches `onmousedown={preventDefault}` to the
149
- * `.sv_dropdown` element in template, but Svelte 5 wires inline
150
- * event handlers through its delegated-event chain rooted at the
151
- * original mount point. Once we `appendChild` the dropdown to
152
- * `document.body` (or to a `<dialog>` ancestor) the delegated chain
153
- * no longer finds this element, the inline handler never fires,
154
- * the browser's default mousedown shifts focus to the option (or to
155
- * `body`), Svelecte's `on_blur` runs `updateDropdownState(false)`
156
- * which closes the dropdown, this helper's MutationObserver moves
157
- * the dropdown back into place, and the `mouseup` / `click` events
158
- * never reach an option element. The visible symptom: keyboard
159
- * selection (Enter) works, mouse click does not.
160
- *
161
- * Adding a *direct* `addEventListener` on the dropdown bypasses
162
- * Svelte's delegation entirely. The listener is reset every
163
- * `portalOut` / `portalIn` cycle.
164
- */
165
- const preventFocusShiftOnMousedown = (e) => {
166
- // Same intent as Svelecte's own `on_mouse_down`. Keep focus on
167
- // the input so `on_blur` does not collapse the dropdown before
168
- // the click event reaches its selection handler.
169
- e.preventDefault();
141
+ // Track whether the dropdown has been moved to the portal target
142
+ // at least once. We move it AT MOUNT and leave it there for the
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) {
167
+ return;
168
+ }
169
+ // Append to the nearest open `<dialog>` ancestor if one exists
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;
170
177
  };
171
178
  const portalOut = () => {
172
179
  if (isPortalled) {
173
180
  return;
174
181
  }
175
- originalParent = dropdown.parentNode;
176
- originalNextSibling = dropdown.nextSibling;
177
- // Clear any `display: none` left over from a previous portalIn
178
- // (see the defensive hide there) so the dropdown is visible
179
- // once teleported into the new host.
182
+ moveToPortalTargetOnce();
183
+ // Clear the defensive `display: none` that `portalIn` may have
184
+ // applied; the dropdown is opening again now.
180
185
  dropdown.style.display = '';
181
- // Append the dropdown to the nearest open native `<dialog>` ancestor
182
- // when one exists, otherwise to `document.body`. A `<dialog>` opened
183
- // via `showModal()` renders in the browser's top layer, which sits
184
- // above every z-indexed element in the regular stacking context.
185
- // Without this hop the dropdown's `z-index: 10000` was still painted
186
- // behind the dialog backdrop and its content (the symptom that
187
- // surfaced as "select options disappear behind the allocate modal").
188
- const dialogHost = trigger.closest('dialog');
189
- const portalTarget = dialogHost && dialogHost.open ? dialogHost : document.body;
190
- portalTarget.appendChild(dropdown);
191
- // Tag the teleported dropdown so any "click outside the
192
- // panel" / "click outside the trigger" handler elsewhere in
193
- // the codebase can recognise the dropdown as a logical
194
- // extension of its trigger and ignore clicks landing inside
195
- // it. The `TableSidebar` side-panel uses this marker (along
196
- // with `closest('.sv_dropdown')`) so clicking an option in a
197
- // teleported filter-row dropdown does not close the
198
- // side-panel under it. Components that want to ignore
199
- // portal clicks should check
200
- // `el.closest('[data-scoria-portal-dropdown]')`.
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.
201
189
  dropdown.setAttribute('data-scoria-portal-dropdown', '');
202
- // Restore the focus-preserving mousedown handler that Svelte's
203
- // delegated event chain no longer reaches after the move.
204
- dropdown.addEventListener('mousedown', preventFocusShiftOnMousedown);
205
190
  isPortalled = true;
206
191
  // First-frame measurement: lay out, then reposition again on
207
192
  // the next frame so we measure the dropdown's real
@@ -220,12 +205,10 @@ export function attachDropdownPortal(options) {
220
205
  return;
221
206
  }
222
207
  throttled.cancel();
223
- dropdown.removeEventListener('mousedown', preventFocusShiftOnMousedown);
224
208
  window.removeEventListener('scroll', handleScroll, { capture: true });
225
209
  window.removeEventListener('resize', handleResize);
226
210
  // Strip the inline overrides so Svelecte's next open starts
227
- // from a clean slate (and so a closed dropdown still occupies
228
- // its Svelecte-managed `display: none` slot).
211
+ // from a clean slate.
229
212
  dropdown.style.position = '';
230
213
  dropdown.style.top = '';
231
214
  dropdown.style.left = '';
@@ -237,27 +220,20 @@ export function attachDropdownPortal(options) {
237
220
  dropdown.style.zIndex = '';
238
221
  dropdown.removeAttribute('data-scoria-portal-dropdown');
239
222
  // Defensive: if Svelecte left the dropdown without its own
240
- // `display: none` (e.g. on certain transition-cancel race paths
241
- // the open/close class flip and the inline style update arrive
242
- // out of order), the dropdown would now be `position: static;
243
- // display: block` in `document.body` and contribute its option
244
- // list's height to the body scroll size - which is what surfaces
245
- // as a stray page-level vertical scrollbar after toggling a
246
- // select. Force `display: none` here; Svelecte will restore the
247
- // inline style on next open via its own class-driven render.
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.
248
229
  if (!dropdown.classList.contains('is-open')) {
249
230
  dropdown.style.display = 'none';
250
231
  }
251
- if (originalParent != null) {
252
- if (originalNextSibling != null && originalNextSibling.parentNode === originalParent) {
253
- originalParent.insertBefore(dropdown, originalNextSibling);
254
- }
255
- else {
256
- originalParent.appendChild(dropdown);
257
- }
258
- }
259
- originalParent = null;
260
- originalNextSibling = null;
232
+ // Note: we do NOT move the dropdown back to its original
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.
261
237
  isPortalled = false;
262
238
  };
263
239
  // Apply / unapply the portal whenever Svelecte toggles the
@@ -285,5 +261,46 @@ export function attachDropdownPortal(options) {
285
261
  return () => {
286
262
  observer.disconnect();
287
263
  portalIn();
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
+ }
288
272
  };
289
273
  }
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
+ };
@@ -1745,12 +1745,33 @@
1745
1745
  box-sizing: border-box;
1746
1746
  text-align: center;
1747
1747
  --sv-border: none;
1748
+ // Suppress the cell's content-driven `min-width: auto`. Without
1749
+ // this, an open select-cell whose Svelecte input is rendering a
1750
+ // long selected-value label (e.g. `TEST-SA1-01 (line ID
1751
+ // 525504)` plus the clear-x icon) reports its `min-content` as
1752
+ // the full label width. With the column grid track sized
1753
+ // `minmax(W, 1fr)`, the track minimum becomes `max(W,
1754
+ // min-content)` and grows past the declared `W`, which forces
1755
+ // every other `1fr` column to shrink in turn (the visible
1756
+ // "all columns resize when the SKU dropdown is opened on a
1757
+ // row that already has a value" symptom). `min-width: 0` makes
1758
+ // the cell honour the column's declared minimum instead.
1759
+ min-width: 0;
1760
+ // Pair with `overflow: hidden` so the input inside cannot
1761
+ // visually overflow the cell once it is forced narrower than
1762
+ // its content - the label clips with ellipsis instead.
1763
+ overflow: hidden;
1748
1764
 
1749
1765
  :global(.select-wrapper) {
1750
1766
  --sv-min-height: 0px;
1751
1767
  --sv-general-padding: 0px;
1752
1768
  --sv-item-wrap-padding: 0px 0.75rem;
1753
1769
  --input-height: var(--height);
1770
+ // `min-width: 0` here too: `.select-wrapper` is a flex
1771
+ // container by default in the regular SingleSelect, but
1772
+ // inside the table cell it should likewise relinquish
1773
+ // its content-driven minimum so the cell shrink works.
1774
+ min-width: 0;
1754
1775
  }
1755
1776
  }
1756
1777
  }
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.32",
4
+ "version": "0.37.35",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },