@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
package/README.md
CHANGED
|
@@ -116,6 +116,7 @@ The docs index is at [docs/README.md](./docs/README.md). The most important entr
|
|
|
116
116
|
- [Taxonomy](./docs/taxonomy.md)
|
|
117
117
|
- [Architecture](./docs/architecture.md)
|
|
118
118
|
- [API Guidelines](./docs/api-guidelines.md)
|
|
119
|
+
- [Using with React, Angular, Vue, and Svelte](./docs/integration/frameworks.md)
|
|
119
120
|
- [Design Tokens](./docs/foundations/tokens.md)
|
|
120
121
|
- [Content Explorer](./docs/patterns/content-explorer.md)
|
|
121
122
|
- [Box Server Integration](./docs/integration/box-server.md)
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export type ButtonType = "button" | "submit" | "reset";
|
|
2
3
|
export declare class BoxButtonElement extends BaseElement {
|
|
4
|
+
static formAssociated: boolean;
|
|
3
5
|
static get observedAttributes(): string[];
|
|
4
6
|
private buttonEl;
|
|
7
|
+
private spinnerEl;
|
|
8
|
+
private iconEl;
|
|
9
|
+
private labelEl;
|
|
10
|
+
private iconSlot;
|
|
11
|
+
private readonly internals;
|
|
12
|
+
constructor();
|
|
13
|
+
/** Loading state: shows a spinner and blocks activation (stays focusable). */
|
|
14
|
+
get isLoading(): boolean;
|
|
15
|
+
set isLoading(value: boolean);
|
|
16
|
+
get type(): ButtonType;
|
|
17
|
+
set type(value: ButtonType);
|
|
5
18
|
get disabled(): boolean;
|
|
6
19
|
set disabled(value: boolean);
|
|
7
20
|
get label(): string;
|
|
@@ -11,6 +24,8 @@ export declare class BoxButtonElement extends BaseElement {
|
|
|
11
24
|
get tone(): string;
|
|
12
25
|
set tone(value: string);
|
|
13
26
|
protected renderTemplate(): void;
|
|
27
|
+
protected setupListeners(): void;
|
|
28
|
+
private syncIconVisibility;
|
|
14
29
|
protected update(): void;
|
|
15
30
|
}
|
|
16
31
|
export declare const defineBoxButtonElement: (tagName?: string) => typeof BoxButtonElement;
|
|
@@ -120,12 +120,79 @@ const buttonStyles = `
|
|
|
120
120
|
opacity: ${boeControl.disabledOpacity};
|
|
121
121
|
box-shadow: none;
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
button[aria-busy="true"] {
|
|
125
|
+
cursor: default;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Leading icon slot — hidden when nothing is slotted (kept off the flex gap). */
|
|
129
|
+
[part="icon"] {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
width: 16px;
|
|
132
|
+
height: 16px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[part="icon"][hidden] {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
[part="icon"] ::slotted(svg) {
|
|
140
|
+
width: 16px;
|
|
141
|
+
height: 16px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Inline loading spinner, shown while is-loading. */
|
|
145
|
+
[part="spinner"] {
|
|
146
|
+
width: 14px;
|
|
147
|
+
height: 14px;
|
|
148
|
+
border-radius: 50%;
|
|
149
|
+
border: 2px solid currentColor;
|
|
150
|
+
border-top-color: transparent;
|
|
151
|
+
animation: boe-btn-spin 0.7s linear infinite;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
[part="spinner"][hidden] {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@keyframes boe-btn-spin {
|
|
159
|
+
to { transform: rotate(360deg); }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
[part="spinner"] { animation-duration: 1.6s; }
|
|
164
|
+
}
|
|
123
165
|
`;
|
|
124
166
|
export class BoxButtonElement extends BaseElement {
|
|
167
|
+
static formAssociated = true;
|
|
125
168
|
static get observedAttributes() {
|
|
126
|
-
return ["disabled", "label", "size", "tone"];
|
|
169
|
+
return ["disabled", "label", "size", "tone", "is-loading", "type"];
|
|
127
170
|
}
|
|
128
171
|
buttonEl;
|
|
172
|
+
spinnerEl;
|
|
173
|
+
iconEl;
|
|
174
|
+
labelEl;
|
|
175
|
+
iconSlot;
|
|
176
|
+
internals;
|
|
177
|
+
constructor() {
|
|
178
|
+
super();
|
|
179
|
+
// Form association lets type="submit"/"reset" drive the owning form.
|
|
180
|
+
this.internals = this.attachInternals();
|
|
181
|
+
}
|
|
182
|
+
/** Loading state: shows a spinner and blocks activation (stays focusable). */
|
|
183
|
+
get isLoading() {
|
|
184
|
+
return this.hasAttribute("is-loading");
|
|
185
|
+
}
|
|
186
|
+
set isLoading(value) {
|
|
187
|
+
this.toggleAttribute("is-loading", Boolean(value));
|
|
188
|
+
}
|
|
189
|
+
get type() {
|
|
190
|
+
const type = this.getAttribute("type");
|
|
191
|
+
return type === "submit" || type === "reset" ? type : "button";
|
|
192
|
+
}
|
|
193
|
+
set type(value) {
|
|
194
|
+
this.setAttribute("type", value);
|
|
195
|
+
}
|
|
129
196
|
get disabled() {
|
|
130
197
|
return this.hasAttribute("disabled");
|
|
131
198
|
}
|
|
@@ -161,9 +228,40 @@ export class BoxButtonElement extends BaseElement {
|
|
|
161
228
|
}
|
|
162
229
|
this.shadowRoot.innerHTML = `
|
|
163
230
|
<style>${buttonStyles}</style>
|
|
164
|
-
<button type="button" part="button"
|
|
231
|
+
<button type="button" part="button">
|
|
232
|
+
<span part="spinner" aria-hidden="true" hidden></span>
|
|
233
|
+
<span part="icon" hidden><slot name="icon"></slot></span>
|
|
234
|
+
<span part="label"></span>
|
|
235
|
+
</button>
|
|
165
236
|
`;
|
|
166
237
|
this.buttonEl = this.shadowRoot.querySelector('[part="button"]');
|
|
238
|
+
this.spinnerEl = this.shadowRoot.querySelector('[part="spinner"]');
|
|
239
|
+
this.iconEl = this.shadowRoot.querySelector('[part="icon"]');
|
|
240
|
+
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
241
|
+
this.iconSlot = this.iconEl.querySelector("slot");
|
|
242
|
+
}
|
|
243
|
+
setupListeners() {
|
|
244
|
+
this.buttonEl.addEventListener("click", event => {
|
|
245
|
+
if (this.disabled || this.isLoading) {
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
event.stopImmediatePropagation();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Drive the owning form for submit/reset (custom elements don't do this
|
|
251
|
+
// natively). Prefer the form-associated form; fall back to the nearest
|
|
252
|
+
// ancestor form so it works without full ElementInternals.form support.
|
|
253
|
+
const form = this.internals.form ?? this.closest("form");
|
|
254
|
+
if (this.type === "submit") {
|
|
255
|
+
form?.requestSubmit();
|
|
256
|
+
}
|
|
257
|
+
else if (this.type === "reset") {
|
|
258
|
+
form?.reset();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
this.iconSlot.addEventListener("slotchange", () => this.syncIconVisibility());
|
|
262
|
+
}
|
|
263
|
+
syncIconVisibility() {
|
|
264
|
+
this.iconEl.hidden = this.iconSlot.assignedNodes({ flatten: true }).length === 0;
|
|
167
265
|
}
|
|
168
266
|
update() {
|
|
169
267
|
if (!this.buttonEl) {
|
|
@@ -171,13 +269,19 @@ export class BoxButtonElement extends BaseElement {
|
|
|
171
269
|
}
|
|
172
270
|
this.buttonEl.dataset.tone = this.tone;
|
|
173
271
|
this.buttonEl.dataset.size = this.size;
|
|
272
|
+
this.labelEl.textContent = this.label;
|
|
273
|
+
const busy = this.isLoading;
|
|
274
|
+
this.spinnerEl.hidden = !busy;
|
|
275
|
+
this.buttonEl.setAttribute("aria-busy", String(busy));
|
|
276
|
+
// A loading button stays focusable but inert; a disabled one is fully off.
|
|
174
277
|
if (this.disabled) {
|
|
175
278
|
this.buttonEl.setAttribute("disabled", "");
|
|
176
279
|
}
|
|
177
280
|
else {
|
|
178
281
|
this.buttonEl.removeAttribute("disabled");
|
|
179
282
|
}
|
|
180
|
-
this.buttonEl.
|
|
283
|
+
this.buttonEl.setAttribute("aria-disabled", String(this.disabled || busy));
|
|
284
|
+
this.syncIconVisibility();
|
|
181
285
|
}
|
|
182
286
|
}
|
|
183
287
|
export const defineBoxButtonElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
@@ -3,6 +3,14 @@ type BoxMenuItem = {
|
|
|
3
3
|
disabled?: boolean;
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
+
/** Render a divider before this item. */
|
|
7
|
+
separator?: boolean;
|
|
8
|
+
/** Render a non-interactive section header instead of a menuitem. */
|
|
9
|
+
header?: boolean;
|
|
10
|
+
/** Render as a link (navigates on activate). */
|
|
11
|
+
href?: string;
|
|
12
|
+
/** Checkable item — renders menuitemcheckbox with aria-checked. */
|
|
13
|
+
checked?: boolean;
|
|
6
14
|
};
|
|
7
15
|
export declare class BoxMenuElement extends BaseElement {
|
|
8
16
|
static get observedAttributes(): string[];
|
|
@@ -13,6 +21,8 @@ export declare class BoxMenuElement extends BaseElement {
|
|
|
13
21
|
set label(value: string);
|
|
14
22
|
get items(): BoxMenuItem[];
|
|
15
23
|
set items(value: BoxMenuItem[]);
|
|
24
|
+
/** Render one item: header, separator + item, link, checkable, or plain. */
|
|
25
|
+
private renderItem;
|
|
16
26
|
protected renderTemplate(): void;
|
|
17
27
|
protected setupListeners(): void;
|
|
18
28
|
private enabledMenuItems;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { applyRovingTabindex, focusRovingItem, nextRovingIndex, } from "../../foundations/a11y/index.js";
|
|
3
|
-
import { boeControl, boeOverlay } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeControl, boeOverlay, boeSpace } from "../../foundations/geometry/index.js";
|
|
4
4
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
5
5
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
6
6
|
const DEFAULT_TAG_NAME = "box-menu";
|
|
@@ -62,6 +62,41 @@ const menuStyles = `
|
|
|
62
62
|
[part="menu-item"]:disabled {
|
|
63
63
|
opacity: ${boeControl.disabledOpacity};
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/* Link items keep the menu-item chrome but render as anchors. */
|
|
67
|
+
a[part="menu-item"] {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="menu-separator"] {
|
|
74
|
+
height: 1px;
|
|
75
|
+
margin: ${boeSpace[1]} 0;
|
|
76
|
+
background: var(--boe-token-stroke-stroke, #e8e8e8);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[part="menu-header"] {
|
|
80
|
+
padding: ${boeSpace[1]} ${boeSpace[2]};
|
|
81
|
+
font-size: 11px;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
letter-spacing: 0.06em;
|
|
84
|
+
text-transform: uppercase;
|
|
85
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Check indicator for checkable items. */
|
|
89
|
+
[part="menu-item"][role="menuitemcheckbox"]::before {
|
|
90
|
+
content: "";
|
|
91
|
+
display: inline-block;
|
|
92
|
+
width: 1em;
|
|
93
|
+
margin-inline-end: ${boeSpace[1]};
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[part="menu-item"][aria-checked="true"]::before {
|
|
98
|
+
content: "✓";
|
|
99
|
+
}
|
|
65
100
|
`;
|
|
66
101
|
export class BoxMenuElement extends BaseElement {
|
|
67
102
|
static get observedAttributes() {
|
|
@@ -101,6 +136,27 @@ export class BoxMenuElement extends BaseElement {
|
|
|
101
136
|
set items(value) {
|
|
102
137
|
this.setAttribute("items", JSON.stringify(value));
|
|
103
138
|
}
|
|
139
|
+
/** Render one item: header, separator + item, link, checkable, or plain. */
|
|
140
|
+
renderItem(item) {
|
|
141
|
+
const separator = item.separator ? `<div part="menu-separator" role="separator"></div>` : "";
|
|
142
|
+
if (item.header) {
|
|
143
|
+
return `${separator}<div part="menu-header" role="presentation">${escapeHtml(item.label)}</div>`;
|
|
144
|
+
}
|
|
145
|
+
const disabled = this.disabled || item.disabled;
|
|
146
|
+
const id = escapeHtml(item.id);
|
|
147
|
+
const label = escapeHtml(item.label);
|
|
148
|
+
// Checkable item — role=menuitemcheckbox with aria-checked.
|
|
149
|
+
if (typeof item.checked === "boolean") {
|
|
150
|
+
const checkedAttr = item.checked ? ' aria-checked="true"' : ' aria-checked="false"';
|
|
151
|
+
return `${separator}<button type="button" part="menu-item" role="menuitemcheckbox" data-item-id="${id}"${checkedAttr} ${disabled ? "disabled" : ""}>${label}</button>`;
|
|
152
|
+
}
|
|
153
|
+
// Link item — an anchor styled as a menuitem.
|
|
154
|
+
if (item.href) {
|
|
155
|
+
const aria = disabled ? ' aria-disabled="true"' : "";
|
|
156
|
+
return `${separator}<a part="menu-item" role="menuitem" data-item-id="${id}" href="${escapeHtml(item.href)}" tabindex="-1"${aria}>${label}</a>`;
|
|
157
|
+
}
|
|
158
|
+
return `${separator}<button type="button" part="menu-item" role="menuitem" data-item-id="${id}" ${disabled ? "disabled" : ""}>${label}</button>`;
|
|
159
|
+
}
|
|
104
160
|
renderTemplate() {
|
|
105
161
|
if (!this.shadowRoot) {
|
|
106
162
|
return;
|
|
@@ -113,15 +169,22 @@ export class BoxMenuElement extends BaseElement {
|
|
|
113
169
|
}
|
|
114
170
|
setupListeners() {
|
|
115
171
|
this.menuEl.addEventListener("click", event => {
|
|
116
|
-
const
|
|
117
|
-
|
|
172
|
+
const el = event.target?.closest('[part="menu-item"]');
|
|
173
|
+
// Anchors have no `.disabled`; both use aria-disabled / native disabled.
|
|
174
|
+
const isDisabled = el?.hasAttribute("disabled") || el?.getAttribute("aria-disabled") === "true";
|
|
175
|
+
if (!el || !this.menuEl.contains(el) || isDisabled) {
|
|
118
176
|
return;
|
|
119
177
|
}
|
|
120
|
-
const itemId =
|
|
178
|
+
const itemId = el.getAttribute("data-item-id");
|
|
121
179
|
const item = this.items.find(entry => entry.id === itemId);
|
|
122
180
|
if (!item || this.disabled || item.disabled) {
|
|
123
181
|
return;
|
|
124
182
|
}
|
|
183
|
+
// Checkable items toggle their aria-checked optimistically; the host owns
|
|
184
|
+
// the source of truth and can re-render from the event.
|
|
185
|
+
if (typeof item.checked === "boolean") {
|
|
186
|
+
el.setAttribute("aria-checked", String(!item.checked));
|
|
187
|
+
}
|
|
125
188
|
this.dispatchEvent(new CustomEvent("item-selected", {
|
|
126
189
|
bubbles: true,
|
|
127
190
|
composed: true,
|
|
@@ -157,13 +220,7 @@ export class BoxMenuElement extends BaseElement {
|
|
|
157
220
|
return;
|
|
158
221
|
}
|
|
159
222
|
this.menuEl.setAttribute("aria-label", this.label);
|
|
160
|
-
this.menuEl.innerHTML = this.items
|
|
161
|
-
.map(item => `
|
|
162
|
-
<button type="button" part="menu-item" role="menuitem" data-item-id="${escapeHtml(item.id)}" ${this.disabled || item.disabled ? "disabled" : ""}>
|
|
163
|
-
${escapeHtml(item.label)}
|
|
164
|
-
</button>
|
|
165
|
-
`)
|
|
166
|
-
.join("");
|
|
223
|
+
this.menuEl.innerHTML = this.items.map(item => this.renderItem(item)).join("");
|
|
167
224
|
const buttons = this.enabledMenuItems();
|
|
168
225
|
applyRovingTabindex(buttons, 0);
|
|
169
226
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface TableColumn {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Cross-axis alignment of the column's cells. */
|
|
6
|
+
align?: "start" | "end" | "center";
|
|
7
|
+
/** Mark the column sortable — clicking its header emits `sort`. */
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface TableRow {
|
|
11
|
+
id: string;
|
|
12
|
+
/** Cell text keyed by column key, or positional strings matching `columns`. */
|
|
13
|
+
cells: Record<string, string> | string[];
|
|
14
|
+
}
|
|
15
|
+
export type TableSelectionMode = "none" | "single" | "multiple";
|
|
16
|
+
/**
|
|
17
|
+
* A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
|
|
18
|
+
* Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
|
|
19
|
+
* / Shift-click range, plus keyboard (Arrow, Shift+Arrow to extend, Space to
|
|
20
|
+
* toggle, Ctrl/Cmd+A select-all, Escape clear). Sorting is host-owned: sortable
|
|
21
|
+
* headers emit `sort` and the host reorders `rows`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class BoxTableElement extends BaseElement {
|
|
24
|
+
static get observedAttributes(): string[];
|
|
25
|
+
private bodyEl;
|
|
26
|
+
private headEl;
|
|
27
|
+
private selected;
|
|
28
|
+
private anchorIndex;
|
|
29
|
+
get columns(): TableColumn[];
|
|
30
|
+
set columns(value: TableColumn[]);
|
|
31
|
+
get rows(): TableRow[];
|
|
32
|
+
set rows(value: TableRow[]);
|
|
33
|
+
get selectionMode(): TableSelectionMode;
|
|
34
|
+
set selectionMode(value: TableSelectionMode);
|
|
35
|
+
get selectedIds(): string[];
|
|
36
|
+
set selectedIds(value: string[]);
|
|
37
|
+
get label(): string;
|
|
38
|
+
set label(value: string);
|
|
39
|
+
private parseJson;
|
|
40
|
+
private cellText;
|
|
41
|
+
protected renderTemplate(): void;
|
|
42
|
+
protected setupListeners(): void;
|
|
43
|
+
private rowElements;
|
|
44
|
+
private onBodyKeydown;
|
|
45
|
+
private toggleSort;
|
|
46
|
+
private activateRow;
|
|
47
|
+
private selectRange;
|
|
48
|
+
private selectAll;
|
|
49
|
+
private setSelection;
|
|
50
|
+
private applySelectionState;
|
|
51
|
+
protected update(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare const defineBoxTableElement: (tagName?: string) => typeof BoxTableElement;
|