@ixfx/components 0.14.4 → 0.14.5

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.
@@ -3,9 +3,8 @@ import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrA
3
3
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
5
  import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-JIhwB49y.js";
6
- import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
7
- import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
8
- import "./interaction-ZTh4htzr.js";
6
+ import { i as animateListItemOut, r as animateListItemIn, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
7
+ import "./interaction-E-uMGHkJ.js";
9
8
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
10
9
  import { LitElement, css, html, nothing } from "lit";
11
10
  import { property, state } from "lit/decorators.js";
@@ -448,9 +447,54 @@ function treeDragCurrent() {
448
447
  };
449
448
  }
450
449
  //#endregion
450
+ //#region src/tree/edge-scroll.ts
451
+ /**
452
+ * Edge auto-scroll maths for drag-and-drop in a scrollable list: how far into
453
+ * a trigger zone the pointer is, and how fast to scroll given how long it has
454
+ * been held there. Pure, so it can be tested without a DOM.
455
+ */
456
+ const ZONE_FRACTION = .2;
457
+ const ZONE_MIN_PX = 24;
458
+ const ZONE_MAX_PX = 64;
459
+ /** Hold time over which the speed ramps from min to max. */
460
+ const EDGE_SCROLL_RAMP_MS = 2500;
461
+ /** Share of the speed you get just inside the zone, rising to 100% at the container's edge. */
462
+ const DEPTH_FLOOR = .35;
463
+ const NONE = {
464
+ direction: 0,
465
+ depth: 0
466
+ };
467
+ /** Height of each trigger zone: proportional to the container, with sane bounds. */
468
+ function edgeZonePx(containerHeight) {
469
+ return Math.min(containerHeight / 2, Math.max(ZONE_MIN_PX, Math.min(ZONE_MAX_PX, containerHeight * ZONE_FRACTION)));
470
+ }
471
+ /** Which trigger zone (if any) the pointer is in. `y` is relative to the container's top edge. */
472
+ function edgeScrollZone(y, containerHeight) {
473
+ if (containerHeight <= 0) return NONE;
474
+ const zone = edgeZonePx(containerHeight);
475
+ if (y < zone) return {
476
+ direction: -1,
477
+ depth: Math.min(1, (zone - y) / zone)
478
+ };
479
+ if (y > containerHeight - zone) return {
480
+ direction: 1,
481
+ depth: Math.min(1, (y - (containerHeight - zone)) / zone)
482
+ };
483
+ return NONE;
484
+ }
485
+ /**
486
+ * Scroll speed in px/s. Non-linear in hold time: it starts gentle so a stray
487
+ * pass through the zone barely moves, then accelerates (quadratically) the
488
+ * longer the pointer stays. Pushing closer to the edge scales it further.
489
+ */
490
+ function edgeScrollSpeed(depth, heldMs) {
491
+ const ramp = Math.min(1, Math.max(0, heldMs) / EDGE_SCROLL_RAMP_MS);
492
+ return (120 + 3080 * ramp * ramp) * (DEPTH_FLOOR + .65 * Math.min(1, Math.max(0, depth)));
493
+ }
494
+ //#endregion
451
495
  //#region src/tree/tree-drop-controller.ts
452
- const EDGE_ZONE_PX = 24;
453
- const MAX_SCROLL_SPEED = 16;
496
+ /** Longest frame step used when integrating scroll, so a stalled tab doesn't jump the list. */
497
+ const MAX_FRAME_MS = 50;
454
498
  function clamp(value, min, max) {
455
499
  return Math.max(min, Math.min(max, value));
456
500
  }
