@c2n/tree 0.0.9

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nguyen Thai Vinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @c2n/tree
2
+
3
+ Hierarchical tree view with expansion, selection, checkbox selection and lazy loading, built with Lit.
4
+
5
+ ```bash
6
+ npm install @c2n/tree
7
+ ```
8
+
9
+ ```html
10
+ <script type="module">
11
+ import '@c2n/tree'
12
+ </script>
13
+
14
+ <c2-tree aria-label="Files" expanded-items="src" value="app">
15
+ <c2-tree-item value="src" label="src">
16
+ <c2-tree-item value="app" label="app.ts"></c2-tree-item>
17
+ <c2-tree-item value="main" label="main.ts"></c2-tree-item>
18
+ </c2-tree-item>
19
+ <c2-tree-item value="readme" label="README.md"></c2-tree-item>
20
+ </c2-tree>
21
+ ```
22
+
23
+ - **Two authoring modes, one result**: nest `c2-tree-item` elements as above, or set `items` to an array of
24
+ `{ value, label, children?, disabled?, hasChildren?, data? }` nodes and the tree renders the same elements
25
+ itself. Both produce identical DOM and the same events, so a docs example and a data-driven app agree.
26
+ - **The tree owns the state**: `value` holds the selected rows and `expanded-items` the expanded ones, both
27
+ `;`-separated in the attribute and arrays in the property. Every row's `level`, `expanded`, `selected`,
28
+ `indeterminate` and roving `tabindex` are written from the tree on each sync — drive the two arrays, not the
29
+ rows.
30
+ - **Expansion**: the toggle opens a branch. `expand-on-click` widens that to the whole row — and to Enter and
31
+ Space — which is what a navigation tree wants, where a branch row is a heading with nothing to select. A leaf
32
+ is never a branch, so a row that is only a link still just follows it.
33
+ - **Selection**: `selection` is `none`, `single` or `multiple`. `checkbox-selection` adds a `c2-checkbox` per
34
+ row and implies `multiple`; `selection-propagation` (`none` | `descendants` | `parents` | `both`) controls how
35
+ far a tick travels, and a partly-selected branch is left indeterminate. Disabled rows are never selected by a
36
+ relative and are left out of the tally, so a branch is not stuck half-ticked by a row nobody can click.
37
+ - **Lazy loading**: mark a branch `has-children` and set `loadChildren` to a promise-returning function. The row
38
+ shows a `c2-spinner` while it runs, a rejection fires `item-load-error` and the branch stays retryable. When
39
+ authoring in markup, listen for `item-expand` and append the children yourself instead.
40
+ - **Keyboard**: a `tree` with a roving `tabindex`. Arrow Up/Down walk the visible rows, Arrow Right expands then
41
+ descends, Arrow Left collapses then ascends, Home/End jump to the ends, Enter and Space select, `*` expands
42
+ the focused row's siblings, and typing jumps to a matching row. Shift extends a range and Ctrl/Cmd toggles.
43
+ - **Cost**: only expanded rows render their children slot, so a collapsed subtree is not laid out and stays
44
+ out of the accessibility tree. The elements are still created, so a tree of many thousands of rows wants
45
+ virtualization rather than this component.
46
+
47
+ `selection-change` and `expansion-change` do not bubble — several components fire `selection-change`, so listen
48
+ on the tree itself rather than an ancestor.
49
+
50
+ Theming: `--c2-tree--*` for the container and `--c2-tree-item--*` for the rows, which inherit through the tree.
51
+ `row--indent`, `row--border-radius` and `guide--color` do most of the work: set `row--border-radius: 0` for the
52
+ edge-to-edge bands of a layers panel, and `actions--opacity: 1` to stop the trailing actions hiding until hover.
53
+ The full list is in `custom-elements.json` and on the docs site.
@@ -0,0 +1,242 @@
1
+ import { LitElement, html, nothing, unsafeCSS } from "lit";
2
+ import { property, query } from "lit/decorators.js";
3
+ import { customElement } from "@c2n/core/element-helper.js";
4
+ import { isServer } from "lit-html/is-server.js";
5
+ import { classMap } from "lit/directives/class-map.js";
6
+ import { ifDefined } from "lit/directives/if-defined.js";
7
+ import "@c2n/checkbox";
8
+ import "@c2n/spinner";
9
+ //#region src/tree-item.scss?inline
10
+ var tree_item_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n color: var(--c2-tree-item--color,#18181b);\n font-size: var(--c2-tree-item--font-size,14px);\n font-weight: var(--c2-tree-item--font-weight);\n font-family: var(--c2-tree-item--font-family);\n line-height: var(--c2-tree-item--line-height,20px);\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host(:focus) {\n outline: none;\n}\n\n.row {\n position: relative;\n color: inherit;\n text-decoration: none;\n display: flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--c2-tree-item__row--gap,6px);\n min-height: var(--c2-tree-item__row--min-height,28px);\n border-radius: var(--c2-tree-item__row--border-radius,4px);\n background: var(--c2-tree-item--background,transparent);\n cursor: pointer;\n user-select: none;\n transition: background-color 250ms cubic-bezier(0.2, 0, 0, 1), color 250ms cubic-bezier(0.2, 0, 0, 1);\n padding-inline-start: calc(var(--c2-tree-item__row--indent,16px) * var(--level, 0) + var(--c2-tree-item__row--padding-inline-start,8px));\n padding-inline-end: var(--c2-tree-item__row--padding-inline-end,8px);\n padding-top: var(--c2-tree-item__row--padding-top,0px);\n padding-bottom: var(--c2-tree-item__row--padding-bottom,0px);\n inline-size: 100%;\n min-inline-size: max-content;\n}\n\n:host([children-outline]) .row::before {\n content: \"\";\n position: absolute;\n inset-block: 0;\n inset-inline-start: calc(var(--c2-tree-item__row--padding-inline-start,8px) + var(--c2-tree-item__toggle--size,16px) / 2);\n inline-size: calc(var(--c2-tree-item__row--indent,16px) * var(--level, 0));\n pointer-events: none;\n background-image: repeating-linear-gradient(to right, var(--c2-tree-item__guide--color,#e4e4e7) 0 var(--c2-tree-item__guide--width,1px), transparent var(--c2-tree-item__guide--width,1px) var(--c2-tree-item__row--indent,16px));\n}\n\n:host([children-outline]:dir(rtl)) .row::before {\n background-image: repeating-linear-gradient(to left, var(--c2-tree-item__guide--color,#e4e4e7) 0 var(--c2-tree-item__guide--width,1px), transparent var(--c2-tree-item__guide--width,1px) var(--c2-tree-item__row--indent,16px));\n}\n\n:host(:not([disabled])) .row:hover {\n color: var(--c2-tree-item__hover--color,var(--c2-tree-item--color,#18181b));\n background: var(--c2-tree-item__hover--background,#f4f4f5);\n}\n\n:host([selected]) .row {\n color: var(--c2-tree-item__selected--color,rgb(2, 101, 220));\n background: var(--c2-tree-item__selected--background,#edf1fe);\n}\n\n:host([selected]:not([disabled])) .row:hover {\n background: var(--c2-tree-item__selected__hover--background,var(--c2-tree-item__selected--background,#e2e9fd));\n}\n\n:host(:focus-visible) .row {\n outline: var(--c2-tree-item__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: var(--c2-tree-item__focus--outline-offset,-2px);\n}\n\n:host([disabled]) {\n opacity: var(--c2-tree-item__disabled--opacity,0.38);\n}\n\n:host([disabled]) .row {\n cursor: default;\n}\n\n.toggle {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n inline-size: var(--c2-tree-item__toggle--size,16px);\n block-size: var(--c2-tree-item__toggle--size,16px);\n color: var(--c2-tree-item__toggle--color,#71717a);\n background: none;\n border: none;\n padding: 0;\n cursor: inherit;\n transform: rotate(var(--c2-tree-item__toggle--rotate-collapsed,0deg));\n transition: transform var(--c2-tree-item__toggle--transition-duration,150ms) cubic-bezier(0.2, 0, 0, 1);\n}\n\n:host([expanded]) .toggle {\n transform: rotate(var(--c2-tree-item__toggle--rotate,90deg));\n}\n\n.toggle.is-leaf {\n visibility: hidden;\n}\n\n.toggle svg,\nslot[name=toggle-icon]::slotted(*) {\n inline-size: 100%;\n block-size: 100%;\n}\n\n.spinner {\n flex: none;\n --c2-spinner--size: var(--c2-tree-item__toggle--size,16px);\n --c2-spinner--stroke-width: 6px;\n}\n\n.checkbox {\n flex: none;\n margin-inline-end: var(--c2-tree-item__checkbox--margin-inline-end,2px);\n}\n\nslot[name=icon]::slotted(*) {\n flex: none;\n color: var(--c2-tree-item__icon--color);\n --c2-feather-icon--size: var(--c2-tree-item__icon--size,16px);\n --c2-mat-icon--size: var(--c2-tree-item__icon--size,16px);\n inline-size: var(--c2-tree-item__icon--size,16px);\n block-size: var(--c2-tree-item__icon--size,16px);\n}\n\n.label {\n flex: 1 1 auto;\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.actions {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--c2-tree-item__actions--gap,2px);\n opacity: var(--c2-tree-item__actions--opacity,0);\n transition: opacity 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n\n.row:hover .actions,\n:host(:focus-visible) .actions,\n.actions:focus-within {\n opacity: var(--c2-tree-item__actions__hover--opacity,1);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .row,\n .toggle,\n .actions {\n transition: none;\n }\n}";
11
+ //#endregion
12
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
13
+ function __decorate(decorators, target, key, desc) {
14
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
15
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
16
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
17
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
18
+ }
19
+ //#endregion
20
+ //#region src/tree-item.ts
21
+ var _TreeItem;
22
+ /** Fired when an item joins, leaves or changes identity, so its `c2-tree` can resync. */
23
+ var TREE_ITEM_CHANGE_EVENT = "c2-tree-item-change";
24
+ /** Fired when the disclosure toggle is activated. */
25
+ var TREE_ITEM_TOGGLE_EVENT = "c2-tree-item-toggle";
26
+ /** Fired when the row's checkbox is ticked or cleared. */
27
+ var TREE_ITEM_CHECK_EVENT = "c2-tree-item-check";
28
+ var TreeItem = class TreeItem extends LitElement {
29
+ static {
30
+ _TreeItem = this;
31
+ }
32
+ constructor(..._args) {
33
+ super(..._args);
34
+ this.value = "";
35
+ this.label = "";
36
+ this.disabled = false;
37
+ this.hasChildren = false;
38
+ this.href = "";
39
+ this.expanded = false;
40
+ this.selected = false;
41
+ this.indeterminate = false;
42
+ this.loading = false;
43
+ this.level = 0;
44
+ this.checkboxSelection = false;
45
+ this.childrenOutline = false;
46
+ this.setSize = 1;
47
+ this.posInSet = 1;
48
+ this.selectable = true;
49
+ this.#resolvedLabel = "";
50
+ this.#handleChildSlotChange = () => {
51
+ this.#notify();
52
+ };
53
+ }
54
+ static {
55
+ this.styles = unsafeCSS(tree_item_default);
56
+ }
57
+ /** Cached so the accessible name is not recomputed from slotted nodes on every sync. */
58
+ #resolvedLabel;
59
+ /** The row as a {@link TreeNode}, so both authoring modes produce the same event details and renderer input. */
60
+ get node() {
61
+ return {
62
+ value: this.value,
63
+ label: this.label || void 0,
64
+ disabled: this.disabled,
65
+ hasChildren: this.isBranch,
66
+ data: this.data
67
+ };
68
+ }
69
+ /** The nested rows, in document order. */
70
+ get childItems() {
71
+ if (isServer || !this.children) return [];
72
+ return [...this.children].filter((child) => child instanceof _TreeItem);
73
+ }
74
+ /** Whether the row can be expanded — it already has children, or declares that it will load some. */
75
+ get isBranch() {
76
+ return this.hasChildren || this.childItems.length > 0;
77
+ }
78
+ /** The accessible name of the row, resolved from the `label` slot, the `label` attribute or `value`. */
79
+ get resolvedLabel() {
80
+ return this.#resolvedLabel || this.label || this.value;
81
+ }
82
+ connectedCallback() {
83
+ super.connectedCallback();
84
+ this.#notify();
85
+ }
86
+ disconnectedCallback() {
87
+ super.disconnectedCallback();
88
+ this.#notify();
89
+ }
90
+ /**
91
+ * Asks the parent tree to resync. Only identity changes qualify: the tree writes `expanded`, `selected`,
92
+ * `level` and friends on every pass, so notifying on those would loop.
93
+ */
94
+ #notify() {
95
+ this.dispatchEvent(new CustomEvent(TREE_ITEM_CHANGE_EVENT, {
96
+ bubbles: true,
97
+ composed: true
98
+ }));
99
+ }
100
+ willUpdate(changed) {
101
+ if (!this.hasUpdated) return;
102
+ if (changed.has("value") || changed.has("disabled") || changed.has("hasChildren")) this.#notify();
103
+ }
104
+ updated(changed) {
105
+ if (changed.has("level")) this.style.setProperty("--level", String(this.level));
106
+ this.#syncAria();
107
+ }
108
+ firstUpdated() {
109
+ this.#readLabelSlot();
110
+ }
111
+ /**
112
+ * ARIA is written imperatively on the host: `role="treeitem"` has to sit on the element the tree moves focus
113
+ * to, and the `role="group"` holding the children must be a descendant of it.
114
+ */
115
+ #syncAria() {
116
+ this.setAttribute("role", "treeitem");
117
+ this.setAttribute("aria-level", String(this.level + 1));
118
+ this.setAttribute("aria-setsize", String(this.setSize));
119
+ this.setAttribute("aria-posinset", String(this.posInSet));
120
+ if (this.isBranch) this.setAttribute("aria-expanded", String(this.expanded));
121
+ else this.removeAttribute("aria-expanded");
122
+ if (this.selectable) this.setAttribute("aria-selected", String(this.selected));
123
+ else this.removeAttribute("aria-selected");
124
+ if (this.disabled) this.setAttribute("aria-disabled", "true");
125
+ else this.removeAttribute("aria-disabled");
126
+ this.setAttribute("aria-label", this.resolvedLabel);
127
+ }
128
+ #readLabelSlot() {
129
+ const assigned = this.labelSlot?.assignedNodes({ flatten: true }) ?? [];
130
+ this.#resolvedLabel = assigned.map((node) => node.textContent ?? "").join("").trim();
131
+ this.#syncAria();
132
+ }
133
+ #handleToggle(event) {
134
+ event.stopPropagation();
135
+ event.preventDefault();
136
+ if (this.disabled) return;
137
+ this.dispatchEvent(new CustomEvent(TREE_ITEM_TOGGLE_EVENT, {
138
+ bubbles: true,
139
+ composed: true
140
+ }));
141
+ }
142
+ #handleCheckboxChange(event) {
143
+ event.stopPropagation();
144
+ const checked = event.target.checked;
145
+ this.dispatchEvent(new CustomEvent(TREE_ITEM_CHECK_EVENT, {
146
+ detail: checked,
147
+ bubbles: true,
148
+ composed: true
149
+ }));
150
+ }
151
+ render() {
152
+ const branch = this.isBranch;
153
+ const content = html`
154
+ ${this.loading ? html`<c2-spinner class="spinner" part="toggle" aria-hidden="true"></c2-spinner>` : html`<span class="toggle ${classMap({ "is-leaf": !branch })}" part="toggle" aria-hidden="true" @click=${this.#handleToggle}>
155
+ <slot name="toggle-icon">
156
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
157
+ <polyline points="9 18 15 12 9 6"></polyline>
158
+ </svg>
159
+ </slot>
160
+ </span>`}
161
+ ${this.checkboxSelection ? html`<c2-checkbox
162
+ class="checkbox"
163
+ part="checkbox"
164
+ tabindex="-1"
165
+ aria-label=${this.resolvedLabel}
166
+ .checked=${this.selected}
167
+ .indeterminate=${this.indeterminate}
168
+ ?disabled=${this.disabled}
169
+ @click=${(event) => {
170
+ event.stopPropagation();
171
+ if (this.href) event.preventDefault();
172
+ }}
173
+ @change=${this.#handleCheckboxChange}
174
+ ></c2-checkbox>` : nothing}
175
+ <slot name="icon"></slot>
176
+ <span class="label" part="label">
177
+ <slot name="label" @slotchange=${this.#readLabelSlot}>${this.label || this.value}</slot>
178
+ </span>
179
+ <span class="actions" part="actions"><slot name="actions"></slot></span>
180
+ `;
181
+ const row = this.href && !this.disabled ? html`<a
182
+ class="row"
183
+ part="row"
184
+ href=${this.href}
185
+ target=${ifDefined(this.target || void 0)}
186
+ rel=${this.target === "_blank" ? "noopener noreferrer" : nothing}
187
+ tabindex="-1"
188
+ >${content}</a
189
+ >` : html`<div class="row" part="row">${content}</div>`;
190
+ return html`
191
+ ${row} ${this.expanded ? html`<div class="group" part="group" role="group"><slot @slotchange=${this.#handleChildSlotChange}></slot></div>` : nothing}
192
+ `;
193
+ }
194
+ #handleChildSlotChange;
195
+ };
196
+ __decorate([property({
197
+ type: String,
198
+ reflect: true
199
+ })], TreeItem.prototype, "value", void 0);
200
+ __decorate([property({ type: String })], TreeItem.prototype, "label", void 0);
201
+ __decorate([property({
202
+ type: Boolean,
203
+ reflect: true
204
+ })], TreeItem.prototype, "disabled", void 0);
205
+ __decorate([property({
206
+ type: Boolean,
207
+ reflect: true,
208
+ attribute: "has-children"
209
+ })], TreeItem.prototype, "hasChildren", void 0);
210
+ __decorate([property({ type: String })], TreeItem.prototype, "href", void 0);
211
+ __decorate([property({ type: String })], TreeItem.prototype, "target", void 0);
212
+ __decorate([property({ attribute: false })], TreeItem.prototype, "data", void 0);
213
+ __decorate([property({
214
+ type: Boolean,
215
+ reflect: true
216
+ })], TreeItem.prototype, "expanded", void 0);
217
+ __decorate([property({
218
+ type: Boolean,
219
+ reflect: true
220
+ })], TreeItem.prototype, "selected", void 0);
221
+ __decorate([property({
222
+ type: Boolean,
223
+ reflect: true
224
+ })], TreeItem.prototype, "indeterminate", void 0);
225
+ __decorate([property({
226
+ type: Boolean,
227
+ reflect: true
228
+ })], TreeItem.prototype, "loading", void 0);
229
+ __decorate([property({ attribute: false })], TreeItem.prototype, "level", void 0);
230
+ __decorate([property({ attribute: false })], TreeItem.prototype, "checkboxSelection", void 0);
231
+ __decorate([property({
232
+ type: Boolean,
233
+ reflect: true,
234
+ attribute: "children-outline"
235
+ })], TreeItem.prototype, "childrenOutline", void 0);
236
+ __decorate([property({ attribute: false })], TreeItem.prototype, "setSize", void 0);
237
+ __decorate([property({ attribute: false })], TreeItem.prototype, "posInSet", void 0);
238
+ __decorate([property({ attribute: false })], TreeItem.prototype, "selectable", void 0);
239
+ __decorate([query("slot[name=\"label\"]")], TreeItem.prototype, "labelSlot", void 0);
240
+ TreeItem = _TreeItem = __decorate([customElement("c2-tree-item")], TreeItem);
241
+ //#endregion
242
+ export { __decorate as a, TreeItem as i, TREE_ITEM_CHECK_EVENT as n, TREE_ITEM_TOGGLE_EVENT as r, TREE_ITEM_CHANGE_EVENT as t };
@@ -0,0 +1,2 @@
1
+ import { i as TreeItem, n as TREE_ITEM_CHECK_EVENT, r as TREE_ITEM_TOGGLE_EVENT, t as TREE_ITEM_CHANGE_EVENT } from "./tree-item-C6XnvUA1.js";
2
+ export { TREE_ITEM_CHANGE_EVENT, TREE_ITEM_CHECK_EVENT, TREE_ITEM_TOGGLE_EVENT, TreeItem };
File without changes