@unofficialbox/box-open-elements 0.2.0 → 0.3.0
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/README.md +1 -0
- package/dist/components/actions/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- package/dist/components/collections/table.d.ts +53 -0
- package/dist/components/collections/table.js +323 -0
- package/dist/components/collections/thumbnail-card.d.ts +19 -0
- package/dist/components/collections/thumbnail-card.js +210 -0
- package/dist/components/feedback/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- package/dist/components/forms/date-field.d.ts +1 -0
- package/dist/components/forms/date-field.js +7 -2
- package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
- package/dist/components/forms/pill-selector-dropdown.js +127 -0
- package/dist/components/forms/search-field.d.ts +1 -0
- package/dist/components/forms/search-field.js +7 -2
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +7 -2
- package/dist/components/forms/text-field.d.ts +1 -0
- package/dist/components/forms/text-field.js +8 -2
- package/dist/components/navigation/breadcrumb.d.ts +30 -0
- package/dist/components/navigation/breadcrumb.js +190 -0
- package/dist/components/navigation/tabs.d.ts +4 -0
- package/dist/components/navigation/tabs.js +53 -5
- package/dist/components/overlays/context-menu.d.ts +42 -0
- package/dist/components/overlays/context-menu.js +260 -0
- package/dist/components/overlays/dialog.d.ts +8 -0
- package/dist/components/overlays/dialog.js +45 -2
- package/dist/components/overlays/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- package/dist/core/form-associated.d.ts +32 -0
- package/dist/core/form-associated.js +114 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/foundations/overlay/index.d.ts +2 -0
- package/dist/foundations/overlay/index.js +1 -0
- package/dist/foundations/overlay/positioning.d.ts +74 -0
- package/dist/foundations/overlay/positioning.js +158 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/patterns/content-explorer/adapters/table.js +8 -1
- package/dist/patterns/search/filter-bar.js +12 -6
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
|
|
1
|
+
import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
|
|
2
2
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
3
3
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
4
4
|
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
@@ -45,11 +45,12 @@ const dateFieldStyles = `
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
${boeFormFieldErrorStyles}
|
|
48
|
+
${boeFormFieldSupportStyles}
|
|
48
49
|
`;
|
|
49
50
|
export class BoxDateFieldElement extends FormAssociatedElement {
|
|
50
51
|
static get observedAttributes() {
|
|
51
52
|
return [
|
|
52
|
-
...FormAssociatedElement.
|
|
53
|
+
...FormAssociatedElement.fieldObservedAttributes,
|
|
53
54
|
"disabled",
|
|
54
55
|
"label",
|
|
55
56
|
"max",
|
|
@@ -60,6 +61,7 @@ export class BoxDateFieldElement extends FormAssociatedElement {
|
|
|
60
61
|
valueInternal = "";
|
|
61
62
|
inputEl;
|
|
62
63
|
labelEl;
|
|
64
|
+
descriptionEl;
|
|
63
65
|
errorEl;
|
|
64
66
|
get disabled() {
|
|
65
67
|
return this.hasAttribute("disabled");
|
|
@@ -135,11 +137,13 @@ export class BoxDateFieldElement extends FormAssociatedElement {
|
|
|
135
137
|
<style>${dateFieldStyles}</style>
|
|
136
138
|
<label part="field">
|
|
137
139
|
<span part="label"></span>
|
|
140
|
+
${formDescriptionMarkup()}
|
|
138
141
|
<input type="date" part="input" />
|
|
139
142
|
${formErrorMessageMarkup()}
|
|
140
143
|
</label>
|
|
141
144
|
`;
|
|
142
145
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
146
|
+
this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
|
|
143
147
|
this.inputEl = this.shadowRoot.querySelector('[part="input"]');
|
|
144
148
|
this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
|
|
145
149
|
}
|
|
@@ -174,6 +178,7 @@ export class BoxDateFieldElement extends FormAssociatedElement {
|
|
|
174
178
|
else {
|
|
175
179
|
this.inputEl.removeAttribute("disabled");
|
|
176
180
|
}
|
|
181
|
+
this.applyFieldSupport(this.labelEl, this.inputEl, this.descriptionEl);
|
|
177
182
|
this.applyInvalidState(this.inputEl, this.errorEl);
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -29,6 +29,17 @@ export declare class BoxPillSelectorDropdownElement extends FormAssociatedElemen
|
|
|
29
29
|
set label(value: string);
|
|
30
30
|
get placeholder(): string;
|
|
31
31
|
set placeholder(value: string);
|
|
32
|
+
/** Allow creating pills from free-typed text (collaborator / email input). */
|
|
33
|
+
get allowCustom(): boolean;
|
|
34
|
+
set allowCustom(value: boolean);
|
|
35
|
+
/** Optional regex a custom entry must match (e.g. an email pattern). */
|
|
36
|
+
get pattern(): string;
|
|
37
|
+
set pattern(value: string);
|
|
38
|
+
private customInputEl;
|
|
39
|
+
/** A custom entry is valid when non-empty and (if set) matches `pattern`. */
|
|
40
|
+
private isValidCustom;
|
|
41
|
+
/** Add a validated custom pill; flag the input and emit `invalid-entry` otherwise. */
|
|
42
|
+
private tryAddCustom;
|
|
32
43
|
get options(): PillOption[];
|
|
33
44
|
set options(value: PillOption[]);
|
|
34
45
|
get value(): string[];
|
|
@@ -67,6 +67,31 @@ const pillSelectorStyles = `
|
|
|
67
67
|
display: inline-block;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
[part="custom-input"] {
|
|
71
|
+
flex: 1 1 6rem;
|
|
72
|
+
min-width: 6rem;
|
|
73
|
+
appearance: none;
|
|
74
|
+
border: none;
|
|
75
|
+
outline: none;
|
|
76
|
+
background: transparent;
|
|
77
|
+
color: var(--boe-token-text-text, #222222);
|
|
78
|
+
font: inherit;
|
|
79
|
+
font-size: 0.85rem;
|
|
80
|
+
padding: 0.3rem 0.2rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[part="custom-input"][hidden] {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[part="custom-input"][aria-invalid="true"] {
|
|
88
|
+
color: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[part="trigger"][hidden] {
|
|
92
|
+
display: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
70
95
|
[part="trigger"] {
|
|
71
96
|
appearance: none;
|
|
72
97
|
display: inline-flex;
|
|
@@ -152,6 +177,8 @@ export class BoxPillSelectorDropdownElement extends FormAssociatedElement {
|
|
|
152
177
|
"options",
|
|
153
178
|
"placeholder",
|
|
154
179
|
"value",
|
|
180
|
+
"allow-custom",
|
|
181
|
+
"pattern",
|
|
155
182
|
];
|
|
156
183
|
}
|
|
157
184
|
valueInternal = [];
|
|
@@ -188,6 +215,58 @@ export class BoxPillSelectorDropdownElement extends FormAssociatedElement {
|
|
|
188
215
|
set placeholder(value) {
|
|
189
216
|
this.setAttribute("placeholder", value);
|
|
190
217
|
}
|
|
218
|
+
/** Allow creating pills from free-typed text (collaborator / email input). */
|
|
219
|
+
get allowCustom() {
|
|
220
|
+
return this.hasAttribute("allow-custom");
|
|
221
|
+
}
|
|
222
|
+
set allowCustom(value) {
|
|
223
|
+
this.toggleAttribute("allow-custom", Boolean(value));
|
|
224
|
+
}
|
|
225
|
+
/** Optional regex a custom entry must match (e.g. an email pattern). */
|
|
226
|
+
get pattern() {
|
|
227
|
+
return this.getAttribute("pattern") ?? "";
|
|
228
|
+
}
|
|
229
|
+
set pattern(value) {
|
|
230
|
+
if (value) {
|
|
231
|
+
this.setAttribute("pattern", value);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
this.removeAttribute("pattern");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
customInputEl = null;
|
|
238
|
+
/** A custom entry is valid when non-empty and (if set) matches `pattern`. */
|
|
239
|
+
isValidCustom(text) {
|
|
240
|
+
const trimmed = text.trim();
|
|
241
|
+
if (!trimmed) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const pattern = this.pattern;
|
|
245
|
+
if (!pattern) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
return new RegExp(`^(?:${pattern})$`).test(trimmed);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Add a validated custom pill; flag the input and emit `invalid-entry` otherwise. */
|
|
256
|
+
tryAddCustom(text) {
|
|
257
|
+
const trimmed = text.trim();
|
|
258
|
+
if (!trimmed) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (!this.isValidCustom(trimmed)) {
|
|
262
|
+
this.customInputEl?.setAttribute("aria-invalid", "true");
|
|
263
|
+
this.dispatchEvent(new CustomEvent("invalid-entry", { bubbles: true, composed: true, detail: { value: trimmed } }));
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
this.customInputEl?.setAttribute("aria-invalid", "false");
|
|
267
|
+
this.addValue(trimmed);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
191
270
|
get options() {
|
|
192
271
|
const raw = this.getAttribute("options");
|
|
193
272
|
if (!raw) {
|
|
@@ -354,6 +433,7 @@ export class BoxPillSelectorDropdownElement extends FormAssociatedElement {
|
|
|
354
433
|
<style>${pillSelectorStyles}</style>
|
|
355
434
|
<div part="field" role="group">
|
|
356
435
|
<span part="pills"></span>
|
|
436
|
+
<input part="custom-input" type="text" hidden />
|
|
357
437
|
<div part="dropdown">
|
|
358
438
|
<button type="button" part="trigger" aria-haspopup="menu"></button>
|
|
359
439
|
</div>
|
|
@@ -362,6 +442,7 @@ export class BoxPillSelectorDropdownElement extends FormAssociatedElement {
|
|
|
362
442
|
`;
|
|
363
443
|
this.fieldEl = this.shadowRoot.querySelector('[part="field"]');
|
|
364
444
|
this.pillsEl = this.shadowRoot.querySelector('[part="pills"]');
|
|
445
|
+
this.customInputEl = this.shadowRoot.querySelector('[part="custom-input"]');
|
|
365
446
|
this.dropdownEl = this.shadowRoot.querySelector('[part="dropdown"]');
|
|
366
447
|
this.triggerEl = this.shadowRoot.querySelector('[part="trigger"]');
|
|
367
448
|
this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
|
|
@@ -393,6 +474,42 @@ export class BoxPillSelectorDropdownElement extends FormAssociatedElement {
|
|
|
393
474
|
}
|
|
394
475
|
this.removeValue(remove.dataset.value ?? "");
|
|
395
476
|
});
|
|
477
|
+
// Custom pill entry: Enter/comma commit the typed value; Backspace on an
|
|
478
|
+
// empty input removes the last pill; paste splits on commas/newlines.
|
|
479
|
+
this.customInputEl?.addEventListener("keydown", event => {
|
|
480
|
+
const input = event.target;
|
|
481
|
+
const key = event.key;
|
|
482
|
+
if (key === "Enter" || key === ",") {
|
|
483
|
+
event.preventDefault();
|
|
484
|
+
if (this.tryAddCustom(input.value)) {
|
|
485
|
+
input.value = "";
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if (key === "Backspace" && input.value === "" && this.valueInternal.length) {
|
|
489
|
+
event.preventDefault();
|
|
490
|
+
this.removeValue(this.valueInternal[this.valueInternal.length - 1]);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
input.setAttribute("aria-invalid", "false");
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
this.customInputEl?.addEventListener("paste", event => {
|
|
497
|
+
const text = event.clipboardData?.getData("text") ?? "";
|
|
498
|
+
if (!/[,\n;]/.test(text)) {
|
|
499
|
+
return; // single token — let it type normally
|
|
500
|
+
}
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
for (const piece of text.split(/[,\n;]+/)) {
|
|
503
|
+
this.tryAddCustom(piece);
|
|
504
|
+
}
|
|
505
|
+
event.target.value = "";
|
|
506
|
+
});
|
|
507
|
+
// Commit a half-typed entry when focus leaves the input.
|
|
508
|
+
this.customInputEl?.addEventListener("blur", () => {
|
|
509
|
+
if (this.customInputEl && this.tryAddCustom(this.customInputEl.value)) {
|
|
510
|
+
this.customInputEl.value = "";
|
|
511
|
+
}
|
|
512
|
+
});
|
|
396
513
|
this.dropdownEl.addEventListener("click", event => {
|
|
397
514
|
const option = event.target.closest('[part="option"]');
|
|
398
515
|
if (!option || !this.dropdownEl.contains(option)) {
|
|
@@ -439,6 +556,16 @@ export class BoxPillSelectorDropdownElement extends FormAssociatedElement {
|
|
|
439
556
|
this.fieldEl.setAttribute("aria-label", this.label);
|
|
440
557
|
this.triggerEl.textContent = `+ ${this.placeholder}`;
|
|
441
558
|
this.triggerEl.setAttribute("aria-expanded", String(this.open));
|
|
559
|
+
// Free-text entry input (collaborator / email token input).
|
|
560
|
+
if (this.customInputEl) {
|
|
561
|
+
const custom = this.allowCustom;
|
|
562
|
+
this.customInputEl.hidden = !custom;
|
|
563
|
+
this.customInputEl.placeholder = this.placeholder;
|
|
564
|
+
this.customInputEl.setAttribute("aria-label", `${this.label} — type to add`);
|
|
565
|
+
// With a free-text input, the +Add trigger is only useful when there are
|
|
566
|
+
// still fixed options to pick; hide it otherwise to keep the field clean.
|
|
567
|
+
this.triggerEl.hidden = custom && this.availableOptions().length === 0;
|
|
568
|
+
}
|
|
442
569
|
this.pillsEl.innerHTML = this.valueInternal
|
|
443
570
|
.map(value => `
|
|
444
571
|
<span part="pill" data-value="${escapeHtml(value)}">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
|
|
1
|
+
import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
|
|
2
2
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
3
3
|
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
4
4
|
const DEFAULT_TAG_NAME = "box-search-field";
|
|
@@ -122,11 +122,12 @@ const searchFieldStyles = `
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
${boeFormFieldErrorStyles}
|
|
125
|
+
${boeFormFieldSupportStyles}
|
|
125
126
|
`;
|
|
126
127
|
export class BoxSearchFieldElement extends FormAssociatedElement {
|
|
127
128
|
static get observedAttributes() {
|
|
128
129
|
return [
|
|
129
|
-
...FormAssociatedElement.
|
|
130
|
+
...FormAssociatedElement.fieldObservedAttributes,
|
|
130
131
|
"disabled",
|
|
131
132
|
"label",
|
|
132
133
|
"placeholder",
|
|
@@ -136,6 +137,7 @@ export class BoxSearchFieldElement extends FormAssociatedElement {
|
|
|
136
137
|
valueInternal = "";
|
|
137
138
|
inputEl;
|
|
138
139
|
labelEl;
|
|
140
|
+
descriptionEl;
|
|
139
141
|
submitEl;
|
|
140
142
|
clearEl;
|
|
141
143
|
errorEl;
|
|
@@ -206,6 +208,7 @@ export class BoxSearchFieldElement extends FormAssociatedElement {
|
|
|
206
208
|
<style>${searchFieldStyles}</style>
|
|
207
209
|
<label part="field">
|
|
208
210
|
<span part="label"></span>
|
|
211
|
+
${formDescriptionMarkup()}
|
|
209
212
|
<div part="input-shell">
|
|
210
213
|
<input type="search" part="input" />
|
|
211
214
|
<button type="button" part="submit">Search</button>
|
|
@@ -215,6 +218,7 @@ export class BoxSearchFieldElement extends FormAssociatedElement {
|
|
|
215
218
|
</label>
|
|
216
219
|
`;
|
|
217
220
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
221
|
+
this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
|
|
218
222
|
this.inputEl = this.shadowRoot.querySelector('[part="input"]');
|
|
219
223
|
this.submitEl = this.shadowRoot.querySelector('[part="submit"]');
|
|
220
224
|
this.clearEl = this.shadowRoot.querySelector('[part="clear"]');
|
|
@@ -280,6 +284,7 @@ export class BoxSearchFieldElement extends FormAssociatedElement {
|
|
|
280
284
|
this.submitEl.removeAttribute("disabled");
|
|
281
285
|
}
|
|
282
286
|
this.clearEl.disabled = this.disabled || this.valueInternal.length === 0;
|
|
287
|
+
this.applyFieldSupport(this.labelEl, this.inputEl, this.descriptionEl);
|
|
283
288
|
this.applyInvalidState(this.inputEl, this.errorEl);
|
|
284
289
|
}
|
|
285
290
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
|
|
1
|
+
import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
|
|
2
2
|
import { boeControl, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
3
|
const DEFAULT_TAG_NAME = "box-select";
|
|
4
4
|
const escapeHtml = (value) => value
|
|
@@ -64,11 +64,12 @@ const selectStyles = `
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
${boeFormFieldErrorStyles}
|
|
67
|
+
${boeFormFieldSupportStyles}
|
|
67
68
|
`;
|
|
68
69
|
export class BoxSelectElement extends FormAssociatedElement {
|
|
69
70
|
static get observedAttributes() {
|
|
70
71
|
return [
|
|
71
|
-
...FormAssociatedElement.
|
|
72
|
+
...FormAssociatedElement.fieldObservedAttributes,
|
|
72
73
|
"disabled",
|
|
73
74
|
"label",
|
|
74
75
|
"options",
|
|
@@ -78,6 +79,7 @@ export class BoxSelectElement extends FormAssociatedElement {
|
|
|
78
79
|
valueInternal = "";
|
|
79
80
|
selectEl;
|
|
80
81
|
labelEl;
|
|
82
|
+
descriptionEl;
|
|
81
83
|
errorEl;
|
|
82
84
|
get value() {
|
|
83
85
|
return this.valueInternal;
|
|
@@ -147,11 +149,13 @@ export class BoxSelectElement extends FormAssociatedElement {
|
|
|
147
149
|
<style>${selectStyles}</style>
|
|
148
150
|
<label part="field">
|
|
149
151
|
<span part="label"></span>
|
|
152
|
+
${formDescriptionMarkup()}
|
|
150
153
|
<select part="select"></select>
|
|
151
154
|
${formErrorMessageMarkup()}
|
|
152
155
|
</label>
|
|
153
156
|
`;
|
|
154
157
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
158
|
+
this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
|
|
155
159
|
this.selectEl = this.shadowRoot.querySelector('[part="select"]');
|
|
156
160
|
this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
|
|
157
161
|
}
|
|
@@ -185,6 +189,7 @@ export class BoxSelectElement extends FormAssociatedElement {
|
|
|
185
189
|
else {
|
|
186
190
|
this.selectEl.removeAttribute("disabled");
|
|
187
191
|
}
|
|
192
|
+
this.applyFieldSupport(this.labelEl, this.selectEl, this.descriptionEl);
|
|
188
193
|
this.applyInvalidState(this.selectEl, this.errorEl);
|
|
189
194
|
}
|
|
190
195
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
|
|
1
|
+
import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
|
|
2
2
|
import { boeControl, boeInputControlStyles, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
3
|
const DEFAULT_TAG_NAME = "box-text-field";
|
|
4
4
|
/** Input chrome tracks BUE `@mixin box-inputs` (`src/styles/_inputs.scss`). */
|
|
@@ -34,11 +34,12 @@ const textFieldStyles = `
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
${boeFormFieldErrorStyles}
|
|
37
|
+
${boeFormFieldSupportStyles}
|
|
37
38
|
`;
|
|
38
39
|
export class BoxTextFieldElement extends FormAssociatedElement {
|
|
39
40
|
static get observedAttributes() {
|
|
40
41
|
return [
|
|
41
|
-
...FormAssociatedElement.
|
|
42
|
+
...FormAssociatedElement.fieldObservedAttributes,
|
|
42
43
|
"disabled",
|
|
43
44
|
"label",
|
|
44
45
|
"placeholder",
|
|
@@ -48,6 +49,7 @@ export class BoxTextFieldElement extends FormAssociatedElement {
|
|
|
48
49
|
valueInternal = "";
|
|
49
50
|
inputEl;
|
|
50
51
|
labelEl;
|
|
52
|
+
descriptionEl;
|
|
51
53
|
errorEl;
|
|
52
54
|
get value() {
|
|
53
55
|
return this.valueInternal;
|
|
@@ -102,11 +104,13 @@ export class BoxTextFieldElement extends FormAssociatedElement {
|
|
|
102
104
|
<style>${textFieldStyles}</style>
|
|
103
105
|
<label part="field">
|
|
104
106
|
<span part="label"></span>
|
|
107
|
+
${formDescriptionMarkup()}
|
|
105
108
|
<input type="text" part="input" />
|
|
106
109
|
${formErrorMessageMarkup()}
|
|
107
110
|
</label>
|
|
108
111
|
`;
|
|
109
112
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
113
|
+
this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
|
|
110
114
|
this.inputEl = this.shadowRoot.querySelector('[part="input"]');
|
|
111
115
|
this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
|
|
112
116
|
}
|
|
@@ -138,6 +142,8 @@ export class BoxTextFieldElement extends FormAssociatedElement {
|
|
|
138
142
|
else {
|
|
139
143
|
this.inputEl.removeAttribute("disabled");
|
|
140
144
|
}
|
|
145
|
+
// After the label text is (re)set, since it clears the required mark.
|
|
146
|
+
this.applyFieldSupport(this.labelEl, this.inputEl, this.descriptionEl);
|
|
141
147
|
this.applyInvalidState(this.inputEl, this.errorEl);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface BreadcrumbItem {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Optional link target. Omit for a purely JS-driven crumb. */
|
|
5
|
+
href?: string;
|
|
6
|
+
/** Value reported on `navigate`; defaults to `href` or `label`. */
|
|
7
|
+
value?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* File-path breadcrumb trail with overflow collapse — box-ui-elements
|
|
11
|
+
* `Breadcrumb`. When the trail is longer than `max-items` the middle crumbs
|
|
12
|
+
* collapse into an ellipsis, always keeping the first crumb and the last two.
|
|
13
|
+
*/
|
|
14
|
+
export declare class BoxBreadcrumbElement extends BaseElement {
|
|
15
|
+
static get observedAttributes(): string[];
|
|
16
|
+
private listEl;
|
|
17
|
+
get items(): BreadcrumbItem[];
|
|
18
|
+
set items(value: BreadcrumbItem[]);
|
|
19
|
+
get label(): string;
|
|
20
|
+
set label(value: string);
|
|
21
|
+
/** Max crumbs shown before the middle collapses to an ellipsis. 0 disables collapse. */
|
|
22
|
+
get maxItems(): number;
|
|
23
|
+
set maxItems(value: number);
|
|
24
|
+
/** Keep the first crumb + last two, collapsing the middle into an ellipsis. */
|
|
25
|
+
private collapse;
|
|
26
|
+
protected renderTemplate(): void;
|
|
27
|
+
protected setupListeners(): void;
|
|
28
|
+
protected update(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare const defineBoxBreadcrumbElement: (tagName?: string) => typeof BoxBreadcrumbElement;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
4
|
+
const DEFAULT_TAG_NAME = "box-breadcrumb";
|
|
5
|
+
const escapeHtml = (value) => value
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """);
|
|
10
|
+
const breadcrumbStyles = `
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
color: inherit;
|
|
14
|
+
font: inherit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[part="list"] {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: ${boeSpace[1]};
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
list-style: none;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[part="crumb"] {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[part="link"] {
|
|
34
|
+
appearance: none;
|
|
35
|
+
border: none;
|
|
36
|
+
background: none;
|
|
37
|
+
font: inherit;
|
|
38
|
+
padding: 2px 6px;
|
|
39
|
+
border-radius: ${boeRadius.med};
|
|
40
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
max-width: 22ch;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
text-overflow: ellipsis;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
transition:
|
|
48
|
+
color ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
|
|
49
|
+
background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[part="link"]:hover {
|
|
53
|
+
color: var(--boe-token-text-text, #222222);
|
|
54
|
+
background: var(--boe-token-surface-surface-secondary, #f4f4f4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[part="link"]:focus-visible {
|
|
58
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
59
|
+
outline-offset: 1px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[part="link"][aria-current="page"] {
|
|
63
|
+
color: var(--boe-token-text-text, #222222);
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
cursor: default;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[part="separator"] {
|
|
69
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
70
|
+
user-select: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="ellipsis"] {
|
|
74
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
75
|
+
padding: 0 2px;
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
/** A collapsed marker inserted when the trail exceeds `max-items`. */
|
|
79
|
+
const ELLIPSIS = Symbol("ellipsis");
|
|
80
|
+
/**
|
|
81
|
+
* File-path breadcrumb trail with overflow collapse — box-ui-elements
|
|
82
|
+
* `Breadcrumb`. When the trail is longer than `max-items` the middle crumbs
|
|
83
|
+
* collapse into an ellipsis, always keeping the first crumb and the last two.
|
|
84
|
+
*/
|
|
85
|
+
export class BoxBreadcrumbElement extends BaseElement {
|
|
86
|
+
static get observedAttributes() {
|
|
87
|
+
return ["items", "label", "max-items"];
|
|
88
|
+
}
|
|
89
|
+
listEl;
|
|
90
|
+
get items() {
|
|
91
|
+
const raw = this.getAttribute("items");
|
|
92
|
+
if (!raw) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
set items(value) {
|
|
104
|
+
this.setAttribute("items", JSON.stringify(value));
|
|
105
|
+
}
|
|
106
|
+
get label() {
|
|
107
|
+
return this.getAttribute("label") ?? "Breadcrumb";
|
|
108
|
+
}
|
|
109
|
+
set label(value) {
|
|
110
|
+
this.setAttribute("label", value);
|
|
111
|
+
}
|
|
112
|
+
/** Max crumbs shown before the middle collapses to an ellipsis. 0 disables collapse. */
|
|
113
|
+
get maxItems() {
|
|
114
|
+
const raw = Number(this.getAttribute("max-items"));
|
|
115
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 4;
|
|
116
|
+
}
|
|
117
|
+
set maxItems(value) {
|
|
118
|
+
this.setAttribute("max-items", String(value));
|
|
119
|
+
}
|
|
120
|
+
/** Keep the first crumb + last two, collapsing the middle into an ellipsis. */
|
|
121
|
+
collapse(items) {
|
|
122
|
+
const max = this.maxItems;
|
|
123
|
+
if (items.length <= max) {
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
return [items[0], ELLIPSIS, ...items.slice(-2)];
|
|
127
|
+
}
|
|
128
|
+
renderTemplate() {
|
|
129
|
+
if (!this.shadowRoot) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.shadowRoot.innerHTML = `
|
|
133
|
+
<style>${breadcrumbStyles}</style>
|
|
134
|
+
<nav part="nav">
|
|
135
|
+
<ol part="list"></ol>
|
|
136
|
+
</nav>
|
|
137
|
+
`;
|
|
138
|
+
this.listEl = this.shadowRoot.querySelector('[part="list"]');
|
|
139
|
+
}
|
|
140
|
+
setupListeners() {
|
|
141
|
+
this.listEl.addEventListener("click", event => {
|
|
142
|
+
const link = event.target.closest('[part="link"]');
|
|
143
|
+
if (!link || link.getAttribute("aria-current") === "page") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const value = link.dataset.value ?? "";
|
|
147
|
+
const href = link.getAttribute("href") ?? undefined;
|
|
148
|
+
// JS-only crumbs (no href) shouldn't navigate the browser.
|
|
149
|
+
if (!href) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
}
|
|
152
|
+
this.dispatchEvent(new CustomEvent("navigate", {
|
|
153
|
+
bubbles: true,
|
|
154
|
+
composed: true,
|
|
155
|
+
detail: { value, href },
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
update() {
|
|
160
|
+
if (!this.listEl) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.shadowRoot?.querySelector('[part="nav"]')?.setAttribute("aria-label", this.label);
|
|
164
|
+
const crumbs = this.collapse(this.items);
|
|
165
|
+
const lastIndex = crumbs.length - 1;
|
|
166
|
+
this.listEl.innerHTML = crumbs
|
|
167
|
+
.map((crumb, index) => {
|
|
168
|
+
const separator = index < lastIndex ? `<span part="separator" aria-hidden="true">/</span>` : "";
|
|
169
|
+
if (crumb === ELLIPSIS) {
|
|
170
|
+
return `<li part="crumb"><span part="ellipsis" aria-label="Hidden path">…</span>${separator}</li>`;
|
|
171
|
+
}
|
|
172
|
+
const isLast = index === lastIndex;
|
|
173
|
+
const value = escapeHtml(crumb.value ?? crumb.href ?? crumb.label);
|
|
174
|
+
const tag = crumb.href && !isLast ? "a" : "button";
|
|
175
|
+
const hrefAttr = crumb.href && !isLast ? ` href="${escapeHtml(crumb.href)}"` : "";
|
|
176
|
+
const typeAttr = tag === "button" ? ' type="button"' : "";
|
|
177
|
+
const current = isLast ? ' aria-current="page"' : "";
|
|
178
|
+
return `<li part="crumb"><${tag} part="link" data-value="${value}"${hrefAttr}${typeAttr}${current}>${escapeHtml(crumb.label)}</${tag}>${separator}</li>`;
|
|
179
|
+
})
|
|
180
|
+
.join("");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export const defineBoxBreadcrumbElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
184
|
+
const existingElement = customElements.get(tagName);
|
|
185
|
+
if (existingElement) {
|
|
186
|
+
return existingElement;
|
|
187
|
+
}
|
|
188
|
+
customElements.define(tagName, BoxBreadcrumbElement);
|
|
189
|
+
return BoxBreadcrumbElement;
|
|
190
|
+
};
|
|
@@ -8,6 +8,8 @@ export declare class BoxTabsElement extends BaseElement {
|
|
|
8
8
|
private valueInternal;
|
|
9
9
|
private lastOptionsJson;
|
|
10
10
|
private tabsEl;
|
|
11
|
+
private panelsEl;
|
|
12
|
+
private readonly uid;
|
|
11
13
|
get label(): string;
|
|
12
14
|
set label(value: string);
|
|
13
15
|
get layout(): "attached" | "separated";
|
|
@@ -19,6 +21,8 @@ export declare class BoxTabsElement extends BaseElement {
|
|
|
19
21
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
20
22
|
private selectValue;
|
|
21
23
|
private renderTabsMarkup;
|
|
24
|
+
/** One tabpanel per option, fed by a slot named for the option value. */
|
|
25
|
+
private renderPanelsMarkup;
|
|
22
26
|
protected renderTemplate(): void;
|
|
23
27
|
protected setupListeners(): void;
|
|
24
28
|
protected update(): void;
|