@ixfx/components 0.14.4 → 0.14.6
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/bundle/index.d.ts +2 -1
- package/bundle/index.js +178 -178
- package/dist/{checked-select-lDgAQQ7v.js → checked-select-CBUidG7n.js} +1 -1
- package/dist/checked-select.js +1 -1
- package/dist/crumbs.js +2 -2
- package/dist/{data-grid-FHr7wZ_k.js → data-grid-Dx3l3dxw.js} +3 -4
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-BfYHNh6B.js → detail-list-q8bs67BM.js} +1 -2
- package/dist/detail-list.js +1 -1
- package/dist/{grid-list-CkU8gHCh.js → grid-list-y6dWJYu3.js} +2 -3
- package/dist/grid-list.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -10
- package/dist/{interaction-ZTh4htzr.js → interaction-E-uMGHkJ.js} +1 -1
- package/dist/{list-native-drag-controller-B9xO0XWn.js → list-native-drag-controller-CcXoS0YX.js} +212 -2
- package/dist/{menu-CwcXXeLM.js → menu-CU9KAm8s.js} +2 -2
- package/dist/{menu-item-BDEekBVy.js → menu-item-BUv8An2k.js} +1 -1
- package/dist/menu.js +2 -2
- package/dist/miller.js +1 -1
- package/dist/narrowed-text.js +1 -1
- package/dist/select-horiz.js +1 -1
- package/dist/{tree-BFoN2Z_P.js → tree-2_TRrLAV.js} +105 -17
- package/dist/tree.js +1 -1
- package/dist/vertical-list.js +1 -2
- package/docs-user/index.json +2 -2
- package/docs-user/tree.md +17 -5
- package/package.json +1 -1
- package/dist/list-item-animation-BUoEKi5s.js +0 -129
|
@@ -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-
|
|
7
|
-
import
|
|
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
|
-
|
|
453
|
-
const
|
|
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
|
-
|
|
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
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (
|
|
744
|
-
|
|
745
|
-
this.#
|
|
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-
|
|
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 };
|
package/dist/vertical-list.js
CHANGED
|
@@ -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-
|
|
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";
|
package/docs-user/index.json
CHANGED
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,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 };
|