@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,361 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boeControl, boeOverlay, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
4
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
5
|
+
import { parsePlacement, trackAnchor, } from "../../foundations/overlay/index.js";
|
|
6
|
+
const DEFAULT_TAG_NAME = "box-guide-tooltip";
|
|
7
|
+
const guideTooltipStyles = `
|
|
8
|
+
:host {
|
|
9
|
+
display: contents;
|
|
10
|
+
color: inherit;
|
|
11
|
+
font: inherit;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Positioned by JS (foundations/overlay) as a viewport-fixed callout, so it
|
|
15
|
+
escapes ancestor overflow and flips/shifts to stay on-screen. */
|
|
16
|
+
[part="callout"] {
|
|
17
|
+
position: fixed;
|
|
18
|
+
inset-block-start: 0;
|
|
19
|
+
inset-inline-start: 0;
|
|
20
|
+
z-index: 60;
|
|
21
|
+
inline-size: max-content;
|
|
22
|
+
max-inline-size: min(20rem, calc(100vw - 2rem));
|
|
23
|
+
padding: ${boeSpace[4]};
|
|
24
|
+
border-radius: ${boeOverlay.radius};
|
|
25
|
+
border: 1px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 24%, transparent);
|
|
26
|
+
background: var(--boe-token-surface-tooltip-surface, #4e4e4e);
|
|
27
|
+
color: rgba(255, 255, 255, 0.95);
|
|
28
|
+
box-shadow: 0 18px 40px rgba(16, 24, 32, 0.28);
|
|
29
|
+
line-height: 1.5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[part="callout"][hidden] {
|
|
33
|
+
display: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[part="close"] {
|
|
37
|
+
position: absolute;
|
|
38
|
+
inset-block-start: ${boeSpace[2]};
|
|
39
|
+
inset-inline-end: ${boeSpace[2]};
|
|
40
|
+
inline-size: 1.5rem;
|
|
41
|
+
block-size: 1.5rem;
|
|
42
|
+
display: inline-grid;
|
|
43
|
+
place-items: center;
|
|
44
|
+
padding: 0;
|
|
45
|
+
border: 0;
|
|
46
|
+
border-radius: ${boeRadius.med};
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: rgba(255, 255, 255, 0.8);
|
|
49
|
+
font: inherit;
|
|
50
|
+
font-size: 1rem;
|
|
51
|
+
line-height: 1;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[part="close"]:hover {
|
|
57
|
+
background: rgba(255, 255, 255, 0.14);
|
|
58
|
+
color: #ffffff;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[part="close"]:focus-visible {
|
|
62
|
+
outline: 2px solid #ffffff;
|
|
63
|
+
outline-offset: 1px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[part="heading"] {
|
|
67
|
+
margin: 0 1.5rem 0.35rem 0;
|
|
68
|
+
font-size: 0.95rem;
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[part="heading"][hidden] {
|
|
73
|
+
display: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
[part="body"] {
|
|
77
|
+
font-size: 0.86rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[part="footer"] {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
gap: ${boeSpace[3]};
|
|
85
|
+
margin-block-start: ${boeSpace[4]};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[part="step"] {
|
|
89
|
+
font-size: 0.76rem;
|
|
90
|
+
color: rgba(255, 255, 255, 0.7);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[part="step"][hidden] {
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[part="actions"] {
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
gap: ${boeSpace[2]};
|
|
100
|
+
margin-inline-start: auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[part="back"],
|
|
104
|
+
[part="next"] {
|
|
105
|
+
appearance: none;
|
|
106
|
+
min-height: 1.9rem;
|
|
107
|
+
padding: 0 ${boeSpace[3]};
|
|
108
|
+
border-radius: ${boeRadius.control};
|
|
109
|
+
font: inherit;
|
|
110
|
+
font-size: 0.82rem;
|
|
111
|
+
font-weight: 700;
|
|
112
|
+
letter-spacing: ${boeControl.letterSpacing};
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard}, border-color ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
[part="back"] {
|
|
118
|
+
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
119
|
+
background: transparent;
|
|
120
|
+
color: rgba(255, 255, 255, 0.92);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
[part="back"]:hover:not(:disabled) {
|
|
124
|
+
border-color: #ffffff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[part="back"][hidden] {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[part="next"] {
|
|
132
|
+
border: 1px solid transparent;
|
|
133
|
+
background: var(--boe-token-surface-surface-brand, #0061d5);
|
|
134
|
+
color: var(--boe-token-text-text-on-brand, #ffffff);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
[part="next"]:hover:not(:disabled) {
|
|
138
|
+
background: var(--boe-token-surface-surface-brand-hover, #006ae9);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[part="back"]:disabled,
|
|
142
|
+
[part="next"]:disabled {
|
|
143
|
+
opacity: 0.5;
|
|
144
|
+
cursor: not-allowed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
${boeNeutralInteractiveStyles('[part="close"]')}
|
|
148
|
+
`;
|
|
149
|
+
/**
|
|
150
|
+
* A guided-tour callout that points at a target element and walks a user through
|
|
151
|
+
* a sequence of steps. Anchor it with `for` (the id of a target) or a slotted
|
|
152
|
+
* `anchor` element; it self-anchors to the host otherwise. It renders a
|
|
153
|
+
* `heading`, a body (default slot), a `step`/`total` indicator, and Back/Next/
|
|
154
|
+
* Close controls, emitting `next`, `back`, and `close` (each with `detail.step`)
|
|
155
|
+
* so the host can advance the tour (moving `for`/`step`). Positioned as a
|
|
156
|
+
* viewport-fixed overlay via `foundations/overlay`.
|
|
157
|
+
*/
|
|
158
|
+
export class BoxGuideTooltipElement extends BaseElement {
|
|
159
|
+
static get observedAttributes() {
|
|
160
|
+
return [
|
|
161
|
+
"open",
|
|
162
|
+
"heading",
|
|
163
|
+
"placement",
|
|
164
|
+
"step",
|
|
165
|
+
"total",
|
|
166
|
+
"for",
|
|
167
|
+
"next-label",
|
|
168
|
+
"back-label",
|
|
169
|
+
"close-label",
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
openValue = false;
|
|
173
|
+
positionCleanup = null;
|
|
174
|
+
headingId = `box-guide-heading-${Math.random().toString(36).slice(2, 10)}`;
|
|
175
|
+
calloutEl;
|
|
176
|
+
closeEl;
|
|
177
|
+
headingEl;
|
|
178
|
+
stepEl;
|
|
179
|
+
backEl;
|
|
180
|
+
nextEl;
|
|
181
|
+
anchorSlot;
|
|
182
|
+
get open() {
|
|
183
|
+
return this.openValue;
|
|
184
|
+
}
|
|
185
|
+
set open(value) {
|
|
186
|
+
const next = Boolean(value);
|
|
187
|
+
if (this.openValue === next) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.openValue = next;
|
|
191
|
+
this.toggleAttribute("open", next);
|
|
192
|
+
this.dispatchEvent(new CustomEvent("open-changed", { bubbles: true, composed: true, detail: { open: next } }));
|
|
193
|
+
if (this.isRendered) {
|
|
194
|
+
this.update();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
get heading() {
|
|
198
|
+
return this.getAttribute("heading") ?? "";
|
|
199
|
+
}
|
|
200
|
+
set heading(value) {
|
|
201
|
+
this.setAttribute("heading", value);
|
|
202
|
+
}
|
|
203
|
+
get placement() {
|
|
204
|
+
return this.getAttribute("placement") ?? "bottom-center";
|
|
205
|
+
}
|
|
206
|
+
set placement(value) {
|
|
207
|
+
this.setAttribute("placement", value);
|
|
208
|
+
}
|
|
209
|
+
resolvedPlacement() {
|
|
210
|
+
return parsePlacement(this.getAttribute("placement")) ?? { side: "bottom", align: "center" };
|
|
211
|
+
}
|
|
212
|
+
/** 1-based current step; paired with `total` for the "n of m" indicator. */
|
|
213
|
+
get step() {
|
|
214
|
+
const raw = Number(this.getAttribute("step"));
|
|
215
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
|
|
216
|
+
}
|
|
217
|
+
set step(value) {
|
|
218
|
+
this.setAttribute("step", String(value));
|
|
219
|
+
}
|
|
220
|
+
get total() {
|
|
221
|
+
const raw = Number(this.getAttribute("total"));
|
|
222
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
|
|
223
|
+
}
|
|
224
|
+
set total(value) {
|
|
225
|
+
this.setAttribute("total", String(value));
|
|
226
|
+
}
|
|
227
|
+
get htmlFor() {
|
|
228
|
+
return this.getAttribute("for") ?? "";
|
|
229
|
+
}
|
|
230
|
+
set htmlFor(value) {
|
|
231
|
+
this.setAttribute("for", value);
|
|
232
|
+
}
|
|
233
|
+
get nextLabel() {
|
|
234
|
+
const explicit = this.getAttribute("next-label");
|
|
235
|
+
if (explicit) {
|
|
236
|
+
return explicit;
|
|
237
|
+
}
|
|
238
|
+
// On the final step Next becomes a finish action.
|
|
239
|
+
return this.total > 0 && this.step >= this.total ? "Done" : "Next";
|
|
240
|
+
}
|
|
241
|
+
get backLabel() {
|
|
242
|
+
return this.getAttribute("back-label") ?? "Back";
|
|
243
|
+
}
|
|
244
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
245
|
+
if (name === "open") {
|
|
246
|
+
this.openValue = this.hasAttribute("open");
|
|
247
|
+
}
|
|
248
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
249
|
+
}
|
|
250
|
+
show() {
|
|
251
|
+
this.open = true;
|
|
252
|
+
}
|
|
253
|
+
hide() {
|
|
254
|
+
this.open = false;
|
|
255
|
+
}
|
|
256
|
+
/** Resolve the element the callout points at. */
|
|
257
|
+
resolveAnchor() {
|
|
258
|
+
const forId = this.htmlFor;
|
|
259
|
+
if (forId) {
|
|
260
|
+
const root = this.getRootNode();
|
|
261
|
+
const found = root.getElementById?.(forId) ??
|
|
262
|
+
(this.ownerDocument ?? document).getElementById(forId);
|
|
263
|
+
if (found) {
|
|
264
|
+
return found;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const slotted = this.anchorSlot?.assignedElements({ flatten: true })[0];
|
|
268
|
+
return slotted ?? this;
|
|
269
|
+
}
|
|
270
|
+
stopTracking() {
|
|
271
|
+
this.positionCleanup?.();
|
|
272
|
+
this.positionCleanup = null;
|
|
273
|
+
}
|
|
274
|
+
disconnectedCallback() {
|
|
275
|
+
this.stopTracking();
|
|
276
|
+
super.disconnectedCallback?.();
|
|
277
|
+
}
|
|
278
|
+
renderTemplate() {
|
|
279
|
+
if (!this.shadowRoot) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.shadowRoot.innerHTML = `
|
|
283
|
+
<style>${guideTooltipStyles}</style>
|
|
284
|
+
<slot name="anchor"></slot>
|
|
285
|
+
<div part="callout" role="dialog" aria-labelledby="${this.headingId}" hidden>
|
|
286
|
+
<button type="button" part="close" aria-label="Close">
|
|
287
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
|
288
|
+
</button>
|
|
289
|
+
<h2 part="heading" id="${this.headingId}" hidden></h2>
|
|
290
|
+
<div part="body"><slot></slot></div>
|
|
291
|
+
<div part="footer">
|
|
292
|
+
<span part="step" hidden></span>
|
|
293
|
+
<div part="actions">
|
|
294
|
+
<button type="button" part="back">Back</button>
|
|
295
|
+
<button type="button" part="next">Next</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
`;
|
|
300
|
+
this.calloutEl = this.shadowRoot.querySelector('[part="callout"]');
|
|
301
|
+
this.closeEl = this.shadowRoot.querySelector('[part="close"]');
|
|
302
|
+
this.headingEl = this.shadowRoot.querySelector('[part="heading"]');
|
|
303
|
+
this.stepEl = this.shadowRoot.querySelector('[part="step"]');
|
|
304
|
+
this.backEl = this.shadowRoot.querySelector('[part="back"]');
|
|
305
|
+
this.nextEl = this.shadowRoot.querySelector('[part="next"]');
|
|
306
|
+
this.anchorSlot = this.shadowRoot.querySelector('slot[name="anchor"]');
|
|
307
|
+
}
|
|
308
|
+
emit(type) {
|
|
309
|
+
this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail: { step: this.step } }));
|
|
310
|
+
}
|
|
311
|
+
setupListeners() {
|
|
312
|
+
this.closeEl.addEventListener("click", () => {
|
|
313
|
+
this.emit("close");
|
|
314
|
+
this.hide();
|
|
315
|
+
});
|
|
316
|
+
this.backEl.addEventListener("click", () => this.emit("back"));
|
|
317
|
+
this.nextEl.addEventListener("click", () => this.emit("next"));
|
|
318
|
+
this.calloutEl.addEventListener("keydown", event => {
|
|
319
|
+
if (event.key === "Escape") {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
this.emit("close");
|
|
322
|
+
this.hide();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
update() {
|
|
327
|
+
if (!this.calloutEl) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.calloutEl.hidden = !this.openValue;
|
|
331
|
+
const heading = this.heading;
|
|
332
|
+
this.headingEl.hidden = !heading;
|
|
333
|
+
this.headingEl.textContent = heading;
|
|
334
|
+
const step = this.step;
|
|
335
|
+
const total = this.total;
|
|
336
|
+
const showStep = step > 0 && total > 0;
|
|
337
|
+
this.stepEl.hidden = !showStep;
|
|
338
|
+
this.stepEl.textContent = showStep ? `${step} of ${total}` : "";
|
|
339
|
+
// Back hides on the first step; Next label finishes on the last.
|
|
340
|
+
this.backEl.hidden = showStep && step <= 1;
|
|
341
|
+
this.backEl.textContent = this.backLabel;
|
|
342
|
+
this.nextEl.textContent = this.nextLabel;
|
|
343
|
+
this.stopTracking();
|
|
344
|
+
if (this.openValue) {
|
|
345
|
+
this.positionCleanup = trackAnchor(this.resolveAnchor(), this.calloutEl, {
|
|
346
|
+
placement: this.resolvedPlacement(),
|
|
347
|
+
offset: 10,
|
|
348
|
+
});
|
|
349
|
+
// Move focus into the callout for keyboard users.
|
|
350
|
+
queueMicrotask(() => this.nextEl?.focus());
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export const defineBoxGuideTooltipElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
355
|
+
const existingElement = customElements.get(tagName);
|
|
356
|
+
if (existingElement) {
|
|
357
|
+
return existingElement;
|
|
358
|
+
}
|
|
359
|
+
customElements.define(tagName, BoxGuideTooltipElement);
|
|
360
|
+
return BoxGuideTooltipElement;
|
|
361
|
+
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Any side (`top`/`bottom`/`left`/`right`) with optional `-start`/`-center`/`-end`
|
|
4
|
+
* alignment, plus the legacy names (`start` = left, `end` = right).
|
|
5
|
+
*/
|
|
6
|
+
export type PopoverPlacement = "top" | "bottom" | "left" | "right" | "start" | "end" | "top-start" | "top-center" | "top-end" | "bottom-start" | "bottom-center" | "bottom-end" | "left-start" | "left-center" | "left-end" | "right-start" | "right-center" | "right-end";
|
|
3
7
|
export declare class BoxPopoverElement extends BaseElement {
|
|
4
8
|
static get observedAttributes(): string[];
|
|
5
9
|
private openValue;
|
|
@@ -7,6 +11,7 @@ export declare class BoxPopoverElement extends BaseElement {
|
|
|
7
11
|
private documentListenersBound;
|
|
8
12
|
private triggerEl;
|
|
9
13
|
private surfaceEl;
|
|
14
|
+
private positionCleanup;
|
|
10
15
|
private readonly focusRestore;
|
|
11
16
|
private readonly onDocumentKeydown;
|
|
12
17
|
private readonly onOutsidePointer;
|
|
@@ -31,5 +36,8 @@ export declare class BoxPopoverElement extends BaseElement {
|
|
|
31
36
|
protected renderTemplate(): void;
|
|
32
37
|
protected setupListeners(): void;
|
|
33
38
|
protected update(): void;
|
|
39
|
+
/** Anchor the surface to the trigger and keep it positioned while open. */
|
|
40
|
+
private startPositioning;
|
|
41
|
+
private stopPositioning;
|
|
34
42
|
}
|
|
35
43
|
export declare const defineBoxPopoverElement: (tagName?: string) => typeof BoxPopoverElement;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { FocusRestore, getTabbableElements } from "../../foundations/a11y/index.js";
|
|
3
|
-
import { boeControl, boeOverlay, boeRadius
|
|
3
|
+
import { boeControl, boeOverlay, boeRadius } 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
|
+
import { parsePlacement, trackAnchor, } from "../../foundations/overlay/index.js";
|
|
6
7
|
const DEFAULT_TAG_NAME = "box-popover";
|
|
8
|
+
/** The attribute value if it names a valid placement, else `"bottom"`. */
|
|
7
9
|
const resolvePlacement = (value) => {
|
|
8
|
-
if (value
|
|
9
|
-
return value;
|
|
10
|
-
}
|
|
10
|
+
if (value && parsePlacement(value))
|
|
11
|
+
return value.trim().toLowerCase();
|
|
11
12
|
return "bottom";
|
|
12
13
|
};
|
|
14
|
+
const OVERLAY_OFFSET = 4;
|
|
13
15
|
const popoverStyles = `
|
|
14
16
|
:host {
|
|
15
17
|
display: inline-block;
|
|
@@ -46,10 +48,13 @@ const popoverStyles = `
|
|
|
46
48
|
|
|
47
49
|
${boeNeutralInteractiveStyles('[part="trigger"]')}
|
|
48
50
|
|
|
51
|
+
/* Positioned by JS (foundations/overlay) as position: fixed in viewport
|
|
52
|
+
coordinates — so it escapes ancestor overflow and flips/shifts to stay in
|
|
53
|
+
view, the job a portal does in box-ui-elements. */
|
|
49
54
|
[part="surface"] {
|
|
50
|
-
position:
|
|
55
|
+
position: fixed;
|
|
51
56
|
z-index: 30;
|
|
52
|
-
inset-block-start:
|
|
57
|
+
inset-block-start: 0;
|
|
53
58
|
inset-inline-start: 0;
|
|
54
59
|
width: min(360px, calc(100vw - 5rem));
|
|
55
60
|
min-width: 200px;
|
|
@@ -62,22 +67,6 @@ const popoverStyles = `
|
|
|
62
67
|
line-height: 1.5;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
:host([placement="top"]) [part="surface"] {
|
|
66
|
-
inset-block-start: auto;
|
|
67
|
-
inset-block-end: calc(100% + ${boeSpace[1]});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
:host([placement="start"]) [part="surface"] {
|
|
71
|
-
inset-block-start: 0;
|
|
72
|
-
inset-inline-start: auto;
|
|
73
|
-
inset-inline-end: calc(100% + ${boeSpace[1]});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
:host([placement="end"]) [part="surface"] {
|
|
77
|
-
inset-block-start: 0;
|
|
78
|
-
inset-inline-start: calc(100% + ${boeSpace[1]});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
70
|
[part="surface"][hidden] {
|
|
82
71
|
display: none;
|
|
83
72
|
}
|
|
@@ -91,6 +80,7 @@ export class BoxPopoverElement extends BaseElement {
|
|
|
91
80
|
documentListenersBound = false;
|
|
92
81
|
triggerEl;
|
|
93
82
|
surfaceEl;
|
|
83
|
+
positionCleanup = null;
|
|
94
84
|
focusRestore = new FocusRestore();
|
|
95
85
|
onDocumentKeydown = (event) => {
|
|
96
86
|
if (!this.openValue || event.key !== "Escape") {
|
|
@@ -164,6 +154,7 @@ export class BoxPopoverElement extends BaseElement {
|
|
|
164
154
|
}
|
|
165
155
|
disconnectedCallback() {
|
|
166
156
|
this.unbindDocumentListeners();
|
|
157
|
+
this.stopPositioning();
|
|
167
158
|
}
|
|
168
159
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
169
160
|
if (name === "open") {
|
|
@@ -297,6 +288,12 @@ export class BoxPopoverElement extends BaseElement {
|
|
|
297
288
|
const justClosed = !this.openValue && this.wasOpen;
|
|
298
289
|
this.surfaceEl.hidden = !this.openValue;
|
|
299
290
|
this.syncDocumentListeners();
|
|
291
|
+
if (this.openValue) {
|
|
292
|
+
this.startPositioning();
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
this.stopPositioning();
|
|
296
|
+
}
|
|
300
297
|
if (justOpened) {
|
|
301
298
|
this.focusRestore.capture(this.triggerEl);
|
|
302
299
|
queueMicrotask(() => this.focusSurface());
|
|
@@ -306,6 +303,24 @@ export class BoxPopoverElement extends BaseElement {
|
|
|
306
303
|
}
|
|
307
304
|
this.wasOpen = this.openValue;
|
|
308
305
|
}
|
|
306
|
+
/** Anchor the surface to the trigger and keep it positioned while open. */
|
|
307
|
+
startPositioning() {
|
|
308
|
+
if (this.positionCleanup) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const placement = parsePlacement(this.getAttribute("placement")) ?? { side: "bottom", align: "start" };
|
|
312
|
+
this.positionCleanup = trackAnchor(this.triggerEl, this.surfaceEl, {
|
|
313
|
+
placement,
|
|
314
|
+
offset: OVERLAY_OFFSET,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
stopPositioning() {
|
|
318
|
+
this.positionCleanup?.();
|
|
319
|
+
this.positionCleanup = null;
|
|
320
|
+
this.surfaceEl.style.left = "";
|
|
321
|
+
this.surfaceEl.style.top = "";
|
|
322
|
+
this.surfaceEl.style.margin = "";
|
|
323
|
+
}
|
|
309
324
|
}
|
|
310
325
|
export const defineBoxPopoverElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
311
326
|
const existingElement = customElements.get(tagName);
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export type TooltipTheme = "default" | "error" | "callout";
|
|
2
3
|
export declare class BoxTooltipElement extends BaseElement {
|
|
3
4
|
static get observedAttributes(): string[];
|
|
5
|
+
private positionCleanup;
|
|
4
6
|
private openValue;
|
|
5
7
|
private tooltipId;
|
|
6
8
|
private triggerHostEl;
|
|
7
9
|
private triggerSlot;
|
|
8
10
|
private fallbackTriggerEl;
|
|
9
11
|
private tooltipEl;
|
|
12
|
+
private labelEl;
|
|
10
13
|
private describedTrigger;
|
|
11
14
|
get label(): string;
|
|
12
15
|
set label(value: string);
|
|
13
16
|
get triggerLabel(): string;
|
|
14
17
|
set triggerLabel(value: string);
|
|
18
|
+
get placement(): string;
|
|
19
|
+
set placement(value: string);
|
|
20
|
+
private resolvedPlacement;
|
|
21
|
+
get theme(): TooltipTheme;
|
|
22
|
+
set theme(value: TooltipTheme);
|
|
15
23
|
get open(): boolean;
|
|
16
24
|
set open(value: boolean);
|
|
17
25
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
@@ -22,5 +30,7 @@ export declare class BoxTooltipElement extends BaseElement {
|
|
|
22
30
|
protected renderTemplate(): void;
|
|
23
31
|
protected setupListeners(): void;
|
|
24
32
|
protected update(): void;
|
|
33
|
+
private stopTracking;
|
|
34
|
+
disconnectedCallback(): void;
|
|
25
35
|
}
|
|
26
36
|
export declare const defineBoxTooltipElement: (tagName?: string) => typeof BoxTooltipElement;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
3
3
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
4
|
+
import { parsePlacement, trackAnchor, } from "../../foundations/overlay/index.js";
|
|
4
5
|
const DEFAULT_TAG_NAME = "box-tooltip";
|
|
5
6
|
const DEFAULT_TRIGGER_LABEL = "More information";
|
|
6
7
|
const tooltipStyles = `
|
|
@@ -37,13 +38,15 @@ const tooltipStyles = `
|
|
|
37
38
|
|
|
38
39
|
${boeNeutralInteractiveStyles('[part="trigger"]')}
|
|
39
40
|
|
|
41
|
+
/* Positioned by JS (foundations/overlay) as position: fixed, so it escapes
|
|
42
|
+
ancestor overflow and flips/shifts to stay in the viewport. */
|
|
40
43
|
[part="tooltip"] {
|
|
41
|
-
position:
|
|
42
|
-
inset-block-start:
|
|
43
|
-
inset-inline-start:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
width: min(
|
|
44
|
+
position: fixed;
|
|
45
|
+
inset-block-start: 0;
|
|
46
|
+
inset-inline-start: 0;
|
|
47
|
+
z-index: 40;
|
|
48
|
+
width: max-content;
|
|
49
|
+
max-width: min(18rem, calc(100vw - 2rem));
|
|
47
50
|
padding: 0.5rem 0.65rem;
|
|
48
51
|
border-radius: ${boeRadius.xlarge};
|
|
49
52
|
border: 1px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, rgba(255, 255, 255, 0.08));
|
|
@@ -53,20 +56,42 @@ const tooltipStyles = `
|
|
|
53
56
|
line-height: 1.45;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
/* Error theme — for validation hints. */
|
|
60
|
+
[part="tooltip"][data-theme="error"] {
|
|
61
|
+
background: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
62
|
+
border-color: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
63
|
+
color: #ffffff;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Callout theme — light surface for richer, longer content. */
|
|
67
|
+
[part="tooltip"][data-theme="callout"] {
|
|
68
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
69
|
+
border-color: var(--boe-token-stroke-stroke, #e8e8e8);
|
|
70
|
+
color: var(--boe-token-text-text, #222222);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="tooltip"] ::slotted(img) {
|
|
74
|
+
max-width: 100%;
|
|
75
|
+
border-radius: ${boeRadius.med};
|
|
76
|
+
display: block;
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
[part="tooltip"][hidden] {
|
|
57
80
|
display: none;
|
|
58
81
|
}
|
|
59
82
|
`;
|
|
60
83
|
export class BoxTooltipElement extends BaseElement {
|
|
61
84
|
static get observedAttributes() {
|
|
62
|
-
return ["label", "open", "trigger-label"];
|
|
85
|
+
return ["label", "open", "trigger-label", "placement", "theme"];
|
|
63
86
|
}
|
|
87
|
+
positionCleanup = null;
|
|
64
88
|
openValue = false;
|
|
65
89
|
tooltipId = `box-tooltip-${Math.random().toString(36).slice(2, 10)}`;
|
|
66
90
|
triggerHostEl;
|
|
67
91
|
triggerSlot;
|
|
68
92
|
fallbackTriggerEl;
|
|
69
93
|
tooltipEl;
|
|
94
|
+
labelEl;
|
|
70
95
|
describedTrigger = null;
|
|
71
96
|
get label() {
|
|
72
97
|
return this.getAttribute("label") ?? "Helpful context";
|
|
@@ -80,6 +105,22 @@ export class BoxTooltipElement extends BaseElement {
|
|
|
80
105
|
set triggerLabel(value) {
|
|
81
106
|
this.setAttribute("trigger-label", value);
|
|
82
107
|
}
|
|
108
|
+
get placement() {
|
|
109
|
+
return this.getAttribute("placement") ?? "bottom-center";
|
|
110
|
+
}
|
|
111
|
+
set placement(value) {
|
|
112
|
+
this.setAttribute("placement", value);
|
|
113
|
+
}
|
|
114
|
+
resolvedPlacement() {
|
|
115
|
+
return parsePlacement(this.getAttribute("placement")) ?? { side: "bottom", align: "center" };
|
|
116
|
+
}
|
|
117
|
+
get theme() {
|
|
118
|
+
const value = this.getAttribute("theme");
|
|
119
|
+
return value === "error" || value === "callout" ? value : "default";
|
|
120
|
+
}
|
|
121
|
+
set theme(value) {
|
|
122
|
+
this.setAttribute("theme", value);
|
|
123
|
+
}
|
|
83
124
|
get open() {
|
|
84
125
|
return this.openValue;
|
|
85
126
|
}
|
|
@@ -138,13 +179,14 @@ export class BoxTooltipElement extends BaseElement {
|
|
|
138
179
|
<span part="trigger-host">
|
|
139
180
|
<slot><button type="button" part="trigger">?</button></slot>
|
|
140
181
|
</span>
|
|
141
|
-
<div id="${this.tooltipId}" part="tooltip" role="tooltip" hidden></div>
|
|
182
|
+
<div id="${this.tooltipId}" part="tooltip" role="tooltip" hidden><span part="label"></span><slot name="content"></slot></div>
|
|
142
183
|
</span>
|
|
143
184
|
`;
|
|
144
185
|
this.triggerHostEl = this.shadowRoot.querySelector('[part="trigger-host"]');
|
|
145
|
-
this.triggerSlot = this.shadowRoot.querySelector("slot");
|
|
186
|
+
this.triggerSlot = this.shadowRoot.querySelector("slot:not([name])");
|
|
146
187
|
this.fallbackTriggerEl = this.shadowRoot.querySelector('[part="trigger"]');
|
|
147
188
|
this.tooltipEl = this.shadowRoot.querySelector('[part="tooltip"]');
|
|
189
|
+
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
148
190
|
}
|
|
149
191
|
setupListeners() {
|
|
150
192
|
this.triggerHostEl.addEventListener("mouseenter", () => this.show());
|
|
@@ -180,19 +222,40 @@ export class BoxTooltipElement extends BaseElement {
|
|
|
180
222
|
if (!this.triggerHostEl || !this.tooltipEl || !this.triggerSlot) {
|
|
181
223
|
return;
|
|
182
224
|
}
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
this.labelEl.textContent = this.label;
|
|
226
|
+
const theme = this.theme;
|
|
227
|
+
if (theme === "default") {
|
|
228
|
+
this.tooltipEl.removeAttribute("data-theme");
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.tooltipEl.setAttribute("data-theme", theme);
|
|
232
|
+
}
|
|
185
233
|
this.tooltipEl.hidden = !this.openValue;
|
|
186
234
|
const trigger = this.resolveDescribedTrigger();
|
|
187
235
|
if (trigger === this.fallbackTriggerEl) {
|
|
188
236
|
this.fallbackTriggerEl.setAttribute("aria-label", this.triggerLabel);
|
|
189
237
|
}
|
|
190
238
|
this.clearDescribedBy();
|
|
239
|
+
this.stopTracking();
|
|
191
240
|
if (this.openValue) {
|
|
192
241
|
trigger.setAttribute("aria-describedby", this.tooltipId);
|
|
193
242
|
this.describedTrigger = trigger;
|
|
243
|
+
// Position as a fixed-coordinate overlay so it escapes ancestor overflow
|
|
244
|
+
// and flips/shifts to stay on-screen.
|
|
245
|
+
this.positionCleanup = trackAnchor(this.triggerHostEl, this.tooltipEl, {
|
|
246
|
+
placement: this.resolvedPlacement(),
|
|
247
|
+
offset: 8,
|
|
248
|
+
});
|
|
194
249
|
}
|
|
195
250
|
}
|
|
251
|
+
stopTracking() {
|
|
252
|
+
this.positionCleanup?.();
|
|
253
|
+
this.positionCleanup = null;
|
|
254
|
+
}
|
|
255
|
+
disconnectedCallback() {
|
|
256
|
+
this.stopTracking();
|
|
257
|
+
super.disconnectedCallback?.();
|
|
258
|
+
}
|
|
196
259
|
}
|
|
197
260
|
export const defineBoxTooltipElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
198
261
|
const existingElement = customElements.get(tagName);
|