@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.
Files changed (58) hide show
  1. package/dist/components/actions/link-button.d.ts +9 -0
  2. package/dist/components/actions/link-button.js +55 -4
  3. package/dist/components/collections/carousel.d.ts +7 -0
  4. package/dist/components/collections/carousel.js +71 -4
  5. package/dist/components/collections/datalist-item.d.ts +6 -0
  6. package/dist/components/collections/datalist-item.js +39 -1
  7. package/dist/components/collections/draggable-list.js +1 -1
  8. package/dist/components/collections/grid-view.js +7 -3
  9. package/dist/components/feedback/alert.d.ts +2 -0
  10. package/dist/components/feedback/alert.js +30 -1
  11. package/dist/components/feedback/badge.d.ts +10 -0
  12. package/dist/components/feedback/badge.js +70 -4
  13. package/dist/components/feedback/chip.d.ts +5 -0
  14. package/dist/components/feedback/chip.js +74 -2
  15. package/dist/components/feedback/error-mask.d.ts +2 -0
  16. package/dist/components/feedback/error-mask.js +23 -0
  17. package/dist/components/feedback/help-text.js +7 -0
  18. package/dist/components/feedback/spinner.d.ts +4 -0
  19. package/dist/components/feedback/spinner.js +30 -2
  20. package/dist/components/feedback/toast.d.ts +5 -0
  21. package/dist/components/feedback/toast.js +59 -11
  22. package/dist/components/forms/category-selector.d.ts +11 -0
  23. package/dist/components/forms/category-selector.js +211 -17
  24. package/dist/components/forms/checkbox.d.ts +6 -0
  25. package/dist/components/forms/checkbox.js +58 -1
  26. package/dist/components/forms/combobox.d.ts +30 -1
  27. package/dist/components/forms/combobox.js +348 -44
  28. package/dist/components/forms/date-field.d.ts +7 -0
  29. package/dist/components/forms/date-field.js +73 -1
  30. package/dist/components/forms/dropdown.d.ts +5 -0
  31. package/dist/components/forms/dropdown.js +32 -3
  32. package/dist/components/forms/radio-group.d.ts +9 -8
  33. package/dist/components/forms/radio-group.js +43 -6
  34. package/dist/components/forms/search-field.d.ts +8 -0
  35. package/dist/components/forms/search-field.js +55 -13
  36. package/dist/components/forms/select.d.ts +18 -8
  37. package/dist/components/forms/select.js +104 -14
  38. package/dist/components/forms/text-field.d.ts +13 -0
  39. package/dist/components/forms/text-field.js +136 -1
  40. package/dist/components/forms/time-field.d.ts +11 -0
  41. package/dist/components/forms/time-field.js +51 -0
  42. package/dist/components/identity/avatar.d.ts +4 -0
  43. package/dist/components/identity/avatar.js +65 -1
  44. package/dist/components/identity/contact-datalist-item.d.ts +8 -0
  45. package/dist/components/identity/contact-datalist-item.js +71 -2
  46. package/dist/components/layout/nav-sidebar.js +23 -0
  47. package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
  48. package/dist/components/layout/sidebar-toggle-button.js +21 -2
  49. package/dist/components/navigation/accordion.d.ts +3 -0
  50. package/dist/components/navigation/accordion.js +17 -2
  51. package/dist/components/overlays/guide-tooltip.d.ts +51 -0
  52. package/dist/components/overlays/guide-tooltip.js +361 -0
  53. package/dist/components/overlays/tooltip.d.ts +10 -0
  54. package/dist/components/overlays/tooltip.js +74 -11
  55. package/dist/foundations/overlay/positioning.js +5 -1
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. 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: absolute;
42
- inset-block-start: calc(100% + 0.5rem);
43
- inset-inline-start: 50%;
44
- transform: translateX(-50%);
45
- z-index: 1;
46
- width: min(13.75rem, calc(100vw - 6rem));
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
- const label = this.label;
184
- this.tooltipEl.textContent = label;
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
- onReposition?.(anchorFloating(anchor, floating, options));
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.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",