@unofficialbox/box-open-elements 0.3.0 → 0.4.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/dist/components/actions/link-button.d.ts +9 -0
- package/dist/components/actions/link-button.js +55 -4
- package/dist/components/collections/carousel.d.ts +7 -0
- package/dist/components/collections/carousel.js +71 -4
- package/dist/components/collections/datalist-item.d.ts +6 -0
- package/dist/components/collections/datalist-item.js +39 -1
- package/dist/components/collections/draggable-list.js +1 -1
- package/dist/components/collections/grid-view.js +7 -3
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +30 -1
- package/dist/components/feedback/badge.d.ts +10 -0
- package/dist/components/feedback/badge.js +70 -4
- package/dist/components/feedback/chip.d.ts +5 -0
- package/dist/components/feedback/chip.js +74 -2
- package/dist/components/feedback/error-mask.d.ts +2 -0
- package/dist/components/feedback/error-mask.js +23 -0
- package/dist/components/feedback/help-text.js +7 -0
- package/dist/components/feedback/spinner.d.ts +4 -0
- package/dist/components/feedback/spinner.js +30 -2
- package/dist/components/feedback/toast.d.ts +5 -0
- package/dist/components/feedback/toast.js +59 -11
- package/dist/components/forms/category-selector.d.ts +11 -0
- package/dist/components/forms/category-selector.js +211 -17
- package/dist/components/forms/checkbox.d.ts +6 -0
- package/dist/components/forms/checkbox.js +58 -1
- package/dist/components/forms/combobox.d.ts +30 -1
- package/dist/components/forms/combobox.js +348 -44
- package/dist/components/forms/date-field.d.ts +7 -0
- package/dist/components/forms/date-field.js +73 -1
- package/dist/components/forms/dropdown.d.ts +5 -0
- package/dist/components/forms/dropdown.js +32 -3
- package/dist/components/forms/radio-group.d.ts +9 -8
- package/dist/components/forms/radio-group.js +43 -6
- package/dist/components/forms/search-field.d.ts +8 -0
- package/dist/components/forms/search-field.js +55 -13
- package/dist/components/forms/select.d.ts +18 -8
- package/dist/components/forms/select.js +104 -14
- package/dist/components/forms/text-field.d.ts +13 -0
- package/dist/components/forms/text-field.js +136 -1
- package/dist/components/forms/time-field.d.ts +11 -0
- package/dist/components/forms/time-field.js +51 -0
- package/dist/components/identity/avatar.d.ts +4 -0
- package/dist/components/identity/avatar.js +65 -1
- package/dist/components/identity/contact-datalist-item.d.ts +8 -0
- package/dist/components/identity/contact-datalist-item.js +71 -2
- package/dist/components/layout/nav-sidebar.js +23 -0
- package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
- package/dist/components/layout/sidebar-toggle-button.js +21 -2
- package/dist/components/navigation/accordion.d.ts +3 -0
- package/dist/components/navigation/accordion.js +17 -2
- package/dist/components/overlays/guide-tooltip.d.ts +51 -0
- package/dist/components/overlays/guide-tooltip.js +361 -0
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +74 -11
- package/dist/foundations/overlay/positioning.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -2,6 +2,14 @@ import { BaseElement } from "../../core/index.js";
|
|
|
2
2
|
export declare class BoxLinkButtonElement extends BaseElement {
|
|
3
3
|
static get observedAttributes(): string[];
|
|
4
4
|
private linkEl;
|
|
5
|
+
private labelEl;
|
|
6
|
+
private contentSlot;
|
|
7
|
+
/** Anchor target (e.g. `_blank`). Opening a new tab auto-adds a safe rel. */
|
|
8
|
+
get target(): string;
|
|
9
|
+
set target(value: string);
|
|
10
|
+
/** Explicit rel; defaults to `noopener noreferrer` when target is `_blank`. */
|
|
11
|
+
get rel(): string;
|
|
12
|
+
set rel(value: string);
|
|
5
13
|
get href(): string;
|
|
6
14
|
set href(value: string);
|
|
7
15
|
get label(): string;
|
|
@@ -9,6 +17,7 @@ export declare class BoxLinkButtonElement extends BaseElement {
|
|
|
9
17
|
get tone(): string;
|
|
10
18
|
set tone(value: string);
|
|
11
19
|
protected renderTemplate(): void;
|
|
20
|
+
protected setupListeners(): void;
|
|
12
21
|
protected update(): void;
|
|
13
22
|
}
|
|
14
23
|
export declare const defineBoxLinkButtonElement: (tagName?: string) => typeof BoxLinkButtonElement;
|
|
@@ -86,9 +86,25 @@ const linkButtonStyles = `
|
|
|
86
86
|
`;
|
|
87
87
|
export class BoxLinkButtonElement extends BaseElement {
|
|
88
88
|
static get observedAttributes() {
|
|
89
|
-
return ["href", "label", "tone"];
|
|
89
|
+
return ["href", "label", "tone", "target", "rel"];
|
|
90
90
|
}
|
|
91
91
|
linkEl;
|
|
92
|
+
labelEl;
|
|
93
|
+
contentSlot;
|
|
94
|
+
/** Anchor target (e.g. `_blank`). Opening a new tab auto-adds a safe rel. */
|
|
95
|
+
get target() {
|
|
96
|
+
return this.getAttribute("target") ?? "";
|
|
97
|
+
}
|
|
98
|
+
set target(value) {
|
|
99
|
+
this.setAttribute("target", value);
|
|
100
|
+
}
|
|
101
|
+
/** Explicit rel; defaults to `noopener noreferrer` when target is `_blank`. */
|
|
102
|
+
get rel() {
|
|
103
|
+
return this.getAttribute("rel") ?? "";
|
|
104
|
+
}
|
|
105
|
+
set rel(value) {
|
|
106
|
+
this.setAttribute("rel", value);
|
|
107
|
+
}
|
|
92
108
|
get href() {
|
|
93
109
|
return this.getAttribute("href") ?? "#";
|
|
94
110
|
}
|
|
@@ -113,9 +129,18 @@ export class BoxLinkButtonElement extends BaseElement {
|
|
|
113
129
|
}
|
|
114
130
|
this.shadowRoot.innerHTML = `
|
|
115
131
|
<style>${linkButtonStyles}</style>
|
|
116
|
-
<a part="link"></a>
|
|
132
|
+
<a part="link"><slot><span part="label"></span></slot></a>
|
|
117
133
|
`;
|
|
118
134
|
this.linkEl = this.shadowRoot.querySelector('[part="link"]');
|
|
135
|
+
this.contentSlot = this.shadowRoot.querySelector("slot");
|
|
136
|
+
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
137
|
+
}
|
|
138
|
+
setupListeners() {
|
|
139
|
+
this.contentSlot.addEventListener("slotchange", () => {
|
|
140
|
+
if (this.isRendered) {
|
|
141
|
+
this.update();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
119
144
|
}
|
|
120
145
|
update() {
|
|
121
146
|
if (!this.linkEl) {
|
|
@@ -123,8 +148,34 @@ export class BoxLinkButtonElement extends BaseElement {
|
|
|
123
148
|
}
|
|
124
149
|
this.linkEl.dataset.tone = this.tone;
|
|
125
150
|
this.linkEl.setAttribute("href", escapeHtml(safeHref(this.href)));
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
// Rich children (slotted) replace the `label` text; keep the fallback in
|
|
152
|
+
// sync. Detect real host content only — assignedNodes() without flatten
|
|
153
|
+
// excludes the fallback span; ignore whitespace-only text nodes.
|
|
154
|
+
const hasSlotted = this.contentSlot
|
|
155
|
+
.assignedNodes()
|
|
156
|
+
.some(node => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? "").trim() !== "");
|
|
157
|
+
this.labelEl.textContent = this.label;
|
|
158
|
+
if (hasSlotted) {
|
|
159
|
+
this.linkEl.removeAttribute("aria-label");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.linkEl.setAttribute("aria-label", this.label);
|
|
163
|
+
}
|
|
164
|
+
const target = this.target;
|
|
165
|
+
if (target) {
|
|
166
|
+
this.linkEl.setAttribute("target", target);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
this.linkEl.removeAttribute("target");
|
|
170
|
+
}
|
|
171
|
+
// Explicit rel wins; opening a new tab defaults to a safe rel.
|
|
172
|
+
const rel = this.rel || (target === "_blank" ? "noopener noreferrer" : "");
|
|
173
|
+
if (rel) {
|
|
174
|
+
this.linkEl.setAttribute("rel", rel);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this.linkEl.removeAttribute("rel");
|
|
178
|
+
}
|
|
128
179
|
}
|
|
129
180
|
}
|
|
130
181
|
export const defineBoxLinkButtonElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
@@ -12,6 +12,9 @@ export declare class BoxCarouselElement extends BaseElement {
|
|
|
12
12
|
private carouselEl;
|
|
13
13
|
private labelEl;
|
|
14
14
|
private stageEl;
|
|
15
|
+
private viewportEl;
|
|
16
|
+
private slottedStageEl;
|
|
17
|
+
private slideSlot;
|
|
15
18
|
private mediaEl;
|
|
16
19
|
private eyebrowEl;
|
|
17
20
|
private titleEl;
|
|
@@ -28,6 +31,10 @@ export declare class BoxCarouselElement extends BaseElement {
|
|
|
28
31
|
get value(): number;
|
|
29
32
|
set value(value: number);
|
|
30
33
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
34
|
+
/** Slotted slides (elements assigned to the `slide` slot). */
|
|
35
|
+
private slottedSlides;
|
|
36
|
+
/** Active slide count — slotted slides when present, else JSON items. */
|
|
37
|
+
private slideCount;
|
|
31
38
|
private normalizeIndex;
|
|
32
39
|
private emitValueChanged;
|
|
33
40
|
private setIndex;
|
|
@@ -182,6 +182,15 @@ const carouselStyles = `
|
|
|
182
182
|
line-height: 1.58;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/* Slotted-slides mode: only the active slide is shown. */
|
|
186
|
+
[part="slotted-stage"][hidden] {
|
|
187
|
+
display: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
[part="slotted-stage"] ::slotted(:not([data-carousel-active])) {
|
|
191
|
+
display: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
185
194
|
@media (max-width: 720px) {
|
|
186
195
|
[part="viewport"] {
|
|
187
196
|
grid-template-columns: 1fr;
|
|
@@ -197,6 +206,9 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
197
206
|
carouselEl;
|
|
198
207
|
labelEl;
|
|
199
208
|
stageEl;
|
|
209
|
+
viewportEl;
|
|
210
|
+
slottedStageEl;
|
|
211
|
+
slideSlot;
|
|
200
212
|
mediaEl;
|
|
201
213
|
eyebrowEl;
|
|
202
214
|
titleEl;
|
|
@@ -244,8 +256,19 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
244
256
|
}
|
|
245
257
|
super.attributeChangedCallback(name, oldValue, newValue);
|
|
246
258
|
}
|
|
259
|
+
/** Slotted slides (elements assigned to the `slide` slot). */
|
|
260
|
+
slottedSlides() {
|
|
261
|
+
return this.slideSlot
|
|
262
|
+
? this.slideSlot.assignedElements({ flatten: true })
|
|
263
|
+
: [];
|
|
264
|
+
}
|
|
265
|
+
/** Active slide count — slotted slides when present, else JSON items. */
|
|
266
|
+
slideCount() {
|
|
267
|
+
const slotted = this.slottedSlides().length;
|
|
268
|
+
return slotted > 0 ? slotted : this.items.length;
|
|
269
|
+
}
|
|
247
270
|
normalizeIndex(value) {
|
|
248
|
-
const maxIndex = Math.max(0, this.
|
|
271
|
+
const maxIndex = Math.max(0, this.slideCount() - 1);
|
|
249
272
|
if (!Number.isFinite(value)) {
|
|
250
273
|
return 0;
|
|
251
274
|
}
|
|
@@ -265,10 +288,11 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
265
288
|
this.emitValueChanged(nextIndex);
|
|
266
289
|
}
|
|
267
290
|
changeBy(delta) {
|
|
268
|
-
|
|
291
|
+
const count = this.slideCount();
|
|
292
|
+
if (count === 0) {
|
|
269
293
|
return;
|
|
270
294
|
}
|
|
271
|
-
const nextIndex = (this.valueInternal + delta +
|
|
295
|
+
const nextIndex = (this.valueInternal + delta + count) % count;
|
|
272
296
|
this.setIndex(nextIndex);
|
|
273
297
|
}
|
|
274
298
|
renderTemplate() {
|
|
@@ -288,6 +312,7 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
288
312
|
<div part="description" hidden></div>
|
|
289
313
|
</div>
|
|
290
314
|
</div>
|
|
315
|
+
<div part="slotted-stage" hidden><slot name="slide"></slot></div>
|
|
291
316
|
</div>
|
|
292
317
|
<div part="controls">
|
|
293
318
|
<div part="nav">
|
|
@@ -302,6 +327,9 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
302
327
|
this.carouselEl = this.shadowRoot.querySelector('[part="carousel"]');
|
|
303
328
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
304
329
|
this.stageEl = this.shadowRoot.querySelector('[part="stage"]');
|
|
330
|
+
this.viewportEl = this.shadowRoot.querySelector('[part="viewport"]');
|
|
331
|
+
this.slottedStageEl = this.shadowRoot.querySelector('[part="slotted-stage"]');
|
|
332
|
+
this.slideSlot = this.shadowRoot.querySelector('slot[name="slide"]');
|
|
305
333
|
this.mediaEl = this.shadowRoot.querySelector('[part="media"]');
|
|
306
334
|
this.eyebrowEl = this.shadowRoot.querySelector('[part="eyebrow"]');
|
|
307
335
|
this.titleEl = this.shadowRoot.querySelector('[part="title"]');
|
|
@@ -315,6 +343,12 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
315
343
|
setupListeners() {
|
|
316
344
|
this.previousEl.addEventListener("click", () => this.changeBy(-1));
|
|
317
345
|
this.nextEl.addEventListener("click", () => this.changeBy(1));
|
|
346
|
+
this.slideSlot.addEventListener("slotchange", () => {
|
|
347
|
+
this.valueInternal = this.normalizeIndex(this.valueInternal);
|
|
348
|
+
if (this.isRendered) {
|
|
349
|
+
this.update();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
318
352
|
this.paginationEl.addEventListener("click", event => {
|
|
319
353
|
const target = event.target?.closest('[part~="dot"]');
|
|
320
354
|
if (!target || !this.paginationEl.contains(target)) {
|
|
@@ -342,11 +376,44 @@ export class BoxCarouselElement extends BaseElement {
|
|
|
342
376
|
}
|
|
343
377
|
const items = this.items;
|
|
344
378
|
const itemsJson = this.getAttribute("items") ?? "";
|
|
345
|
-
const activeItem = items[this.valueInternal] ?? null;
|
|
346
379
|
const label = this.label;
|
|
347
380
|
this.carouselEl.setAttribute("aria-label", label);
|
|
348
381
|
this.labelEl.textContent = label;
|
|
349
382
|
this.paginationEl.setAttribute("aria-label", `${label} slides`);
|
|
383
|
+
// Slotted-slides mode takes precedence when the host provides slide content.
|
|
384
|
+
const slotted = this.slottedSlides();
|
|
385
|
+
if (slotted.length > 0) {
|
|
386
|
+
this.viewportEl.hidden = true;
|
|
387
|
+
this.slottedStageEl.hidden = false;
|
|
388
|
+
this.stageEl.hidden = false;
|
|
389
|
+
this.controlsEl.hidden = false;
|
|
390
|
+
this.emptyEl.hidden = true;
|
|
391
|
+
slotted.forEach((slide, index) => {
|
|
392
|
+
const active = index === this.valueInternal;
|
|
393
|
+
slide.toggleAttribute("data-carousel-active", active);
|
|
394
|
+
slide.setAttribute("aria-hidden", active ? "false" : "true");
|
|
395
|
+
});
|
|
396
|
+
this.paginationEl.innerHTML = slotted
|
|
397
|
+
.map((slide, index) => {
|
|
398
|
+
const dotPart = index === this.valueInternal ? "dot dot-selected" : "dot";
|
|
399
|
+
const title = slide.getAttribute("data-title") ?? slide.getAttribute("aria-label") ?? `${index + 1}`;
|
|
400
|
+
return `
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
part="${dotPart}"
|
|
404
|
+
aria-label="Go to slide ${index + 1}: ${escapeHtml(title)}"
|
|
405
|
+
aria-current="${index === this.valueInternal ? "true" : "false"}"
|
|
406
|
+
data-index="${index}"
|
|
407
|
+
></button>
|
|
408
|
+
`;
|
|
409
|
+
})
|
|
410
|
+
.join("");
|
|
411
|
+
this.lastItemsJson = "";
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
this.slottedStageEl.hidden = true;
|
|
415
|
+
this.viewportEl.hidden = false;
|
|
416
|
+
const activeItem = items[this.valueInternal] ?? null;
|
|
350
417
|
if (!activeItem) {
|
|
351
418
|
this.stageEl.hidden = true;
|
|
352
419
|
this.controlsEl.hidden = true;
|
|
@@ -10,8 +10,13 @@ export declare class BoxDatalistItemElement extends BaseElement {
|
|
|
10
10
|
static get observedAttributes(): string[];
|
|
11
11
|
private itemEl;
|
|
12
12
|
private thumbEl;
|
|
13
|
+
private bodyEl;
|
|
13
14
|
private labelEl;
|
|
14
15
|
private metaEl;
|
|
16
|
+
private contentSlot;
|
|
17
|
+
/** Active-descendant highlight — set by a host that keeps DOM focus elsewhere. */
|
|
18
|
+
get active(): boolean;
|
|
19
|
+
set active(value: boolean);
|
|
15
20
|
get label(): string;
|
|
16
21
|
set label(value: string);
|
|
17
22
|
get meta(): string;
|
|
@@ -27,6 +32,7 @@ export declare class BoxDatalistItemElement extends BaseElement {
|
|
|
27
32
|
private choose;
|
|
28
33
|
protected renderTemplate(): void;
|
|
29
34
|
protected setupListeners(): void;
|
|
35
|
+
private syncSlot;
|
|
30
36
|
protected update(): void;
|
|
31
37
|
}
|
|
32
38
|
export declare const defineBoxDatalistItemElement: (tagName?: string) => typeof BoxDatalistItemElement;
|
|
@@ -28,6 +28,11 @@ const datalistItemStyles = `
|
|
|
28
28
|
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Active-descendant highlight (host sets active; DOM focus stays on the input). */
|
|
32
|
+
[part="item"][data-active="true"] {
|
|
33
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
[part="item"][data-disabled="true"] {
|
|
32
37
|
opacity: 0.55;
|
|
33
38
|
cursor: not-allowed;
|
|
@@ -73,6 +78,16 @@ const datalistItemStyles = `
|
|
|
73
78
|
[part="meta"][hidden] {
|
|
74
79
|
display: none;
|
|
75
80
|
}
|
|
81
|
+
|
|
82
|
+
/* Arbitrary slotted content replaces the default label/meta when present. */
|
|
83
|
+
[part="content"]:not(.has-content) {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[part="body"].has-slotted [part="label"],
|
|
88
|
+
[part="body"].has-slotted [part="meta"] {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
76
91
|
`;
|
|
77
92
|
/**
|
|
78
93
|
* A single selectable row for a picker/typeahead list — a leading glyph, a
|
|
@@ -83,12 +98,21 @@ const datalistItemStyles = `
|
|
|
83
98
|
*/
|
|
84
99
|
export class BoxDatalistItemElement extends BaseElement {
|
|
85
100
|
static get observedAttributes() {
|
|
86
|
-
return ["disabled", "icon", "label", "meta", "selected", "value"];
|
|
101
|
+
return ["active", "disabled", "icon", "label", "meta", "selected", "value"];
|
|
87
102
|
}
|
|
88
103
|
itemEl;
|
|
89
104
|
thumbEl;
|
|
105
|
+
bodyEl;
|
|
90
106
|
labelEl;
|
|
91
107
|
metaEl;
|
|
108
|
+
contentSlot;
|
|
109
|
+
/** Active-descendant highlight — set by a host that keeps DOM focus elsewhere. */
|
|
110
|
+
get active() {
|
|
111
|
+
return this.hasAttribute("active");
|
|
112
|
+
}
|
|
113
|
+
set active(value) {
|
|
114
|
+
this.toggleAttribute("active", value);
|
|
115
|
+
}
|
|
92
116
|
get label() {
|
|
93
117
|
return this.getAttribute("label") ?? "";
|
|
94
118
|
}
|
|
@@ -146,13 +170,16 @@ export class BoxDatalistItemElement extends BaseElement {
|
|
|
146
170
|
<span part="body">
|
|
147
171
|
<span part="label"></span>
|
|
148
172
|
<span part="meta" hidden></span>
|
|
173
|
+
<slot part="content"></slot>
|
|
149
174
|
</span>
|
|
150
175
|
</div>
|
|
151
176
|
`;
|
|
152
177
|
this.itemEl = this.shadowRoot.querySelector('[part="item"]');
|
|
153
178
|
this.thumbEl = this.shadowRoot.querySelector('[part="thumb"]');
|
|
179
|
+
this.bodyEl = this.shadowRoot.querySelector('[part="body"]');
|
|
154
180
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
155
181
|
this.metaEl = this.shadowRoot.querySelector('[part="meta"]');
|
|
182
|
+
this.contentSlot = this.shadowRoot.querySelector('slot[part="content"]');
|
|
156
183
|
}
|
|
157
184
|
setupListeners() {
|
|
158
185
|
this.itemEl.addEventListener("click", () => this.choose());
|
|
@@ -162,6 +189,15 @@ export class BoxDatalistItemElement extends BaseElement {
|
|
|
162
189
|
this.choose();
|
|
163
190
|
}
|
|
164
191
|
});
|
|
192
|
+
this.contentSlot.addEventListener("slotchange", () => this.syncSlot());
|
|
193
|
+
}
|
|
194
|
+
syncSlot() {
|
|
195
|
+
if (!this.contentSlot) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const hasContent = this.contentSlot.assignedNodes({ flatten: true }).length > 0;
|
|
199
|
+
this.contentSlot.classList.toggle("has-content", hasContent);
|
|
200
|
+
this.bodyEl.classList.toggle("has-slotted", hasContent);
|
|
165
201
|
}
|
|
166
202
|
update() {
|
|
167
203
|
if (!this.itemEl || !this.thumbEl || !this.labelEl || !this.metaEl) {
|
|
@@ -173,7 +209,9 @@ export class BoxDatalistItemElement extends BaseElement {
|
|
|
173
209
|
this.itemEl.setAttribute("aria-selected", selected ? "true" : "false");
|
|
174
210
|
this.itemEl.dataset.selected = selected ? "true" : "false";
|
|
175
211
|
this.itemEl.dataset.disabled = disabled ? "true" : "false";
|
|
212
|
+
this.itemEl.dataset.active = this.active ? "true" : "false";
|
|
176
213
|
this.itemEl.tabIndex = disabled ? -1 : 0;
|
|
214
|
+
this.syncSlot();
|
|
177
215
|
if (disabled) {
|
|
178
216
|
this.itemEl.setAttribute("aria-disabled", "true");
|
|
179
217
|
}
|
|
@@ -172,7 +172,7 @@ export class BoxDraggableListElement extends BaseElement {
|
|
|
172
172
|
<path d="M6 4h.01M10 4h.01M6 8h.01M10 8h.01M6 12h.01M10 12h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
173
173
|
</svg>
|
|
174
174
|
</button>
|
|
175
|
-
<span part="item-label">${escapeHtml(item.label)}</span>
|
|
175
|
+
<slot name="row-${escapeHtml(item.value)}" part="item-content"><span part="item-label">${escapeHtml(item.label)}</span></slot>
|
|
176
176
|
</li>
|
|
177
177
|
`;
|
|
178
178
|
})
|
|
@@ -202,6 +202,8 @@ export class BoxGridViewElement extends BaseElement {
|
|
|
202
202
|
const metaMarkup = item.meta
|
|
203
203
|
? `<span part="meta">${escapeHtml(item.meta)}</span>`
|
|
204
204
|
: "";
|
|
205
|
+
// Per-item named slot lets a host override the tile body (slotRenderer
|
|
206
|
+
// parity); the default thumb/label/meta is the slot's fallback content.
|
|
205
207
|
return `
|
|
206
208
|
<button
|
|
207
209
|
type="button"
|
|
@@ -211,9 +213,11 @@ export class BoxGridViewElement extends BaseElement {
|
|
|
211
213
|
aria-selected="${String(isSelected)}"
|
|
212
214
|
tabindex="${item.value === tabbableValue ? "0" : "-1"}"
|
|
213
215
|
>
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
<slot name="tile-${escapeHtml(item.value)}" part="tile-slot">
|
|
217
|
+
<span part="thumb" aria-hidden="true">${iconMarkup}</span>
|
|
218
|
+
<span part="tile-label">${escapeHtml(item.label)}</span>
|
|
219
|
+
${metaMarkup}
|
|
220
|
+
</slot>
|
|
217
221
|
</button>
|
|
218
222
|
`;
|
|
219
223
|
})
|
|
@@ -6,6 +6,7 @@ export declare class BoxAlertElement extends BaseElement {
|
|
|
6
6
|
private toneLabelEl;
|
|
7
7
|
private titleEl;
|
|
8
8
|
private messageEl;
|
|
9
|
+
private richSlot;
|
|
9
10
|
private dismissEl;
|
|
10
11
|
get open(): boolean;
|
|
11
12
|
set open(value: boolean);
|
|
@@ -21,6 +22,7 @@ export declare class BoxAlertElement extends BaseElement {
|
|
|
21
22
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
22
23
|
dismiss(): void;
|
|
23
24
|
protected renderTemplate(): void;
|
|
25
|
+
private hasRichContent;
|
|
24
26
|
protected setupListeners(): void;
|
|
25
27
|
protected update(): void;
|
|
26
28
|
}
|
|
@@ -90,6 +90,22 @@ const alertStyles = `
|
|
|
90
90
|
display: none;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/* Rich body — slotted markup (links, buttons) beyond the plain message.
|
|
94
|
+
Hidden (taking no grid gap) until content is assigned. */
|
|
95
|
+
[part="rich"] {
|
|
96
|
+
display: block;
|
|
97
|
+
line-height: 1.45;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[part="rich"]:not(.has-content) {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[part="rich"]::slotted(a) {
|
|
105
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
.sr-only {
|
|
94
110
|
position: absolute;
|
|
95
111
|
inline-size: 1px;
|
|
@@ -129,6 +145,7 @@ export class BoxAlertElement extends BaseElement {
|
|
|
129
145
|
toneLabelEl;
|
|
130
146
|
titleEl;
|
|
131
147
|
messageEl;
|
|
148
|
+
richSlot;
|
|
132
149
|
dismissEl;
|
|
133
150
|
get open() {
|
|
134
151
|
return this.openValue;
|
|
@@ -202,6 +219,7 @@ export class BoxAlertElement extends BaseElement {
|
|
|
202
219
|
<span part="tone-label" class="sr-only"></span>
|
|
203
220
|
<h2 part="title" id="alert-title" hidden></h2>
|
|
204
221
|
<span part="description message" hidden></span>
|
|
222
|
+
<slot part="rich"></slot>
|
|
205
223
|
</div>
|
|
206
224
|
<button type="button" part="dismiss" aria-label="Dismiss alert">Dismiss</button>
|
|
207
225
|
</div>
|
|
@@ -210,19 +228,30 @@ export class BoxAlertElement extends BaseElement {
|
|
|
210
228
|
this.toneLabelEl = this.shadowRoot.querySelector('[part="tone-label"]');
|
|
211
229
|
this.titleEl = this.shadowRoot.querySelector('[part="title"]');
|
|
212
230
|
this.messageEl = this.shadowRoot.querySelector('[part~="description"]');
|
|
231
|
+
this.richSlot = this.shadowRoot.querySelector('slot[part="rich"]');
|
|
213
232
|
this.dismissEl = this.shadowRoot.querySelector('[part="dismiss"]');
|
|
214
233
|
}
|
|
234
|
+
hasRichContent() {
|
|
235
|
+
return this.richSlot?.assignedNodes({ flatten: true }).length > 0;
|
|
236
|
+
}
|
|
215
237
|
setupListeners() {
|
|
216
238
|
this.dismissEl.addEventListener("click", () => {
|
|
217
239
|
this.dismiss();
|
|
218
240
|
});
|
|
241
|
+
this.richSlot.addEventListener("slotchange", () => {
|
|
242
|
+
if (this.isRendered) {
|
|
243
|
+
this.update();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
219
246
|
}
|
|
220
247
|
update() {
|
|
221
248
|
if (!this.alertEl) {
|
|
222
249
|
return;
|
|
223
250
|
}
|
|
224
|
-
const
|
|
251
|
+
const hasRich = this.hasRichContent();
|
|
252
|
+
const visible = this.openValue && Boolean(this.heading || this.message || hasRich);
|
|
225
253
|
this.hidden = !visible;
|
|
254
|
+
this.richSlot.classList.toggle("has-content", hasRich);
|
|
226
255
|
if (!visible) {
|
|
227
256
|
return;
|
|
228
257
|
}
|
|
@@ -2,10 +2,20 @@ import { BaseElement } from "../../core/index.js";
|
|
|
2
2
|
export declare class BoxBadgeElement extends BaseElement {
|
|
3
3
|
static get observedAttributes(): string[];
|
|
4
4
|
private badgeEl;
|
|
5
|
+
private lastText;
|
|
5
6
|
get label(): string;
|
|
6
7
|
set label(value: string);
|
|
7
8
|
get tone(): string;
|
|
8
9
|
set tone(value: string);
|
|
10
|
+
/** When the label is a count, cap the displayed number, e.g. max=99 → "99+". */
|
|
11
|
+
get max(): number | null;
|
|
12
|
+
set max(value: number | null);
|
|
13
|
+
/** Hide the badge entirely when the count is zero or the label is empty. */
|
|
14
|
+
get hideWhenZero(): boolean;
|
|
15
|
+
set hideWhenZero(value: boolean);
|
|
16
|
+
/** The label after applying the `max` cap (e.g. "128" with max=99 → "99+"). */
|
|
17
|
+
private displayText;
|
|
18
|
+
private isEmptyCount;
|
|
9
19
|
protected renderTemplate(): void;
|
|
10
20
|
protected update(): void;
|
|
11
21
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
3
|
-
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
3
|
+
import { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, } from "../../foundations/motion/index.js";
|
|
4
4
|
const DEFAULT_TAG_NAME = "box-badge";
|
|
5
5
|
const badgeStyles = `
|
|
6
6
|
:host {
|
|
@@ -52,12 +52,31 @@ const badgeStyles = `
|
|
|
52
52
|
background: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
|
|
53
53
|
color: #ffffff;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/* Hidden when used as a count that is zero/empty with hide-when-zero. */
|
|
57
|
+
:host([hidden]) {
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Count "pop" when the value changes (opt-in via the animate attribute). */
|
|
62
|
+
[part="badge"].boe-pop {
|
|
63
|
+
animation: boe-badge-pop ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes boe-badge-pop {
|
|
67
|
+
0% { transform: scale(0.7); }
|
|
68
|
+
60% { transform: scale(1.15); }
|
|
69
|
+
100% { transform: scale(1); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
${boeReducedMotionStyles('[part="badge"].boe-pop', "animation: none;")}
|
|
55
73
|
`;
|
|
56
74
|
export class BoxBadgeElement extends BaseElement {
|
|
57
75
|
static get observedAttributes() {
|
|
58
|
-
return ["label", "tone"];
|
|
76
|
+
return ["label", "tone", "max", "hide-when-zero", "animate"];
|
|
59
77
|
}
|
|
60
78
|
badgeEl;
|
|
79
|
+
lastText = null;
|
|
61
80
|
get label() {
|
|
62
81
|
return this.getAttribute("label") ?? "";
|
|
63
82
|
}
|
|
@@ -70,6 +89,42 @@ export class BoxBadgeElement extends BaseElement {
|
|
|
70
89
|
set tone(value) {
|
|
71
90
|
this.setAttribute("tone", value);
|
|
72
91
|
}
|
|
92
|
+
/** When the label is a count, cap the displayed number, e.g. max=99 → "99+". */
|
|
93
|
+
get max() {
|
|
94
|
+
const raw = this.getAttribute("max");
|
|
95
|
+
if (raw === null)
|
|
96
|
+
return null;
|
|
97
|
+
const parsed = Number(raw);
|
|
98
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
99
|
+
}
|
|
100
|
+
set max(value) {
|
|
101
|
+
if (value === null) {
|
|
102
|
+
this.removeAttribute("max");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.setAttribute("max", String(value));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Hide the badge entirely when the count is zero or the label is empty. */
|
|
109
|
+
get hideWhenZero() {
|
|
110
|
+
return this.hasAttribute("hide-when-zero");
|
|
111
|
+
}
|
|
112
|
+
set hideWhenZero(value) {
|
|
113
|
+
this.toggleAttribute("hide-when-zero", value);
|
|
114
|
+
}
|
|
115
|
+
/** The label after applying the `max` cap (e.g. "128" with max=99 → "99+"). */
|
|
116
|
+
displayText() {
|
|
117
|
+
const raw = this.label;
|
|
118
|
+
const max = this.max;
|
|
119
|
+
if (max !== null && /^\d+$/.test(raw.trim()) && Number(raw) > max) {
|
|
120
|
+
return `${max}+`;
|
|
121
|
+
}
|
|
122
|
+
return raw;
|
|
123
|
+
}
|
|
124
|
+
isEmptyCount() {
|
|
125
|
+
const raw = this.label.trim();
|
|
126
|
+
return raw === "" || raw === "0";
|
|
127
|
+
}
|
|
73
128
|
renderTemplate() {
|
|
74
129
|
if (!this.shadowRoot) {
|
|
75
130
|
return;
|
|
@@ -84,9 +139,20 @@ export class BoxBadgeElement extends BaseElement {
|
|
|
84
139
|
if (!this.badgeEl) {
|
|
85
140
|
return;
|
|
86
141
|
}
|
|
142
|
+
// Hide when used as a count that is zero/empty and hide-when-zero is set.
|
|
143
|
+
const shouldHide = this.hideWhenZero && this.isEmptyCount();
|
|
144
|
+
this.toggleAttribute("hidden", shouldHide);
|
|
145
|
+
const text = this.displayText();
|
|
87
146
|
this.badgeEl.dataset.tone = this.tone;
|
|
88
|
-
this.badgeEl.setAttribute("aria-label",
|
|
89
|
-
this.badgeEl.textContent =
|
|
147
|
+
this.badgeEl.setAttribute("aria-label", text);
|
|
148
|
+
this.badgeEl.textContent = text;
|
|
149
|
+
// Re-trigger the pop animation when an animated count actually changes.
|
|
150
|
+
if (this.hasAttribute("animate") && this.lastText !== null && this.lastText !== text) {
|
|
151
|
+
this.badgeEl.classList.remove("boe-pop");
|
|
152
|
+
void this.badgeEl.offsetWidth; // force reflow so the animation restarts
|
|
153
|
+
this.badgeEl.classList.add("boe-pop");
|
|
154
|
+
}
|
|
155
|
+
this.lastText = text;
|
|
90
156
|
}
|
|
91
157
|
}
|
|
92
158
|
export const defineBoxBadgeElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
@@ -8,7 +8,11 @@ export declare class BoxChipElement extends BaseElement {
|
|
|
8
8
|
static get observedAttributes(): string[];
|
|
9
9
|
private chipEl;
|
|
10
10
|
private labelEl;
|
|
11
|
+
private iconSlot;
|
|
11
12
|
private removeEl;
|
|
13
|
+
/** Chip size: `medium` (default) or `small`. */
|
|
14
|
+
get size(): string;
|
|
15
|
+
set size(value: string);
|
|
12
16
|
get label(): string;
|
|
13
17
|
set label(value: string);
|
|
14
18
|
get value(): string;
|
|
@@ -28,6 +32,7 @@ export declare class BoxChipElement extends BaseElement {
|
|
|
28
32
|
protected renderTemplate(): void;
|
|
29
33
|
protected setupListeners(): void;
|
|
30
34
|
private syncRemoveButton;
|
|
35
|
+
private syncIconSlot;
|
|
31
36
|
protected update(): void;
|
|
32
37
|
}
|
|
33
38
|
export declare const defineBoxChipElement: (tagName?: string) => typeof BoxChipElement;
|