@ixfx/components 0.16.1 → 0.16.3
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 +272 -58
- package/bundle/index.js +400 -337
- package/dist/ac-token.js +1 -1
- package/dist/{crumb-navigation-YnFVFqrx.js → crumb-navigation-D3ndPgFg.js} +2 -2
- package/dist/crumbs.js +1 -1
- package/dist/{data-grid-Cqxuwgbb.js → data-grid-8TsiF2ES.js} +142 -60
- package/dist/{data-grid-Csh5EJW9.d.ts → data-grid-CslzF16R.d.ts} +21 -3
- package/dist/data-grid.d.ts +2 -2
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-DSZ1DDor.d.ts → detail-list-n1MbNbhF.d.ts} +11 -3
- package/dist/{detail-list-Bo5ze3SN.js → detail-list-w3GXKO2k.js} +71 -19
- package/dist/detail-list.d.ts +2 -2
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-CT9kX3TQ.d.ts → flat-list-D_4y3wgn.d.ts} +7 -2
- package/dist/flat-list.d.ts +1 -1
- package/dist/flat-list.js +5 -2
- package/dist/{grid-list-DDLrWRxm.d.ts → grid-list-CqYS28NY.d.ts} +60 -14
- package/dist/{grid-list-D0dJNVEF.js → grid-list-Cz2tKcNZ.js} +55 -17
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-DETte0Dh.d.ts → grouped-item-lister-CS4yInuw.d.ts} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{incr-search-CgbL-OlS.js → incr-search-Dc6gYpJG.js} +34 -5
- package/dist/incr-search.d.ts +1 -1
- package/dist/incr-search.js +1 -1
- package/dist/{index-CUErGhCg.d.ts → index-Bm9a9S3_.d.ts} +2 -1
- package/dist/{index-Bnm-kKGC.d.ts → index-DyaHA9Kk.d.ts} +20 -1
- package/dist/{index-D-lZNlHa.d.ts → index-VAs9akt4.d.ts} +18 -2
- package/dist/{index-DxyyVqzA.d.ts → index-ZaA90u09.d.ts} +1 -0
- package/dist/index.d.ts +45 -14
- package/dist/index.js +93 -17
- package/dist/{list-selection-controller-5L0Nj9tR.js → list-selection-controller-D4iIrvJH.js} +199 -57
- package/dist/list-selection-types-Dnamh2Lu.d.ts +109 -0
- package/dist/{menu-CU9KAm8s.js → menu-BX1FHxY8.js} +16 -1
- package/dist/{menu-item-BUv8An2k.js → menu-item-oxU1VKiJ.js} +34 -9
- package/dist/menu.d.ts +1 -1
- package/dist/menu.js +2 -2
- package/dist/{miller-DUqiUAWQ.js → miller-Be7x4ufN.js} +142 -10
- package/dist/miller.d.ts +1 -1
- package/dist/miller.js +1 -1
- package/dist/narrowed-text.js +1 -1
- package/dist/partial-keys-Ck6CiprB.js +32 -0
- package/dist/sunburst-tree.d.ts +1 -1
- package/dist/sunburst-tree.js +1 -1
- package/dist/{sunbursttree-list-DePuFYlU.d.ts → sunbursttree-list-Bk20giTF.d.ts} +1 -1
- package/dist/{sunbursttree-list-B_M0YTEA.js → sunbursttree-list-DQF6DcsS.js} +1 -1
- package/dist/timeline.d.ts +1 -1
- package/dist/{tree-DAlgvaHB.js → tree-BaXYyt5U.js} +185 -69
- package/dist/tree.d.ts +1 -1
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-SAx56U-l.d.ts → vertical-list-DtHX26JO.d.ts} +11 -2
- package/dist/{vertical-list-DyKPKGxK.js → vertical-list-o5hfqH87.js} +54 -19
- package/dist/vertical-list.d.ts +2 -2
- package/dist/vertical-list.js +1 -1
- package/docs-user/README.md +1 -0
- package/docs-user/data-grid.md +10 -3
- package/docs-user/detail-list.md +1 -1
- package/docs-user/grid-list.md +4 -1
- package/docs-user/index.json +6 -2
- package/docs-user/llms.txt +1 -0
- package/docs-user/menu.md +11 -1
- package/docs-user/miller.md +4 -0
- package/docs-user/multicol-list.md +6 -0
- package/docs-user/select-horiz.md +1 -1
- package/docs-user/tree.md +6 -0
- package/docs-user/user-selection-intents.md +86 -0
- package/docs-user/vertical-list.md +9 -1
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/dist/list-selection-types-PjKdhF-a.d.ts +0 -45
- /package/dist/{index-DV3iaUBW.d.ts → index-DV3iaUBW2.d.ts} +0 -0
package/dist/{list-selection-controller-5L0Nj9tR.js → list-selection-controller-D4iIrvJH.js}
RENAMED
|
@@ -42,6 +42,7 @@ var ListDragSelectController = class {
|
|
|
42
42
|
/** Combine mode of the most recent preview (drives the commit flag). */
|
|
43
43
|
#lastMode = `replace`;
|
|
44
44
|
#preview = /* @__PURE__ */ new Set();
|
|
45
|
+
#covered = /* @__PURE__ */ new Set();
|
|
45
46
|
#raf = 0;
|
|
46
47
|
constructor(host, config) {
|
|
47
48
|
this.#host = host;
|
|
@@ -129,6 +130,7 @@ var ListDragSelectController = class {
|
|
|
129
130
|
this.#suppressClick = false;
|
|
130
131
|
this.#base = /* @__PURE__ */ new Set();
|
|
131
132
|
this.#lastMode = `replace`;
|
|
133
|
+
this.#covered = /* @__PURE__ */ new Set();
|
|
132
134
|
this.#lastModifiers = {
|
|
133
135
|
shiftKey: event.shiftKey,
|
|
134
136
|
ctrlKey: event.ctrlKey,
|
|
@@ -211,6 +213,7 @@ var ListDragSelectController = class {
|
|
|
211
213
|
preview = toggled;
|
|
212
214
|
} else preview = hitSet;
|
|
213
215
|
this.#preview = preview;
|
|
216
|
+
this.#covered = hitSet;
|
|
214
217
|
try {
|
|
215
218
|
this.#config.onPreview(preview, rect);
|
|
216
219
|
} catch {}
|
|
@@ -236,7 +239,11 @@ var ListDragSelectController = class {
|
|
|
236
239
|
this.#dragging = false;
|
|
237
240
|
const final = this.#preview;
|
|
238
241
|
try {
|
|
239
|
-
this.#config.onCommit(final, {
|
|
242
|
+
this.#config.onCommit(final, {
|
|
243
|
+
additive: this.#lastMode !== `replace`,
|
|
244
|
+
covered: this.#covered,
|
|
245
|
+
mode: this.#lastMode === `invert` ? `toggle` : this.#lastMode
|
|
246
|
+
});
|
|
240
247
|
} catch {}
|
|
241
248
|
try {
|
|
242
249
|
this.#config.onPreview(/* @__PURE__ */ new Set(), void 0);
|
|
@@ -296,10 +303,20 @@ var ListDragSelectController = class {
|
|
|
296
303
|
this.#base = /* @__PURE__ */ new Set();
|
|
297
304
|
this.#lastMode = `replace`;
|
|
298
305
|
this.#preview = /* @__PURE__ */ new Set();
|
|
306
|
+
this.#covered = /* @__PURE__ */ new Set();
|
|
299
307
|
}
|
|
300
308
|
};
|
|
301
309
|
//#endregion
|
|
302
310
|
//#region src/interaction/list-selection-controller.ts
|
|
311
|
+
/** Capture the modifier / pointer-type state of a click for `handleClick`. */
|
|
312
|
+
function gestureOf(e) {
|
|
313
|
+
return {
|
|
314
|
+
ctrlKey: e.ctrlKey,
|
|
315
|
+
metaKey: e.metaKey,
|
|
316
|
+
shiftKey: e.shiftKey,
|
|
317
|
+
pointerType: e.pointerType
|
|
318
|
+
};
|
|
319
|
+
}
|
|
303
320
|
/** Whether the interaction mode allows building a multi-item selection. */
|
|
304
321
|
function isMultiMode(mode) {
|
|
305
322
|
return mode === `checked` || mode === `sticky` || mode === `vscode` || mode === `standard`;
|
|
@@ -399,36 +416,36 @@ var ListSelectionController = class {
|
|
|
399
416
|
/** Select the contiguous ordered range from the anchor to `item`. Anchor unchanged. */
|
|
400
417
|
rangeTo(item) {
|
|
401
418
|
if (this.#config.getModes().selectionMode === `none`) return;
|
|
402
|
-
|
|
419
|
+
const range = this.#rangeItems(item);
|
|
420
|
+
if (range === void 0) {
|
|
403
421
|
this.replace(item);
|
|
404
422
|
return;
|
|
405
423
|
}
|
|
424
|
+
this.#apply(new Set(range));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The selectable items between the anchor and `item` (inclusive), or
|
|
428
|
+
* `undefined` when there is no usable range (no anchor, an unresolved end,
|
|
429
|
+
* or a different group) and the gesture degrades to a plain `replace`.
|
|
430
|
+
*/
|
|
431
|
+
#rangeItems(item) {
|
|
432
|
+
if (this.#anchor === void 0) return void 0;
|
|
406
433
|
const ordered = this.#config.getOrderedItems();
|
|
407
434
|
const anchorIdx = ordered.indexOf(this.#anchor);
|
|
408
435
|
const targetIdx = ordered.indexOf(item);
|
|
409
|
-
if (anchorIdx === -1 || targetIdx === -1)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
436
|
+
if (anchorIdx === -1 || targetIdx === -1) return void 0;
|
|
437
|
+
let scope = ordered;
|
|
438
|
+
let a = anchorIdx;
|
|
439
|
+
let b = targetIdx;
|
|
413
440
|
if (this.#clamped()) {
|
|
414
441
|
const group = this.#groupOf(this.#anchor);
|
|
415
|
-
if (group !== this.#groupOf(item))
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const a = inGroup.indexOf(this.#anchor);
|
|
421
|
-
const b = inGroup.indexOf(item);
|
|
422
|
-
if (a === -1 || b === -1) {
|
|
423
|
-
this.replace(item);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
this.#apply(new Set(inGroup.slice(Math.min(a, b), Math.max(a, b) + 1).filter((t) => this.#config.isSelectable(t))));
|
|
427
|
-
return;
|
|
442
|
+
if (group !== this.#groupOf(item)) return void 0;
|
|
443
|
+
scope = ordered.filter((t) => this.#groupOf(t) === group);
|
|
444
|
+
a = scope.indexOf(this.#anchor);
|
|
445
|
+
b = scope.indexOf(item);
|
|
446
|
+
if (a === -1 || b === -1) return void 0;
|
|
428
447
|
}
|
|
429
|
-
|
|
430
|
-
const end = Math.max(anchorIdx, targetIdx);
|
|
431
|
-
this.#apply(new Set(ordered.slice(start, end + 1).filter((t) => this.#config.isSelectable(t))));
|
|
448
|
+
return scope.slice(Math.min(a, b), Math.max(a, b) + 1).filter((t) => this.#config.isSelectable(t));
|
|
432
449
|
}
|
|
433
450
|
/** Clear the selection. No change event when the selection is already empty. */
|
|
434
451
|
clear() {
|
|
@@ -436,14 +453,23 @@ var ListSelectionController = class {
|
|
|
436
453
|
if (this.#selected.size === 0) return;
|
|
437
454
|
this.#apply(/* @__PURE__ */ new Set());
|
|
438
455
|
}
|
|
439
|
-
/**
|
|
440
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Select every ordered item that passes `isSelectable`. Fires an `all`
|
|
458
|
+
* intent first; the caller may take over.
|
|
459
|
+
*/
|
|
460
|
+
selectAll(source = `api`) {
|
|
441
461
|
let ordered = this.#config.getOrderedItems();
|
|
442
|
-
|
|
462
|
+
let groupKey = null;
|
|
443
463
|
if (this.#clamped()) {
|
|
444
|
-
|
|
445
|
-
if (
|
|
464
|
+
groupKey = (this.#anchor === void 0 ? null : this.#groupOf(this.#anchor)) ?? this.#config.getFallbackGroupKey?.() ?? null;
|
|
465
|
+
if (groupKey !== null) ordered = ordered.filter((t) => this.#groupOf(t) === groupKey);
|
|
446
466
|
}
|
|
467
|
+
if (this.#intercepted({
|
|
468
|
+
kind: `all`,
|
|
469
|
+
keys: [],
|
|
470
|
+
...groupKey === null ? {} : { groupKey },
|
|
471
|
+
source
|
|
472
|
+
})) return;
|
|
447
473
|
const next = new Set(ordered.filter((t) => this.#config.isSelectable(t)));
|
|
448
474
|
if (next.size === 0) return;
|
|
449
475
|
this.#apply(next);
|
|
@@ -478,9 +504,22 @@ var ListSelectionController = class {
|
|
|
478
504
|
* non-empty selection (e.g. a Cmd-drag that toggles everything off) but is
|
|
479
505
|
* a no-op when nothing is selected.
|
|
480
506
|
*/
|
|
481
|
-
applyDragSelection(items) {
|
|
507
|
+
applyDragSelection(items, info) {
|
|
482
508
|
if (!this.canDragSelect()) return;
|
|
483
|
-
const
|
|
509
|
+
const final = [...items];
|
|
510
|
+
const covered = info ? [...info.covered].filter((t) => this.#config.isSelectable(t)) : final;
|
|
511
|
+
const mode = info?.mode ?? (info?.additive ? `toggle` : `replace`);
|
|
512
|
+
if (this.#intercepted({
|
|
513
|
+
kind: `marquee`,
|
|
514
|
+
keys: this.#keysOf(covered),
|
|
515
|
+
additive: mode !== `replace`,
|
|
516
|
+
mode,
|
|
517
|
+
source: `pointer`
|
|
518
|
+
})) {
|
|
519
|
+
this.#anchor = final.at(-1) ?? this.#anchor;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const next = this.#clampToFirstGroup(new Set(final.filter((t) => this.#config.isSelectable(t))));
|
|
484
523
|
if (next.size === 0 && this.#selected.size === 0) return;
|
|
485
524
|
this.#anchor = [...next].at(-1);
|
|
486
525
|
this.#apply(next);
|
|
@@ -503,54 +542,67 @@ var ListSelectionController = class {
|
|
|
503
542
|
}
|
|
504
543
|
/**
|
|
505
544
|
* Replace the selection with the items resolved from `keys`. Keys that
|
|
506
|
-
* `resolve` cannot map
|
|
507
|
-
*
|
|
508
|
-
*
|
|
545
|
+
* `resolve` cannot map yet are kept pending and selected when their items
|
|
546
|
+
* appear (see `resolvePending()`); keys that resolve to a non-selectable
|
|
547
|
+
* item are ignored. `selectionMode="none"` is a no-op. When multi-select is
|
|
548
|
+
* not available, only the last key is kept. `onChange` fires once.
|
|
509
549
|
*/
|
|
510
550
|
selectKeys(keys, resolve) {
|
|
511
551
|
if (this.#config.getModes().selectionMode === `none`) return;
|
|
512
552
|
let next = /* @__PURE__ */ new Set();
|
|
553
|
+
const missing = /* @__PURE__ */ new Set();
|
|
513
554
|
for (const key of new Set(keys)) {
|
|
514
555
|
const item = resolve(key);
|
|
515
|
-
if (item
|
|
556
|
+
if (item === void 0) missing.add(key);
|
|
557
|
+
else if (this.#config.isSelectable(item)) next.add(item);
|
|
516
558
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
next
|
|
559
|
+
let pending = missing;
|
|
560
|
+
if (!this.#effectiveMulti()) {
|
|
561
|
+
if (next.size > 0) {
|
|
562
|
+
next = new Set([...next].slice(-1));
|
|
563
|
+
pending = EMPTY_KEYS;
|
|
564
|
+
} else pending = new Set([...missing].slice(-1));
|
|
520
565
|
}
|
|
521
566
|
this.#anchor = [...next].at(-1);
|
|
522
|
-
this.#
|
|
567
|
+
this.#pending = capKeys(pending);
|
|
568
|
+
this.#apply(next, true);
|
|
523
569
|
}
|
|
524
570
|
/**
|
|
525
571
|
* Add the items resolved from `keys` to the selection (multi-select only;
|
|
526
|
-
* a no-op otherwise).
|
|
572
|
+
* a no-op otherwise). Keys not present yet are kept pending. `onChange`
|
|
573
|
+
* fires once if anything changed.
|
|
527
574
|
*/
|
|
528
575
|
addKeys(keys, resolve) {
|
|
529
576
|
if (!this.#effectiveMulti()) return;
|
|
530
577
|
const next = new Set(this.#selected);
|
|
578
|
+
const pending = new Set(this.#pending);
|
|
531
579
|
let changed = false;
|
|
532
580
|
for (const key of new Set(keys)) {
|
|
533
581
|
const item = resolve(key);
|
|
534
|
-
if (item
|
|
582
|
+
if (item === void 0) pending.add(key);
|
|
583
|
+
else if (!next.has(item) && this.#config.isSelectable(item)) {
|
|
535
584
|
next.add(item);
|
|
536
585
|
changed = true;
|
|
537
586
|
}
|
|
538
587
|
}
|
|
539
|
-
if (changed) this.#apply(next);
|
|
588
|
+
if (changed) this.#apply(next, true);
|
|
589
|
+
this.#pending = capKeys(pending);
|
|
540
590
|
}
|
|
541
591
|
/**
|
|
542
|
-
* Remove the items resolved from `keys` from the selection
|
|
543
|
-
* fires once if anything changed.
|
|
592
|
+
* Remove the items resolved from `keys` from the selection, and drop them
|
|
593
|
+
* from the pending keys. `onChange` fires once if anything changed.
|
|
544
594
|
*/
|
|
545
595
|
removeKeys(keys, resolve) {
|
|
596
|
+
const wanted = new Set(keys);
|
|
597
|
+
if (this.#pending.size > 0) this.#pending = new Set([...this.#pending].filter((k) => !wanted.has(k)));
|
|
546
598
|
if (this.#selected.size === 0) return;
|
|
547
599
|
const next = new Set(this.#selected);
|
|
548
600
|
let changed = false;
|
|
549
|
-
for (const key of
|
|
601
|
+
for (const key of wanted) {
|
|
550
602
|
const item = resolve(key);
|
|
551
603
|
if (item !== void 0 && next.delete(item)) changed = true;
|
|
552
604
|
}
|
|
553
|
-
if (changed) this.#apply(next);
|
|
605
|
+
if (changed) this.#apply(next, true);
|
|
554
606
|
}
|
|
555
607
|
/** Selected keys waiting for their items to appear. */
|
|
556
608
|
get pendingKeys() {
|
|
@@ -625,32 +677,122 @@ var ListSelectionController = class {
|
|
|
625
677
|
const item = resolve(key);
|
|
626
678
|
return item !== void 0 && this.#config.isSelectable(item) ? item : void 0;
|
|
627
679
|
}
|
|
628
|
-
/**
|
|
680
|
+
/**
|
|
681
|
+
* Route a click on `item` according to the current interaction mode. In
|
|
682
|
+
* `manual` mode nothing is selected, but the intent is still reported so
|
|
683
|
+
* the caller can apply it.
|
|
684
|
+
*/
|
|
629
685
|
handleClick(item, gesture) {
|
|
630
686
|
const { selectionMode, interactionMode: mode } = this.#config.getModes();
|
|
631
|
-
if (mode === `manual`) return;
|
|
632
687
|
if (selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
|
|
688
|
+
const touch = gesture.pointerType === `touch`;
|
|
689
|
+
const modified = gesture.ctrlKey || gesture.metaKey;
|
|
690
|
+
const multi = selectionMode === `multiple` || mode === `vscode`;
|
|
633
691
|
switch (mode) {
|
|
634
692
|
case `implicit`:
|
|
635
|
-
this
|
|
693
|
+
this.#replaceIntent(item, `pointer`);
|
|
694
|
+
break;
|
|
695
|
+
case `manual`:
|
|
696
|
+
if (modified) this.#toggleIntent(item, `pointer`);
|
|
697
|
+
else if (gesture.shiftKey) this.#extendIntent(item, `pointer`);
|
|
698
|
+
else this.#replaceIntent(item, `pointer`);
|
|
636
699
|
break;
|
|
637
700
|
case `standard`:
|
|
638
701
|
case `vscode`:
|
|
639
|
-
if (
|
|
640
|
-
if (
|
|
641
|
-
} else if (gesture.shiftKey &&
|
|
642
|
-
else this
|
|
702
|
+
if (modified) {
|
|
703
|
+
if (multi) this.#toggleIntent(item, `pointer`);
|
|
704
|
+
} else if (gesture.shiftKey && multi) this.#extendIntent(item, `pointer`);
|
|
705
|
+
else this.#replaceIntent(item, `pointer`);
|
|
643
706
|
break;
|
|
644
707
|
case `checked`: break;
|
|
645
|
-
case `sticky`: this
|
|
708
|
+
case `sticky`: this.#toggleIntent(item, touch ? `touch` : `pointer`);
|
|
646
709
|
}
|
|
647
710
|
}
|
|
648
711
|
/** Route a Space keypress on `item` according to the current interaction mode. */
|
|
649
712
|
handleSpace(item) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
713
|
+
if (this.#config.getModes().interactionMode === `implicit`) this.#replaceIntent(item, `keyboard`);
|
|
714
|
+
else this.#toggleIntent(item, `keyboard`);
|
|
715
|
+
}
|
|
716
|
+
/** Route a checkbox (checked-mode) toggle on `item`. */
|
|
717
|
+
handleCheckbox(item, source = `pointer`) {
|
|
718
|
+
this.#toggleIntent(item, source);
|
|
719
|
+
}
|
|
720
|
+
/** Route a plain "select only this" gesture (Enter, double-click) on `item`. */
|
|
721
|
+
handleReplace(item, source = `pointer`) {
|
|
722
|
+
this.#replaceIntent(item, source);
|
|
723
|
+
}
|
|
724
|
+
/** Route Shift+Arrow: extend the range from the anchor to the cursor `item`. */
|
|
725
|
+
handleExtend(item) {
|
|
726
|
+
this.#extendIntent(item, `keyboard`);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Route Escape / empty-space click: clear the selection. `reportOnly`
|
|
730
|
+
* suppresses the default action in `manual` mode (the caller owns it).
|
|
731
|
+
*/
|
|
732
|
+
handleClear(source = `keyboard`, reportOnly = false) {
|
|
733
|
+
if (this.#intercepted({
|
|
734
|
+
kind: `clear`,
|
|
735
|
+
keys: [],
|
|
736
|
+
source
|
|
737
|
+
})) return;
|
|
738
|
+
if (reportOnly && this.#config.getModes().interactionMode === `manual`) return;
|
|
739
|
+
this.clear();
|
|
740
|
+
}
|
|
741
|
+
#replaceIntent(item, source) {
|
|
742
|
+
if (this.#config.getModes().selectionMode === `none`) return;
|
|
743
|
+
if (!this.#config.isSelectable(item)) return;
|
|
744
|
+
if (this.#intercepted({
|
|
745
|
+
kind: `replace`,
|
|
746
|
+
keys: this.#keysOf([item]),
|
|
747
|
+
source
|
|
748
|
+
})) {
|
|
749
|
+
this.#anchor = item;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (this.#config.getModes().interactionMode === `manual`) return;
|
|
753
|
+
this.replace(item);
|
|
754
|
+
}
|
|
755
|
+
#toggleIntent(item, source) {
|
|
756
|
+
if (!this.#config.isSelectable(item)) return;
|
|
757
|
+
if (this.#intercepted({
|
|
758
|
+
kind: `toggle`,
|
|
759
|
+
keys: this.#keysOf([item]),
|
|
760
|
+
source
|
|
761
|
+
})) return;
|
|
762
|
+
if (this.#config.getModes().interactionMode === `manual`) return;
|
|
763
|
+
this.toggle(item);
|
|
764
|
+
}
|
|
765
|
+
#extendIntent(item, source) {
|
|
766
|
+
if (this.#config.getModes().selectionMode === `none`) return;
|
|
767
|
+
const range = this.#anchor === void 0 ? void 0 : this.#rangeItems(item);
|
|
768
|
+
const anchorKey = this.#anchor === void 0 ? void 0 : this.#config.getKey?.(this.#anchor);
|
|
769
|
+
const targetKey = this.#config.getKey?.(item);
|
|
770
|
+
if (this.#intercepted({
|
|
771
|
+
kind: `extend`,
|
|
772
|
+
keys: this.#keysOf(range ?? [item]),
|
|
773
|
+
...anchorKey === void 0 ? {} : { anchorKey },
|
|
774
|
+
...targetKey === void 0 ? {} : { targetKey },
|
|
775
|
+
source
|
|
776
|
+
})) {
|
|
777
|
+
if (this.#anchor === void 0) this.#anchor = item;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (this.#config.getModes().interactionMode === `manual`) return;
|
|
781
|
+
this.rangeTo(item);
|
|
782
|
+
}
|
|
783
|
+
#keysOf(items) {
|
|
784
|
+
const getKey = this.#config.getKey;
|
|
785
|
+
if (!getKey) return [];
|
|
786
|
+
const out = [];
|
|
787
|
+
for (const item of items) {
|
|
788
|
+
const key = getKey(item);
|
|
789
|
+
if (key !== void 0) out.push(key);
|
|
790
|
+
}
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
/** Report an intent; `true` when the caller took over. */
|
|
794
|
+
#intercepted(intent) {
|
|
795
|
+
return this.#config.onIntent?.(intent) ?? false;
|
|
654
796
|
}
|
|
655
797
|
/** Drop the range-selection anchor (e.g. when the interaction mode changes). */
|
|
656
798
|
clearAnchor() {
|
|
@@ -707,4 +849,4 @@ var ListSelectionController = class {
|
|
|
707
849
|
}
|
|
708
850
|
};
|
|
709
851
|
//#endregion
|
|
710
|
-
export {
|
|
852
|
+
export { ListDragSelectController as i, gestureOf as n, isMultiMode as r, ListSelectionController as t };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//#region src/interaction/list-selection-types.d.ts
|
|
2
|
+
type ListSelectionMode = `none` | `single` | `multiple`;
|
|
3
|
+
/**
|
|
4
|
+
* Controls how pointer and keyboard gestures map to selection changes.
|
|
5
|
+
*
|
|
6
|
+
* - `implicit` — click always replaces selection (no modifier support)
|
|
7
|
+
* - `standard` — click replaces; Ctrl/Cmd+click toggles; Shift+click range-selects (multiple only)
|
|
8
|
+
* - `manual` — no auto-selection; drive state via select()/deselect()/clearSelection()
|
|
9
|
+
* - `checked` — every click toggles; implies multiple selection regardless of selectionMode
|
|
10
|
+
* - `sticky` — every tap/click toggles (no checkbox column); implies multiple selection, for touchscreens
|
|
11
|
+
* - `vscode` — like standard for clicks; Shift+Arrow extends range from the keyboard anchor
|
|
12
|
+
*
|
|
13
|
+
* Switching interactionMode clears the current selection.
|
|
14
|
+
*/
|
|
15
|
+
type ListInteractionMode = `implicit` | `standard` | `manual` | `checked` | `sticky` | `vscode`;
|
|
16
|
+
type ListSelectDetail = {
|
|
17
|
+
readonly selected: ReadonlySet<Element>;
|
|
18
|
+
readonly previous: ReadonlySet<Element>;
|
|
19
|
+
/**
|
|
20
|
+
* The selection as stable key strings (`data-key` / `data-value`). Empty
|
|
21
|
+
* when items carry no key. Additive to `selected`.
|
|
22
|
+
*/
|
|
23
|
+
readonly selectedKeys: ReadonlySet<string>;
|
|
24
|
+
/** The previous selection as keys. Additive to `previous`. */
|
|
25
|
+
readonly previousKeys: ReadonlySet<string>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The key-based subset of `list-select` detail common to every list-like
|
|
29
|
+
* component (including `ixfx-data-grid`), so one adapter can read any of them.
|
|
30
|
+
*/
|
|
31
|
+
type ListSelectKeysDetail = Pick<ListSelectDetail, `selectedKeys` | `previousKeys`>;
|
|
32
|
+
/**
|
|
33
|
+
* What the user (or API) is trying to do to the selection. See
|
|
34
|
+
* {@link ListSelectIntentDetail}.
|
|
35
|
+
*/
|
|
36
|
+
type ListSelectIntentKind = `replace` | `toggle` | `extend` | `all` | `marquee` | `match` | `clear`;
|
|
37
|
+
/**
|
|
38
|
+
* How a `marquee` combines the covered keys with the current selection:
|
|
39
|
+
* `replace` (plain drag: the selection becomes the covered keys), `add`
|
|
40
|
+
* (Shift-drag: covered keys are added), `toggle` (Cmd/Ctrl-drag: each covered
|
|
41
|
+
* key flips, so a drag can deselect).
|
|
42
|
+
*/
|
|
43
|
+
type ListMarqueeMode = `replace` | `add` | `toggle`;
|
|
44
|
+
type ListSelectIntentSource = `pointer` | `keyboard` | `touch` | `api`;
|
|
45
|
+
/**
|
|
46
|
+
* Detail of the cancelable `list-select-intent` event: fired *before* a
|
|
47
|
+
* component changes its selection, in every interaction mode (including
|
|
48
|
+
* `manual`). Calling `preventDefault()` means "the caller applies this
|
|
49
|
+
* itself" — the component then changes nothing (cursor and range anchor still
|
|
50
|
+
* move). Without `preventDefault()` behaviour is unchanged.
|
|
51
|
+
*
|
|
52
|
+
* Only gestures and `selectAll()` fire it. The imperative `select()` /
|
|
53
|
+
* `selectKeys()` / `clearSelection()` family never does, so a handler can
|
|
54
|
+
* apply its decision through them without re-entering itself.
|
|
55
|
+
*/
|
|
56
|
+
type ListSelectIntentDetail = {
|
|
57
|
+
readonly kind: ListSelectIntentKind;
|
|
58
|
+
/** Keys directly affected. Empty for `all` / `clear`. For `extend`, the range (a convenience: callers should use the endpoints). */
|
|
59
|
+
readonly keys: readonly string[];
|
|
60
|
+
/** Range anchor, for `extend`. */
|
|
61
|
+
readonly anchorKey?: string;
|
|
62
|
+
/** Range end, for `extend`. */
|
|
63
|
+
readonly targetKey?: string;
|
|
64
|
+
/** `marquee`: a modifier was held, i.e. `mode` is not `replace`. */
|
|
65
|
+
readonly additive?: boolean;
|
|
66
|
+
/** `marquee`: how the covered `keys` combine with the selection. */
|
|
67
|
+
readonly mode?: ListMarqueeMode;
|
|
68
|
+
/** `match`: the search text. `keys` holds the current matches (a convenience). */
|
|
69
|
+
readonly query?: string;
|
|
70
|
+
/**
|
|
71
|
+
* `all`: the group the select-all is confined to (grouped lists with
|
|
72
|
+
* cross-group selection off). Absent when it is not confined to a group.
|
|
73
|
+
*/
|
|
74
|
+
readonly groupKey?: string | null;
|
|
75
|
+
/**
|
|
76
|
+
* `toggle` from a branch checkbox in a tree: the toggle applies to the node
|
|
77
|
+
* and every loaded node under it (`keys` lists them), as a rule rather than
|
|
78
|
+
* a key list.
|
|
79
|
+
*/
|
|
80
|
+
readonly descendants?: boolean;
|
|
81
|
+
/** `toggle` with `descendants`: the state the branch is being set to (`true` = checked). */
|
|
82
|
+
readonly checked?: boolean;
|
|
83
|
+
/** Multi-column lists (`ixfx-multicol-list`): the column the intent is for. Keys are that column's item keys. */
|
|
84
|
+
readonly columnId?: string;
|
|
85
|
+
readonly source: ListSelectIntentSource;
|
|
86
|
+
};
|
|
87
|
+
type ListTickleDetail = {
|
|
88
|
+
readonly item: Element;
|
|
89
|
+
};
|
|
90
|
+
type ListActivateDetail = {
|
|
91
|
+
readonly item: Element;
|
|
92
|
+
};
|
|
93
|
+
type ListItemClickDetail = {
|
|
94
|
+
readonly item: Element;
|
|
95
|
+
};
|
|
96
|
+
type ListSelectEvent = CustomEvent<ListSelectDetail>;
|
|
97
|
+
type ListSelectIntentEvent = CustomEvent<ListSelectIntentDetail>;
|
|
98
|
+
type ListTickleEvent = CustomEvent<ListTickleDetail>;
|
|
99
|
+
type ListActivateEvent = CustomEvent<ListActivateDetail>;
|
|
100
|
+
type ListItemClickEvent = CustomEvent<ListItemClickDetail>;
|
|
101
|
+
interface ListElementEventMap {
|
|
102
|
+
'list-select': ListSelectEvent;
|
|
103
|
+
'list-select-intent': ListSelectIntentEvent;
|
|
104
|
+
'list-tickle': ListTickleEvent;
|
|
105
|
+
'list-activate': ListActivateEvent;
|
|
106
|
+
'list-item-click': ListItemClickEvent;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { ListTickleEvent as _, ListItemClickDetail as a, ListSelectDetail as c, ListSelectIntentEvent as d, ListSelectIntentKind as f, ListTickleDetail as g, ListSelectionMode as h, ListInteractionMode as i, ListSelectEvent as l, ListSelectKeysDetail as m, ListActivateEvent as n, ListItemClickEvent as o, ListSelectIntentSource as p, ListElementEventMap as r, ListMarqueeMode as s, ListActivateDetail as t, ListSelectIntentDetail as u };
|
|
@@ -4,7 +4,7 @@ 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 "./icon-1gpwtPx8.js";
|
|
6
6
|
import "./interaction-E-uMGHkJ.js";
|
|
7
|
-
import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-
|
|
7
|
+
import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-oxU1VKiJ.js";
|
|
8
8
|
import { LitElement, css, html, nothing } from "lit";
|
|
9
9
|
import { property, query, state } from "lit/decorators.js";
|
|
10
10
|
//#region src/menu/menu-bar.ts
|
|
@@ -16,6 +16,15 @@ let MenuBar = class MenuBar extends LitElement {
|
|
|
16
16
|
this._handlePointerMove = () => {
|
|
17
17
|
this._setInputMode(`mouse`);
|
|
18
18
|
};
|
|
19
|
+
this._handlePointerOver = (e) => {
|
|
20
|
+
if (!this._openTrigger || e.pointerType === `touch`) return;
|
|
21
|
+
const triggers = this._getTriggers();
|
|
22
|
+
const path = e.composedPath();
|
|
23
|
+
const trigger = triggers.find((t) => path.includes(t));
|
|
24
|
+
if (!trigger || trigger === this._openTrigger) return;
|
|
25
|
+
this._openTrigger.closeMenu();
|
|
26
|
+
trigger.openMenu();
|
|
27
|
+
};
|
|
19
28
|
this._handleTriggerOpen = (e) => {
|
|
20
29
|
this._openTrigger = e.target;
|
|
21
30
|
};
|
|
@@ -53,6 +62,7 @@ let MenuBar = class MenuBar extends LitElement {
|
|
|
53
62
|
this.addEventListener(`focusin`, this._handleFocusIn);
|
|
54
63
|
this.addEventListener(`keydown`, this._handleKeydown);
|
|
55
64
|
this.addEventListener(`pointermove`, this._handlePointerMove);
|
|
65
|
+
this.addEventListener(`pointerover`, this._handlePointerOver);
|
|
56
66
|
}
|
|
57
67
|
disconnectedCallback() {
|
|
58
68
|
super.disconnectedCallback();
|
|
@@ -61,6 +71,7 @@ let MenuBar = class MenuBar extends LitElement {
|
|
|
61
71
|
this.removeEventListener(`focusin`, this._handleFocusIn);
|
|
62
72
|
this.removeEventListener(`keydown`, this._handleKeydown);
|
|
63
73
|
this.removeEventListener(`pointermove`, this._handlePointerMove);
|
|
74
|
+
this.removeEventListener(`pointerover`, this._handlePointerOver);
|
|
64
75
|
}
|
|
65
76
|
_setInputMode(mode) {
|
|
66
77
|
if (this._inputMode === mode) return;
|
|
@@ -75,6 +86,8 @@ let MenuBar = class MenuBar extends LitElement {
|
|
|
75
86
|
:host {
|
|
76
87
|
display: flex;
|
|
77
88
|
align-items: center;
|
|
89
|
+
gap: var(--menu-bar-gap, var(--space-xs));
|
|
90
|
+
padding: var(--menu-bar-padding, var(--space-xs));
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/* Keyboard mode: suppress hover highlight on trigger buttons */
|
|
@@ -385,6 +398,7 @@ let MenuSubTrigger = class MenuSubTrigger extends LitElement {
|
|
|
385
398
|
}
|
|
386
399
|
_handleKeydown(e) {
|
|
387
400
|
if (this.disabled) return;
|
|
401
|
+
if (e.target !== this !== (e.key === `ArrowLeft` || e.key === `Escape`)) return;
|
|
388
402
|
switch (e.key) {
|
|
389
403
|
case `ArrowRight`:
|
|
390
404
|
case `Enter`:
|
|
@@ -408,6 +422,7 @@ let MenuSubTrigger = class MenuSubTrigger extends LitElement {
|
|
|
408
422
|
e.preventDefault();
|
|
409
423
|
e.stopPropagation();
|
|
410
424
|
this._hideSubmenu();
|
|
425
|
+
this._itemEl?.focus();
|
|
411
426
|
}
|
|
412
427
|
}
|
|
413
428
|
static {
|
|
@@ -5,6 +5,7 @@ import { t as popupSurfaceStyles } from "./popup-D80sj2QC.js";
|
|
|
5
5
|
import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
|
|
6
6
|
import "./icon-1gpwtPx8.js";
|
|
7
7
|
import "./interaction-E-uMGHkJ.js";
|
|
8
|
+
import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
|
|
8
9
|
import { LitElement, css, html } from "lit";
|
|
9
10
|
import { property, query, state } from "lit/decorators.js";
|
|
10
11
|
//#region src/menu/menu-container.ts
|
|
@@ -24,6 +25,7 @@ let MenuContainer = class MenuContainer extends LitElement {
|
|
|
24
25
|
const item = items[index];
|
|
25
26
|
item.tabIndex = 0;
|
|
26
27
|
item.focus({ preventScroll: true });
|
|
28
|
+
if (this.tickled.inputMode === `keyboard`) item.scrollIntoView({ block: `nearest` });
|
|
27
29
|
}
|
|
28
30
|
},
|
|
29
31
|
onStateChange: () => {
|
|
@@ -78,16 +80,29 @@ let MenuContainer = class MenuContainer extends LitElement {
|
|
|
78
80
|
popupSurfaceStyles,
|
|
79
81
|
themeFallbacks,
|
|
80
82
|
tickledStyles,
|
|
83
|
+
scrollFadeStyles(`y`),
|
|
81
84
|
css`
|
|
82
85
|
:host {
|
|
83
86
|
display: block;
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
/* The surface (border, shadow) stays fixed; only the inner scroller
|
|
90
|
+
scrolls, so the scroll fade masks the items and not the surface edges. */
|
|
86
91
|
.popover {
|
|
87
|
-
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
max-height: var(--menu-max-height, min(60vh, 400px));
|
|
88
95
|
min-width: 160px;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
.scroller {
|
|
99
|
+
min-height: 0;
|
|
100
|
+
padding: var(--space-s);
|
|
101
|
+
overflow-y: auto;
|
|
102
|
+
overscroll-behavior: contain;
|
|
103
|
+
--scroll-fade-width: var(--space-l);
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
:host {
|
|
92
107
|
opacity: 1;
|
|
93
108
|
transform: translateY(0) scale(1);
|
|
@@ -159,7 +174,9 @@ let MenuContainer = class MenuContainer extends LitElement {
|
|
|
159
174
|
@keydown=${this._handleKeydown}
|
|
160
175
|
@mouseover=${this._handleMouseOver}
|
|
161
176
|
>
|
|
162
|
-
<
|
|
177
|
+
<div class="scroller scroll-fade-y">
|
|
178
|
+
<slot @slotchange=${this._updateIconSections}></slot>
|
|
179
|
+
</div>
|
|
163
180
|
</div>
|
|
164
181
|
`;
|
|
165
182
|
}
|
|
@@ -541,7 +558,6 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
541
558
|
:host {
|
|
542
559
|
display: inline-block;
|
|
543
560
|
position: relative;
|
|
544
|
-
--ixfx-button-padding: var(--ixfx-button-padding, var(--space-s) var(--space-m));
|
|
545
561
|
}
|
|
546
562
|
|
|
547
563
|
/* Context mode with a context-target: dissolve the host's box so the
|
|
@@ -584,7 +600,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
584
600
|
border: none;
|
|
585
601
|
color: var(--surface-3-text);
|
|
586
602
|
cursor: pointer;
|
|
587
|
-
padding: var(--ixfx-button-padding);
|
|
603
|
+
padding: var(--ixfx-button-padding, var(--space-s) var(--space-m));
|
|
588
604
|
font-family: var(--font-family, inherit);
|
|
589
605
|
font-size: var(--text-m);
|
|
590
606
|
font-weight: 600;
|
|
@@ -610,18 +626,24 @@ let MenuTrigger = class MenuTrigger extends LitElement {
|
|
|
610
626
|
position: fixed;
|
|
611
627
|
position-anchor: var(--anchor-name);
|
|
612
628
|
inset: auto;
|
|
613
|
-
|
|
629
|
+
/* Offset only along the placement axis so the popup's edge lines up
|
|
630
|
+
with the trigger's; flip fallbacks mirror the margin with it. */
|
|
631
|
+
margin: var(--menu-popup-offset, var(--space-xs)) 0;
|
|
614
632
|
padding: 0;
|
|
615
633
|
border: none;
|
|
616
634
|
background: transparent;
|
|
617
635
|
z-index: 100;
|
|
618
|
-
max-height: min(60vh, 400px);
|
|
619
636
|
max-width: min(90vw, 320px);
|
|
620
|
-
|
|
621
|
-
|
|
637
|
+
/* Scrolling happens inside ixfx-menu-container (with a scroll fade).
|
|
638
|
+
Overriding the UA overflow:auto also stops the menu's shadow being clipped. */
|
|
639
|
+
overflow: visible;
|
|
622
640
|
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
623
641
|
}
|
|
624
642
|
|
|
643
|
+
:host([placement="right-start"]) .popup {
|
|
644
|
+
margin: 0 var(--menu-popup-offset, var(--space-xs));
|
|
645
|
+
}
|
|
646
|
+
|
|
625
647
|
.popup[popover] {
|
|
626
648
|
opacity: 1;
|
|
627
649
|
transform: translateY(0) scale(1);
|
|
@@ -670,7 +692,10 @@ __decorate([property({
|
|
|
670
692
|
})], MenuTrigger.prototype, "iconName", void 0);
|
|
671
693
|
__decorate([property({ attribute: false })], MenuTrigger.prototype, "registry", void 0);
|
|
672
694
|
__decorate([property({ attribute: false })], MenuTrigger.prototype, "keyboardManager", void 0);
|
|
673
|
-
__decorate([property({
|
|
695
|
+
__decorate([property({
|
|
696
|
+
type: String,
|
|
697
|
+
reflect: true
|
|
698
|
+
})], MenuTrigger.prototype, "placement", void 0);
|
|
674
699
|
__decorate([property({
|
|
675
700
|
type: Boolean,
|
|
676
701
|
attribute: `tab-stop`
|