@ixfx/components 0.7.4 → 0.7.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.
- package/bundle/index.d.ts +536 -178
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +1324 -442
- package/bundle/index.js.map +1 -1
- package/dist/ac-token.js +1 -1
- package/dist/crumbs.d.ts +5 -1
- package/dist/crumbs.d.ts.map +1 -1
- package/dist/crumbs.js +10 -0
- package/dist/crumbs.js.map +1 -1
- package/dist/{data-grid-BddUjAti.d.ts → data-grid-CtJ35v8L.d.ts} +26 -1
- package/dist/data-grid-CtJ35v8L.d.ts.map +1 -0
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +88 -7
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-Br0WAnry.d.ts → detail-list-BS5dRDda.d.ts} +20 -12
- package/dist/detail-list-BS5dRDda.d.ts.map +1 -0
- package/dist/detail-list.d.ts +2 -2
- package/dist/detail-list.js +51 -135
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-B_orPsot.d.ts → grid-list-D-GbghSH.d.ts} +18 -1
- package/dist/grid-list-D-GbghSH.d.ts.map +1 -0
- package/dist/grid-list.d.ts +1 -1
- package/dist/grid-list.js +76 -15
- package/dist/grid-list.js.map +1 -1
- package/dist/incr-search-DpbSJtgj.js +962 -0
- package/dist/incr-search-DpbSJtgj.js.map +1 -0
- package/dist/incr-search.d.ts +2 -2
- package/dist/incr-search.js +2 -3
- package/dist/index-CBS4z1v-.d.ts +365 -0
- package/dist/index-CBS4z1v-.d.ts.map +1 -0
- package/dist/index.d.ts +8 -8
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/{list-selection-controller-CUo-Hhcm.js → list-selection-controller-DTFI40li.js} +2 -76
- package/dist/list-selection-controller-DTFI40li.js.map +1 -0
- package/dist/miller.d.ts +81 -3
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +327 -68
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.js +1 -2
- package/dist/narrowed-text.js.map +1 -1
- package/dist/{tree-B74zdI60.js → tree-DAii2A1g.js} +77 -15
- package/dist/tree-DAii2A1g.js.map +1 -0
- package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
- package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
- package/dist/tree.d.ts +22 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-CQSEVeOT.d.ts → vertical-list-DwtCPy1b.d.ts} +17 -14
- package/dist/vertical-list-DwtCPy1b.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +1 -1
- package/dist/vertical-list.js +59 -127
- package/dist/vertical-list.js.map +1 -1
- package/docs-user/data-grid.md +19 -0
- package/docs-user/detail-list.md +1 -1
- package/docs-user/grid-list.md +24 -0
- package/docs-user/incr-search.md +12 -2
- package/docs-user/index.json +2 -2
- package/docs-user/tree.md +22 -1
- package/docs-user/vertical-list.md +7 -1
- package/package.json +1 -1
- package/dist/data-grid-BddUjAti.d.ts.map +0 -1
- package/dist/detail-list-Br0WAnry.d.ts.map +0 -1
- package/dist/element-search-Y8SRfPGM.js +0 -185
- package/dist/element-search-Y8SRfPGM.js.map +0 -1
- package/dist/grid-list-B_orPsot.d.ts.map +0 -1
- package/dist/incr-search-vCzuao-2.js +0 -137
- package/dist/incr-search-vCzuao-2.js.map +0 -1
- package/dist/index-Cu5i8zZY.d.ts +0 -160
- package/dist/index-Cu5i8zZY.d.ts.map +0 -1
- package/dist/list-selection-controller-CUo-Hhcm.js.map +0 -1
- package/dist/tree-B74zdI60.js.map +0 -1
- package/dist/vertical-list-CQSEVeOT.d.ts.map +0 -1
package/dist/vertical-list.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { t as TickledController } from "./tickled-controller-C66DgJHl.js";
|
|
2
2
|
import { t as themeFallbacks } from "./fallbacks-DNI9xt0F.js";
|
|
3
3
|
import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
|
|
4
|
-
import {
|
|
5
|
-
import { i as keyForElement, t as ListSelectionController } from "./list-selection-controller-CUo-Hhcm.js";
|
|
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-DpbSJtgj.js";
|
|
6
5
|
import { n as animateListItemIn, r as animateListItemOut } from "./list-item-animation-DuiwgN3l.js";
|
|
6
|
+
import { t as ListSelectionController } from "./list-selection-controller-DTFI40li.js";
|
|
7
7
|
import { LitElement, css, html, render } from "lit";
|
|
8
|
-
import { property
|
|
8
|
+
import { property } from "lit/decorators.js";
|
|
9
|
+
import { createCommandRegistry } from "@clinth/ui-commands";
|
|
9
10
|
//#region src/vertical-list/vertical-list.ts
|
|
10
11
|
const ITEM_SELECTOR = `li, [data-list-item]`;
|
|
11
12
|
let VerticalListElement = class VerticalListElement extends LitElement {
|
|
12
|
-
constructor(...
|
|
13
|
-
super(...
|
|
13
|
+
constructor(..._args) {
|
|
14
|
+
super(..._args);
|
|
14
15
|
this.selectionMode = `single`;
|
|
15
16
|
this.interactionMode = `standard`;
|
|
16
17
|
this.itemWrap = `stop`;
|
|
17
|
-
this._searchVisible = false;
|
|
18
|
-
this._searchQuery = ``;
|
|
19
18
|
this.#customWrappers = /* @__PURE__ */ new Map();
|
|
20
19
|
this.#wrappedElements = /* @__PURE__ */ new Set();
|
|
21
20
|
this.#animatedItems = /* @__PURE__ */ new WeakSet();
|
|
@@ -47,6 +46,11 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
47
46
|
onStateChange: () => this._syncItemStates(),
|
|
48
47
|
itemWrap: this.itemWrap
|
|
49
48
|
});
|
|
49
|
+
this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
|
|
50
|
+
itemSelector: ITEM_SELECTOR,
|
|
51
|
+
getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
52
|
+
highlightKey: `vertical-list-search`
|
|
53
|
+
}) });
|
|
50
54
|
this._onPointerOver = (e) => {
|
|
51
55
|
const item = this._findListItem(e.composedPath());
|
|
52
56
|
if (item) this.tickled.handlePointerEnter(item);
|
|
@@ -55,11 +59,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
55
59
|
this.tickled.handlePointerLeave(e);
|
|
56
60
|
};
|
|
57
61
|
this._onKeyDown = (e) => {
|
|
58
|
-
if (
|
|
59
|
-
e.preventDefault();
|
|
60
|
-
this._openSearch();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
62
|
+
if (this.search.isOpen) return;
|
|
63
63
|
if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
|
|
64
64
|
if (this.selection.canSelectAll()) {
|
|
65
65
|
e.preventDefault();
|
|
@@ -67,7 +67,6 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
67
67
|
}
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
if (this._searchVisible) return;
|
|
71
70
|
switch (e.key) {
|
|
72
71
|
case `ArrowDown`:
|
|
73
72
|
e.preventDefault();
|
|
@@ -115,40 +114,31 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
115
114
|
const item = this._findListItem(e.composedPath());
|
|
116
115
|
if (item) this._activateItem(item);
|
|
117
116
|
};
|
|
118
|
-
this._onSearchInput = (e) => {
|
|
119
|
-
const input = e.target;
|
|
120
|
-
this._searchQuery = input.value;
|
|
121
|
-
this.#search?.setQuery(this._searchQuery);
|
|
122
|
-
};
|
|
123
|
-
this._onSearchKeydown = (e) => {
|
|
124
|
-
if (e.key === `Escape`) {
|
|
125
|
-
e.stopPropagation();
|
|
126
|
-
this._closeSearch();
|
|
127
|
-
} else if (e.key === `ArrowDown`) {
|
|
128
|
-
e.preventDefault();
|
|
129
|
-
this.tickled.handleKeyboardNavigation(`next`);
|
|
130
|
-
} else if (e.key === `ArrowUp`) {
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
this.tickled.handleKeyboardNavigation(`previous`);
|
|
133
|
-
} else if (e.key === `Enter`) {
|
|
134
|
-
e.preventDefault();
|
|
135
|
-
e.stopPropagation();
|
|
136
|
-
const id = this.tickled.tickledId ?? this._getVisibleItems()[0];
|
|
137
|
-
if (id) {
|
|
138
|
-
this._dispatch(`list-item-click`, { item: id });
|
|
139
|
-
this._activateItem(id);
|
|
140
|
-
this._closeSearch();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
117
|
}
|
|
145
|
-
#search;
|
|
146
118
|
#customWrappers;
|
|
147
119
|
#wrappedElements;
|
|
148
120
|
/** Watches for items added to the light DOM so they can be animated in. */
|
|
149
121
|
#itemObserver;
|
|
150
122
|
/** Items that have already played (or been skipped for) their enter animation. */
|
|
151
123
|
#animatedItems;
|
|
124
|
+
#searchRegistry;
|
|
125
|
+
#unregisterSearchCommands;
|
|
126
|
+
/** Command registry carrying this list's `list.search.*` commands. */
|
|
127
|
+
get registry() {
|
|
128
|
+
return this.#searchRegistry;
|
|
129
|
+
}
|
|
130
|
+
#effectiveSearchModes() {
|
|
131
|
+
const raw = this.searchModes?.trim();
|
|
132
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
133
|
+
const requested = new Set(raw.split(/\s+/));
|
|
134
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
135
|
+
}
|
|
136
|
+
#syncSearchCommands() {
|
|
137
|
+
if (!this.#searchRegistry) return;
|
|
138
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
139
|
+
this.#unregisterSearchCommands?.();
|
|
140
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
141
|
+
}
|
|
152
142
|
connectedCallback() {
|
|
153
143
|
super.connectedCallback();
|
|
154
144
|
if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
|
|
@@ -160,6 +150,8 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
160
150
|
for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
|
|
161
151
|
this.#itemObserver ??= new MutationObserver((records) => this.#onItemsMutated(records));
|
|
162
152
|
this.#itemObserver.observe(this, { childList: true });
|
|
153
|
+
this.#searchRegistry ??= createCommandRegistry();
|
|
154
|
+
this.#syncSearchCommands();
|
|
163
155
|
}
|
|
164
156
|
disconnectedCallback() {
|
|
165
157
|
super.disconnectedCallback();
|
|
@@ -169,8 +161,9 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
169
161
|
this.removeEventListener(`click`, this._onClick);
|
|
170
162
|
this.removeEventListener(`dblclick`, this._onDblClick);
|
|
171
163
|
this.#itemObserver?.disconnect();
|
|
172
|
-
this.#
|
|
173
|
-
this.#
|
|
164
|
+
this.#unregisterSearchCommands?.();
|
|
165
|
+
this.#unregisterSearchCommands = void 0;
|
|
166
|
+
this.#searchRegistry = void 0;
|
|
174
167
|
}
|
|
175
168
|
/**
|
|
176
169
|
* Animate in items appended to the light DOM (however they got there —
|
|
@@ -193,17 +186,9 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
193
186
|
if (fresh.length >= total) return;
|
|
194
187
|
for (const el of fresh) animateListItemIn(el);
|
|
195
188
|
}
|
|
196
|
-
firstUpdated() {
|
|
197
|
-
this.#search = createElementIncrSearch({
|
|
198
|
-
container: this,
|
|
199
|
-
itemSelector: ITEM_SELECTOR,
|
|
200
|
-
getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
201
|
-
getKey: (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
202
|
-
highlightKey: `vertical-list-search`
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
189
|
updated(changed) {
|
|
206
190
|
super.updated(changed);
|
|
191
|
+
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
207
192
|
if (changed.has(`interactionMode`)) {
|
|
208
193
|
this.selection.clearAnchor();
|
|
209
194
|
const previousSelected = this.selection.selected;
|
|
@@ -271,11 +256,13 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
271
256
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
272
257
|
}
|
|
273
258
|
/**
|
|
274
|
-
* Dispatch a command to this list
|
|
275
|
-
*
|
|
276
|
-
*
|
|
259
|
+
* Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
|
|
260
|
+
* forward commands to all sub-components. Routes through the command registry
|
|
261
|
+
* (so `list.search.*` and any other registered commands work).
|
|
277
262
|
*/
|
|
278
|
-
dispatch(
|
|
263
|
+
dispatch(commandId, args) {
|
|
264
|
+
this.#searchRegistry?.invoke(commandId, args);
|
|
265
|
+
}
|
|
279
266
|
select(item) {
|
|
280
267
|
this.selection.replace(item);
|
|
281
268
|
}
|
|
@@ -485,22 +472,15 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
485
472
|
composed: true
|
|
486
473
|
}));
|
|
487
474
|
}
|
|
488
|
-
_openSearch() {
|
|
489
|
-
this._searchVisible = true;
|
|
490
|
-
this.updateComplete.then(() => this._searchInputEl?.focus());
|
|
491
|
-
}
|
|
492
|
-
_closeSearch() {
|
|
493
|
-
this._searchVisible = false;
|
|
494
|
-
this._searchQuery = ``;
|
|
495
|
-
this.#search?.clearQuery();
|
|
496
|
-
this.focus();
|
|
497
|
-
}
|
|
498
475
|
_activateItem(item) {
|
|
499
476
|
if (this.interactionMode !== `manual`) this.selection.replace(item);
|
|
500
477
|
this._dispatch(`list-activate`, { item });
|
|
501
478
|
}
|
|
502
479
|
static {
|
|
503
|
-
this.styles = [
|
|
480
|
+
this.styles = [
|
|
481
|
+
themeFallbacks,
|
|
482
|
+
listSearchStyles,
|
|
483
|
+
css`
|
|
504
484
|
:host {
|
|
505
485
|
display: block;
|
|
506
486
|
position: relative;
|
|
@@ -551,6 +531,10 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
551
531
|
display: flex;
|
|
552
532
|
align-items: center;
|
|
553
533
|
padding: var(--ixfx-list-item-padding);
|
|
534
|
+
/* Inset each row from the scroll container's edges (margin, not padding:
|
|
535
|
+
padding is inside the border box, so the tickle/selection outlines
|
|
536
|
+
would still sit flush against the edge and be clipped). */
|
|
537
|
+
margin: 0 var(--list-gutter, 4px);
|
|
554
538
|
min-height: var(--list-item-height, 28px);
|
|
555
539
|
cursor: pointer;
|
|
556
540
|
user-select: none;
|
|
@@ -563,6 +547,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
563
547
|
/* Custom shadow-DOM components handle their own internal layout/padding */
|
|
564
548
|
::slotted([data-list-item]) {
|
|
565
549
|
display: block;
|
|
550
|
+
margin: 0 var(--list-gutter, 4px);
|
|
566
551
|
cursor: pointer;
|
|
567
552
|
user-select: none;
|
|
568
553
|
transition: background var(--transition, 0.1s ease-out),
|
|
@@ -618,49 +603,8 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
618
603
|
color: inherit;
|
|
619
604
|
outline: var(--item-border-tickled);
|
|
620
605
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
position: absolute;
|
|
624
|
-
top: 8px;
|
|
625
|
-
right: 8px;
|
|
626
|
-
z-index: 10;
|
|
627
|
-
display: flex;
|
|
628
|
-
align-items: center;
|
|
629
|
-
gap: 4px;
|
|
630
|
-
background: var(--surface-4, #2a2a2a);
|
|
631
|
-
border: 1px solid var(--border, #444);
|
|
632
|
-
border-radius: var(--radius-m, 4px);
|
|
633
|
-
padding: 4px 6px;
|
|
634
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
.search-overlay[hidden] {
|
|
638
|
-
display: none;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
.search-input {
|
|
642
|
-
background: transparent;
|
|
643
|
-
border: none;
|
|
644
|
-
outline: none;
|
|
645
|
-
color: inherit;
|
|
646
|
-
font-size: var(--text-m, 0.875em);
|
|
647
|
-
min-width: 140px;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
.search-close {
|
|
651
|
-
background: transparent;
|
|
652
|
-
border: none;
|
|
653
|
-
color: var(--surface-muted-text, #888);
|
|
654
|
-
cursor: pointer;
|
|
655
|
-
font-size: 1.1em;
|
|
656
|
-
line-height: 1;
|
|
657
|
-
padding: 0 2px;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
.search-close:hover {
|
|
661
|
-
color: inherit;
|
|
662
|
-
}
|
|
663
|
-
`];
|
|
606
|
+
`
|
|
607
|
+
];
|
|
664
608
|
}
|
|
665
609
|
render() {
|
|
666
610
|
return html`
|
|
@@ -668,21 +612,7 @@ let VerticalListElement = class VerticalListElement extends LitElement {
|
|
|
668
612
|
<ul part="list">
|
|
669
613
|
<slot></slot>
|
|
670
614
|
</ul>
|
|
671
|
-
|
|
672
|
-
class="search-overlay"
|
|
673
|
-
part="search-overlay"
|
|
674
|
-
?hidden=${!this._searchVisible}
|
|
675
|
-
>
|
|
676
|
-
<input
|
|
677
|
-
type="text"
|
|
678
|
-
class="search-input"
|
|
679
|
-
placeholder="Search…"
|
|
680
|
-
.value=${this._searchQuery}
|
|
681
|
-
@input=${this._onSearchInput}
|
|
682
|
-
@keydown=${this._onSearchKeydown}
|
|
683
|
-
/>
|
|
684
|
-
<button class="search-close" @click=${this._closeSearch} title="Close search (Esc)">×</button>
|
|
685
|
-
</div>
|
|
615
|
+
${this.search.renderField()}
|
|
686
616
|
</div>
|
|
687
617
|
`;
|
|
688
618
|
}
|
|
@@ -701,9 +631,11 @@ __decorate([property({
|
|
|
701
631
|
attribute: `item-wrap`
|
|
702
632
|
})], VerticalListElement.prototype, "itemWrap", void 0);
|
|
703
633
|
__decorate([property({ attribute: false })], VerticalListElement.prototype, "checkboxRenderer", void 0);
|
|
704
|
-
__decorate([
|
|
705
|
-
|
|
706
|
-
|
|
634
|
+
__decorate([property({
|
|
635
|
+
type: String,
|
|
636
|
+
attribute: `search-modes`,
|
|
637
|
+
reflect: true
|
|
638
|
+
})], VerticalListElement.prototype, "searchModes", void 0);
|
|
707
639
|
VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
|
|
708
640
|
//#endregion
|
|
709
641
|
export { VerticalListElement };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vertical-list.js","names":["#search","#animatedItems","#itemObserver","#onItemsMutated","#customWrappers","#wrappedElements","#itemForKey","customElement"],"sources":["../src/vertical-list/vertical-list.ts"],"sourcesContent":["import type { CommandId } from '@clinth/ui-commands';\nimport type { PropertyValues } from 'lit';\nimport type { ElementIncrSearch } from '../incr-search/element-search.js';\nimport type {\n ListActivateDetail,\n ListElementEventMap,\n ListInteractionMode,\n ListItemClickDetail,\n ListSelectDetail,\n ListSelectionMode,\n ListTickleDetail,\n} from '../interaction/list-selection-types.js';\nimport { css, html, LitElement, render } from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\nimport { createElementIncrSearch } from '../incr-search/element-search.js';\nimport { keyForElement } from '../interaction/keyed-list.js';\nimport { animateListItemIn, animateListItemOut } from '../interaction/list-item-animation.js';\nimport { ListSelectionController } from '../interaction/list-selection-controller.js';\nimport { TickledController } from '../interaction/tickled-controller.js';\nimport { themeFallbacks } from '../styles/fallbacks.js';\nimport { safeCustomElement as customElement } from '../util/custom-elements.js';\n\nexport type {\n ListActivateDetail,\n ListActivateEvent,\n ListElementEventMap,\n ListInteractionMode,\n ListItemClickDetail,\n ListItemClickEvent,\n ListSelectDetail,\n ListSelectEvent,\n ListSelectionMode,\n ListTickleDetail,\n ListTickleEvent,\n} from './types.js';\n\nexport interface CustomCheckboxOptions {\n renderer: (checked: boolean) => unknown;\n margin?: string;\n width?: string;\n}\n\nexport interface AddItemOptions {\n /**\n * In `checked` interaction mode, adds the item without a checkbox column and\n * without the ability to be checked/selected. Equivalent to setting the\n * `not-checked` attribute on the item. Ignored outside `checked` mode.\n */\n notChecked?: boolean;\n}\n\nconst ITEM_SELECTOR = `li, [data-list-item]`;\n\n@customElement(`ixfx-vertical-list`)\nexport class VerticalListElement extends LitElement {\n @property({ type: String, attribute: `selection-mode` })\n selectionMode: ListSelectionMode = `single`;\n\n @property({ type: String, attribute: `interaction-mode` })\n interactionMode: ListInteractionMode = `standard`;\n\n @property({ attribute: false })\n filterPredicate: ((el: Element) => boolean) | undefined;\n\n /**\n * Whether keyboard navigation wraps from last to first and vice versa.\n * - 'stop' (default): navigation stops at the boundary\n * - 'wrap': navigation wraps around\n */\n @property({ type: String, attribute: `item-wrap` })\n itemWrap: `stop` | `wrap` = `stop`;\n\n /**\n * Optional renderer for the checkbox shown in `checked` interactionMode.\n * Receives the current `checked` state and returns a Lit template result.\n *\n * When set, the per-row checkbox becomes a `<span role=\"checkbox\">` whose\n * contents are whatever this returns (rendered with `lit`'s `render()`).\n * When unset, a native `<input type=\"checkbox\">` is used.\n *\n * @example\n * list.checkboxRenderer = (checked) => checked\n * ? html`<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\"\n * fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\n * stroke-linecap=\"round\" stroke-linejoin=\"round\"\n * style=\"opacity:0.5\">\n * <polyline points=\"3,8 7,12 13,4\" />\n * </svg>`\n * : html``;\n */\n @property({ attribute: false })\n checkboxRenderer: ((checked: boolean) => unknown) | CustomCheckboxOptions | undefined;\n\n @state() private _searchVisible = false;\n @state() private _searchQuery = ``;\n\n @query(`.search-input`)\n private _searchInputEl!: HTMLInputElement;\n\n #search: ElementIncrSearch | undefined;\n #customWrappers = new Map<HTMLLIElement, Element>();\n #wrappedElements = new Set<Element>();\n\n /** Watches for items added to the light DOM so they can be animated in. */\n #itemObserver: MutationObserver | undefined;\n /** Items that have already played (or been skipped for) their enter animation. */\n #animatedItems = new WeakSet<Element>();\n\n private readonly selection = new ListSelectionController<Element>({\n getOrderedItems: () => this._getVisibleItems(),\n isSelectable: el => this._isCheckable(el),\n getModes: () => ({ selectionMode: this.selectionMode, interactionMode: this.interactionMode }),\n getKey: keyForElement,\n onChange: (selected, previous) => {\n this._syncItemStates();\n this._dispatch<ListSelectDetail>(`list-select`, {\n selected,\n previous,\n selectedKeys: this.selection.selectedKeys,\n });\n },\n });\n\n private tickled = new TickledController<Element>(this, {\n getItems: () => this._getVisibleItems().map(el => ({\n id: el,\n enabled: !el.hasAttribute(`not-checked`),\n })),\n onTickle: (id) => {\n if (id !== undefined) {\n this._dispatch<ListTickleDetail>(`list-tickle`, { item: id });\n }\n },\n onStateChange: () => this._syncItemStates(),\n itemWrap: this.itemWrap,\n });\n\n override connectedCallback(): void {\n super.connectedCallback();\n if (!this.hasAttribute(`tabindex`))\n this.tabIndex = 0;\n this.addEventListener(`pointerover`, this._onPointerOver);\n this.addEventListener(`pointerleave`, this._onPointerLeave);\n this.addEventListener(`keydown`, this._onKeyDown);\n this.addEventListener(`click`, this._onClick);\n this.addEventListener(`dblclick`, this._onDblClick);\n // Mark whatever's already here as \"known\" so the first populate doesn't\n // animate; anything added later collapses in.\n for (const el of this.querySelectorAll(ITEM_SELECTOR))\n this.#animatedItems.add(el);\n this.#itemObserver ??= new MutationObserver(records => this.#onItemsMutated(records));\n this.#itemObserver.observe(this, { childList: true });\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener(`pointerover`, this._onPointerOver);\n this.removeEventListener(`pointerleave`, this._onPointerLeave);\n this.removeEventListener(`keydown`, this._onKeyDown);\n this.removeEventListener(`click`, this._onClick);\n this.removeEventListener(`dblclick`, this._onDblClick);\n this.#itemObserver?.disconnect();\n this.#search?.dispose();\n this.#search = undefined;\n }\n\n /**\n * Animate in items appended to the light DOM (however they got there —\n * {@link addItem}, direct `appendChild`, a framework binding). A bulk populate\n * (roughly everything at once) is left alone, as are the wrapper `<li>`s the\n * checked-mode plumbing shuffles around.\n */\n #onItemsMutated(records: MutationRecord[]): void {\n if (!this.isConnected)\n return;\n const fresh: HTMLElement[] = [];\n for (const record of records) {\n for (const node of record.addedNodes) {\n if (!(node instanceof HTMLElement) || node.parentElement !== this)\n continue;\n if (!node.matches(ITEM_SELECTOR))\n continue;\n if (this.#animatedItems.has(node) || this.#customWrappers.has(node as HTMLLIElement) || this.#wrappedElements.has(node))\n continue;\n this.#animatedItems.add(node);\n fresh.push(node);\n }\n }\n if (fresh.length === 0)\n return;\n const total = this.querySelectorAll(ITEM_SELECTOR).length;\n if (fresh.length >= total)\n return; // first populate / wholesale replace — don't animate\n for (const el of fresh)\n animateListItemIn(el);\n }\n\n override firstUpdated(): void {\n this.#search = createElementIncrSearch({\n container: this,\n itemSelector: ITEM_SELECTOR,\n getLabel: el => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,\n getKey: el =>\n el.getAttribute(`data-key`)\n ?? el.getAttribute(`data-search-label`)\n ?? el.textContent?.trim()\n ?? ``,\n highlightKey: `vertical-list-search`,\n });\n }\n\n override updated(changed: PropertyValues): void {\n super.updated(changed);\n if (changed.has(`interactionMode`)) {\n this.selection.clearAnchor();\n const previousSelected = this.selection.selected;\n // Save wrapper→inner map before _syncCheckboxes() clears it on unwrap\n const leavingChecked = (changed.get(`interactionMode`) as ListInteractionMode) === `checked`;\n const savedWrappers = leavingChecked ? new Map(this.#customWrappers) : null;\n\n this._syncCheckboxes();\n\n // Remap the selection across the wrap/unwrap element-reference boundary\n if (previousSelected.size > 0) {\n if (savedWrappers && savedWrappers.size > 0) {\n // Leaving checked: wrapper refs → inner element refs\n this.selection.remap(el => savedWrappers.get(el as HTMLLIElement) ?? el);\n } else if (this.#customWrappers.size > 0) {\n // Entering checked: inner element refs → wrapper refs\n const innerToWrapper = new Map([...this.#customWrappers].map(([w, i]) => [i, w]));\n this.selection.remap(el => innerToWrapper.get(el) ?? el);\n }\n }\n\n this._syncItemStates();\n } else if (changed.has(`checkboxRenderer`) && this.interactionMode === `checked`) {\n this._syncCheckboxes();\n }\n }\n\n // --- Public API ---\n\n /** The most recently selected item, or `undefined` if nothing is selected. */\n get selectedItem(): Element | undefined {\n return [...this.selection.selected].at(-1);\n }\n\n /** The full current selection set (read-only). */\n get selectedItems(): ReadonlySet<Element> {\n return this.selection.selected;\n }\n\n addItem(item: Element | string, options?: AddItemOptions): void {\n let el: Element;\n if (typeof item === `string`) {\n const li = document.createElement(`li`);\n li.textContent = item;\n el = li;\n } else {\n el = item;\n }\n if (options?.notChecked)\n el.setAttribute(`not-checked`, ``);\n this.appendChild(el);\n if (this.interactionMode === `checked`)\n this._syncCheckboxes();\n }\n\n /**\n * Collapse + fade `item` out, then detach it. The rows below slide up to\n * close the gap. Resolves once the element has actually been removed.\n * Under `prefers-reduced-motion: reduce` it's removed immediately.\n */\n removeItem(item: Element): Promise<void> {\n this.selection.deselect(item);\n const done = animateListItemOut(item as HTMLElement).then(() => {\n item.remove();\n if (this.interactionMode === `checked`)\n this._syncCheckboxes();\n });\n return done;\n }\n\n /** {@link removeItem} for several items at once. */\n removeItems(items: Iterable<Element>): Promise<void> {\n return Promise.all([...items].map(item => this.removeItem(item))).then(() => undefined);\n }\n\n clearItems(): void {\n this.#customWrappers.clear();\n this.replaceChildren();\n this.selection.reset();\n this._syncItemStates();\n }\n\n /**\n * Replace the entire item list in one call.\n * Equivalent to `clearItems()` followed by `addItem()` for each element.\n * Preserves `interactionMode` checkbox wrapping.\n */\n setItems(items: readonly Element[]): void {\n this.clearItems();\n for (const item of items) {\n this.appendChild(item);\n }\n if (this.interactionMode === `checked`)\n this._syncCheckboxes();\n }\n\n /**\n * Dispatch a command to this list. No-op by default.\n * Intended for use with `ixfx-grouped-item-lister`'s `dispatch()` method,\n * which forwards commands to all sub-components.\n */\n dispatch(_commandId: CommandId, _args?: Record<string, unknown>): void {}\n\n select(item: Element): void {\n this.selection.replace(item);\n }\n\n selectMany(items: Iterable<Element>): void {\n this.selection.selectMany(items);\n }\n\n deselect(item: Element): void {\n this.selection.deselect(item);\n }\n\n clearSelection(): void {\n this.selection.clear();\n }\n\n selectAll(): void {\n if (this.selection.canSelectAll())\n this.selection.selectAll();\n }\n\n // --- Key-based selection & cursor ---\n\n /** The selection as stable `data-key` / `data-value` strings. */\n get selectedKeys(): ReadonlySet<string> {\n return this.selection.selectedKeys;\n }\n\n /** The key of the item under the cursor, or `undefined`. */\n get tickledKey(): string | undefined {\n const id = this.tickled.tickledId;\n return id ? keyForElement(id) : undefined;\n }\n\n /** Replace the selection with the items carrying these keys (unknown keys ignored). */\n selectKeys(keys: Iterable<string>): void {\n this.selection.selectKeys(keys, key => this.#itemForKey(key));\n }\n\n /** Add items by key to the selection (multi-select modes only). */\n addSelectedKeys(keys: Iterable<string>): void {\n this.selection.addKeys(keys, key => this.#itemForKey(key));\n }\n\n /** Remove items by key from the selection. */\n removeSelectedKeys(keys: Iterable<string>): void {\n this.selection.removeKeys(keys, key => this.#itemForKey(key));\n }\n\n /** Move the cursor to the item with this key and scroll it into view. */\n focusKey(key: string): void {\n const el = this.#itemForKey(key);\n if (!el)\n return;\n this.tickled.focus(el);\n el.scrollIntoView({ block: `nearest`, inline: `nearest` });\n }\n\n #itemForKey(key: string): Element | undefined {\n for (const el of this.querySelectorAll(ITEM_SELECTOR)) {\n if (this.#wrappedElements.has(el))\n continue;\n if (keyForElement(el) === key)\n return el;\n }\n return undefined;\n }\n\n * selectedFilter(kind: `selected` | `checked` | `either`): Generator<Element> {\n const wanted = (el: Element) => {\n switch (kind) {\n case `selected`: return el.hasAttribute(`data-selected`);\n case `checked`: return el.hasAttribute(`data-checked`);\n case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);\n }\n };\n\n for (const el of this.querySelectorAll(ITEM_SELECTOR)) {\n if (this.#wrappedElements.has(el))\n continue;\n if ((el as HTMLElement).hidden)\n continue;\n if (wanted(el))\n yield el;\n }\n }\n\n // --- Helpers ---\n\n private _getVisibleItems(): Element[] {\n return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {\n if (this.#wrappedElements.has(el))\n return false;\n if ((el as HTMLElement).hidden)\n return false;\n if (this.filterPredicate && !this.filterPredicate(el))\n return false;\n return true;\n });\n }\n\n // Whether an item participates in checked-mode selection. Items carrying the\n // `not-checked` attribute are skipped; outside `checked` mode it has no effect.\n // For wrapped custom elements the flag lives on the inner element, so resolve\n // through the wrapper map.\n private _isCheckable(item: Element): boolean {\n if (this.interactionMode !== `checked`)\n return true;\n const real = this.#customWrappers.get(item as HTMLLIElement) ?? item;\n return !real.hasAttribute(`not-checked`);\n }\n\n private _syncItemStates(): void {\n for (const el of this.querySelectorAll(ITEM_SELECTOR)) {\n if (this.#wrappedElements.has(el))\n continue;\n const item = this.#customWrappers.get(el as HTMLLIElement) ?? el;\n el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));\n el.toggleAttribute(`data-selected`, this.selection.selected.has(el));\n if (this.interactionMode === `checked`) {\n el.toggleAttribute(`data-checked`, !item.hasAttribute(`not-checked`) && this.selection.selected.has(el));\n } else {\n el.removeAttribute(`data-checked`);\n }\n }\n this.dataset.inputMode = this.tickled.inputMode;\n this._syncCheckboxes();\n }\n\n private _syncCheckboxes(): void {\n const inCheckedMode = this.interactionMode === `checked`;\n const wantsCustom = typeof this.checkboxRenderer === `function`;\n\n if (inCheckedMode) {\n for (const inner of Array.from(this.children)) {\n if (!inner.hasAttribute(`data-list-item`))\n continue;\n if (inner.parentElement !== this)\n continue; // idempotency: already wrapped\n const wrapper = document.createElement(`li`);\n wrapper.className = `ixfx-custom-row`;\n const key = inner.getAttribute(`data-key`);\n const label = inner.getAttribute(`data-search-label`);\n if (key !== null)\n wrapper.setAttribute(`data-key`, key);\n if (label !== null)\n wrapper.setAttribute(`data-search-label`, label);\n inner.removeAttribute(`data-list-item`);\n this.insertBefore(wrapper, inner);\n wrapper.appendChild(inner);\n this.#customWrappers.set(wrapper, inner);\n this.#wrappedElements.add(inner);\n }\n } else {\n for (const [wrapper, inner] of this.#customWrappers) {\n if (wrapper.parentElement !== this)\n continue;\n inner.setAttribute(`data-list-item`, ``);\n this.insertBefore(inner, wrapper);\n wrapper.remove();\n }\n this.#customWrappers.clear();\n this.#wrappedElements.clear();\n }\n\n for (const el of this.querySelectorAll<HTMLElement>(`li`)) {\n const existing = el.querySelector<HTMLElement>(`.ixfx-list-checkbox`);\n if (!inCheckedMode) {\n if (existing)\n existing.remove();\n continue;\n }\n // Rows marked `not-checked` get no checkbox column and can't be checked\n const itemEl = this.#customWrappers.get(el as HTMLLIElement) ?? el;\n if (itemEl.hasAttribute(`not-checked`)) {\n if (existing)\n existing.remove();\n continue;\n }\n\n // In checked mode: if the existing checkbox is the wrong variant, drop it.\n const existingIsCustom = existing?.classList.contains(`ixfx-list-checkbox-custom`) ?? false;\n if (existing && existingIsCustom !== wantsCustom)\n existing.remove();\n\n let cb: HTMLElement | null = el.querySelector<HTMLElement>(`.ixfx-list-checkbox`);\n if (!cb) {\n if (wantsCustom) {\n const opts = this.checkboxRenderer as CustomCheckboxOptions;\n const margin = opts.margin ?? `0`;\n const width = opts.width ?? `24px`;\n cb = this._createCustomCheckbox(el, margin, width);\n } else {\n cb = this._createNativeCheckboxCell(el);\n }\n el.prepend(cb);\n }\n\n const checked = this.selection.selected.has(el);\n if (wantsCustom) {\n cb.setAttribute(`aria-checked`, String(checked));\n cb.toggleAttribute(`data-checked`, checked);\n if (this.checkboxRenderer) {\n const renderer = typeof this.checkboxRenderer === `function`\n ? this.checkboxRenderer\n : (this.checkboxRenderer as CustomCheckboxOptions).renderer;\n render(renderer(checked), cb);\n }\n } else {\n const input = cb.querySelector<HTMLInputElement>(`input.ixfx-list-checkbox`);\n if (input)\n input.checked = checked;\n }\n }\n }\n\n /**\n * Native checkbox cell: a span that stretches to the row's full cross-axis\n * (so the entire column height is a click target) with a real `<input>`\n * centred inside. The span owns the click; the input is purely visual and\n * has `pointer-events:none` so a single click produces one toggle.\n */\n private _createNativeCheckboxCell(el: HTMLElement): HTMLSpanElement {\n const cell = document.createElement(`span`);\n cell.className = `ixfx-list-checkbox ixfx-list-checkbox-native`;\n cell.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:18px;margin:0 6px 0 0;cursor:pointer;pointer-events:auto`;\n cell.tabIndex = -1;\n cell.setAttribute(`role`, `checkbox`);\n cell.setAttribute(`part`, `checkbox`);\n cell.addEventListener(`click`, (e) => {\n e.stopPropagation();\n this.selection.toggle(el);\n });\n const input = document.createElement(`input`);\n input.type = `checkbox`;\n input.className = `ixfx-list-checkbox`;\n input.style.cssText = `pointer-events:none;cursor:pointer;margin:0`;\n cell.appendChild(input);\n return cell;\n }\n\n /**\n * Custom checkbox cell: same shape as the native cell (a stretching span)\n * so behaviour is identical. The renderer's content goes inside.\n */\n private _createCustomCheckbox(el: HTMLElement, margin: string, width: string): HTMLSpanElement {\n const span = document.createElement(`span`);\n span.className = `ixfx-list-checkbox ixfx-list-checkbox-custom`;\n span.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:${width};margin:${margin};cursor:pointer;pointer-events:auto`;\n span.tabIndex = -1;\n span.setAttribute(`role`, `checkbox`);\n span.setAttribute(`part`, `checkbox`);\n span.addEventListener(`click`, (e) => {\n e.stopPropagation();\n this.selection.toggle(el);\n });\n return span;\n }\n\n private _findListItem(path: EventTarget[]): Element | undefined {\n for (const target of path) {\n if (!(target instanceof Element))\n continue;\n if (target === this)\n break;\n if (target.parentElement === this && target.matches(ITEM_SELECTOR))\n return target;\n }\n return undefined;\n }\n\n private _dispatch<T>(name: keyof ListElementEventMap, detail: T): void {\n this.dispatchEvent(new CustomEvent<T>(name as string, {\n detail,\n bubbles: true,\n composed: true,\n }));\n }\n\n private _openSearch(): void {\n this._searchVisible = true;\n void this.updateComplete.then(() => this._searchInputEl?.focus());\n }\n\n private _closeSearch(): void {\n this._searchVisible = false;\n this._searchQuery = ``;\n void this.#search?.clearQuery();\n this.focus();\n }\n\n // --- Event handlers ---\n\n private _onPointerOver = (e: PointerEvent): void => {\n const item = this._findListItem(e.composedPath());\n if (item)\n this.tickled.handlePointerEnter(item);\n };\n\n private _onPointerLeave = (e: PointerEvent): void => {\n this.tickled.handlePointerLeave(e);\n };\n\n private _onKeyDown = (e: KeyboardEvent): void => {\n if ((e.ctrlKey || e.metaKey) && e.key === `f`) {\n e.preventDefault();\n this._openSearch();\n return;\n }\n if ((e.ctrlKey || e.metaKey) && e.key === `a`) {\n if (this.selection.canSelectAll()) {\n e.preventDefault();\n this.selection.selectAll();\n }\n return;\n }\n if (this._searchVisible)\n return;\n\n switch (e.key) {\n case `ArrowDown`: {\n e.preventDefault();\n this.tickled.handleKeyboardNavigation(`next`);\n // vscode: Shift+Arrow extends range from anchor\n if (e.shiftKey && this.interactionMode === `vscode`) {\n const cur = this.tickled.tickledId;\n if (cur)\n this.selection.rangeTo(cur);\n }\n break;\n }\n case `ArrowUp`: {\n e.preventDefault();\n this.tickled.handleKeyboardNavigation(`previous`);\n if (e.shiftKey && this.interactionMode === `vscode`) {\n const cur = this.tickled.tickledId;\n if (cur)\n this.selection.rangeTo(cur);\n }\n break;\n }\n case `Enter`: {\n const id = this.tickled.tickledId;\n if (id) {\n e.preventDefault();\n this._dispatch<ListItemClickDetail>(`list-item-click`, { item: id });\n this._activateItem(id);\n }\n break;\n }\n case ` `: {\n const id = this.tickled.tickledId;\n if (id) {\n e.preventDefault();\n this.selection.handleSpace(id);\n }\n break;\n }\n case `Escape`:\n this.selection.clear();\n break;\n }\n };\n\n private _onClick = (e: MouseEvent): void => {\n const item = this._findListItem(e.composedPath());\n if (!item)\n return;\n this._dispatch<ListItemClickDetail>(`list-item-click`, { item });\n this.selection.handleClick(item, e);\n };\n\n private _onDblClick = (e: MouseEvent): void => {\n const item = this._findListItem(e.composedPath());\n if (item)\n this._activateItem(item);\n };\n\n private _activateItem(item: Element): void {\n // manual mode: never auto-select; just fire the event\n if (this.interactionMode !== `manual`) {\n this.selection.replace(item);\n }\n this._dispatch<ListActivateDetail>(`list-activate`, { item });\n }\n\n private _onSearchInput = (e: Event): void => {\n const input = e.target as HTMLInputElement;\n this._searchQuery = input.value;\n void this.#search?.setQuery(this._searchQuery);\n };\n\n private _onSearchKeydown = (e: KeyboardEvent): void => {\n if (e.key === `Escape`) {\n e.stopPropagation();\n this._closeSearch();\n } else if (e.key === `ArrowDown`) {\n e.preventDefault();\n this.tickled.handleKeyboardNavigation(`next`);\n } else if (e.key === `ArrowUp`) {\n e.preventDefault();\n this.tickled.handleKeyboardNavigation(`previous`);\n } else if (e.key === `Enter`) {\n e.preventDefault();\n e.stopPropagation();\n const id = this.tickled.tickledId ?? this._getVisibleItems()[0];\n if (id) {\n this._dispatch<ListItemClickDetail>(`list-item-click`, { item: id });\n this._activateItem(id);\n this._closeSearch();\n }\n }\n };\n\n // --- Styles ---\n\n static override styles = [themeFallbacks, css`\n :host {\n display: block;\n position: relative;\n background: var(--list-bg, var(--surface-3));\n color: var(--list-text, var(--surface-3-text));\n border-radius: var(--list-radius, var(--radius-s));\n border: 1px solid var(--list-border, var(--border));\n width: 100%;\n height: 100%;\n overflow: hidden;\n box-sizing: border-box;\n outline: none;\n --ixfx-list-item-padding: var(--ixfx-list-item-padding, var(--list-item-padding, 4px var(--space-m, 8px)));\n }\n\n :host(:focus-visible) {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n\n .container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n box-sizing: border-box;\n /* Scrollbar theming: end users override these CSS variables to control\n the native scrollbar (scrollbar-width, scrollbar-gutter, scrollbar-color). */\n scrollbar-width: var(--list-scrollbar-width, auto);\n scrollbar-gutter: var(--list-scrollbar-gutter, auto);\n scrollbar-color: var(--list-scrollbar-color, auto);\n }\n\n ::slotted([hidden]) {\n display: none !important;\n }\n\n ::slotted(li) {\n display: flex;\n align-items: center;\n padding: var(--ixfx-list-item-padding);\n min-height: var(--list-item-height, 28px);\n cursor: pointer;\n user-select: none;\n transition: background var(--transition, 0.1s ease-out),\n outline-color var(--transition, 0.1s ease-out);\n box-sizing: border-box;\n outline: 1px solid transparent;\n }\n\n /* Custom shadow-DOM components handle their own internal layout/padding */\n ::slotted([data-list-item]) {\n display: block;\n cursor: pointer;\n user-select: none;\n transition: background var(--transition, 0.1s ease-out),\n outline-color var(--transition, 0.1s ease-out);\n outline: 1px solid transparent;\n }\n\n /* In checked mode, only the checkbox column is clickable; the row body\n gets a default cursor so it doesn't suggest otherwise. */\n :host([interaction-mode=\"checked\"]) ::slotted(li),\n :host([interaction-mode=\"checked\"]) ::slotted([data-list-item]) {\n cursor: default;\n }\n\n /* Wrapper inserted around custom [data-list-item] elements in checked mode.\n Provides the flex layout so the injected checkbox can stretch to the\n row's full height via align-self:stretch. */\n .ixfx-custom-row {\n display: flex;\n align-items: center;\n min-height: var(--list-item-height, 28px);\n box-sizing: border-box;\n cursor: default;\n user-select: none;\n }\n\n ::slotted([data-tickled]) {\n background: var(--item-bg-tickled);\n outline: var(--item-border-tickled);\n }\n\n ::slotted([data-selected]) {\n background: var(--item-bg-selected);\n color: var(--item-text-selected);\n outline: 1px solid transparent;\n }\n\n ::slotted([data-selected][data-tickled]) {\n background: var(--item-bg-selected-tickled);\n outline: 2px dashed var(--surface-3);\n outline-offset: -2px;\n }\n\n /* checked mode: checkbox (injected into li) communicates selection — suppress bg highlight */\n :host([interaction-mode=\"checked\"]) ::slotted([data-selected]) {\n background: transparent;\n color: inherit;\n outline: 1px solid transparent;\n }\n\n :host([interaction-mode=\"checked\"]) ::slotted([data-selected][data-tickled]) {\n background: var(--item-bg-tickled);\n color: inherit;\n outline: var(--item-border-tickled);\n }\n\n .search-overlay {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 10;\n display: flex;\n align-items: center;\n gap: 4px;\n background: var(--surface-4, #2a2a2a);\n border: 1px solid var(--border, #444);\n border-radius: var(--radius-m, 4px);\n padding: 4px 6px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n }\n\n .search-overlay[hidden] {\n display: none;\n }\n\n .search-input {\n background: transparent;\n border: none;\n outline: none;\n color: inherit;\n font-size: var(--text-m, 0.875em);\n min-width: 140px;\n }\n\n .search-close {\n background: transparent;\n border: none;\n color: var(--surface-muted-text, #888);\n cursor: pointer;\n font-size: 1.1em;\n line-height: 1;\n padding: 0 2px;\n }\n\n .search-close:hover {\n color: inherit;\n }\n `];\n\n // --- Render ---\n\n override render() {\n return html`\n <div class=\"container\">\n <ul part=\"list\">\n <slot></slot>\n </ul>\n <div\n class=\"search-overlay\"\n part=\"search-overlay\"\n ?hidden=${!this._searchVisible}\n >\n <input\n type=\"text\"\n class=\"search-input\"\n placeholder=\"Search…\"\n .value=${this._searchQuery}\n @input=${this._onSearchInput}\n @keydown=${this._onSearchKeydown}\n />\n <button class=\"search-close\" @click=${this._closeSearch} title=\"Close search (Esc)\">×</button>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ixfx-vertical-list': VerticalListElement;\n }\n\n interface HTMLElementEventMap extends ListElementEventMap {}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vertical-list.js","names":["#searchRegistry","#effectiveSearchModes","#unregisterSearchCommands","#animatedItems","#itemObserver","#onItemsMutated","#syncSearchCommands","#customWrappers","#wrappedElements","#itemForKey","customElement"],"sources":["../src/vertical-list/vertical-list.ts"],"sourcesContent":["import type { CommandId, CommandRegistry } from '@clinth/ui-commands';\nimport type { PropertyValues } from 'lit';\nimport type { ListSearchMode } from '../incr-search/index.js';\nimport type {\n ListActivateDetail,\n ListElementEventMap,\n ListInteractionMode,\n ListItemClickDetail,\n ListSelectDetail,\n ListSelectionMode,\n ListTickleDetail,\n} from '../interaction/list-selection-types.js';\nimport { createCommandRegistry } from '@clinth/ui-commands';\nimport { css, html, LitElement, render } from 'lit';\nimport { property } from 'lit/decorators.js';\nimport {\n createElementListSearchAdapter,\n LIST_SEARCH_MODES,\n ListSearchController,\n listSearchStyles,\n registerListSearchCommands,\n} from '../incr-search/index.js';\nimport { keyForElement } from '../interaction/keyed-list.js';\nimport { animateListItemIn, animateListItemOut } from '../interaction/list-item-animation.js';\nimport { ListSelectionController } from '../interaction/list-selection-controller.js';\nimport { TickledController } from '../interaction/tickled-controller.js';\nimport { themeFallbacks } from '../styles/fallbacks.js';\nimport { safeCustomElement as customElement } from '../util/custom-elements.js';\n\nexport type {\n ListActivateDetail,\n ListActivateEvent,\n ListElementEventMap,\n ListInteractionMode,\n ListItemClickDetail,\n ListItemClickEvent,\n ListSelectDetail,\n ListSelectEvent,\n ListSelectionMode,\n ListTickleDetail,\n ListTickleEvent,\n} from './types.js';\n\nexport interface CustomCheckboxOptions {\n renderer: (checked: boolean) => unknown;\n margin?: string;\n width?: string;\n}\n\nexport interface AddItemOptions {\n /**\n * In `checked` interaction mode, adds the item without a checkbox column and\n * without the ability to be checked/selected. Equivalent to setting the\n * `not-checked` attribute on the item. Ignored outside `checked` mode.\n */\n notChecked?: boolean;\n}\n\nconst ITEM_SELECTOR = `li, [data-list-item]`;\n\n@customElement(`ixfx-vertical-list`)\nexport class VerticalListElement extends LitElement {\n @property({ type: String, attribute: `selection-mode` })\n selectionMode: ListSelectionMode = `single`;\n\n @property({ type: String, attribute: `interaction-mode` })\n interactionMode: ListInteractionMode = `standard`;\n\n @property({ attribute: false })\n filterPredicate: ((el: Element) => boolean) | undefined;\n\n /**\n * Whether keyboard navigation wraps from last to first and vice versa.\n * - 'stop' (default): navigation stops at the boundary\n * - 'wrap': navigation wraps around\n */\n @property({ type: String, attribute: `item-wrap` })\n itemWrap: `stop` | `wrap` = `stop`;\n\n /**\n * Optional renderer for the checkbox shown in `checked` interactionMode.\n * Receives the current `checked` state and returns a Lit template result.\n *\n * When set, the per-row checkbox becomes a `<span role=\"checkbox\">` whose\n * contents are whatever this returns (rendered with `lit`'s `render()`).\n * When unset, a native `<input type=\"checkbox\">` is used.\n *\n * @example\n * list.checkboxRenderer = (checked) => checked\n * ? html`<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\"\n * fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\n * stroke-linecap=\"round\" stroke-linejoin=\"round\"\n * style=\"opacity:0.5\">\n * <polyline points=\"3,8 7,12 13,4\" />\n * </svg>`\n * : html``;\n */\n @property({ attribute: false })\n checkboxRenderer: ((checked: boolean) => unknown) | CustomCheckboxOptions | undefined;\n\n /**\n * Space-separated subset of `navigate select filter` — limits which\n * incremental-search modes are offered. Unset ⇒ all three; empty ⇒ search\n * disabled. See `docs/infra-incremental.md`.\n */\n @property({ type: String, attribute: `search-modes`, reflect: true })\n searchModes: string | undefined;\n\n #customWrappers = new Map<HTMLLIElement, Element>();\n #wrappedElements = new Set<Element>();\n\n /** Watches for items added to the light DOM so they can be animated in. */\n #itemObserver: MutationObserver | undefined;\n /** Items that have already played (or been skipped for) their enter animation. */\n #animatedItems = new WeakSet<Element>();\n\n private readonly selection = new ListSelectionController<Element>({\n getOrderedItems: () => this._getVisibleItems(),\n isSelectable: el => this._isCheckable(el),\n getModes: () => ({ selectionMode: this.selectionMode, interactionMode: this.interactionMode }),\n getKey: keyForElement,\n onChange: (selected, previous) => {\n this._syncItemStates();\n this._dispatch<ListSelectDetail>(`list-select`, {\n selected,\n previous,\n selectedKeys: this.selection.selectedKeys,\n });\n },\n });\n\n private tickled = new TickledController<Element>(this, {\n getItems: () => this._getVisibleItems().map(el => ({\n id: el,\n enabled: !el.hasAttribute(`not-checked`),\n })),\n onTickle: (id) => {\n if (id !== undefined) {\n this._dispatch<ListTickleDetail>(`list-tickle`, { item: id });\n }\n },\n onStateChange: () => this._syncItemStates(),\n itemWrap: this.itemWrap,\n });\n\n /** Incremental search (navigate / select / filter). See `docs/infra-incremental.md`. */\n readonly search = new ListSearchController<Element>(this, {\n adapter: createElementListSearchAdapter(this, {\n itemSelector: ITEM_SELECTOR,\n getLabel: el => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,\n highlightKey: `vertical-list-search`,\n }),\n });\n\n #searchRegistry: CommandRegistry<unknown> | undefined;\n #unregisterSearchCommands: (() => void) | undefined;\n\n /** Command registry carrying this list's `list.search.*` commands. */\n get registry(): CommandRegistry<unknown> | undefined {\n return this.#searchRegistry;\n }\n\n #effectiveSearchModes(): ListSearchMode[] {\n const raw = this.searchModes?.trim();\n if (raw === undefined)\n return [...LIST_SEARCH_MODES];\n const requested = new Set(raw.split(/\\s+/));\n return LIST_SEARCH_MODES.filter(m => requested.has(m));\n }\n\n #syncSearchCommands(): void {\n if (!this.#searchRegistry)\n return;\n this.search.setModes(this.#effectiveSearchModes());\n this.#unregisterSearchCommands?.();\n this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n if (!this.hasAttribute(`tabindex`))\n this.tabIndex = 0;\n this.addEventListener(`pointerover`, this._onPointerOver);\n this.addEventListener(`pointerleave`, this._onPointerLeave);\n this.addEventListener(`keydown`, this._onKeyDown);\n this.addEventListener(`click`, this._onClick);\n this.addEventListener(`dblclick`, this._onDblClick);\n // Mark whatever's already here as \"known\" so the first populate doesn't\n // animate; anything added later collapses in.\n for (const el of this.querySelectorAll(ITEM_SELECTOR))\n this.#animatedItems.add(el);\n this.#itemObserver ??= new MutationObserver(records => this.#onItemsMutated(records));\n this.#itemObserver.observe(this, { childList: true });\n this.#searchRegistry ??= createCommandRegistry();\n this.#syncSearchCommands();\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener(`pointerover`, this._onPointerOver);\n this.removeEventListener(`pointerleave`, this._onPointerLeave);\n this.removeEventListener(`keydown`, this._onKeyDown);\n this.removeEventListener(`click`, this._onClick);\n this.removeEventListener(`dblclick`, this._onDblClick);\n this.#itemObserver?.disconnect();\n this.#unregisterSearchCommands?.();\n this.#unregisterSearchCommands = undefined;\n this.#searchRegistry = undefined;\n }\n\n /**\n * Animate in items appended to the light DOM (however they got there —\n * {@link addItem}, direct `appendChild`, a framework binding). A bulk populate\n * (roughly everything at once) is left alone, as are the wrapper `<li>`s the\n * checked-mode plumbing shuffles around.\n */\n #onItemsMutated(records: MutationRecord[]): void {\n if (!this.isConnected)\n return;\n const fresh: HTMLElement[] = [];\n for (const record of records) {\n for (const node of record.addedNodes) {\n if (!(node instanceof HTMLElement) || node.parentElement !== this)\n continue;\n if (!node.matches(ITEM_SELECTOR))\n continue;\n if (this.#animatedItems.has(node) || this.#customWrappers.has(node as HTMLLIElement) || this.#wrappedElements.has(node))\n continue;\n this.#animatedItems.add(node);\n fresh.push(node);\n }\n }\n if (fresh.length === 0)\n return;\n const total = this.querySelectorAll(ITEM_SELECTOR).length;\n if (fresh.length >= total)\n return; // first populate / wholesale replace — don't animate\n for (const el of fresh)\n animateListItemIn(el);\n }\n\n override updated(changed: PropertyValues): void {\n super.updated(changed);\n if (changed.has(`searchModes`)) {\n this.#syncSearchCommands();\n }\n if (changed.has(`interactionMode`)) {\n this.selection.clearAnchor();\n const previousSelected = this.selection.selected;\n // Save wrapper→inner map before _syncCheckboxes() clears it on unwrap\n const leavingChecked = (changed.get(`interactionMode`) as ListInteractionMode) === `checked`;\n const savedWrappers = leavingChecked ? new Map(this.#customWrappers) : null;\n\n this._syncCheckboxes();\n\n // Remap the selection across the wrap/unwrap element-reference boundary\n if (previousSelected.size > 0) {\n if (savedWrappers && savedWrappers.size > 0) {\n // Leaving checked: wrapper refs → inner element refs\n this.selection.remap(el => savedWrappers.get(el as HTMLLIElement) ?? el);\n } else if (this.#customWrappers.size > 0) {\n // Entering checked: inner element refs → wrapper refs\n const innerToWrapper = new Map([...this.#customWrappers].map(([w, i]) => [i, w]));\n this.selection.remap(el => innerToWrapper.get(el) ?? el);\n }\n }\n\n this._syncItemStates();\n } else if (changed.has(`checkboxRenderer`) && this.interactionMode === `checked`) {\n this._syncCheckboxes();\n }\n }\n\n // --- Public API ---\n\n /** The most recently selected item, or `undefined` if nothing is selected. */\n get selectedItem(): Element | undefined {\n return [...this.selection.selected].at(-1);\n }\n\n /** The full current selection set (read-only). */\n get selectedItems(): ReadonlySet<Element> {\n return this.selection.selected;\n }\n\n addItem(item: Element | string, options?: AddItemOptions): void {\n let el: Element;\n if (typeof item === `string`) {\n const li = document.createElement(`li`);\n li.textContent = item;\n el = li;\n } else {\n el = item;\n }\n if (options?.notChecked)\n el.setAttribute(`not-checked`, ``);\n this.appendChild(el);\n if (this.interactionMode === `checked`)\n this._syncCheckboxes();\n }\n\n /**\n * Collapse + fade `item` out, then detach it. The rows below slide up to\n * close the gap. Resolves once the element has actually been removed.\n * Under `prefers-reduced-motion: reduce` it's removed immediately.\n */\n removeItem(item: Element): Promise<void> {\n this.selection.deselect(item);\n const done = animateListItemOut(item as HTMLElement).then(() => {\n item.remove();\n if (this.interactionMode === `checked`)\n this._syncCheckboxes();\n });\n return done;\n }\n\n /** {@link removeItem} for several items at once. */\n removeItems(items: Iterable<Element>): Promise<void> {\n return Promise.all([...items].map(item => this.removeItem(item))).then(() => undefined);\n }\n\n clearItems(): void {\n this.#customWrappers.clear();\n this.replaceChildren();\n this.selection.reset();\n this._syncItemStates();\n }\n\n /**\n * Replace the entire item list in one call.\n * Equivalent to `clearItems()` followed by `addItem()` for each element.\n * Preserves `interactionMode` checkbox wrapping.\n */\n setItems(items: readonly Element[]): void {\n this.clearItems();\n for (const item of items) {\n this.appendChild(item);\n }\n if (this.interactionMode === `checked`)\n this._syncCheckboxes();\n }\n\n /**\n * Dispatch a command to this list — used by `ixfx-grouped-item-lister` to\n * forward commands to all sub-components. Routes through the command registry\n * (so `list.search.*` and any other registered commands work).\n */\n dispatch(commandId: CommandId, args?: Record<string, unknown>): void {\n this.#searchRegistry?.invoke(commandId, args);\n }\n\n select(item: Element): void {\n this.selection.replace(item);\n }\n\n selectMany(items: Iterable<Element>): void {\n this.selection.selectMany(items);\n }\n\n deselect(item: Element): void {\n this.selection.deselect(item);\n }\n\n clearSelection(): void {\n this.selection.clear();\n }\n\n selectAll(): void {\n if (this.selection.canSelectAll())\n this.selection.selectAll();\n }\n\n // --- Key-based selection & cursor ---\n\n /** The selection as stable `data-key` / `data-value` strings. */\n get selectedKeys(): ReadonlySet<string> {\n return this.selection.selectedKeys;\n }\n\n /** The key of the item under the cursor, or `undefined`. */\n get tickledKey(): string | undefined {\n const id = this.tickled.tickledId;\n return id ? keyForElement(id) : undefined;\n }\n\n /** Replace the selection with the items carrying these keys (unknown keys ignored). */\n selectKeys(keys: Iterable<string>): void {\n this.selection.selectKeys(keys, key => this.#itemForKey(key));\n }\n\n /** Add items by key to the selection (multi-select modes only). */\n addSelectedKeys(keys: Iterable<string>): void {\n this.selection.addKeys(keys, key => this.#itemForKey(key));\n }\n\n /** Remove items by key from the selection. */\n removeSelectedKeys(keys: Iterable<string>): void {\n this.selection.removeKeys(keys, key => this.#itemForKey(key));\n }\n\n /** Move the cursor to the item with this key and scroll it into view. */\n focusKey(key: string): void {\n const el = this.#itemForKey(key);\n if (!el)\n return;\n this.tickled.focus(el);\n el.scrollIntoView({ block: `nearest`, inline: `nearest` });\n }\n\n #itemForKey(key: string): Element | undefined {\n for (const el of this.querySelectorAll(ITEM_SELECTOR)) {\n if (this.#wrappedElements.has(el))\n continue;\n if (keyForElement(el) === key)\n return el;\n }\n return undefined;\n }\n\n * selectedFilter(kind: `selected` | `checked` | `either`): Generator<Element> {\n const wanted = (el: Element) => {\n switch (kind) {\n case `selected`: return el.hasAttribute(`data-selected`);\n case `checked`: return el.hasAttribute(`data-checked`);\n case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);\n }\n };\n\n for (const el of this.querySelectorAll(ITEM_SELECTOR)) {\n if (this.#wrappedElements.has(el))\n continue;\n if ((el as HTMLElement).hidden)\n continue;\n if (wanted(el))\n yield el;\n }\n }\n\n // --- Helpers ---\n\n private _getVisibleItems(): Element[] {\n return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {\n if (this.#wrappedElements.has(el))\n return false;\n if ((el as HTMLElement).hidden)\n return false;\n if (this.filterPredicate && !this.filterPredicate(el))\n return false;\n return true;\n });\n }\n\n // Whether an item participates in checked-mode selection. Items carrying the\n // `not-checked` attribute are skipped; outside `checked` mode it has no effect.\n // For wrapped custom elements the flag lives on the inner element, so resolve\n // through the wrapper map.\n private _isCheckable(item: Element): boolean {\n if (this.interactionMode !== `checked`)\n return true;\n const real = this.#customWrappers.get(item as HTMLLIElement) ?? item;\n return !real.hasAttribute(`not-checked`);\n }\n\n private _syncItemStates(): void {\n for (const el of this.querySelectorAll(ITEM_SELECTOR)) {\n if (this.#wrappedElements.has(el))\n continue;\n const item = this.#customWrappers.get(el as HTMLLIElement) ?? el;\n el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));\n el.toggleAttribute(`data-selected`, this.selection.selected.has(el));\n if (this.interactionMode === `checked`) {\n el.toggleAttribute(`data-checked`, !item.hasAttribute(`not-checked`) && this.selection.selected.has(el));\n } else {\n el.removeAttribute(`data-checked`);\n }\n }\n this.dataset.inputMode = this.tickled.inputMode;\n this._syncCheckboxes();\n }\n\n private _syncCheckboxes(): void {\n const inCheckedMode = this.interactionMode === `checked`;\n const wantsCustom = typeof this.checkboxRenderer === `function`;\n\n if (inCheckedMode) {\n for (const inner of Array.from(this.children)) {\n if (!inner.hasAttribute(`data-list-item`))\n continue;\n if (inner.parentElement !== this)\n continue; // idempotency: already wrapped\n const wrapper = document.createElement(`li`);\n wrapper.className = `ixfx-custom-row`;\n const key = inner.getAttribute(`data-key`);\n const label = inner.getAttribute(`data-search-label`);\n if (key !== null)\n wrapper.setAttribute(`data-key`, key);\n if (label !== null)\n wrapper.setAttribute(`data-search-label`, label);\n inner.removeAttribute(`data-list-item`);\n this.insertBefore(wrapper, inner);\n wrapper.appendChild(inner);\n this.#customWrappers.set(wrapper, inner);\n this.#wrappedElements.add(inner);\n }\n } else {\n for (const [wrapper, inner] of this.#customWrappers) {\n if (wrapper.parentElement !== this)\n continue;\n inner.setAttribute(`data-list-item`, ``);\n this.insertBefore(inner, wrapper);\n wrapper.remove();\n }\n this.#customWrappers.clear();\n this.#wrappedElements.clear();\n }\n\n for (const el of this.querySelectorAll<HTMLElement>(`li`)) {\n const existing = el.querySelector<HTMLElement>(`.ixfx-list-checkbox`);\n if (!inCheckedMode) {\n if (existing)\n existing.remove();\n continue;\n }\n // Rows marked `not-checked` get no checkbox column and can't be checked\n const itemEl = this.#customWrappers.get(el as HTMLLIElement) ?? el;\n if (itemEl.hasAttribute(`not-checked`)) {\n if (existing)\n existing.remove();\n continue;\n }\n\n // In checked mode: if the existing checkbox is the wrong variant, drop it.\n const existingIsCustom = existing?.classList.contains(`ixfx-list-checkbox-custom`) ?? false;\n if (existing && existingIsCustom !== wantsCustom)\n existing.remove();\n\n let cb: HTMLElement | null = el.querySelector<HTMLElement>(`.ixfx-list-checkbox`);\n if (!cb) {\n if (wantsCustom) {\n const opts = this.checkboxRenderer as CustomCheckboxOptions;\n const margin = opts.margin ?? `0`;\n const width = opts.width ?? `24px`;\n cb = this._createCustomCheckbox(el, margin, width);\n } else {\n cb = this._createNativeCheckboxCell(el);\n }\n el.prepend(cb);\n }\n\n const checked = this.selection.selected.has(el);\n if (wantsCustom) {\n cb.setAttribute(`aria-checked`, String(checked));\n cb.toggleAttribute(`data-checked`, checked);\n if (this.checkboxRenderer) {\n const renderer = typeof this.checkboxRenderer === `function`\n ? this.checkboxRenderer\n : (this.checkboxRenderer as CustomCheckboxOptions).renderer;\n render(renderer(checked), cb);\n }\n } else {\n const input = cb.querySelector<HTMLInputElement>(`input.ixfx-list-checkbox`);\n if (input)\n input.checked = checked;\n }\n }\n }\n\n /**\n * Native checkbox cell: a span that stretches to the row's full cross-axis\n * (so the entire column height is a click target) with a real `<input>`\n * centred inside. The span owns the click; the input is purely visual and\n * has `pointer-events:none` so a single click produces one toggle.\n */\n private _createNativeCheckboxCell(el: HTMLElement): HTMLSpanElement {\n const cell = document.createElement(`span`);\n cell.className = `ixfx-list-checkbox ixfx-list-checkbox-native`;\n cell.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:18px;margin:0 6px 0 0;cursor:pointer;pointer-events:auto`;\n cell.tabIndex = -1;\n cell.setAttribute(`role`, `checkbox`);\n cell.setAttribute(`part`, `checkbox`);\n cell.addEventListener(`click`, (e) => {\n e.stopPropagation();\n this.selection.toggle(el);\n });\n const input = document.createElement(`input`);\n input.type = `checkbox`;\n input.className = `ixfx-list-checkbox`;\n input.style.cssText = `pointer-events:none;cursor:pointer;margin:0`;\n cell.appendChild(input);\n return cell;\n }\n\n /**\n * Custom checkbox cell: same shape as the native cell (a stretching span)\n * so behaviour is identical. The renderer's content goes inside.\n */\n private _createCustomCheckbox(el: HTMLElement, margin: string, width: string): HTMLSpanElement {\n const span = document.createElement(`span`);\n span.className = `ixfx-list-checkbox ixfx-list-checkbox-custom`;\n span.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:${width};margin:${margin};cursor:pointer;pointer-events:auto`;\n span.tabIndex = -1;\n span.setAttribute(`role`, `checkbox`);\n span.setAttribute(`part`, `checkbox`);\n span.addEventListener(`click`, (e) => {\n e.stopPropagation();\n this.selection.toggle(el);\n });\n return span;\n }\n\n private _findListItem(path: EventTarget[]): Element | undefined {\n for (const target of path) {\n if (!(target instanceof Element))\n continue;\n if (target === this)\n break;\n if (target.parentElement === this && target.matches(ITEM_SELECTOR))\n return target;\n }\n return undefined;\n }\n\n private _dispatch<T>(name: keyof ListElementEventMap, detail: T): void {\n this.dispatchEvent(new CustomEvent<T>(name as string, {\n detail,\n bubbles: true,\n composed: true,\n }));\n }\n\n // --- Event handlers ---\n\n private _onPointerOver = (e: PointerEvent): void => {\n const item = this._findListItem(e.composedPath());\n if (item)\n this.tickled.handlePointerEnter(item);\n };\n\n private _onPointerLeave = (e: PointerEvent): void => {\n this.tickled.handlePointerLeave(e);\n };\n\n private _onKeyDown = (e: KeyboardEvent): void => {\n if (this.search.isOpen)\n return;\n if ((e.ctrlKey || e.metaKey) && e.key === `a`) {\n if (this.selection.canSelectAll()) {\n e.preventDefault();\n this.selection.selectAll();\n }\n return;\n }\n\n switch (e.key) {\n case `ArrowDown`: {\n e.preventDefault();\n this.tickled.handleKeyboardNavigation(`next`);\n // vscode: Shift+Arrow extends range from anchor\n if (e.shiftKey && this.interactionMode === `vscode`) {\n const cur = this.tickled.tickledId;\n if (cur)\n this.selection.rangeTo(cur);\n }\n break;\n }\n case `ArrowUp`: {\n e.preventDefault();\n this.tickled.handleKeyboardNavigation(`previous`);\n if (e.shiftKey && this.interactionMode === `vscode`) {\n const cur = this.tickled.tickledId;\n if (cur)\n this.selection.rangeTo(cur);\n }\n break;\n }\n case `Enter`: {\n const id = this.tickled.tickledId;\n if (id) {\n e.preventDefault();\n this._dispatch<ListItemClickDetail>(`list-item-click`, { item: id });\n this._activateItem(id);\n }\n break;\n }\n case ` `: {\n const id = this.tickled.tickledId;\n if (id) {\n e.preventDefault();\n this.selection.handleSpace(id);\n }\n break;\n }\n case `Escape`:\n this.selection.clear();\n break;\n }\n };\n\n private _onClick = (e: MouseEvent): void => {\n const item = this._findListItem(e.composedPath());\n if (!item)\n return;\n this._dispatch<ListItemClickDetail>(`list-item-click`, { item });\n this.selection.handleClick(item, e);\n };\n\n private _onDblClick = (e: MouseEvent): void => {\n const item = this._findListItem(e.composedPath());\n if (item)\n this._activateItem(item);\n };\n\n private _activateItem(item: Element): void {\n // manual mode: never auto-select; just fire the event\n if (this.interactionMode !== `manual`) {\n this.selection.replace(item);\n }\n this._dispatch<ListActivateDetail>(`list-activate`, { item });\n }\n\n // --- Styles ---\n\n static override styles = [themeFallbacks, listSearchStyles, css`\n :host {\n display: block;\n position: relative;\n background: var(--list-bg, var(--surface-3));\n color: var(--list-text, var(--surface-3-text));\n border-radius: var(--list-radius, var(--radius-s));\n border: 1px solid var(--list-border, var(--border));\n width: 100%;\n height: 100%;\n overflow: hidden;\n box-sizing: border-box;\n outline: none;\n --ixfx-list-item-padding: var(--ixfx-list-item-padding, var(--list-item-padding, 4px var(--space-m, 8px)));\n }\n\n :host(:focus-visible) {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n\n .container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n box-sizing: border-box;\n /* Scrollbar theming: end users override these CSS variables to control\n the native scrollbar (scrollbar-width, scrollbar-gutter, scrollbar-color). */\n scrollbar-width: var(--list-scrollbar-width, auto);\n scrollbar-gutter: var(--list-scrollbar-gutter, auto);\n scrollbar-color: var(--list-scrollbar-color, auto);\n }\n\n ::slotted([hidden]) {\n display: none !important;\n }\n\n ::slotted(li) {\n display: flex;\n align-items: center;\n padding: var(--ixfx-list-item-padding);\n /* Inset each row from the scroll container's edges (margin, not padding:\n padding is inside the border box, so the tickle/selection outlines\n would still sit flush against the edge and be clipped). */\n margin: 0 var(--list-gutter, 4px);\n min-height: var(--list-item-height, 28px);\n cursor: pointer;\n user-select: none;\n transition: background var(--transition, 0.1s ease-out),\n outline-color var(--transition, 0.1s ease-out);\n box-sizing: border-box;\n outline: 1px solid transparent;\n }\n\n /* Custom shadow-DOM components handle their own internal layout/padding */\n ::slotted([data-list-item]) {\n display: block;\n margin: 0 var(--list-gutter, 4px);\n cursor: pointer;\n user-select: none;\n transition: background var(--transition, 0.1s ease-out),\n outline-color var(--transition, 0.1s ease-out);\n outline: 1px solid transparent;\n }\n\n /* In checked mode, only the checkbox column is clickable; the row body\n gets a default cursor so it doesn't suggest otherwise. */\n :host([interaction-mode=\"checked\"]) ::slotted(li),\n :host([interaction-mode=\"checked\"]) ::slotted([data-list-item]) {\n cursor: default;\n }\n\n /* Wrapper inserted around custom [data-list-item] elements in checked mode.\n Provides the flex layout so the injected checkbox can stretch to the\n row's full height via align-self:stretch. */\n .ixfx-custom-row {\n display: flex;\n align-items: center;\n min-height: var(--list-item-height, 28px);\n box-sizing: border-box;\n cursor: default;\n user-select: none;\n }\n\n ::slotted([data-tickled]) {\n background: var(--item-bg-tickled);\n outline: var(--item-border-tickled);\n }\n\n ::slotted([data-selected]) {\n background: var(--item-bg-selected);\n color: var(--item-text-selected);\n outline: 1px solid transparent;\n }\n\n ::slotted([data-selected][data-tickled]) {\n background: var(--item-bg-selected-tickled);\n outline: 2px dashed var(--surface-3);\n outline-offset: -2px;\n }\n\n /* checked mode: checkbox (injected into li) communicates selection — suppress bg highlight */\n :host([interaction-mode=\"checked\"]) ::slotted([data-selected]) {\n background: transparent;\n color: inherit;\n outline: 1px solid transparent;\n }\n\n :host([interaction-mode=\"checked\"]) ::slotted([data-selected][data-tickled]) {\n background: var(--item-bg-tickled);\n color: inherit;\n outline: var(--item-border-tickled);\n }\n `];\n\n // --- Render ---\n\n override render() {\n return html`\n <div class=\"container\">\n <ul part=\"list\">\n <slot></slot>\n </ul>\n ${this.search.renderField()}\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ixfx-vertical-list': VerticalListElement;\n }\n\n interface HTMLElementEventMap extends ListElementEventMap {}\n}\n"],"mappings":"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"}
|
package/docs-user/data-grid.md
CHANGED
|
@@ -253,6 +253,25 @@ Grid is focusable (`tabindex=0`). When not editing:
|
|
|
253
253
|
|
|
254
254
|
---
|
|
255
255
|
|
|
256
|
+
## Incremental search
|
|
257
|
+
|
|
258
|
+
A built-in type-ahead popover with **navigate** (cursor + scroll to best match),
|
|
259
|
+
**select** (matching rows become the selection), and **filter** (non-matching
|
|
260
|
+
rows dropped from the flat list) modes. No default shortcut:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
grid.registry.invoke('list.search.filter'); // or .navigate / .select
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
A row is matched against `rowSearchText(row)` — by default its visible cells'
|
|
267
|
+
values joined by a space; override with the `rowSearchText` property. `select`
|
|
268
|
+
mode needs `selection-mode="multiple"` to hold more than one row. Character
|
|
269
|
+
highlights are not drawn (a match position spans multiple cells). `filterPredicate`
|
|
270
|
+
(a `(row) => boolean` property) filters rows directly. `search-modes` limits the
|
|
271
|
+
offered modes. Full model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
256
275
|
## CSS variables & parts
|
|
257
276
|
|
|
258
277
|
| Variable | Default | Description |
|
package/docs-user/detail-list.md
CHANGED
|
@@ -313,7 +313,7 @@ With wrapping:
|
|
|
313
313
|
|
|
314
314
|
## Incremental search
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
A built-in type-ahead popover with **navigate** / **select** / **filter** modes — see [`docs/infra-incremental.md`](../../docs/infra-incremental.md). No default shortcut; open it with `list.registry.invoke('list.search.filter')` (or `.navigate` / `.select`). A filter change re-runs the column geometry automatically.
|
|
317
317
|
|
|
318
318
|
Search matches against `data-search-label` (or `textContent` as fallback). Use `data-search-label` to match against a different string than what's displayed:
|
|
319
319
|
|
package/docs-user/grid-list.md
CHANGED
|
@@ -215,6 +215,30 @@ All bubble and are `composed`.
|
|
|
215
215
|
|
|
216
216
|
---
|
|
217
217
|
|
|
218
|
+
## Incremental search
|
|
219
|
+
|
|
220
|
+
A built-in type-ahead popover with **navigate** / **select** / **filter** modes.
|
|
221
|
+
No default shortcut — open it via a command on `grid.registry`:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
grid.registry.invoke('list.search.filter'); // or .navigate / .select
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Search matches `data-label` (or `[data-grid-list-caption]` text, then
|
|
228
|
+
`data-search-label`, then `textContent`). Filter mode genuinely removes items
|
|
229
|
+
from the virtualised layout. Add a page-level highlight rule to see matched
|
|
230
|
+
characters:
|
|
231
|
+
|
|
232
|
+
```css
|
|
233
|
+
::highlight(grid-list-search) { background: var(--accent); color: var(--accent-text); }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`filterPredicate` (an `(el: Element) => boolean` property) filters items directly,
|
|
237
|
+
independent of search. Limit modes with `search-modes="navigate filter"`. Full
|
|
238
|
+
model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
218
242
|
## Virtualisation
|
|
219
243
|
|
|
220
244
|
Only the cells in (or near) the viewport are in the DOM.
|
package/docs-user/incr-search.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# Incremental Search
|
|
2
2
|
|
|
3
3
|
Utilities for incremental (type-ahead) search over data collections or DOM-backed lists.
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
- **`ListSearchController`** — the shared brain behind the built-in search
|
|
6
|
+
popover in `ixfx-grid-list` / `ixfx-vertical-list` / `ixfx-detail-list` /
|
|
7
|
+
`ixfx-data-grid` / `ixfx-tree-list` / `ixfx-miller-list`. Three modes
|
|
8
|
+
(navigate / select / filter), driven by `list.search.*` commands. See
|
|
9
|
+
[`docs/infra-incremental.md`](../../docs/infra-incremental.md) — start there
|
|
10
|
+
for anything component-facing.
|
|
11
|
+
- **`createIncrSearch`** — the low-level fuzzy engine (fzf) both the controller
|
|
12
|
+
and the helpers below are built on.
|
|
13
|
+
- **`createElementIncrSearch`** / **`IncrSearchTreeController`** — *deprecated*
|
|
14
|
+
filter-only controllers driven by an external `<input>`. Kept for standalone
|
|
15
|
+
use; components now use `ListSearchController`.
|
|
6
16
|
|
|
7
17
|
## Plain UL list
|
|
8
18
|
|
package/docs-user/index.json
CHANGED
package/docs-user/tree.md
CHANGED
|
@@ -367,7 +367,28 @@ Keyboard navigation follows the crumb path. Arrow keys move between breadcrumb s
|
|
|
367
367
|
|
|
368
368
|
## Incremental search
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
`ixfx-tree-list` and `ixfx-miller-list` have a **built-in type-ahead popover**
|
|
371
|
+
with three modes:
|
|
372
|
+
|
|
373
|
+
- **navigate** — expands the match's ancestors and moves the cursor to it; ↑/↓
|
|
374
|
+
cycle between matches; Enter activates and closes.
|
|
375
|
+
- **select** — the selection becomes the set of matching nodes.
|
|
376
|
+
- **filter** — non-matching nodes (and branches with no matching descendant) are
|
|
377
|
+
hidden; branches with a match auto-expand so the match is visible.
|
|
378
|
+
|
|
379
|
+
No default shortcut — open it via a command on `el.registry`:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
el.registry.invoke('list.search.navigate'); // or .select / .filter
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Limit the offered modes with the `search-modes` attribute
|
|
386
|
+
(`search-modes="navigate filter"`). Full model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
|
|
387
|
+
|
|
388
|
+
### Deprecated: `IncrSearchTreeController`
|
|
389
|
+
|
|
390
|
+
The external-`<input>` filter-only controller still works but is superseded by
|
|
391
|
+
the built-in popover.
|
|
371
392
|
|
|
372
393
|
```typescript
|
|
373
394
|
import { IncrSearchTreeController } from '@ixfx/components';
|