@unofficialbox/box-open-elements 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/actions/link-button.d.ts +9 -0
- package/dist/components/actions/link-button.js +55 -4
- package/dist/components/collections/carousel.d.ts +7 -0
- package/dist/components/collections/carousel.js +71 -4
- package/dist/components/collections/datalist-item.d.ts +6 -0
- package/dist/components/collections/datalist-item.js +39 -1
- package/dist/components/collections/draggable-list.js +1 -1
- package/dist/components/collections/grid-view.js +7 -3
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +30 -1
- package/dist/components/feedback/badge.d.ts +10 -0
- package/dist/components/feedback/badge.js +70 -4
- package/dist/components/feedback/chip.d.ts +5 -0
- package/dist/components/feedback/chip.js +74 -2
- package/dist/components/feedback/error-mask.d.ts +2 -0
- package/dist/components/feedback/error-mask.js +23 -0
- package/dist/components/feedback/help-text.js +7 -0
- package/dist/components/feedback/spinner.d.ts +4 -0
- package/dist/components/feedback/spinner.js +30 -2
- package/dist/components/feedback/toast.d.ts +5 -0
- package/dist/components/feedback/toast.js +59 -11
- package/dist/components/forms/category-selector.d.ts +11 -0
- package/dist/components/forms/category-selector.js +211 -17
- package/dist/components/forms/checkbox.d.ts +6 -0
- package/dist/components/forms/checkbox.js +58 -1
- package/dist/components/forms/combobox.d.ts +30 -1
- package/dist/components/forms/combobox.js +348 -44
- package/dist/components/forms/date-field.d.ts +7 -0
- package/dist/components/forms/date-field.js +73 -1
- package/dist/components/forms/dropdown.d.ts +5 -0
- package/dist/components/forms/dropdown.js +32 -3
- package/dist/components/forms/radio-group.d.ts +9 -8
- package/dist/components/forms/radio-group.js +43 -6
- package/dist/components/forms/search-field.d.ts +8 -0
- package/dist/components/forms/search-field.js +55 -13
- package/dist/components/forms/select.d.ts +18 -8
- package/dist/components/forms/select.js +104 -14
- package/dist/components/forms/text-field.d.ts +13 -0
- package/dist/components/forms/text-field.js +136 -1
- package/dist/components/forms/time-field.d.ts +11 -0
- package/dist/components/forms/time-field.js +51 -0
- package/dist/components/identity/avatar.d.ts +4 -0
- package/dist/components/identity/avatar.js +65 -1
- package/dist/components/identity/contact-datalist-item.d.ts +8 -0
- package/dist/components/identity/contact-datalist-item.js +71 -2
- package/dist/components/layout/nav-sidebar.js +23 -0
- package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
- package/dist/components/layout/sidebar-toggle-button.js +21 -2
- package/dist/components/navigation/accordion.d.ts +3 -0
- package/dist/components/navigation/accordion.js +17 -2
- package/dist/components/overlays/guide-tooltip.d.ts +51 -0
- package/dist/components/overlays/guide-tooltip.js +361 -0
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +74 -11
- package/dist/foundations/overlay/positioning.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -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,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);
|
|
@@ -146,7 +146,11 @@ export const trackAnchor = (anchor, floating, options = {}, onReposition) => {
|
|
|
146
146
|
const reposition = () => {
|
|
147
147
|
if (!anchor.isConnected || !floating.isConnected)
|
|
148
148
|
return;
|
|
149
|
-
|
|
149
|
+
// Compute+apply the position first, then notify. Do NOT inline this into
|
|
150
|
+
// `onReposition?.(anchorFloating(...))`: optional-call short-circuits its
|
|
151
|
+
// argument when the callback is undefined, so anchorFloating would never run.
|
|
152
|
+
const result = anchorFloating(anchor, floating, options);
|
|
153
|
+
onReposition?.(result);
|
|
150
154
|
};
|
|
151
155
|
reposition();
|
|
152
156
|
window.addEventListener("scroll", reposition, true);
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export * from "./components/layout/split-view.js";
|
|
|
80
80
|
export * from "./components/overlays/context-menu.js";
|
|
81
81
|
export * from "./components/overlays/dialog.js";
|
|
82
82
|
export * from "./components/overlays/drawer.js";
|
|
83
|
+
export * from "./components/overlays/guide-tooltip.js";
|
|
83
84
|
export * from "./components/overlays/popover.js";
|
|
84
85
|
export * from "./components/overlays/tooltip.js";
|
|
85
86
|
export * from "./patterns/content-explorer/index.js";
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,7 @@ export * from "./components/layout/split-view.js";
|
|
|
89
89
|
export * from "./components/overlays/context-menu.js";
|
|
90
90
|
export * from "./components/overlays/dialog.js";
|
|
91
91
|
export * from "./components/overlays/drawer.js";
|
|
92
|
+
export * from "./components/overlays/guide-tooltip.js";
|
|
92
93
|
export * from "./components/overlays/popover.js";
|
|
93
94
|
export * from "./components/overlays/tooltip.js";
|
|
94
95
|
// Patterns: content explorer (headless blocks, contracts, transport, adapters)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unofficialbox/box-open-elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://unofficialbox.github.io/box-open-elements",
|