@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
-
import { boeControl, boeSpace } from "../../foundations/geometry/index.js";
|
|
2
|
+
import { boeControl, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
3
|
import { boeFocusVisibleStyles, } from "../../foundations/tokens/index.js";
|
|
4
4
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
5
5
|
const DEFAULT_TAG_NAME = "box-tabs";
|
|
@@ -88,6 +88,22 @@ const tabsStyles = `
|
|
|
88
88
|
outline-offset: -2px;
|
|
89
89
|
box-shadow: none;
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/* Panels carry no default padding — an empty panel is zero-height, and
|
|
93
|
+
consumers style their own slotted content. */
|
|
94
|
+
[part="panel"] {
|
|
95
|
+
outline: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[part="panel"][hidden] {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[part="panel"]:focus-visible {
|
|
103
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
104
|
+
outline-offset: 2px;
|
|
105
|
+
border-radius: ${boeRadius.med};
|
|
106
|
+
}
|
|
91
107
|
`;
|
|
92
108
|
export class BoxTabsElement extends BaseElement {
|
|
93
109
|
static get observedAttributes() {
|
|
@@ -96,6 +112,8 @@ export class BoxTabsElement extends BaseElement {
|
|
|
96
112
|
valueInternal = "";
|
|
97
113
|
lastOptionsJson = "";
|
|
98
114
|
tabsEl;
|
|
115
|
+
panelsEl;
|
|
116
|
+
uid = `boe-tabs-${Math.random().toString(36).slice(2, 8)}`;
|
|
99
117
|
get label() {
|
|
100
118
|
return this.getAttribute("label") ?? "Tabs";
|
|
101
119
|
}
|
|
@@ -169,17 +187,21 @@ export class BoxTabsElement extends BaseElement {
|
|
|
169
187
|
: index === options.length - 1
|
|
170
188
|
? "last"
|
|
171
189
|
: "middle";
|
|
190
|
+
const selected = option.value === selectedValue;
|
|
191
|
+
const value = escapeHtml(option.value);
|
|
172
192
|
return `
|
|
173
193
|
<button
|
|
174
194
|
type="button"
|
|
175
195
|
part="tab"
|
|
176
196
|
role="tab"
|
|
197
|
+
id="${this.uid}-tab-${value}"
|
|
198
|
+
aria-controls="${this.uid}-panel-${value}"
|
|
177
199
|
data-layout="${this.layout}"
|
|
178
200
|
data-position="${position}"
|
|
179
|
-
data-selected="${String(
|
|
180
|
-
data-value="${
|
|
181
|
-
aria-selected="${String(
|
|
182
|
-
tabindex="${
|
|
201
|
+
data-selected="${String(selected)}"
|
|
202
|
+
data-value="${value}"
|
|
203
|
+
aria-selected="${String(selected)}"
|
|
204
|
+
tabindex="${selected ? "0" : "-1"}"
|
|
183
205
|
>
|
|
184
206
|
${escapeHtml(option.label)}
|
|
185
207
|
</button>
|
|
@@ -187,6 +209,25 @@ export class BoxTabsElement extends BaseElement {
|
|
|
187
209
|
})
|
|
188
210
|
.join("");
|
|
189
211
|
}
|
|
212
|
+
/** One tabpanel per option, fed by a slot named for the option value. */
|
|
213
|
+
renderPanelsMarkup(options, selectedValue) {
|
|
214
|
+
return options
|
|
215
|
+
.map(option => {
|
|
216
|
+
const value = escapeHtml(option.value);
|
|
217
|
+
const selected = option.value === selectedValue;
|
|
218
|
+
return `
|
|
219
|
+
<div
|
|
220
|
+
part="panel"
|
|
221
|
+
role="tabpanel"
|
|
222
|
+
id="${this.uid}-panel-${value}"
|
|
223
|
+
aria-labelledby="${this.uid}-tab-${value}"
|
|
224
|
+
tabindex="0"
|
|
225
|
+
${selected ? "" : "hidden"}
|
|
226
|
+
><slot name="${value}"></slot></div>
|
|
227
|
+
`;
|
|
228
|
+
})
|
|
229
|
+
.join("");
|
|
230
|
+
}
|
|
190
231
|
renderTemplate() {
|
|
191
232
|
if (!this.shadowRoot) {
|
|
192
233
|
return;
|
|
@@ -194,8 +235,10 @@ export class BoxTabsElement extends BaseElement {
|
|
|
194
235
|
this.shadowRoot.innerHTML = `
|
|
195
236
|
<style>${tabsStyles}</style>
|
|
196
237
|
<div part="tabs" role="tablist"></div>
|
|
238
|
+
<div part="panels"></div>
|
|
197
239
|
`;
|
|
198
240
|
this.tabsEl = this.shadowRoot.querySelector('[part="tabs"]');
|
|
241
|
+
this.panelsEl = this.shadowRoot.querySelector('[part="panels"]');
|
|
199
242
|
}
|
|
200
243
|
setupListeners() {
|
|
201
244
|
this.tabsEl.addEventListener("click", event => {
|
|
@@ -254,6 +297,7 @@ export class BoxTabsElement extends BaseElement {
|
|
|
254
297
|
this.tabsEl.setAttribute("aria-label", this.label);
|
|
255
298
|
if (optionsJson !== this.lastOptionsJson) {
|
|
256
299
|
this.tabsEl.innerHTML = this.renderTabsMarkup(options, selectedValue);
|
|
300
|
+
this.panelsEl.innerHTML = this.renderPanelsMarkup(options, selectedValue);
|
|
257
301
|
this.lastOptionsJson = optionsJson;
|
|
258
302
|
return;
|
|
259
303
|
}
|
|
@@ -277,6 +321,10 @@ export class BoxTabsElement extends BaseElement {
|
|
|
277
321
|
tab.setAttribute("aria-selected", String(selected));
|
|
278
322
|
tab.tabIndex = selected ? 0 : -1;
|
|
279
323
|
});
|
|
324
|
+
// Show only the selected tab's panel.
|
|
325
|
+
this.panelsEl.querySelectorAll('[part="panel"]').forEach((panel, index) => {
|
|
326
|
+
panel.hidden = options[index]?.value !== selectedValue;
|
|
327
|
+
});
|
|
280
328
|
}
|
|
281
329
|
}
|
|
282
330
|
export const defineBoxTabsElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface ContextMenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
/** Render a divider *before* this item. */
|
|
7
|
+
separator?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Right-click context menu — box-ui-elements `ContextMenu`. The default slot is
|
|
11
|
+
* the target area; right-clicking (or Shift+F10 / the ContextMenu key) opens a
|
|
12
|
+
* menu at the pointer, positioned with `foundations/overlay` so it stays in the
|
|
13
|
+
* viewport. Full keyboard menu: arrows, Home/End, Enter/Space, Escape.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BoxContextMenuElement extends BaseElement {
|
|
16
|
+
static get observedAttributes(): string[];
|
|
17
|
+
private openValue;
|
|
18
|
+
private targetEl;
|
|
19
|
+
private surfaceEl;
|
|
20
|
+
private readonly focusRestore;
|
|
21
|
+
private documentBound;
|
|
22
|
+
private readonly onOutside;
|
|
23
|
+
private readonly onReposition;
|
|
24
|
+
get items(): ContextMenuItem[];
|
|
25
|
+
set items(value: ContextMenuItem[]);
|
|
26
|
+
get disabled(): boolean;
|
|
27
|
+
set disabled(value: boolean);
|
|
28
|
+
get open(): boolean;
|
|
29
|
+
disconnectedCallback(): void;
|
|
30
|
+
protected renderTemplate(): void;
|
|
31
|
+
protected setupListeners(): void;
|
|
32
|
+
private enabledItems;
|
|
33
|
+
private onMenuKeydown;
|
|
34
|
+
private openAt;
|
|
35
|
+
close(): void;
|
|
36
|
+
private selectItem;
|
|
37
|
+
private renderItems;
|
|
38
|
+
private bindDocument;
|
|
39
|
+
private unbindDocument;
|
|
40
|
+
protected update(): void;
|
|
41
|
+
}
|
|
42
|
+
export declare const defineBoxContextMenuElement: (tagName?: string) => typeof BoxContextMenuElement;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { FocusRestore, nextRovingIndex } from "../../foundations/a11y/index.js";
|
|
3
|
+
import { boeOverlay, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
4
|
+
import { resolvePosition } from "../../foundations/overlay/index.js";
|
|
5
|
+
const DEFAULT_TAG_NAME = "box-context-menu";
|
|
6
|
+
const escapeHtml = (value) => value
|
|
7
|
+
.replaceAll("&", "&")
|
|
8
|
+
.replaceAll("<", "<")
|
|
9
|
+
.replaceAll(">", ">")
|
|
10
|
+
.replaceAll('"', """);
|
|
11
|
+
const contextMenuStyles = `
|
|
12
|
+
:host {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
color: inherit;
|
|
15
|
+
font: inherit;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[part="target"] {
|
|
19
|
+
display: contents;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[part="surface"] {
|
|
23
|
+
position: fixed;
|
|
24
|
+
z-index: 40;
|
|
25
|
+
inset-block-start: 0;
|
|
26
|
+
inset-inline-start: 0;
|
|
27
|
+
min-width: 180px;
|
|
28
|
+
max-width: min(320px, 90vw);
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: ${boeSpace[1]};
|
|
31
|
+
border: ${boeOverlay.border};
|
|
32
|
+
border-radius: ${boeOverlay.radius};
|
|
33
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
34
|
+
box-shadow: ${boeOverlay.shadow};
|
|
35
|
+
display: grid;
|
|
36
|
+
gap: 1px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[part="surface"][hidden] {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[part="item"] {
|
|
44
|
+
appearance: none;
|
|
45
|
+
border: none;
|
|
46
|
+
background: none;
|
|
47
|
+
font: inherit;
|
|
48
|
+
text-align: start;
|
|
49
|
+
width: 100%;
|
|
50
|
+
min-height: ${boeOverlay.itemMinHeight};
|
|
51
|
+
padding: ${boeSpace[1]} ${boeSpace[3]};
|
|
52
|
+
border-radius: ${boeOverlay.itemRadius};
|
|
53
|
+
color: var(--boe-token-text-text, #222222);
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[part="item"]:hover:not(:disabled),
|
|
58
|
+
[part="item"]:focus-visible {
|
|
59
|
+
background: var(--boe-token-surface-surface-secondary, #f4f4f4);
|
|
60
|
+
outline: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[part="item"]:focus-visible {
|
|
64
|
+
box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[part="item"]:disabled {
|
|
68
|
+
opacity: 0.4;
|
|
69
|
+
cursor: not-allowed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[part="divider"] {
|
|
73
|
+
height: 1px;
|
|
74
|
+
margin: ${boeSpace[1]} 0;
|
|
75
|
+
background: var(--boe-token-stroke-stroke, #e8e8e8);
|
|
76
|
+
border-radius: ${boeRadius.size};
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
/**
|
|
80
|
+
* Right-click context menu — box-ui-elements `ContextMenu`. The default slot is
|
|
81
|
+
* the target area; right-clicking (or Shift+F10 / the ContextMenu key) opens a
|
|
82
|
+
* menu at the pointer, positioned with `foundations/overlay` so it stays in the
|
|
83
|
+
* viewport. Full keyboard menu: arrows, Home/End, Enter/Space, Escape.
|
|
84
|
+
*/
|
|
85
|
+
export class BoxContextMenuElement extends BaseElement {
|
|
86
|
+
static get observedAttributes() {
|
|
87
|
+
return ["items", "disabled"];
|
|
88
|
+
}
|
|
89
|
+
openValue = false;
|
|
90
|
+
targetEl;
|
|
91
|
+
surfaceEl;
|
|
92
|
+
focusRestore = new FocusRestore();
|
|
93
|
+
documentBound = false;
|
|
94
|
+
onOutside = (event) => {
|
|
95
|
+
if (!this.openValue)
|
|
96
|
+
return;
|
|
97
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
98
|
+
if (!path.some(node => node === this.surfaceEl))
|
|
99
|
+
this.close();
|
|
100
|
+
};
|
|
101
|
+
onReposition = () => {
|
|
102
|
+
if (this.openValue)
|
|
103
|
+
this.close();
|
|
104
|
+
};
|
|
105
|
+
get items() {
|
|
106
|
+
const raw = this.getAttribute("items");
|
|
107
|
+
if (!raw)
|
|
108
|
+
return [];
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(raw);
|
|
111
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
set items(value) {
|
|
118
|
+
this.setAttribute("items", JSON.stringify(value));
|
|
119
|
+
}
|
|
120
|
+
get disabled() {
|
|
121
|
+
return this.hasAttribute("disabled");
|
|
122
|
+
}
|
|
123
|
+
set disabled(value) {
|
|
124
|
+
this.toggleAttribute("disabled", Boolean(value));
|
|
125
|
+
}
|
|
126
|
+
get open() {
|
|
127
|
+
return this.openValue;
|
|
128
|
+
}
|
|
129
|
+
disconnectedCallback() {
|
|
130
|
+
this.unbindDocument();
|
|
131
|
+
}
|
|
132
|
+
renderTemplate() {
|
|
133
|
+
if (!this.shadowRoot)
|
|
134
|
+
return;
|
|
135
|
+
this.shadowRoot.innerHTML = `
|
|
136
|
+
<style>${contextMenuStyles}</style>
|
|
137
|
+
<div part="target"><slot></slot></div>
|
|
138
|
+
<div part="surface" role="menu" hidden></div>
|
|
139
|
+
`;
|
|
140
|
+
this.targetEl = this.shadowRoot.querySelector('[part="target"]');
|
|
141
|
+
this.surfaceEl = this.shadowRoot.querySelector('[part="surface"]');
|
|
142
|
+
}
|
|
143
|
+
setupListeners() {
|
|
144
|
+
this.addEventListener("contextmenu", event => {
|
|
145
|
+
if (this.disabled || this.items.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
this.openAt(event.clientX, event.clientY);
|
|
149
|
+
});
|
|
150
|
+
// Keyboard opener: Shift+F10 or the ContextMenu key on the focused target.
|
|
151
|
+
this.addEventListener("keydown", event => {
|
|
152
|
+
if (this.disabled)
|
|
153
|
+
return;
|
|
154
|
+
const isOpener = event.key === "ContextMenu" || (event.shiftKey && event.key === "F10");
|
|
155
|
+
if (isOpener && this.items.length) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
const rect = this.getBoundingClientRect();
|
|
158
|
+
this.openAt(rect.left, rect.bottom);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
this.surfaceEl.addEventListener("click", event => {
|
|
162
|
+
const item = event.target.closest('[part="item"]');
|
|
163
|
+
if (!item || item.disabled)
|
|
164
|
+
return;
|
|
165
|
+
this.selectItem(item.dataset.id ?? "");
|
|
166
|
+
});
|
|
167
|
+
this.surfaceEl.addEventListener("keydown", event => this.onMenuKeydown(event));
|
|
168
|
+
}
|
|
169
|
+
enabledItems() {
|
|
170
|
+
return Array.from(this.surfaceEl.querySelectorAll('[part="item"]:not(:disabled)'));
|
|
171
|
+
}
|
|
172
|
+
onMenuKeydown(event) {
|
|
173
|
+
if (event.key === "Escape") {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
this.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const items = this.enabledItems();
|
|
179
|
+
if (items.length === 0)
|
|
180
|
+
return;
|
|
181
|
+
const current = items.indexOf(event.target);
|
|
182
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
event.target.click();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const next = nextRovingIndex(event.key, Math.max(current, 0), items.length, { orientation: "vertical" });
|
|
188
|
+
if (next !== null && next !== current) {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
items[next].focus();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
openAt(clientX, clientY) {
|
|
194
|
+
this.renderItems();
|
|
195
|
+
this.surfaceEl.hidden = false;
|
|
196
|
+
const { width, height } = this.surfaceEl.getBoundingClientRect();
|
|
197
|
+
const { x, y } = resolvePosition({ top: clientY, left: clientX, width: 0, height: 0 }, { width, height }, { width: window.innerWidth, height: window.innerHeight }, { placement: { side: "bottom", align: "start" }, offset: 0 });
|
|
198
|
+
this.surfaceEl.style.left = `${Math.round(x)}px`;
|
|
199
|
+
this.surfaceEl.style.top = `${Math.round(y)}px`;
|
|
200
|
+
this.openValue = true;
|
|
201
|
+
this.bindDocument();
|
|
202
|
+
this.dispatchEvent(new CustomEvent("open-changed", { bubbles: true, composed: true, detail: { open: true } }));
|
|
203
|
+
this.focusRestore.capture(document.activeElement);
|
|
204
|
+
queueMicrotask(() => this.enabledItems()[0]?.focus());
|
|
205
|
+
}
|
|
206
|
+
close() {
|
|
207
|
+
if (!this.openValue)
|
|
208
|
+
return;
|
|
209
|
+
this.openValue = false;
|
|
210
|
+
this.surfaceEl.hidden = true;
|
|
211
|
+
this.unbindDocument();
|
|
212
|
+
this.dispatchEvent(new CustomEvent("open-changed", { bubbles: true, composed: true, detail: { open: false } }));
|
|
213
|
+
this.focusRestore.restore();
|
|
214
|
+
}
|
|
215
|
+
selectItem(id) {
|
|
216
|
+
const item = this.items.find(entry => entry.id === id);
|
|
217
|
+
if (!item || item.disabled)
|
|
218
|
+
return;
|
|
219
|
+
this.dispatchEvent(new CustomEvent("item-selected", { bubbles: true, composed: true, detail: item }));
|
|
220
|
+
this.close();
|
|
221
|
+
}
|
|
222
|
+
renderItems() {
|
|
223
|
+
this.surfaceEl.innerHTML = this.items
|
|
224
|
+
.map(item => {
|
|
225
|
+
const divider = item.separator ? `<div part="divider" role="separator"></div>` : "";
|
|
226
|
+
const disabled = item.disabled ? " disabled" : "";
|
|
227
|
+
return `${divider}<button type="button" part="item" role="menuitem" data-id="${escapeHtml(item.id)}"${disabled}>${escapeHtml(item.label)}</button>`;
|
|
228
|
+
})
|
|
229
|
+
.join("");
|
|
230
|
+
}
|
|
231
|
+
bindDocument() {
|
|
232
|
+
if (this.documentBound)
|
|
233
|
+
return;
|
|
234
|
+
document.addEventListener("pointerdown", this.onOutside, true);
|
|
235
|
+
document.addEventListener("scroll", this.onReposition, true);
|
|
236
|
+
window.addEventListener("resize", this.onReposition);
|
|
237
|
+
this.documentBound = true;
|
|
238
|
+
}
|
|
239
|
+
unbindDocument() {
|
|
240
|
+
if (!this.documentBound)
|
|
241
|
+
return;
|
|
242
|
+
document.removeEventListener("pointerdown", this.onOutside, true);
|
|
243
|
+
document.removeEventListener("scroll", this.onReposition, true);
|
|
244
|
+
window.removeEventListener("resize", this.onReposition);
|
|
245
|
+
this.documentBound = false;
|
|
246
|
+
}
|
|
247
|
+
update() {
|
|
248
|
+
// The menu content is rendered on open; nothing to sync while closed.
|
|
249
|
+
if (this.openValue)
|
|
250
|
+
this.renderItems();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export const defineBoxContextMenuElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
254
|
+
const existingElement = customElements.get(tagName);
|
|
255
|
+
if (existingElement) {
|
|
256
|
+
return existingElement;
|
|
257
|
+
}
|
|
258
|
+
customElements.define(tagName, BoxContextMenuElement);
|
|
259
|
+
return BoxContextMenuElement;
|
|
260
|
+
};
|
|
@@ -3,6 +3,8 @@ export declare class BoxDialogElement extends BaseElement {
|
|
|
3
3
|
static get observedAttributes(): string[];
|
|
4
4
|
private openValue;
|
|
5
5
|
private wasOpen;
|
|
6
|
+
private scrollLocked;
|
|
7
|
+
private previousBodyOverflow;
|
|
6
8
|
private hostEl;
|
|
7
9
|
private titleEl;
|
|
8
10
|
private descriptionEl;
|
|
@@ -16,6 +18,12 @@ export declare class BoxDialogElement extends BaseElement {
|
|
|
16
18
|
set description(value: string);
|
|
17
19
|
get confirmLabel(): string;
|
|
18
20
|
set confirmLabel(value: string);
|
|
21
|
+
/** Modal size — box-ui-elements Modal sizes. Default `medium`. */
|
|
22
|
+
get size(): "small" | "medium" | "large" | "fullscreen";
|
|
23
|
+
set size(value: "small" | "medium" | "large" | "fullscreen");
|
|
24
|
+
/** Lock/unlock background page scroll while the modal is open. */
|
|
25
|
+
private setScrollLock;
|
|
26
|
+
disconnectedCallback(): void;
|
|
19
27
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
20
28
|
show(): void;
|
|
21
29
|
close(): void;
|
|
@@ -31,6 +31,16 @@ const dialogStyles = `
|
|
|
31
31
|
color: var(--boe-token-text-text, #222222);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* Size options — box-ui-elements Modal sizes. Default (medium) is the base. */
|
|
35
|
+
[part="dialog"][data-size="small"] { width: min(380px, calc(100vw - 3rem)); }
|
|
36
|
+
[part="dialog"][data-size="large"] { width: min(760px, calc(100vw - 3rem)); }
|
|
37
|
+
[part="dialog"][data-size="fullscreen"] {
|
|
38
|
+
width: calc(100vw - 2rem);
|
|
39
|
+
height: calc(100vh - 2rem);
|
|
40
|
+
max-width: none;
|
|
41
|
+
border-radius: ${boeRadius.large};
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
[part="header"] h2 {
|
|
35
45
|
margin: 0;
|
|
36
46
|
font: inherit;
|
|
@@ -96,10 +106,12 @@ const dialogStyles = `
|
|
|
96
106
|
`;
|
|
97
107
|
export class BoxDialogElement extends BaseElement {
|
|
98
108
|
static get observedAttributes() {
|
|
99
|
-
return ["confirm-label", "description", "heading", "open"];
|
|
109
|
+
return ["confirm-label", "description", "heading", "open", "size"];
|
|
100
110
|
}
|
|
101
111
|
openValue = false;
|
|
102
112
|
wasOpen = false;
|
|
113
|
+
scrollLocked = false;
|
|
114
|
+
previousBodyOverflow = "";
|
|
103
115
|
hostEl;
|
|
104
116
|
titleEl = null;
|
|
105
117
|
descriptionEl = null;
|
|
@@ -140,6 +152,31 @@ export class BoxDialogElement extends BaseElement {
|
|
|
140
152
|
set confirmLabel(value) {
|
|
141
153
|
this.setAttribute("confirm-label", value);
|
|
142
154
|
}
|
|
155
|
+
/** Modal size — box-ui-elements Modal sizes. Default `medium`. */
|
|
156
|
+
get size() {
|
|
157
|
+
const size = this.getAttribute("size");
|
|
158
|
+
return size === "small" || size === "large" || size === "fullscreen" ? size : "medium";
|
|
159
|
+
}
|
|
160
|
+
set size(value) {
|
|
161
|
+
this.setAttribute("size", value);
|
|
162
|
+
}
|
|
163
|
+
/** Lock/unlock background page scroll while the modal is open. */
|
|
164
|
+
setScrollLock(locked) {
|
|
165
|
+
if (locked === this.scrollLocked) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.scrollLocked = locked;
|
|
169
|
+
if (locked) {
|
|
170
|
+
this.previousBodyOverflow = document.body.style.overflow;
|
|
171
|
+
document.body.style.overflow = "hidden";
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
document.body.style.overflow = this.previousBodyOverflow;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
disconnectedCallback() {
|
|
178
|
+
this.setScrollLock(false);
|
|
179
|
+
}
|
|
143
180
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
144
181
|
if (name === "open") {
|
|
145
182
|
this.openValue = this.hasAttribute("open");
|
|
@@ -203,6 +240,7 @@ export class BoxDialogElement extends BaseElement {
|
|
|
203
240
|
}
|
|
204
241
|
if (!this.openValue) {
|
|
205
242
|
const wasOpen = this.wasOpen;
|
|
243
|
+
this.setScrollLock(false);
|
|
206
244
|
this.hostEl.innerHTML = "";
|
|
207
245
|
this.titleEl = null;
|
|
208
246
|
this.descriptionEl = null;
|
|
@@ -218,11 +256,12 @@ export class BoxDialogElement extends BaseElement {
|
|
|
218
256
|
if (justOpened) {
|
|
219
257
|
this.focusRestore.capture();
|
|
220
258
|
}
|
|
259
|
+
this.setScrollLock(true);
|
|
221
260
|
if (!this.hostEl.querySelector('[part="dialog"]')) {
|
|
222
261
|
this.hostEl.innerHTML = `
|
|
223
262
|
<style>${dialogStyles}</style>
|
|
224
263
|
<div part="backdrop">
|
|
225
|
-
<section part="dialog" role="dialog" aria-modal="true" tabindex="-1" aria-labelledby="dialog-title">
|
|
264
|
+
<section part="dialog" role="dialog" aria-modal="true" tabindex="-1" aria-labelledby="dialog-title" data-size="${this.size}">
|
|
226
265
|
<header part="header">
|
|
227
266
|
<h2 id="dialog-title"></h2>
|
|
228
267
|
</header>
|
|
@@ -239,6 +278,10 @@ export class BoxDialogElement extends BaseElement {
|
|
|
239
278
|
this.titleEl = this.hostEl.querySelector("#dialog-title");
|
|
240
279
|
this.descriptionEl = this.hostEl.querySelector('[part="description"]');
|
|
241
280
|
this.confirmEl = this.hostEl.querySelector('[part="confirm"]');
|
|
281
|
+
const dialogEl = this.hostEl.querySelector('[part="dialog"]');
|
|
282
|
+
if (dialogEl) {
|
|
283
|
+
dialogEl.dataset.size = this.size;
|
|
284
|
+
}
|
|
242
285
|
if (this.titleEl) {
|
|
243
286
|
this.titleEl.textContent = this.heading;
|
|
244
287
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* A guided-tour callout that points at a target element and walks a user through
|
|
4
|
+
* a sequence of steps. Anchor it with `for` (the id of a target) or a slotted
|
|
5
|
+
* `anchor` element; it self-anchors to the host otherwise. It renders a
|
|
6
|
+
* `heading`, a body (default slot), a `step`/`total` indicator, and Back/Next/
|
|
7
|
+
* Close controls, emitting `next`, `back`, and `close` (each with `detail.step`)
|
|
8
|
+
* so the host can advance the tour (moving `for`/`step`). Positioned as a
|
|
9
|
+
* viewport-fixed overlay via `foundations/overlay`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class BoxGuideTooltipElement extends BaseElement {
|
|
12
|
+
static get observedAttributes(): string[];
|
|
13
|
+
private openValue;
|
|
14
|
+
private positionCleanup;
|
|
15
|
+
private headingId;
|
|
16
|
+
private calloutEl;
|
|
17
|
+
private closeEl;
|
|
18
|
+
private headingEl;
|
|
19
|
+
private stepEl;
|
|
20
|
+
private backEl;
|
|
21
|
+
private nextEl;
|
|
22
|
+
private anchorSlot;
|
|
23
|
+
get open(): boolean;
|
|
24
|
+
set open(value: boolean);
|
|
25
|
+
get heading(): string;
|
|
26
|
+
set heading(value: string);
|
|
27
|
+
get placement(): string;
|
|
28
|
+
set placement(value: string);
|
|
29
|
+
private resolvedPlacement;
|
|
30
|
+
/** 1-based current step; paired with `total` for the "n of m" indicator. */
|
|
31
|
+
get step(): number;
|
|
32
|
+
set step(value: number);
|
|
33
|
+
get total(): number;
|
|
34
|
+
set total(value: number);
|
|
35
|
+
get htmlFor(): string;
|
|
36
|
+
set htmlFor(value: string);
|
|
37
|
+
private get nextLabel();
|
|
38
|
+
private get backLabel();
|
|
39
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
40
|
+
show(): void;
|
|
41
|
+
hide(): void;
|
|
42
|
+
/** Resolve the element the callout points at. */
|
|
43
|
+
private resolveAnchor;
|
|
44
|
+
private stopTracking;
|
|
45
|
+
disconnectedCallback(): void;
|
|
46
|
+
protected renderTemplate(): void;
|
|
47
|
+
private emit;
|
|
48
|
+
protected setupListeners(): void;
|
|
49
|
+
protected update(): void;
|
|
50
|
+
}
|
|
51
|
+
export declare const defineBoxGuideTooltipElement: (tagName?: string) => typeof BoxGuideTooltipElement;
|