@unofficialbox/box-open-elements 0.2.1 → 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/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/link-button.d.ts +9 -0
- package/dist/components/actions/link-button.js +55 -4
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- 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/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/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/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- 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 +8 -0
- package/dist/components/forms/date-field.js +80 -3
- package/dist/components/forms/dropdown.d.ts +5 -0
- package/dist/components/forms/dropdown.js +32 -3
- 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/radio-group.d.ts +9 -8
- package/dist/components/forms/radio-group.js +43 -6
- package/dist/components/forms/search-field.d.ts +9 -0
- package/dist/components/forms/search-field.js +62 -15
- package/dist/components/forms/select.d.ts +19 -8
- package/dist/components/forms/select.js +110 -15
- package/dist/components/forms/text-field.d.ts +14 -0
- package/dist/components/forms/text-field.js +144 -3
- 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/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/guide-tooltip.d.ts +51 -0
- package/dist/components/overlays/guide-tooltip.js +361 -0
- package/dist/components/overlays/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +74 -11
- 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 +162 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
4
|
+
const DEFAULT_TAG_NAME = "box-thumbnail-card";
|
|
5
|
+
/** Rich file/grid card — box-ui-elements `ThumbnailCard`. */
|
|
6
|
+
const thumbnailCardStyles = `
|
|
7
|
+
:host {
|
|
8
|
+
display: inline-block;
|
|
9
|
+
color: inherit;
|
|
10
|
+
font: inherit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
[part="card"] {
|
|
14
|
+
display: grid;
|
|
15
|
+
grid-template-rows: auto auto;
|
|
16
|
+
width: 100%;
|
|
17
|
+
min-width: 160px;
|
|
18
|
+
border: ${boePanel.border};
|
|
19
|
+
border-radius: ${boeRadius.large};
|
|
20
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
transition:
|
|
23
|
+
border-color ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
|
|
24
|
+
box-shadow ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:host([interactive]) [part="card"] {
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:host([highlight-on-hover]) [part="card"]:hover,
|
|
32
|
+
:host([interactive]) [part="card"]:hover {
|
|
33
|
+
border-color: var(--boe-token-stroke-stroke-hover, #bcbcbc);
|
|
34
|
+
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[part="card"]:focus-visible {
|
|
38
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[part="thumbnail"] {
|
|
43
|
+
display: grid;
|
|
44
|
+
place-items: center;
|
|
45
|
+
aspect-ratio: 16 / 10;
|
|
46
|
+
background: var(--boe-token-surface-surface-secondary, #f4f4f4);
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[part="thumbnail"] ::slotted(img) {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
object-fit: cover;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[part="details"] {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: auto 1fr auto;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: ${boeSpace[2]};
|
|
61
|
+
padding: ${boeSpace[3]};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[part="icon"] {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[part="icon"]:not(:has(*)) {
|
|
70
|
+
display: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="text"] {
|
|
74
|
+
min-width: 0;
|
|
75
|
+
display: grid;
|
|
76
|
+
gap: 2px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[part="title"] {
|
|
80
|
+
font-size: ${boeSpace[3]};
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
color: var(--boe-token-text-text, #222222);
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
text-overflow: ellipsis;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[part="subtitle"] {
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
text-overflow: ellipsis;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[part="subtitle"][hidden] {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[part="action"] {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[part="action"]:not(:has(*)) {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
export class BoxThumbnailCardElement extends BaseElement {
|
|
109
|
+
static get observedAttributes() {
|
|
110
|
+
return ["title", "subtitle", "interactive", "highlight-on-hover"];
|
|
111
|
+
}
|
|
112
|
+
cardEl;
|
|
113
|
+
titleEl;
|
|
114
|
+
subtitleEl;
|
|
115
|
+
get cardTitle() {
|
|
116
|
+
return this.getAttribute("title") ?? "";
|
|
117
|
+
}
|
|
118
|
+
set cardTitle(value) {
|
|
119
|
+
this.setAttribute("title", value);
|
|
120
|
+
}
|
|
121
|
+
get subtitle() {
|
|
122
|
+
return this.getAttribute("subtitle") ?? "";
|
|
123
|
+
}
|
|
124
|
+
set subtitle(value) {
|
|
125
|
+
if (value) {
|
|
126
|
+
this.setAttribute("subtitle", value);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.removeAttribute("subtitle");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** When set, the card is a button: focusable, keyboard-activated, emits `activate`. */
|
|
133
|
+
get interactive() {
|
|
134
|
+
return this.hasAttribute("interactive");
|
|
135
|
+
}
|
|
136
|
+
set interactive(value) {
|
|
137
|
+
this.toggleAttribute("interactive", Boolean(value));
|
|
138
|
+
}
|
|
139
|
+
renderTemplate() {
|
|
140
|
+
if (!this.shadowRoot) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.shadowRoot.innerHTML = `
|
|
144
|
+
<style>${thumbnailCardStyles}</style>
|
|
145
|
+
<div part="card">
|
|
146
|
+
<div part="thumbnail"><slot name="thumbnail"></slot></div>
|
|
147
|
+
<div part="details">
|
|
148
|
+
<span part="icon"><slot name="icon"></slot></span>
|
|
149
|
+
<div part="text">
|
|
150
|
+
<div part="title"></div>
|
|
151
|
+
<div part="subtitle"></div>
|
|
152
|
+
</div>
|
|
153
|
+
<span part="action"><slot name="action"></slot></span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
`;
|
|
157
|
+
this.cardEl = this.shadowRoot.querySelector('[part="card"]');
|
|
158
|
+
this.titleEl = this.shadowRoot.querySelector('[part="title"]');
|
|
159
|
+
this.subtitleEl = this.shadowRoot.querySelector('[part="subtitle"]');
|
|
160
|
+
}
|
|
161
|
+
setupListeners() {
|
|
162
|
+
this.cardEl.addEventListener("click", () => this.activate());
|
|
163
|
+
this.cardEl.addEventListener("keydown", event => {
|
|
164
|
+
if (!this.interactive) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const key = event.key;
|
|
168
|
+
if (key === "Enter" || key === " ") {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
this.activate();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
activate() {
|
|
175
|
+
if (!this.interactive) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.dispatchEvent(new CustomEvent("activate", {
|
|
179
|
+
bubbles: true,
|
|
180
|
+
composed: true,
|
|
181
|
+
detail: { title: this.cardTitle },
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
update() {
|
|
185
|
+
if (!this.cardEl || !this.titleEl || !this.subtitleEl) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.titleEl.textContent = this.cardTitle;
|
|
189
|
+
this.subtitleEl.textContent = this.subtitle;
|
|
190
|
+
this.subtitleEl.hidden = this.subtitle.length === 0;
|
|
191
|
+
if (this.interactive) {
|
|
192
|
+
this.cardEl.setAttribute("role", "button");
|
|
193
|
+
this.cardEl.setAttribute("tabindex", "0");
|
|
194
|
+
this.cardEl.setAttribute("aria-label", this.cardTitle);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
this.cardEl.removeAttribute("role");
|
|
198
|
+
this.cardEl.removeAttribute("tabindex");
|
|
199
|
+
this.cardEl.removeAttribute("aria-label");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export const defineBoxThumbnailCardElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
204
|
+
const existingElement = customElements.get(tagName);
|
|
205
|
+
if (existingElement) {
|
|
206
|
+
return existingElement;
|
|
207
|
+
}
|
|
208
|
+
customElements.define(tagName, BoxThumbnailCardElement);
|
|
209
|
+
return BoxThumbnailCardElement;
|
|
210
|
+
};
|
|
@@ -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) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Overlays badges on the corners of its content — the box-ui-elements
|
|
4
|
+
* `Badgeable` wrapper. The subject goes in the default slot; each corner is a
|
|
5
|
+
* named slot (`top-left`, `top-right`, `bottom-left`, `bottom-right`). Corners
|
|
6
|
+
* with no assigned content are hidden so they don't capture pointer events.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BoxBadgeableElement extends BaseElement {
|
|
9
|
+
private corners;
|
|
10
|
+
protected renderTemplate(): void;
|
|
11
|
+
protected setupListeners(): void;
|
|
12
|
+
protected update(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare const defineBoxBadgeableElement: (tagName?: string) => typeof BoxBadgeableElement;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
const DEFAULT_TAG_NAME = "box-badgeable";
|
|
3
|
+
/** Corners a badge can be pinned to, matching box-ui-elements' Badgeable. */
|
|
4
|
+
const CORNERS = ["top-left", "top-right", "bottom-left", "bottom-right"];
|
|
5
|
+
const badgeableStyles = `
|
|
6
|
+
:host {
|
|
7
|
+
display: inline-block;
|
|
8
|
+
color: inherit;
|
|
9
|
+
font: inherit;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
[part="container"] {
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[part="badge"] {
|
|
18
|
+
position: absolute;
|
|
19
|
+
z-index: 1;
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Slotted badge content stays interactive (e.g. a dismiss button). */
|
|
25
|
+
[part="badge"] ::slotted(*) {
|
|
26
|
+
pointer-events: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[part="badge"][hidden] {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[part="badge"][data-corner="top-left"] { inset-block-start: 0; inset-inline-start: 0; transform: translate(-35%, -35%); }
|
|
34
|
+
[part="badge"][data-corner="top-right"] { inset-block-start: 0; inset-inline-end: 0; transform: translate(35%, -35%); }
|
|
35
|
+
[part="badge"][data-corner="bottom-left"] { inset-block-end: 0; inset-inline-start: 0; transform: translate(-35%, 35%); }
|
|
36
|
+
[part="badge"][data-corner="bottom-right"] { inset-block-end: 0; inset-inline-end: 0; transform: translate(35%, 35%); }
|
|
37
|
+
`;
|
|
38
|
+
/**
|
|
39
|
+
* Overlays badges on the corners of its content — the box-ui-elements
|
|
40
|
+
* `Badgeable` wrapper. The subject goes in the default slot; each corner is a
|
|
41
|
+
* named slot (`top-left`, `top-right`, `bottom-left`, `bottom-right`). Corners
|
|
42
|
+
* with no assigned content are hidden so they don't capture pointer events.
|
|
43
|
+
*/
|
|
44
|
+
export class BoxBadgeableElement extends BaseElement {
|
|
45
|
+
corners = new Map();
|
|
46
|
+
renderTemplate() {
|
|
47
|
+
if (!this.shadowRoot) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.shadowRoot.innerHTML = `
|
|
51
|
+
<style>${badgeableStyles}</style>
|
|
52
|
+
<span part="container">
|
|
53
|
+
<slot></slot>
|
|
54
|
+
${CORNERS.map(corner => `<span part="badge" data-corner="${corner}" hidden><slot name="${corner}"></slot></span>`).join("")}
|
|
55
|
+
</span>
|
|
56
|
+
`;
|
|
57
|
+
this.corners.clear();
|
|
58
|
+
for (const corner of CORNERS) {
|
|
59
|
+
const wrapper = this.shadowRoot.querySelector(`[data-corner="${corner}"]`);
|
|
60
|
+
const slot = wrapper.querySelector("slot");
|
|
61
|
+
this.corners.set(corner, { wrapper, slot });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
setupListeners() {
|
|
65
|
+
for (const { wrapper, slot } of this.corners.values()) {
|
|
66
|
+
slot.addEventListener("slotchange", () => {
|
|
67
|
+
wrapper.hidden = slot.assignedNodes({ flatten: true }).length === 0;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
update() {
|
|
72
|
+
for (const { wrapper, slot } of this.corners.values()) {
|
|
73
|
+
wrapper.hidden = slot.assignedNodes({ flatten: true }).length === 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export const defineBoxBadgeableElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
78
|
+
const existingElement = customElements.get(tagName);
|
|
79
|
+
if (existingElement) {
|
|
80
|
+
return existingElement;
|
|
81
|
+
}
|
|
82
|
+
customElements.define(tagName, BoxBadgeableElement);
|
|
83
|
+
return BoxBadgeableElement;
|
|
84
|
+
};
|
|
@@ -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;
|
|
@@ -42,12 +42,59 @@ const chipStyles = `
|
|
|
42
42
|
background: color-mix(in srgb, var(--boe-token-surface-surface-hover, #f4f4f4) 70%, var(--boe-token-surface-surface-secondary, #fbfbfb) 30%);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
[part="chip"][data-tone="brand"]
|
|
45
|
+
[part="chip"][data-tone="brand"],
|
|
46
|
+
[part="chip"][data-tone="info"] {
|
|
46
47
|
border-color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 26%, transparent);
|
|
47
48
|
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
|
|
48
49
|
color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 72%, var(--boe-token-text-text, #222222));
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
[part="chip"][data-tone="success"] {
|
|
53
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 32%, transparent);
|
|
54
|
+
background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 12%, var(--boe-token-surface-surface, #ffffff) 88%);
|
|
55
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 60%, var(--boe-token-text-text, #222222));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[part="chip"][data-tone="warning"] {
|
|
59
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 40%, transparent);
|
|
60
|
+
background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 16%, var(--boe-token-surface-surface, #ffffff) 84%);
|
|
61
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 58%, var(--boe-token-text-text, #222222));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[part="chip"][data-tone="error"] {
|
|
65
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 30%, transparent);
|
|
66
|
+
background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
|
|
67
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 70%, var(--boe-token-text-text, #222222));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Compact size. */
|
|
71
|
+
[part="chip"][data-size="small"] {
|
|
72
|
+
gap: 0.25rem;
|
|
73
|
+
padding: 0.14rem 0.28rem 0.14rem 0.55rem;
|
|
74
|
+
font-size: 0.74rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[part="chip"][data-size="small"]:not([data-removable="true"]) {
|
|
78
|
+
padding-inline-end: 0.55rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Leading icon slot — hidden (and taking no gap) until content is assigned. */
|
|
82
|
+
[part="icon"] {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
flex: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[part="icon"]:not(.has-content) {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[part="icon"]::slotted(*) {
|
|
93
|
+
inline-size: 0.95rem;
|
|
94
|
+
block-size: 0.95rem;
|
|
95
|
+
display: block;
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
[part="chip"][data-selected="true"],
|
|
52
99
|
[part="chip"][data-selected="true"]:hover:not([data-disabled="true"]),
|
|
53
100
|
[part="chip"][data-selected="true"]:active:not([data-disabled="true"]) {
|
|
@@ -94,11 +141,19 @@ const chipStyles = `
|
|
|
94
141
|
*/
|
|
95
142
|
export class BoxChipElement extends BaseElement {
|
|
96
143
|
static get observedAttributes() {
|
|
97
|
-
return ["disabled", "label", "removable", "selectable", "selected", "tone", "value"];
|
|
144
|
+
return ["disabled", "label", "removable", "selectable", "selected", "size", "tone", "value"];
|
|
98
145
|
}
|
|
99
146
|
chipEl;
|
|
100
147
|
labelEl;
|
|
148
|
+
iconSlot;
|
|
101
149
|
removeEl = null;
|
|
150
|
+
/** Chip size: `medium` (default) or `small`. */
|
|
151
|
+
get size() {
|
|
152
|
+
return this.getAttribute("size") === "small" ? "small" : "medium";
|
|
153
|
+
}
|
|
154
|
+
set size(value) {
|
|
155
|
+
this.setAttribute("size", value);
|
|
156
|
+
}
|
|
102
157
|
get label() {
|
|
103
158
|
return this.getAttribute("label") ?? "";
|
|
104
159
|
}
|
|
@@ -169,13 +224,16 @@ export class BoxChipElement extends BaseElement {
|
|
|
169
224
|
this.shadowRoot.innerHTML = `
|
|
170
225
|
<style>${chipStyles}</style>
|
|
171
226
|
<span part="chip">
|
|
227
|
+
<slot name="icon" part="icon"></slot>
|
|
172
228
|
<span part="label"></span>
|
|
173
229
|
</span>
|
|
174
230
|
`;
|
|
175
231
|
this.chipEl = this.shadowRoot.querySelector('[part="chip"]');
|
|
176
232
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
233
|
+
this.iconSlot = this.shadowRoot.querySelector('slot[name="icon"]');
|
|
177
234
|
}
|
|
178
235
|
setupListeners() {
|
|
236
|
+
this.iconSlot.addEventListener("slotchange", () => this.syncIconSlot());
|
|
179
237
|
this.chipEl.addEventListener("click", event => {
|
|
180
238
|
if (event.target.closest('[part="remove"]')) {
|
|
181
239
|
return;
|
|
@@ -217,11 +275,25 @@ export class BoxChipElement extends BaseElement {
|
|
|
217
275
|
this.removeEl = null;
|
|
218
276
|
}
|
|
219
277
|
}
|
|
278
|
+
syncIconSlot() {
|
|
279
|
+
if (!this.iconSlot) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const hasContent = this.iconSlot.assignedNodes({ flatten: true }).length > 0;
|
|
283
|
+
this.iconSlot.classList.toggle("has-content", hasContent);
|
|
284
|
+
}
|
|
220
285
|
update() {
|
|
221
286
|
if (!this.chipEl) {
|
|
222
287
|
return;
|
|
223
288
|
}
|
|
224
289
|
this.chipEl.dataset.tone = this.tone;
|
|
290
|
+
if (this.size === "medium") {
|
|
291
|
+
this.chipEl.removeAttribute("data-size");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
this.chipEl.dataset.size = this.size;
|
|
295
|
+
}
|
|
296
|
+
this.syncIconSlot();
|
|
225
297
|
this.chipEl.dataset.removable = this.removable ? "true" : "false";
|
|
226
298
|
this.chipEl.dataset.selected = this.selected ? "true" : "false";
|
|
227
299
|
this.chipEl.dataset.disabled = this.disabled ? "true" : "false";
|
|
@@ -9,6 +9,7 @@ export declare class BoxErrorMaskElement extends BaseElement {
|
|
|
9
9
|
static get observedAttributes(): string[];
|
|
10
10
|
private titleEl;
|
|
11
11
|
private messageEl;
|
|
12
|
+
private bodySlot;
|
|
12
13
|
private actionEl;
|
|
13
14
|
get heading(): string;
|
|
14
15
|
set heading(value: string);
|
|
@@ -19,6 +20,7 @@ export declare class BoxErrorMaskElement extends BaseElement {
|
|
|
19
20
|
get actionLabel(): string;
|
|
20
21
|
set actionLabel(value: string);
|
|
21
22
|
protected renderTemplate(): void;
|
|
23
|
+
protected setupListeners(): void;
|
|
22
24
|
private syncActionButton;
|
|
23
25
|
protected update(): void;
|
|
24
26
|
}
|