@@ -467,6 +511,15 @@ var TreeDropController = class {
467
511
  #scrollRaf = 0;
468
512
  #pointerX = 0;
469
513
  #pointerY = 0;
514
+ /** Direction of the edge zone the pointer has been held in, and since when (ms). 0 = not in a zone. */
515
+ #edgeDirection = 0;
516
+ #edgeSince = 0;
517
+ #lastTickAt = 0;
518
+ /** Sub-pixel scroll carried between frames (`scrollTop` only takes whole pixels at low speeds). */
519
+ #scrollRemainder = 0;
520
+ /** Last `dragover`, replayed after a scroll so the drop indicator follows the content under a still pointer. */
521
+ #lastDragOver;
522
+ #reevaluate = false;
470
523
  #boundContainer;
471
524
  constructor(host, config) {
472
525
  this.#host = host;
@@ -530,6 +583,7 @@ var TreeDropController = class {
530
583
  if (this.#config.getDndMode() === `none` || !event.dataTransfer) return;
531
584
  this.#pointerX = event.clientX;
532
585
  this.#pointerY = event.clientY;
586
+ this.#lastDragOver = event;
533
587
  const drag = this.#config.resolveDrag(event.dataTransfer, `hover`);
534
588
  if (!drag) {
535
589
  this.#reject(event);
@@ -730,26 +784,56 @@ var TreeDropController = class {
730
784
  #kickScroll() {
731
785
  if (this.#scrollRaf === 0) this.#scrollRaf = requestAnimationFrame(this.#scrollTick);
732
786
  }
733
- #scrollTick = () => {
787
+ /**
788
+ * While the pointer sits in the top/bottom trigger zone, scroll that way.
789
+ * Speed grows non-linearly with how long it has been held there (see
790
+ * `edgeScrollSpeed`), and resets when the pointer leaves the zone or flips
791
+ * to the other one.
792
+ */
793
+ #scrollTick = (now) => {
734
794
  this.#scrollRaf = 0;
735
795
  if (!this.#active) return;
736
796
  const container = this.#config.getScrollContainer();
737
797
  if (!container) return;
798
+ if (this.#reevaluate && this.#lastDragOver) {
799
+ this.#reevaluate = false;
800
+ this.#onDragOver(this.#lastDragOver);
801
+ if (!this.#active) return;
802
+ }
738
803
  const rect = container.getBoundingClientRect();
739
- const y = this.#pointerY - rect.top;
740
- let dy = 0;
741
- if (y < EDGE_ZONE_PX) dy = -Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, y) / EDGE_ZONE_PX));
742
- else if (y > rect.height - EDGE_ZONE_PX) dy = Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, rect.height - y) / EDGE_ZONE_PX));
743
- if (dy !== 0 && this.#pointerX >= rect.left && this.#pointerX <= rect.right) {
744
- container.scrollTop += dy;
745
- this.#kickScroll();
804
+ const zone = this.#pointerX >= rect.left && this.#pointerX <= rect.right ? edgeScrollZone(this.#pointerY - rect.top, rect.height) : {
805
+ direction: 0,
806
+ depth: 0
807
+ };
808
+ if (zone.direction !== this.#edgeDirection) {
809
+ this.#edgeDirection = zone.direction;
810
+ this.#edgeSince = now;
811
+ this.#lastTickAt = now;
812
+ this.#scrollRemainder = 0;
813
+ }
814
+ if (zone.direction === 0) return;
815
+ const dt = Math.min(MAX_FRAME_MS, Math.max(0, now - this.#lastTickAt));
816
+ this.#lastTickAt = now;
817
+ const speed = edgeScrollSpeed(zone.depth, now - this.#edgeSince);
818
+ const wanted = zone.direction * speed * dt / 1e3 + this.#scrollRemainder;
819
+ const whole = Math.trunc(wanted);
820
+ this.#scrollRemainder = wanted - whole;
821
+ if (whole !== 0) {
822
+ const before = container.scrollTop;
823
+ container.scrollTop = before + whole;
824
+ if (container.scrollTop !== before) this.#reevaluate = true;
746
825
  }
826
+ this.#kickScroll();
747
827
  };
748
828
  #stopScroll() {
749
829
  if (this.#scrollRaf !== 0) {
750
830
  cancelAnimationFrame(this.#scrollRaf);
751
831
  this.#scrollRaf = 0;
752
832
  }
833
+ this.#edgeDirection = 0;
834
+ this.#scrollRemainder = 0;
835
+ this.#reevaluate = false;
836
+ this.#lastDragOver = void 0;
753
837
  }
754
838
  #setDropState(state) {
755
839
  const unchanged = state === void 0 ? this.#current === void 0 : state.referenceKey === this.#current?.referenceKey && state.position === this.#current?.position && state.depth === this.#current?.depth && state.index === this.#current?.index;
@@ -812,6 +896,8 @@ function flattenTree(root, options) {
812
896
  function indexTree(root) {
813
897
  const parentByKey = /* @__PURE__ */ new Map();
814
898
  const depthsWithIcons = /* @__PURE__ */ new Set();
899
+ const seenKeys = /* @__PURE__ */ new Set([root.item.key]);
900
+ const duplicateKeys = /* @__PURE__ */ new Set();
815
901
  const stack = [];
816
902
  if (root.children) for (const child of root.children) {
817
903
  parentByKey.set(child.item.key, root.item.key);
@@ -823,6 +909,8 @@ function indexTree(root) {
823
909
  }
824
910
  while (stack.length > 0) {
825
911
  const { node, depth, parentKey } = stack.pop();
912
+ if (seenKeys.has(node.item.key)) duplicateKeys.add(node.item.key);
913
+ seenKeys.add(node.item.key);
826
914
  parentByKey.set(node.item.key, parentKey);
827
915
  if (node.item.icon) depthsWithIcons.add(depth);
828
916
  if (node.children) for (let i = node.children.length - 1; i >= 0; i--) {
@@ -834,6 +922,10 @@ function indexTree(root) {
834
922
  });
835
923
  }
836
924
  }
925
+ if (duplicateKeys.size > 0) {
926
+ const sample = [...duplicateKeys].slice(0, 5).map((k) => `"${k}"`).join(`, `);
927
+ console.warn(`[ixfx-tree] ${duplicateKeys.size} duplicate item key(s) in tree (e.g. ${sample}). Keys must be unique across the whole tree; duplicates break checkbox state, expansion and selection.`);
928
+ }
837
929
  return {
838
930
  parentByKey,
839
931
  depthsWithIcons
@@ -2588,10 +2680,6 @@ let TreeBaseElement = class TreeBaseElement extends LitElement {
2588
2680
  _handleItemClick(node, depth, event) {
2589
2681
  if (this.#drop?.isDragging || this.#nativeDrag?.isDragging || this.#postDragSuppressTimer !== void 0) return;
2590
2682
  if (event && event.detail >= 2) return;
2591
- if (this.interactionMode === `checked` && node.item.selectable !== false && event && !(event.target instanceof Element && event.target.closest(`.tree-label`))) {
2592
- this._handleCheckboxClick(event, node, depth);
2593
- return;
2594
- }
2595
2683
  if (this.#pendingNode && this._expandedKeys.has(this.#pendingNode.item.key)) this.#abortPending();
2596
2684
  this.tickled.handlePointerEnter(node.item.key);
2597
2685
  this._focusedDepth = depth;
package/dist/tree.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-BFoN2Z_P.js";
1
+ import { a as TreeDataModel, i as TreeDropController, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-2_TRrLAV.js";
2
2
  export { TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeListElement };
@@ -2,9 +2,8 @@ import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
2
2
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
3
3
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
4
4
  import { c as createElementListSearchAdapter, i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, u as keyForElement } from "./incr-search-JIhwB49y.js";
5
- import { t as ListNativeDragController } from "./list-native-drag-controller-B9xO0XWn.js";
5
+ import { i as animateListItemOut, r as animateListItemIn, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
6
6
  import { r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-BeE70lbB.js";
7
- import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-BUoEKi5s.js";
8
7
  import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
9
8
  import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
10
9
  import "./editable-label.js";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
- "version": "0.14.4",
4
- "generated": "2026-10-16T16:32:35.695Z",
3
+ "version": "0.14.5",
4
+ "generated": "2026-10-16T17:04:34.889Z",
5
5
  "components": [
6
6
  {
7
7
  "name": "ac-text",
package/docs-user/tree.md CHANGED
@@ -176,7 +176,7 @@ Controls how pointer and keyboard gestures translate to selection changes.
176
176
  | `'implicit'` | Single selection follows every click; click also toggles expand/collapse |
177
177
  | `'standard'` | Plain click selects + toggles expand; Ctrl/Cmd+click toggles item; Shift+click range-selects |
178
178
  | `'manual'` | Component never auto-selects; use `select()` / `deselect()` / `clearSelection()` to drive state |
179
- | `'checked'` | Checkboxes on every row; clicking the checkbox — or anywhere left of the label (indent, caret, icon) — recursively selects/deselects loaded descendants; label click only expands/collapses |
179
+ | `'checked'` | Checkboxes on every row; clicking the checkbox recursively selects/deselects loaded descendants; any other click on the row (caret, label, icon, whitespace) only expands/collapses |
180
180
  | `'vscode'` | Like `standard` for clicks; Shift+Up/Down uses snapshot-based toggle-range (VS Code file-explorer style) |
181
181
 
182
182
  Switching `interactionMode` clears the current selection.
@@ -206,7 +206,7 @@ tree.checkboxRenderer = (state) => state === `unchecked`
206
206
 
207
207
  When `checkboxRenderer` is set, the checkbox becomes a `<span class="tree-checkbox" role="checkbox" part="checkbox">` holding your template, with `aria-checked` set to `"true"`, `"false"`, or `"mixed"` to match the tri-state. The span owns the click (same as the native input), so clicking anywhere in it — including empty space when you render nothing for `unchecked` — toggles the row.
208
208
 
209
- To keep the checkbox easy to aim at, the entire row region left of the label — indent gutter, checkbox, caret and icon — acts as one checkbox hit target in `checked` mode. Only clicks on the label itself expand/collapse.
209
+ In `checked` mode **only the checkbox toggles the checked state**: the native input, or the custom renderer's `.tree-checkbox-custom` area when `checkboxRenderer` is set. Clicks anywhere else on the row, including the caret, label, icon and surrounding whitespace, only expand/collapse a branch and never change the selection. If your custom checkbox is small, enlarge the target with `--tree-checkbox-size`.
210
210
 
211
211
  Both the native input and the custom span expose the `checkbox` CSS part for external styling:
212
212
 
@@ -238,7 +238,7 @@ const root: TreeNode = {
238
238
 
239
239
  In `checked` mode:
240
240
 
241
- - An unselectable row is drawn **without a checkbox**, and clicks in its leading region (indent, caret, icon) expand/collapse it like a normal row instead of toggling a checkbox.
241
+ - An unselectable row is drawn **without a checkbox**, and there is nothing on the row that toggles selection; clicks expand/collapse it like a normal row.
242
242
  - Its selectable descendants behave as normal. Unselectable leaves are ignored when working out a parent's `checked` / `indeterminate` state, so a branch whose only selectable children are all checked shows as checked.
243
243
  - Only the item's own flag decides whether a checkbox is drawn. A branch that `selectionFilter="leaf"` stops being selected itself still gets a checkbox, because that checkbox toggles its descendants.
244
244
 
@@ -459,7 +459,7 @@ Opt in with the single **`item-dnd`** attribute:
459
459
  | `acceptDropTypes` | `accept-drop-types` | `string` | – | Space-separated MIME list; an external `dragenter` is only honoured when `dataTransfer.types` intersects it. |
460
460
  | `dndExpandHoverMs` | `dnd-expand-hover-ms` | `number` | `500` | Hover-to-open delay: hovering a collapsed branch this long during a drag expands it (spring-loaded folders). |
461
461
  | `reorderable` | `reorderable` | `boolean` | `true` | When `false` (`reorderable="false"`), a drag that only reorders siblings — or drops a node back into its current parent — is rejected: no drop indicator, no `tree-drop`. Use for models with no meaningful child order (a filesystem). Reparenting, including via the x-position ancestor walk, is unaffected. |
462
- | `dragPreviewRenderer` | – | `ListDragPreviewRenderer<TreeNode>` | – | Custom drag image (same contract as `ixfx-grid-list`); unset ⇒ built-in cards/list preview. |
462
+ | `dragPreviewRenderer` | – | `ListDragPreviewRenderer<TreeNode>` | – | Custom drag image (same contract as `ixfx-grid-list`); unset ⇒ built-in row preview: a compact pill with the dragged row's icon and label (taken from the node's `item.icon` / `item.label` / `item.iconColor`), plus a count badge when several rows are dragged. |
463
463
  | `canDrag` | – | `(ctx) => boolean` | – | Per-row gate at `dragstart`. Return `false` for every row (with `item-dnd` still set) for a **drop-target-only** tree. |
464
464
  | `canDrop` | – | `(ctx: TreeDropContext) => boolean` | – | Sync gate on every `dragover`; controls the valid indicator and whether the drop is allowed. |
465
465
  | `parseExternalDrop` | – | `(dt: DataTransfer) => TreeNode[] \| undefined` | – | Turn a foreign payload into nodes to insert (`external` / `both`). |
@@ -479,9 +479,21 @@ While dragging over the tree:
479
479
  - Hovering a collapsed branch for `dndExpandHoverMs` springs it open.
480
480
  - With `reorderable="false"`, `before`/`after` between current siblings gives no
481
481
  feedback — only reparenting drops resolve a target.
482
- - Dragging near the top/bottom edge auto-scrolls.
482
+ - Dragging into the top or bottom edge zone auto-scrolls the list (see below).
483
483
  - Dropping a node onto itself or into its own subtree is rejected (no indicator).
484
484
 
485
+ ### Edge auto-scroll while dragging
486
+
487
+ When the list is scrollable, holding a drag in the top or bottom trigger zone scrolls that way so you can reach rows that are off-screen:
488
+
489
+ - **Zone:** 20% of the list's height at each edge, clamped to 24–64px (never more than half the list).
490
+ - **Accelerates the longer you hold.** Speed starts gentle (about 120px/s) and ramps quadratically to about 3200px/s over roughly 2.5s of holding, so a brief pass through the zone barely moves the list but a deliberate hold covers long distances quickly. Moving closer to the very edge scales the speed further (35% just inside the zone, 100% at the edge).
491
+ - **Resets** when the pointer leaves the zone or switches to the opposite edge, and stops when the drag leaves the list or drops.
492
+ - **The drop indicator keeps up:** after each scroll the target is re-resolved for the unmoved pointer, so the highlighted row/line always matches what is under it, even if the browser isn't sending `dragover` events.
493
+ - Spring-loaded expansion still works while scrolling: hovering a collapsed branch long enough opens it, and the list keeps scrolling past it.
494
+
495
+ The constants live in [edge-scroll.ts](edge-scroll.ts).
496
+
485
497
  ### Applying drops without a model
486
498
 
487
499
  ```ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ixfx/components",
3
3
  "type": "module",
4
- "version": "0.14.4",
4
+ "version": "0.14.5",
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "description": "Web components for ixfx — Lit-based UI toolkit",
7
7
  "author": "Clint Heyer",
@@ -1,129 +0,0 @@
1
- import { css } from "lit";
2
- //#region src/interaction/list-item-animation.ts
3
- /**
4
- * Shared insertion / removal animation for the list & tree components.
5
- *
6
- * Two flavours, because the components split into two layout families:
7
- *
8
- * - **Flow layouts** (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-tree-*`) —
9
- * rows sit in normal flow, so a new row can collapse in from zero height and a
10
- * leaving row can collapse back to zero, and the rows around it slide to make
11
- * or close the gap for free. Use {@link animateListItemIn} /
12
- * {@link animateListItemOut} (Web Animations API, exact measured sizes).
13
- *
14
- * - **Virtualised layouts** (`ixfx-grid-list`, `ixfx-data-grid`) — rows are
15
- * absolutely positioned (`transform` / `top`), so collapsing a row's box moves
16
- * nothing. Those components instead fade the entering / leaving row with the
17
- * {@link listItemFadeKeyframes} keyframes and slide the survivors by
18
- * transitioning their position for a short window after the change.
19
- *
20
- * Everything here is a no-op under `prefers-reduced-motion: reduce`.
21
- */
22
- /** Default duration (ms) for both flavours; overridable per call / per component. */
23
- const LIST_ITEM_MOTION_MS = 200;
24
- function prefersReducedMotion() {
25
- return typeof window !== `undefined` && typeof window.matchMedia === `function` && window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
26
- }
27
- /**
28
- * Opacity keyframes for the virtualised components. Each one adds its own
29
- * one-line rule binding `.<row>.ixfx-item-entering` / `.ixfx-item-leaving` to
30
- * these, plus a `prefers-reduced-motion` guard on its own selectors.
31
- */
32
- const listItemFadeKeyframes = css`
33
- @keyframes ixfx-list-item-in {
34
- from { opacity: 0; }
35
- to { opacity: 1; }
36
- }
37
- @keyframes ixfx-list-item-out {
38
- from { opacity: 1; }
39
- to { opacity: 0; }
40
- }
41
- `;
42
- function measure(el, axis) {
43
- const rect = el.getBoundingClientRect();
44
- return axis === `inline` ? rect.width : rect.height;
45
- }
46
- function resolveDuration(el, explicit) {
47
- if (explicit !== void 0) return explicit;
48
- const raw = getComputedStyle(el).getPropertyValue(`--ixfx-list-item-move-duration`).trim();
49
- if (raw) {
50
- const n = Number.parseFloat(raw);
51
- if (!Number.isNaN(n)) return raw.endsWith(`ms`) ? n : raw.endsWith(`s`) ? n * 1e3 : n;
52
- }
53
- return 200;
54
- }
55
- function collapsedFrame(axis) {
56
- const prop = axis === `inline` ? `width` : `height`;
57
- const minProp = axis === `inline` ? `minWidth` : `minHeight`;
58
- return {
59
- [prop]: `0px`,
60
- [minProp]: `0px`,
61
- opacity: 0,
62
- overflow: `hidden`
63
- };
64
- }
65
- function fullFrame(size, axis) {
66
- const prop = axis === `inline` ? `width` : `height`;
67
- const minProp = axis === `inline` ? `minWidth` : `minHeight`;
68
- return {
69
- [prop]: `${size}px`,
70
- [minProp]: `0px`,
71
- opacity: 1,
72
- overflow: `hidden`
73
- };
74
- }
75
- /**
76
- * Collapse + fade `el` in from nothing to its natural size. Call it just after
77
- * the element has been inserted (it must already be laid out at full size so it
78
- * can be measured). No-op under reduced motion.
79
- */
80
- function animateListItemIn(el, options = {}) {
81
- if (prefersReducedMotion()) return void 0;
82
- const { axis = `block`, easing = `ease` } = options;
83
- const duration = resolveDuration(el, options.duration);
84
- const size = measure(el, axis);
85
- if (size <= 0) return void 0;
86
- return el.animate([collapsedFrame(axis), fullFrame(size, axis)], {
87
- duration,
88
- easing
89
- });
90
- }
91
- function outResult(finished, animation) {
92
- return Object.assign(finished, { animation });
93
- }
94
- /**
95
- * Start the collapse + fade-out animation for `el` and return the handle.
96
- * Returns `undefined` when no animation was started (reduced motion or zero
97
- * size). The caller owns the handle and must `cancel()` it if the removal is
98
- * reversed — a retained `fill: forwards` frame otherwise keeps overriding the
99
- * element's ordinary CSS indefinitely.
100
- */
101
- function startListItemOutAnimation(el, options = {}) {
102
- if (prefersReducedMotion()) return void 0;
103
- const { axis = `block`, easing = `ease` } = options;
104
- const duration = resolveDuration(el, options.duration);
105
- const size = measure(el, axis);
106
- if (size <= 0) return void 0;
107
- return el.animate([fullFrame(size, axis), collapsedFrame(axis)], {
108
- duration,
109
- easing,
110
- fill: `forwards`
111
- });
112
- }
113
- /**
114
- * Collapse + fade `el` out. Resolves once the animation finishes (or straight
115
- * away under reduced motion); the caller detaches the element after. The final
116
- * collapsed state is held (`fill: forwards`) so there's no flash back to full
117
- * size in the gap before removal.
118
- *
119
- * The returned promise carries an `.animation` handle. If the removal is
120
- * reversed (the same key becomes visible again), cancel that handle so its
121
- * filled final frame stops overriding the reused element's style.
122
- */
123
- function animateListItemOut(el, options = {}) {
124
- const animation = startListItemOutAnimation(el, options);
125
- if (!animation) return outResult(Promise.resolve(), void 0);
126
- return outResult(animation.finished.then(() => void 0, () => void 0), animation);
127
- }
128
- //#endregion
129
- export { prefersReducedMotion as a, listItemFadeKeyframes as i, animateListItemIn as n, startListItemOutAnimation as o, animateListItemOut as r, LIST_ITEM_MOTION_MS as t };