@c2n/cascader 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,36 @@
1
+ # @c2n/cascader
2
+
3
+ `<c2-cascader>` selects a path through nested data in one multi-column floating panel.
4
+
5
+ ```bash
6
+ npm install @c2n/cascader
7
+ ```
8
+
9
+ ```html
10
+ <c2-cascader id="location" aria-label="Choose a location" placeholder="Please select"></c2-cascader>
11
+ ```
12
+
13
+ ```js
14
+ import '@c2n/cascader'
15
+
16
+ const cascader = document.querySelector('#location')
17
+ cascader.options = [
18
+ {
19
+ value: 'zhejiang',
20
+ label: 'Zhejiang',
21
+ children: [
22
+ {
23
+ value: 'hangzhou',
24
+ label: 'Hangzhou',
25
+ children: [{ value: 'west-lake', label: 'West Lake' }],
26
+ },
27
+ ],
28
+ },
29
+ ]
30
+
31
+ cascader.addEventListener('cascader-change', (event) => {
32
+ console.log(event.detail.value) // ['zhejiang', 'hangzhou', 'west-lake']
33
+ })
34
+ ```
35
+
36
+ Set `expand-trigger="hover"` to open child columns on hover. Set `change-on-select` when branches as well as leaves are valid values. The component is form-associated and submits its selected path separated by semicolons.
@@ -0,0 +1,352 @@
1
+ import { LitElement, html, nothing, svg, unsafeCSS } from "lit";
2
+ import { property, query, state } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import { customElement } from "@c2n/core/element-helper.js";
5
+ import { arrayPropertyConverter, jsonPropertyConverter } from "@c2n/core/lit-helper.js";
6
+ import "@c2n/overlay";
7
+ //#region src/cascader.scss?inline
8
+ var cascader_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n vertical-align: middle;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.trigger {\n box-sizing: border-box;\n display: inline-flex;\n width: var(--c2-cascader__trigger--width,220px);\n min-height: var(--c2-cascader__trigger--min-height,36px);\n margin: 0;\n padding: var(--c2-cascader__trigger--padding,6px 10px 6px 12px);\n align-items: center;\n gap: var(--c2-cascader__trigger--gap,8px);\n border: var(--c2-cascader__trigger--border,1px solid #bcbcc6);\n border-radius: var(--c2-cascader__trigger--border-radius,6px);\n background: var(--c2-cascader__trigger--background,#ffffff);\n color: var(--c2-cascader__trigger--color,#18181b);\n font-family: inherit;\n font-size: var(--c2-cascader__trigger--font-size,14px);\n line-height: 1.4;\n text-align: start;\n cursor: pointer;\n transition: background 250ms cubic-bezier(0.2, 0, 0, 1), border 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.trigger:hover:not(:disabled) {\n border: var(--c2-cascader__trigger__hover--border,1px solid #a1a1aa);\n background: var(--c2-cascader__trigger__hover--background,#fafafa);\n}\n.trigger:focus-visible {\n outline: var(--c2-cascader__trigger__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: var(--c2-cascader__trigger__focus--outline-offset,0px);\n}\n.trigger:disabled {\n cursor: default;\n opacity: var(--c2-cascader__trigger__disabled--opacity,0.38);\n}\n\n:host([open]) .trigger {\n border: var(--c2-cascader__trigger__open--border,1px solid rgb(2, 101, 220));\n}\n:host([open]) .trigger .trigger-icon {\n transform: rotate(180deg);\n}\n\n:host([readonly]) .trigger {\n cursor: default;\n}\n\n.value {\n min-width: 0;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.placeholder {\n color: var(--c2-cascader__placeholder--color,#71717a);\n}\n\n::slotted([slot=prefix-icon]),\n::slotted([slot=suffix-icon]),\n.trigger-icon,\n.option-chevron,\n.option-check {\n width: var(--c2-cascader__icon--size,16px);\n height: var(--c2-cascader__icon--size,16px);\n flex: none;\n color: var(--c2-cascader__icon--color,#71717a);\n --c2-feather-icon--size: var(--c2-cascader__icon--size,16px);\n --c2-mat-icon--font-size: var(--c2-cascader__icon--size,16px);\n}\n\n.trigger-icon {\n display: block;\n transition: transform 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n\n.panel {\n box-sizing: border-box;\n display: flex;\n max-width: min(var(--c2-cascader__panel--max-width,720px), var(--c2-overlay--available-width, 100vw));\n overflow-x: auto;\n border: var(--c2-cascader__panel--border,1px solid #e4e4e7);\n border-radius: var(--c2-cascader__panel--border-radius,8px);\n background: var(--c2-cascader__panel--background,#ffffff);\n box-shadow: var(--c2-cascader__panel--box-shadow,0 12px 32px rgba(24, 24, 27, 0.14));\n}\n\n.column {\n box-sizing: border-box;\n min-width: var(--c2-cascader__column--min-width,150px);\n max-width: var(--c2-cascader__column--max-width,240px);\n max-height: min(var(--c2-cascader__column--max-height,280px), var(--c2-overlay--available-height, 100vh));\n padding: var(--c2-cascader__column--padding,4px);\n overflow-y: auto;\n border-right: var(--c2-cascader__column--border,1px solid #e4e4e7);\n}\n.column:last-child {\n border-right: 0;\n}\n\n.option {\n box-sizing: border-box;\n display: flex;\n width: 100%;\n min-height: var(--c2-cascader__option--min-height,36px);\n margin: 0;\n padding: var(--c2-cascader__option--padding,8px 10px);\n align-items: center;\n gap: var(--c2-cascader__option--gap,8px);\n border: 0;\n border-radius: var(--c2-cascader__option--border-radius,6px);\n background: transparent;\n color: var(--c2-cascader__option--color,#18181b);\n font-family: inherit;\n font-size: var(--c2-cascader__option--font-size,14px);\n line-height: 1.35;\n text-align: start;\n cursor: pointer;\n}\n.option:hover:not(:disabled), .option:focus-visible {\n background: var(--c2-cascader__option__hover--background,#f4f4f5);\n}\n.option:focus-visible {\n outline: var(--c2-cascader__trigger__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: -2px;\n}\n.option:disabled {\n cursor: default;\n opacity: var(--c2-cascader__option__disabled--opacity,0.38);\n}\n\n.option--active {\n background: var(--c2-cascader__option__active--background,#e8f2ff);\n color: var(--c2-cascader__option__active--color,#18181b);\n font-weight: 600;\n}\n\n.option--selected {\n color: var(--c2-cascader__option__selected--color,rgb(2, 101, 220));\n}\n\n.option-content {\n min-width: 0;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.option-check {\n color: var(--c2-cascader__option__selected--color,rgb(2, 101, 220));\n}\n\n.empty {\n padding: var(--c2-cascader__empty--padding,18px);\n color: var(--c2-cascader__empty--color,#71717a);\n font-size: var(--c2-cascader__option--font-size,14px);\n white-space: nowrap;\n}";
9
+ //#endregion
10
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
11
+ function __decorate(decorators, target, key, desc) {
12
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
13
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
14
+ 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;
15
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
16
+ }
17
+ //#endregion
18
+ //#region src/cascader.ts
19
+ var Cascader = class Cascader extends LitElement {
20
+ constructor(..._args) {
21
+ super(..._args);
22
+ this.internals = this.attachInternals();
23
+ this.defaultValue = [];
24
+ this.defaultValueCaptured = false;
25
+ this.customValidityMessage = "";
26
+ this.disabledByForm = false;
27
+ this.activePath = [];
28
+ this.options = [];
29
+ this.value = [];
30
+ this.name = "";
31
+ this.placeholder = "Please select";
32
+ this.separator = " / ";
33
+ this.ariaLabel = null;
34
+ this.open = false;
35
+ this.disabled = false;
36
+ this.readonly = false;
37
+ this.required = false;
38
+ this.changeOnSelect = false;
39
+ this.expandTrigger = "click";
40
+ }
41
+ static {
42
+ this.formAssociated = true;
43
+ }
44
+ static {
45
+ this.styles = unsafeCSS(cascader_default);
46
+ }
47
+ static {
48
+ this.shadowRootOptions = {
49
+ ...LitElement.shadowRootOptions,
50
+ delegatesFocus: true
51
+ };
52
+ }
53
+ /** The resolved option path for the current `value`. Invalid trailing values are omitted. */
54
+ get selectedOptions() {
55
+ return this.resolvePath(this.value);
56
+ }
57
+ /** Human-readable selected path. */
58
+ get displayValue() {
59
+ return this.selectedOptions.map((option) => option.label).join(this.separator);
60
+ }
61
+ get form() {
62
+ return this.internals.form;
63
+ }
64
+ get labels() {
65
+ return this.internals.labels;
66
+ }
67
+ get validity() {
68
+ return this.internals.validity;
69
+ }
70
+ get validationMessage() {
71
+ return this.internals.validationMessage;
72
+ }
73
+ get willValidate() {
74
+ return this.internals.willValidate;
75
+ }
76
+ connectedCallback() {
77
+ super.connectedCallback();
78
+ if (!this.defaultValueCaptured) {
79
+ this.defaultValue = arrayPropertyConverter.fromAttribute(this.getAttribute("value") ?? "");
80
+ this.defaultValueCaptured = true;
81
+ }
82
+ }
83
+ formResetCallback() {
84
+ this.value = [...this.defaultValue];
85
+ }
86
+ formDisabledCallback(disabled) {
87
+ this.disabledByForm = disabled && !this.hasAttribute("disabled");
88
+ }
89
+ formStateRestoreCallback(state) {
90
+ if (typeof state === "string") this.value = arrayPropertyConverter.fromAttribute(state);
91
+ }
92
+ checkValidity() {
93
+ return this.internals.checkValidity();
94
+ }
95
+ reportValidity() {
96
+ return this.internals.reportValidity();
97
+ }
98
+ setCustomValidity(message) {
99
+ this.customValidityMessage = message;
100
+ this.syncFormState();
101
+ }
102
+ /** Opens, closes or toggles the floating panel. */
103
+ toggle(force) {
104
+ if (this.readonly || this.effectiveDisabled) {
105
+ if (this.panel?.matches(":popover-open")) this.panel.hidePopover();
106
+ return;
107
+ }
108
+ const next = force ?? !this.panel.matches(":popover-open");
109
+ if (next && !this.panel.matches(":popover-open")) this.panel.showPopover();
110
+ if (!next && this.panel.matches(":popover-open")) this.panel.hidePopover();
111
+ }
112
+ willUpdate(changed) {
113
+ if (changed.has("value") && typeof this.value === "string") this.value = arrayPropertyConverter.fromAttribute(this.value);
114
+ if (changed.has("options") && !Array.isArray(this.options)) this.options = [];
115
+ }
116
+ updated(changed) {
117
+ if (changed.has("open") || changed.has("disabled") || changed.has("readonly")) this.toggle(this.open);
118
+ if (changed.has("value") || changed.has("required") || changed.has("disabled") || changed.has("name")) this.syncFormState();
119
+ }
120
+ get effectiveDisabled() {
121
+ return this.disabled || this.disabledByForm;
122
+ }
123
+ resolvePath(values) {
124
+ const path = [];
125
+ let options = this.options;
126
+ for (const value of values) {
127
+ const option = options.find((candidate) => candidate.value === value);
128
+ if (!option) break;
129
+ path.push(option);
130
+ options = option.children ?? [];
131
+ }
132
+ return path;
133
+ }
134
+ get columns() {
135
+ const columns = [this.options];
136
+ for (const option of this.activePath) {
137
+ if (!option.children?.length) break;
138
+ columns.push(option.children);
139
+ }
140
+ return columns;
141
+ }
142
+ handleOverlayToggle(event) {
143
+ const open = event.newState === "open";
144
+ this.open = open;
145
+ if (open) this.activePath = this.selectedOptions;
146
+ this.dispatchEvent(new CustomEvent("open-change", {
147
+ detail: { open },
148
+ bubbles: true,
149
+ composed: true
150
+ }));
151
+ }
152
+ handleTriggerKeydown(event) {
153
+ if (![
154
+ "ArrowDown",
155
+ "ArrowUp",
156
+ "Enter",
157
+ " "
158
+ ].includes(event.key)) return;
159
+ event.preventDefault();
160
+ this.toggle(true);
161
+ this.focusOption(0, event.key === "ArrowUp" ? -1 : 0);
162
+ }
163
+ optionPath(level, option) {
164
+ return [...this.activePath.slice(0, level), option];
165
+ }
166
+ activateOption(level, option, commitBranch) {
167
+ if (option.disabled) return;
168
+ const path = this.optionPath(level, option);
169
+ this.activePath = path;
170
+ const complete = !option.children?.length;
171
+ if (complete || commitBranch) this.commit(path, complete);
172
+ if (complete) this.toggle(false);
173
+ }
174
+ handleOptionClick(level, option) {
175
+ this.activateOption(level, option, this.changeOnSelect);
176
+ if (option.children?.length) this.focusOption(level + 1, 0);
177
+ }
178
+ handleOptionPointerEnter(level, option) {
179
+ if (this.expandTrigger !== "hover" || option.disabled || !option.children?.length) return;
180
+ this.activePath = this.optionPath(level, option);
181
+ }
182
+ commit(path, complete) {
183
+ this.value = path.map((option) => option.value);
184
+ const detail = {
185
+ value: [...this.value],
186
+ labels: path.map((option) => option.label),
187
+ options: [...path],
188
+ complete
189
+ };
190
+ this.dispatchEvent(new CustomEvent("cascader-change", {
191
+ detail,
192
+ bubbles: true,
193
+ composed: true
194
+ }));
195
+ this.dispatchEvent(new Event("input", {
196
+ bubbles: true,
197
+ composed: true
198
+ }));
199
+ this.dispatchEvent(new Event("change", {
200
+ bubbles: true,
201
+ composed: true
202
+ }));
203
+ }
204
+ async focusOption(level, index) {
205
+ await this.updateComplete;
206
+ const options = [...this.renderRoot.querySelectorAll(`[data-level="${level}"]`)].filter((option) => option.getAttribute("aria-disabled") !== "true");
207
+ if (!options.length) return;
208
+ options[index < 0 ? options.length - 1 : Math.min(index, options.length - 1)]?.focus();
209
+ }
210
+ handleOptionKeydown(event, level, option) {
211
+ const enabled = this.columns[level].filter((item) => !item.disabled);
212
+ const enabledIndex = enabled.indexOf(option);
213
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
214
+ event.preventDefault();
215
+ const nextIndex = (enabledIndex + (event.key === "ArrowDown" ? 1 : -1) + enabled.length) % enabled.length;
216
+ this.focusOption(level, nextIndex);
217
+ } else if (event.key === "Home" || event.key === "End") {
218
+ event.preventDefault();
219
+ this.focusOption(level, event.key === "Home" ? 0 : -1);
220
+ } else if (event.key === "ArrowRight" && option.children?.length) {
221
+ event.preventDefault();
222
+ this.activateOption(level, option, false);
223
+ this.focusOption(level + 1, 0);
224
+ } else if (event.key === "ArrowLeft" && level > 0) {
225
+ event.preventDefault();
226
+ const parentOptions = this.columns[level - 1].filter((item) => !item.disabled);
227
+ this.focusOption(level - 1, parentOptions.indexOf(this.activePath[level - 1]));
228
+ } else if (event.key === "Enter" || event.key === " ") {
229
+ event.preventDefault();
230
+ this.handleOptionClick(level, option);
231
+ } else if (event.key === "Escape") {
232
+ event.preventDefault();
233
+ this.toggle(false);
234
+ this.trigger.focus();
235
+ }
236
+ }
237
+ syncFormState() {
238
+ const serialized = this.value.join(";");
239
+ this.internals.setFormValue(this.effectiveDisabled ? null : serialized || null, serialized);
240
+ const missing = this.required && this.value.length === 0;
241
+ const flags = this.customValidityMessage ? { customError: true } : missing ? { valueMissing: true } : {};
242
+ const message = this.customValidityMessage || (missing ? "Please select an option." : "");
243
+ this.internals.setValidity(flags, message, this.trigger);
244
+ }
245
+ renderChevron() {
246
+ return svg`<svg class="trigger-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>`;
247
+ }
248
+ renderOption(option, level) {
249
+ const active = this.activePath[level]?.value === option.value;
250
+ const selected = this.value[level] === option.value;
251
+ const hasChildren = Boolean(option.children?.length);
252
+ return html`
253
+ <button
254
+ type="button"
255
+ role="treeitem"
256
+ class=${classMap({
257
+ option: true,
258
+ "option--active": active,
259
+ "option--selected": selected
260
+ })}
261
+ data-level=${level}
262
+ aria-level=${level + 1}
263
+ aria-expanded=${hasChildren ? String(active) : nothing}
264
+ aria-selected=${selected ? "true" : "false"}
265
+ aria-disabled=${option.disabled ? "true" : "false"}
266
+ ?disabled=${option.disabled}
267
+ @click=${() => this.handleOptionClick(level, option)}
268
+ @pointerenter=${() => this.handleOptionPointerEnter(level, option)}
269
+ @keydown=${(event) => this.handleOptionKeydown(event, level, option)}
270
+ >
271
+ <span class="option-content">${this.itemRenderer?.({
272
+ option,
273
+ level,
274
+ active,
275
+ selected
276
+ }) ?? option.label}</span>
277
+ ${hasChildren ? svg`<svg class="option-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"></path></svg>` : selected ? svg`<svg class="option-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"></path></svg>` : nothing}
278
+ </button>
279
+ `;
280
+ }
281
+ render() {
282
+ const displayValue = this.displayValue;
283
+ return html`
284
+ <button
285
+ id="trigger"
286
+ class="trigger"
287
+ type="button"
288
+ aria-label=${this.ariaLabel || nothing}
289
+ aria-haspopup="tree"
290
+ aria-expanded=${this.open ? "true" : "false"}
291
+ popovertarget=${this.readonly || this.effectiveDisabled ? nothing : "panel-overlay"}
292
+ ?disabled=${this.effectiveDisabled}
293
+ @keydown=${this.handleTriggerKeydown}
294
+ >
295
+ <slot name="prefix-icon"></slot>
296
+ <span class=${classMap({
297
+ value: true,
298
+ placeholder: !displayValue
299
+ })}>${displayValue || this.placeholder}</span>
300
+ <slot name="suffix-icon">${this.renderChevron()}</slot>
301
+ </button>
302
+ <c2-overlay id="panel-overlay" popover free-width @toggle=${this.handleOverlayToggle}>
303
+ <div class="panel" role="tree" aria-label=${this.ariaLabel || "Cascader options"}>
304
+ ${this.options.length ? this.columns.map((column, level) => html`
305
+ <div class="column" role="group" aria-label=${`Level ${level + 1}`}>${column.map((option) => this.renderOption(option, level))}</div>
306
+ `) : html`<div class="empty" role="status"><slot name="empty">No options available.</slot></div>`}
307
+ </div>
308
+ </c2-overlay>
309
+ `;
310
+ }
311
+ };
312
+ __decorate([state()], Cascader.prototype, "disabledByForm", void 0);
313
+ __decorate([state()], Cascader.prototype, "activePath", void 0);
314
+ __decorate([property({ converter: jsonPropertyConverter })], Cascader.prototype, "options", void 0);
315
+ __decorate([property({
316
+ converter: arrayPropertyConverter,
317
+ reflect: true
318
+ })], Cascader.prototype, "value", void 0);
319
+ __decorate([property({ type: String })], Cascader.prototype, "name", void 0);
320
+ __decorate([property({ type: String })], Cascader.prototype, "placeholder", void 0);
321
+ __decorate([property({ type: String })], Cascader.prototype, "separator", void 0);
322
+ __decorate([property({ attribute: "aria-label" })], Cascader.prototype, "ariaLabel", void 0);
323
+ __decorate([property({
324
+ type: Boolean,
325
+ reflect: true
326
+ })], Cascader.prototype, "open", void 0);
327
+ __decorate([property({
328
+ type: Boolean,
329
+ reflect: true
330
+ })], Cascader.prototype, "disabled", void 0);
331
+ __decorate([property({
332
+ type: Boolean,
333
+ reflect: true
334
+ })], Cascader.prototype, "readonly", void 0);
335
+ __decorate([property({
336
+ type: Boolean,
337
+ reflect: true
338
+ })], Cascader.prototype, "required", void 0);
339
+ __decorate([property({
340
+ type: Boolean,
341
+ attribute: "change-on-select"
342
+ })], Cascader.prototype, "changeOnSelect", void 0);
343
+ __decorate([property({
344
+ attribute: "expand-trigger",
345
+ reflect: true
346
+ })], Cascader.prototype, "expandTrigger", void 0);
347
+ __decorate([property({ attribute: false })], Cascader.prototype, "itemRenderer", void 0);
348
+ __decorate([query("#trigger", true)], Cascader.prototype, "trigger", void 0);
349
+ __decorate([query("#panel-overlay", true)], Cascader.prototype, "panel", void 0);
350
+ Cascader = __decorate([customElement("c2-cascader")], Cascader);
351
+ //#endregion
352
+ export { Cascader };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@c2n/cascader",
3
+ "version": "0.0.9",
4
+ "type": "module",
5
+ "main": "dist/cascader.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/src/cascader.d.ts",
9
+ "default": "./dist/cascader.js"
10
+ },
11
+ "./react": {
12
+ "types": "./react.d.ts",
13
+ "default": "./react.js"
14
+ },
15
+ "./vue": {
16
+ "types": "./vue.d.ts",
17
+ "default": "./vue.js"
18
+ },
19
+ "./custom-elements.json": "./custom-elements.json"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "types",
24
+ "react.d.ts",
25
+ "react.js",
26
+ "vue.d.ts",
27
+ "vue.js"
28
+ ],
29
+ "keywords": [
30
+ "cascader",
31
+ "web component",
32
+ "lit"
33
+ ],
34
+ "license": "MIT",
35
+ "author": "code2nguyen@gmail.com",
36
+ "publishConfig": {
37
+ "registry": "https://registry.npmjs.org",
38
+ "access": "public"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/code2nguyen/web-components.git"
43
+ },
44
+ "scripts": {
45
+ "dev": "vite",
46
+ "build": "wireit",
47
+ "build:only": "vite build",
48
+ "type-check": "wireit"
49
+ },
50
+ "wireit": {
51
+ "type-check": {
52
+ "dependencies": [
53
+ "../../core:build",
54
+ "../overlay:build"
55
+ ],
56
+ "command": "tsc -p tsconfig.lib.json --composite false"
57
+ },
58
+ "build": {
59
+ "dependencies": [
60
+ "type-check"
61
+ ],
62
+ "command": "vite build"
63
+ }
64
+ },
65
+ "dependencies": {
66
+ "@c2n/core": "0.0.9",
67
+ "@c2n/overlay": "0.0.9",
68
+ "lit": "3.3.3"
69
+ },
70
+ "devDependencies": {
71
+ "@c2n/config": "*"
72
+ },
73
+ "customElements": "custom-elements.json",
74
+ "gitHead": "e4768cce5e0fbd5bc3165cd493a1e8d57cf0fff4"
75
+ }
package/react.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // JSX types for the custom elements of @c2n/cascader. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/cascader/react'
6
+ //
7
+ // Register the elements at module scope before React renders, or React writes an object prop as an
8
+ // attribute and it stringifies.
9
+
10
+ import type { DetailedHTMLProps, HTMLAttributes } from 'react'
11
+ import type { Cascader } from '@c2n/cascader'
12
+
13
+ /** Standard React host-element attributes plus the element's own public properties. */
14
+ type C2Props<T> = DetailedHTMLProps<HTMLAttributes<T>, T> & Partial<Omit<T, keyof HTMLElement>>
15
+
16
+ declare module 'react' {
17
+ namespace JSX {
18
+ interface IntrinsicElements {
19
+ 'c2-cascader': C2Props<Cascader>
20
+ }
21
+ }
22
+ }
23
+
24
+ export {}
package/react.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}
@@ -0,0 +1,186 @@
1
+ import { LitElement, type PropertyValues } from 'lit';
2
+ import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
3
+ import type { Overlay } from '@c2n/overlay';
4
+ import '@c2n/overlay';
5
+ /** One node in a cascader hierarchy. */
6
+ export interface CascaderOption {
7
+ value: string;
8
+ label: string;
9
+ disabled?: boolean;
10
+ children?: CascaderOption[];
11
+ data?: unknown;
12
+ }
13
+ export interface CascaderItemContext {
14
+ option: CascaderOption;
15
+ level: number;
16
+ active: boolean;
17
+ selected: boolean;
18
+ }
19
+ /** Returns custom content for an option row while the cascader keeps interaction and accessibility. */
20
+ export type CascaderItemRenderer = (context: CascaderItemContext) => unknown;
21
+ export interface CascaderChangeEventDetail {
22
+ value: string[];
23
+ labels: string[];
24
+ options: CascaderOption[];
25
+ complete: boolean;
26
+ }
27
+ /** Events fired by {@link Cascader}, keyed for `addEventListener`. */
28
+ export interface CascaderEventMap {
29
+ 'cascader-change': CustomEvent<CascaderChangeEventDetail>;
30
+ 'open-change': CustomEvent<{
31
+ open: boolean;
32
+ }>;
33
+ input: Event;
34
+ change: Event;
35
+ }
36
+ export interface Cascader {
37
+ addEventListener: TypedAddEventListener<Cascader, CascaderEventMap>;
38
+ removeEventListener: TypedRemoveEventListener<Cascader, CascaderEventMap>;
39
+ }
40
+ /**
41
+ * Hierarchical single-value picker that opens every active level in one floating panel. Supply nested `options`, then
42
+ * read the selected value path from `value`. Branches open another column; choosing a leaf commits the path.
43
+ *
44
+ * @tag c2-cascader
45
+ *
46
+ * @slot prefix-icon - Icon shown before the selected path or placeholder.
47
+ * @slot suffix-icon - Replaces the trigger chevron.
48
+ * @slot empty - Replaces the message shown when `options` is empty.
49
+ *
50
+ * @event {CustomEvent<CascaderChangeEventDetail>} cascader-change - Fired when a leaf, or a branch with `change-on-select`, is committed. Detail contains value, label and option paths plus whether the last option is a leaf.
51
+ * @event {CustomEvent<{ open: boolean }>} open-change - Fired when the floating panel opens or closes.
52
+ * @event {Event} input - Fired after `value` changes through user interaction.
53
+ * @event {Event} change - Fired after `value` changes through user interaction.
54
+ *
55
+ * @cssproperty {pixel} [--c2-cascader__trigger--min-height=36px]
56
+ * @cssproperty {pixel} [--c2-cascader__trigger--width=220px]
57
+ * @cssproperty {padding} [--c2-cascader__trigger--padding=6px 10px 6px 12px]
58
+ * @cssproperty {pixel} [--c2-cascader__trigger--gap=8px]
59
+ * @cssproperty {color} [--c2-cascader__trigger--background=#ffffff]
60
+ * @cssproperty {color} [--c2-cascader__trigger--color=#18181b]
61
+ * @cssproperty {font-size} [--c2-cascader__trigger--font-size=14px]
62
+ * @cssproperty {border} [--c2-cascader__trigger--border=1px solid #bcbcc6]
63
+ * @cssproperty {border-radius} [--c2-cascader__trigger--border-radius=6px]
64
+ * @cssproperty {border} [--c2-cascader__trigger__hover--border=1px solid #a1a1aa]
65
+ * @cssproperty {color} [--c2-cascader__trigger__hover--background=#fafafa]
66
+ * @cssproperty {border} [--c2-cascader__trigger__open--border=1px solid rgb(2, 101, 220)]
67
+ * @cssproperty {outline} [--c2-cascader__trigger__focus--outline=2px solid rgba(2, 101, 220, 0.4)]
68
+ * @cssproperty {pixel} [--c2-cascader__trigger__focus--outline-offset=0px]
69
+ * @cssproperty {opacity} [--c2-cascader__trigger__disabled--opacity=0.38]
70
+ * @cssproperty {color} [--c2-cascader__placeholder--color=#71717a]
71
+ * @cssproperty {pixel} [--c2-cascader__icon--size=16px]
72
+ * @cssproperty {color} [--c2-cascader__icon--color=#71717a]
73
+ * @cssproperty {color} [--c2-cascader__panel--background=#ffffff]
74
+ * @cssproperty {border} [--c2-cascader__panel--border=1px solid #e4e4e7]
75
+ * @cssproperty {border-radius} [--c2-cascader__panel--border-radius=8px]
76
+ * @cssproperty {box-shadow} [--c2-cascader__panel--box-shadow=0 12px 32px rgba(24, 24, 27, 0.14)]
77
+ * @cssproperty {pixel} [--c2-cascader__panel--max-width=720px]
78
+ * @cssproperty {pixel} [--c2-cascader__column--min-width=150px]
79
+ * @cssproperty {pixel} [--c2-cascader__column--max-width=240px]
80
+ * @cssproperty {pixel} [--c2-cascader__column--max-height=280px]
81
+ * @cssproperty {padding} [--c2-cascader__column--padding=4px]
82
+ * @cssproperty {border} [--c2-cascader__column--border=1px solid #e4e4e7]
83
+ * @cssproperty {pixel} [--c2-cascader__option--min-height=36px]
84
+ * @cssproperty {padding} [--c2-cascader__option--padding=8px 10px]
85
+ * @cssproperty {pixel} [--c2-cascader__option--gap=8px]
86
+ * @cssproperty {border-radius} [--c2-cascader__option--border-radius=6px]
87
+ * @cssproperty {color} [--c2-cascader__option--color=#18181b]
88
+ * @cssproperty {font-size} [--c2-cascader__option--font-size=14px]
89
+ * @cssproperty {color} [--c2-cascader__option__hover--background=#f4f4f5]
90
+ * @cssproperty {color} [--c2-cascader__option__active--background=#e8f2ff]
91
+ * @cssproperty {color} [--c2-cascader__option__active--color=#18181b]
92
+ * @cssproperty {color} [--c2-cascader__option__selected--color=rgb(2, 101, 220)]
93
+ * @cssproperty {opacity} [--c2-cascader__option__disabled--opacity=0.38]
94
+ * @cssproperty {padding} [--c2-cascader__empty--padding=18px]
95
+ * @cssproperty {color} [--c2-cascader__empty--color=#71717a]
96
+ *
97
+ * @internalcomponent c2-overlay
98
+ */
99
+ export declare class Cascader extends LitElement {
100
+ static formAssociated: boolean;
101
+ static styles: import("lit").CSSResult;
102
+ static shadowRootOptions: {
103
+ delegatesFocus: boolean;
104
+ clonable?: boolean;
105
+ customElementRegistry?: CustomElementRegistry | null;
106
+ mode: ShadowRootMode;
107
+ serializable?: boolean;
108
+ slotAssignment?: SlotAssignmentMode;
109
+ };
110
+ private readonly internals;
111
+ private defaultValue;
112
+ private defaultValueCaptured;
113
+ private customValidityMessage;
114
+ private disabledByForm;
115
+ private activePath;
116
+ /** Nested hierarchy shown in the floating panel. In markup this is a JSON array. */
117
+ options: CascaderOption[];
118
+ /** Selected value path. In markup, separate levels with semicolons. */
119
+ value: string[];
120
+ /** Name used when the cascader participates in a form. */
121
+ name: string;
122
+ /** Text shown before a path is selected. */
123
+ placeholder: string;
124
+ /** String placed between selected labels in the trigger. */
125
+ separator: string;
126
+ /** Accessible name forwarded to the trigger and option tree. */
127
+ ariaLabel: string | null;
128
+ /** Whether the floating panel is open. */
129
+ open: boolean;
130
+ /** Disables opening, selection and form submission. */
131
+ disabled: boolean;
132
+ /** Displays the selected path without allowing changes. */
133
+ readonly: boolean;
134
+ /** Requires a committed path for native form validation. */
135
+ required: boolean;
136
+ /** Commit branch values as they are chosen instead of waiting for a leaf. */
137
+ changeOnSelect: boolean;
138
+ /** Open child columns on pointer hover instead of click. */
139
+ expandTrigger: 'click' | 'hover';
140
+ /** Replaces an option row's content while interaction and hierarchy controls remain owned by the cascader. */
141
+ itemRenderer: CascaderItemRenderer | undefined;
142
+ trigger: HTMLButtonElement;
143
+ panel: Overlay;
144
+ /** The resolved option path for the current `value`. Invalid trailing values are omitted. */
145
+ get selectedOptions(): CascaderOption[];
146
+ /** Human-readable selected path. */
147
+ get displayValue(): string;
148
+ get form(): HTMLFormElement | null;
149
+ get labels(): NodeList;
150
+ get validity(): ValidityState;
151
+ get validationMessage(): string;
152
+ get willValidate(): boolean;
153
+ connectedCallback(): void;
154
+ formResetCallback(): void;
155
+ formDisabledCallback(disabled: boolean): void;
156
+ formStateRestoreCallback(state: string | File | FormData | null): void;
157
+ checkValidity(): boolean;
158
+ reportValidity(): boolean;
159
+ setCustomValidity(message: string): void;
160
+ /** Opens, closes or toggles the floating panel. */
161
+ toggle(force?: boolean): void;
162
+ protected willUpdate(changed: PropertyValues<this>): void;
163
+ protected updated(changed: PropertyValues<this>): void;
164
+ private get effectiveDisabled();
165
+ private resolvePath;
166
+ private get columns();
167
+ private handleOverlayToggle;
168
+ private handleTriggerKeydown;
169
+ private optionPath;
170
+ private activateOption;
171
+ private handleOptionClick;
172
+ private handleOptionPointerEnter;
173
+ private commit;
174
+ private focusOption;
175
+ private handleOptionKeydown;
176
+ private syncFormState;
177
+ private renderChevron;
178
+ private renderOption;
179
+ render(): import("lit-html").TemplateResult<1>;
180
+ }
181
+ declare global {
182
+ interface HTMLElementTagNameMap {
183
+ 'c2-cascader': Cascader;
184
+ }
185
+ }
186
+ //# sourceMappingURL=cascader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cascader.d.ts","sourceRoot":"","sources":["../../src/cascader.ts"],"names":[],"mappings":"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"}
package/vue.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // Vue template types for the custom elements of @c2n/cascader. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/cascader/vue'
6
+ //
7
+ // Tell the compiler about the tags as well, or every c2-* tag is resolved as a Vue component and renders
8
+ // nothing: template.compilerOptions.isCustomElement = (tag) => tag.startsWith('c2-') in vite.config.ts.
9
+
10
+ import type { DefineComponent, HTMLAttributes } from 'vue'
11
+ import type { Cascader, CascaderEventMap } from '@c2n/cascader'
12
+
13
+ /** The element's own public properties, plus every attribute Vue understands on a host element. */
14
+ type C2Props<T> = Partial<Omit<T, keyof HTMLElement>> & HTMLAttributes
15
+
16
+ declare module 'vue' {
17
+ interface GlobalComponents {
18
+ 'c2-cascader': DefineComponent<
19
+ C2Props<Cascader> & {
20
+ 'aria-label'?: unknown
21
+ 'change-on-select'?: unknown
22
+ 'expand-trigger'?: unknown
23
+ onOpenChange?: (event: CascaderEventMap['open-change']) => void
24
+ onCascaderChange?: (event: CascaderEventMap['cascader-change']) => void
25
+ onInput?: (event: CascaderEventMap['input']) => void
26
+ onChange?: (event: CascaderEventMap['change']) => void
27
+ }
28
+ >
29
+ }
30
+ }
31
+
32
+ declare module '@vue/runtime-dom' {
33
+ interface HTMLAttributes {
34
+ /** Vue's own definition omits it, and slotting a plain element into a component needs it. */
35
+ slot?: string
36
+ }
37
+ }
38
+
39
+ export {}
package/vue.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}