@fusion.dev/helix-headless 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -7,210 +7,24 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { anchorElement, customElement, HelixElement, hostAria, MixinControllerOpen, PopoverController, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /** Tags that exist to hold something, and say nothing when they hold nothing. */
11
10
  const GENERIC = new Set(['div', 'p', 'section', 'span']);
12
- /**
13
- * @summary A short description that appears beside what it describes.
14
- *
15
- * @description
16
- * The trigger is slotted and stays the consumer's own element. The bubble is
17
- * rendered here, in the top layer through the popover API, so it clears any
18
- * ancestor with `overflow: hidden` without a `z-index` anywhere.
19
- *
20
- * Position comes from `FloatingController`, which flips to another side when
21
- * the preferred one does not fit and slides along it to stay in view.
22
- *
23
- * **Two forms, and they are not equivalent for a screen reader.**
24
- *
25
- * Wrapping is the convenience form and is right where the bubble decorates a
26
- * control that already says what it is. The trigger stays the consumer's own
27
- * element, slotted, and nothing here can describe it: an IDREF cannot cross a
28
- * shadow boundary, an element reference is valid only into the same tree or a
29
- * shadow-including *ancestor* (the bubble's tree is neither, it is inward), and
30
- * a string on the trigger means writing to markup the consumer server-rendered.
31
- * All three measured.
32
- *
33
- * `for` is the form that works. The tooltip sits *beside* the trigger in the
34
- * consumer's own tree and names it by id, so both are in one tree and an IDREF
35
- * between them is legal in the ordinary way:
36
- *
37
- * ```html
38
- * <helix-button id="save" aria-describedby="save-tip">Save</helix-button>
39
- * <helix-tooltip id="save-tip" for="save" content="Save your work"></helix-tooltip>
40
- * ```
41
- *
42
- * Note that the reference points at *this host*, not at the bubble inside its
43
- * shadow root: the description is computed from the flat tree, which descends
44
- * into shadow content on its own.
45
- *
46
- * **The consumer writes that `aria-describedby`, and nothing here writes it for
47
- * them.** WebAwesome and Spectrum Web Components both do write it, with
48
- * `setAttribute` on the trigger at runtime, which is the one thing this library
49
- * declines everywhere: React rendered that element on the server without the
50
- * attribute and reports a mismatch it will not patch up. Two ids in the markup
51
- * is the whole cost of the version that is correct before any JavaScript runs.
52
- *
53
- * Because it is the consumer's line to write, it is also theirs to forget, so
54
- * `for` audits itself on first open and says so once, rather than on every
55
- * open, since a warning that repeats is one people learn to filter out.
56
- *
57
- * It shows on hover and on focus, and hides on leave, on blur, on Escape, and
58
- * once the trigger has been activated: the hint is spent when the control has
59
- * been used.
60
- * A tooltip that answers only the pointer is one a keyboard reader never sees.
61
- *
62
- * The pointer waits out `delay` first, or sweeping a row of controls flashes a
63
- * tooltip for each of them. Focus does not wait: a keyboard reader landing on a
64
- * control has already committed to it.
65
- *
66
- * Open state comes from `MixinControllerOpen`, so it announces `helix-before-open` and
67
- * `helix-before-close` like every other overlay here, and a consumer can drive it with
68
- * `open` where they need to.
69
- *
70
- * @since 0.1.0
71
- * @status experimental
72
- * @category overlays
73
- * @aliases Hint, Popup label
74
- * @documentation https://helix.fusion.dev/components/tooltip
75
- *
76
- * @slot - The trigger.
77
- * @slot content - The bubble's content, when `content` is not enough. Anything
78
- * slotted here replaces the property, which is this slot's fallback.
79
- *
80
- * @event {HelixBeforeCloseEvent} helix-before-close - Fired before it hides, and cancelable.
81
- * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the bubble shows, and cancelable.
82
- * @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
83
- *
84
- * @csspart arrow - The point, resting against the trigger.
85
- * @csspart tooltip - The bubble.
86
- */
87
11
  let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpen(HelixElement) {
88
12
  constructor() {
89
13
  super(...arguments);
90
- /**
91
- * The text shown in the bubble.
92
- *
93
- * The common case, and the fallback for the `content` slot: slot something
94
- * there and it replaces this. Both cannot show at once, which is the point
95
- * of using fallback content rather than rendering whichever is present.
96
- */
97
14
  this.content = '';
98
- /**
99
- * How long the pointer must rest before the bubble appears, in milliseconds.
100
- *
101
- * Without one, sweeping across a row of controls flashes a tooltip for each
102
- * of them, which is noise rather than help. Focus opens with no delay: a
103
- * keyboard reader arriving on a control has already committed to it.
104
- */
105
15
  this.delay = 400;
106
- /**
107
- * How long the bubble stays after the pointer leaves, in milliseconds.
108
- */
109
16
  this.delayOut = 100;
110
- /**
111
- * Whether the tooltip never shows.
112
- */
113
17
  this.disabled = false;
114
- /**
115
- * The id of the element this describes, in this tooltip's own tree.
116
- *
117
- * Set it and the tooltip stops wrapping: it renders no trigger, listens on
118
- * the element it names, and points its bubble there. Leave it unset and the
119
- * default slot is the trigger, which is the older form.
120
- *
121
- * Resolved through `getRootNode()`, so a tooltip inside another component's
122
- * shadow root names an element in that root rather than in the document.
123
- *
124
- * **In React this arrives as a property and not as an attribute**, which is
125
- * worth knowing before someone reads the DOM and thinks it did not take.
126
- * Measured against React 19.2.8: `for` exists on the prototype, so React sets
127
- * `element.for = 'save'` and writes nothing to the markup, where `content`
128
- * and `placement`, which are not properties on the class at that moment, come
129
- * through as attributes. Both paths reach the same place through Lit, so
130
- * nothing is broken by it; a `[for]` selector would find nothing, which is
131
- * why this does not reflect and nothing styles on it.
132
- */
133
18
  this.for = '';
134
- /**
135
- * Space between the trigger and the point of the bubble, in pixels.
136
- *
137
- * **Measured from the point, which is what the arrow being back changed.**
138
- * The arrow is drawn half outside the bubble, so a gap measured to the
139
- * bubble's edge is a smaller gap to the tip: at the default it left the point
140
- * two pixels off the trigger, which reads as touching. The controller is
141
- * given this plus however far the arrow actually protrudes, so eight means
142
- * eight from whatever is nearest, arrow or edge, and a consumer who changes
143
- * it changes the visible gap rather than a number the arrow then eats into.
144
- */
145
19
  this.offset = 8;
146
- /**
147
- * Whether the bubble survives the trigger being used.
148
- *
149
- * **Activation takes a tooltip down, because a hint about what a control will
150
- * do is spent once the control has done it.** That is right for a gear that
151
- * opens a drawer and wrong for a control whose tooltip *becomes* the answer:
152
- * `CopyButton` swaps its content from Copy to Copied on the same click, so
153
- * the bubble would be hidden at the exact moment it has something to say and
154
- * the confirmation would never be on screen long enough to read.
155
- *
156
- * So this is not "keep it open", it is "the hint is not spent": set it where
157
- * the words change to describe what just happened, and leave it alone
158
- * everywhere else. Escape, blur and pointer-leave still close it, because
159
- * those are a reader saying they are done rather than a control saying it
160
- * worked.
161
- */
162
20
  this.persist = false;
163
- /**
164
- * Preferred side.
165
- *
166
- * It moves if that side does not fit.
167
- */
168
21
  this.placement = 'top';
169
- /**
170
- * The id of a box to keep the bubble inside, instead of the viewport.
171
- *
172
- * Resolved the way `for` is, so an id in the same tree, which is what lets a
173
- * component name a box in its own shadow root. A chart does: its bubble is
174
- * centered on a reading, and the first and last readings sit at the ends of
175
- * the plot, so half the box hangs past the drawing and, measured on a chart
176
- * in a 320px card, three pixels past the card itself.
177
- *
178
- * Clamping is on the cross axis only, which is what makes this safe to point
179
- * at a box the bubble genuinely does not fit inside. See `within` on
180
- * `FloatingController` for what happens if it is applied to the rest.
181
- */
182
22
  this.within = '';
183
- /**
184
- * What is currently keeping the bubble up.
185
- *
186
- * The pointer and focus are two holders, so a single timer with nothing
187
- * counting them hides on whichever leaves last: moving the pointer off a
188
- * control that still has keyboard focus would take the bubble away from under
189
- * the reader. Same shape as the toast timer, and the same answer.
190
- */
191
23
  this.holds = new Set();
192
- /* The boundary is the host and not the bubble, for the reason `Popover` gives:
193
- the host is `display: contents`, so a press on the trigger is inside the
194
- host and outside the bubble, and a boundary of the bubble alone would treat
195
- the press that opened it as a press outside. */
196
24
  this.bubbleLayer = new PopoverController(this, {
197
25
  anchor: () => this.trigger,
198
26
  arrow: () => this.renderRoot.querySelector('[part="arrow"]'),
199
- /* Annotated, because without it the array's element type is inferred from
200
- `this`, whose own type depends on this controller, and TypeScript stops
201
- at the cycle. */
202
27
  boundary: () => [this, this.named],
203
- /* The declared gap plus the arrow's own reach, so the gap is to the point.
204
- *
205
- * A square of side `n` rotated forty-five degrees about its own center
206
- * reaches `n / sqrt(2)` from that center, and the arrow is placed with its
207
- * center on the bubble's edge, so that is exactly how far past the edge the
208
- * tip goes. Read from the element rather than assumed, because
209
- * `--tooltip-arrow-size` is a hook and a theme may have moved it.
210
- *
211
- * `offsetWidth` and not `getBoundingClientRect`, which reports the rotated
212
- * box and would count the reach twice. Zero while the bubble is closed and
213
- * unrendered, which is right: there is nothing to place then either. */
214
28
  offset: () => {
215
29
  const point = this.renderRoot.querySelector('[part="arrow"]');
216
30
  return this.offset + (point?.offsetWidth ?? 0) / Math.SQRT2;
@@ -220,45 +34,19 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
220
34
  surface: () => this.renderRoot.querySelector('[part="tooltip"]'),
221
35
  within: () => this.enclosure,
222
36
  });
223
- /**
224
- * Re-links the description when what is in the bubble changes.
225
- *
226
- * `updated` alone is not enough: content arriving from the light DOM changes
227
- * no property here, so nothing would schedule the update that re-reads it. A
228
- * consumer rendering the bubble's content after this upgrades, which is what
229
- * a framework does, would leave the trigger describing nothing.
230
- */
231
37
  this.handleContentChange = () => {
232
38
  this.requestUpdate();
233
39
  };
234
- /** Shows after the pointer has rested for `delay`. */
235
40
  this.showLater = () => {
236
41
  this.holds.add('pointer');
237
42
  clearTimeout(this.timer);
238
43
  this.timer = setTimeout(() => this.show(), this.delay);
239
44
  };
240
- /** Hides after `delayOut`, so crossing the gap to the bubble does not close it. */
241
45
  this.hideLater = () => {
242
46
  this.holds.delete('pointer');
243
47
  clearTimeout(this.timer);
244
48
  this.timer = setTimeout(() => this.release(), this.delayOut);
245
49
  };
246
- /**
247
- * Focus opens the bubble, but only the kind of focus a reader chose.
248
- *
249
- * **A mouse press focuses the button it pressed**, so an unconditional focus
250
- * hold leaves the bubble up after any click for a reader who was not asking
251
- * for it: moving the pointer away releases the pointer hold, finds a focus
252
- * hold still set, and leaves the tooltip on screen until something else blurs
253
- * the control. `persist` is what makes that reachable, activation otherwise
254
- * clearing every hold before anyone can see it.
255
- *
256
- * `:focus-visible` is the platform's own answer to "did they mean it", and a
257
- * button does not match it on a mouse press. Keyboard focus still opens with no
258
- * delay, which is the case this branch exists for: a reader who tabbed to a
259
- * control has already committed to it. Where it does not match there is nothing
260
- * to do, the pointer having opened the bubble already if a pointer is involved.
261
- */
262
50
  this.showNow = (event) => {
263
51
  const focused = event.target;
264
52
  if (focused instanceof Element && !focused.matches(':focus-visible')) {
@@ -271,30 +59,6 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
271
59
  this.holds.delete('focus');
272
60
  this.release();
273
61
  };
274
- /**
275
- * Activating the trigger closes the bubble.
276
- *
277
- * A tooltip is a hint about what a control will do, and once the control has
278
- * been used the hint is spent. Nothing else takes it down either:
279
- * `pointerleave` cannot fire while the pointer is still on the button it just
280
- * pressed, and `focusout` does not fire when activation leaves focus where it
281
- * was. Without this, a gear that opened a drawer leaves its own label
282
- * floating over the drawer until the reader moves the mouse away.
283
- *
284
- * `click` rather than `pointerdown`, because it is the one event both a mouse
285
- * and a keyboard produce: Enter and Space on a focused button raise it, and a
286
- * hint that outlived activation for keyboard readers alone would be the same
287
- * bug with a smaller audience.
288
- *
289
- * Holds are cleared as well as hiding, the same as Escape. Leaving them set
290
- * means the pointer is still recorded as being over the trigger, so the next
291
- * `focusout` anywhere releases nothing and the bubble comes back on a
292
- * `release` it should have been dropped from.
293
- *
294
- * `persist` opts out entirely, holds included: a tooltip whose content becomes
295
- * the confirmation has not spent its hint, it has changed it, and the pointer
296
- * really is still over the trigger. See the property.
297
- */
298
62
  this.handleActivate = () => {
299
63
  if (this.persist) {
300
64
  return;
@@ -302,67 +66,24 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
302
66
  this.holds.clear();
303
67
  this.hide();
304
68
  };
305
- /**
306
- * Escape closes the bubble from wherever the reader is.
307
- *
308
- * On the document, not on the host: a tooltip opened by hover has focus
309
- * somewhere else entirely, so a press never reaches this element and Escape
310
- * would work for everything except the case the pattern is named for, a
311
- * reader escaping a tooltip obscuring what is under it.
312
- *
313
- * Not stopped from propagating: a tooltip is not what the reader was trying
314
- * to escape if it sits inside something else that also closes.
315
- */
316
69
  this.handleKeydown = (event) => {
317
70
  if (event.key === 'Escape' && this.open) {
318
71
  this.holds.clear();
319
72
  this.hide();
320
73
  }
321
74
  };
322
- /** Said once per element, however many times it opens. */
323
75
  this.audited = false;
324
76
  }
325
- /**
326
- * The element the bubble points at.
327
- *
328
- * The trigger, not the host. The host is `display: contents` so it wraps
329
- * without adding a box, and an element that generates no box measures as
330
- * nothing at the document origin: the bubble was placed against the corner
331
- * of the viewport rather than against the button.
332
- */
333
77
  get trigger() {
334
- /* Named, or slotted. The named one is looked up every time rather than
335
- cached: `for` can be pointed somewhere else, and the element it names can
336
- upgrade, move or be replaced by a framework without telling this. */
337
78
  const named = this.named;
338
79
  if (named) {
339
80
  return named;
340
81
  }
341
- /* Through the util, whose extra is the descent: the first assigned element
342
- may itself draw nothing, and an icon button wrapped in a
343
- `display: contents` span takes the bubble to the corner of the window. */
344
82
  return anchorElement(this.renderRoot.querySelector('slot:not([name])'), this);
345
83
  }
346
- /**
347
- * Places the bubble again, for an anchor that moved without the page moving.
348
- *
349
- * Public because a chart needs it. Its mark walks from reading to reading
350
- * while the page holds still, and `autoUpdate` watches scrolling, resizing
351
- * and layout shift, none of which is that. Everything else floating here is
352
- * anchored to something the page moves, which is why nothing wanted this
353
- * until a chart did.
354
- */
355
84
  reposition() {
356
85
  this.bubbleLayer.reposition();
357
86
  }
358
- /**
359
- * The element `for` names, or null.
360
- *
361
- * `getRootNode()` rather than `document`, which is what makes this work inside
362
- * another component: an id is unique to a tree, and a tooltip in a shadow root
363
- * names an element in that root. Client only, since the server has no tree to
364
- * search.
365
- */
366
87
  get named() {
367
88
  if (this.for === '') {
368
89
  return null;
@@ -370,13 +91,6 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
370
91
  const root = this.getRootNode();
371
92
  return root.getElementById?.(this.for) ?? null;
372
93
  }
373
- /**
374
- * The element `within` names, or null.
375
- *
376
- * Same resolution as `named`, and separate from it because they answer
377
- * different questions: one is what the bubble points at and the other is what
378
- * it must stay inside, and a component often has one and not the other.
379
- */
380
94
  get enclosure() {
381
95
  if (this.within === '') {
382
96
  return null;
@@ -389,22 +103,6 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
389
103
  get bubble() {
390
104
  return this.renderRoot.querySelector('[part="tooltip"]');
391
105
  }
392
- /**
393
- * Whether there is anything to show.
394
- *
395
- * Asked of the slot rather than of the property, because either can carry
396
- * the content. `assignedNodes` without `flatten` answers what the consumer
397
- * provided and not what the fallback would render, which is exactly the
398
- * question: a bubble with only fallback and an empty property is empty.
399
- *
400
- * Text or an element, because neither alone is the test. An element on its
401
- * own would count `<span slot="content"> </span>` as something to say,
402
- * and text on its own would count an icon as nothing, when an icon is the
403
- * whole point of slotting rather than setting the property.
404
- *
405
- * Client only. The server has no assignment to inspect, and nothing calls
406
- * this there: a tooltip opens in response to a pointer or to focus.
407
- */
408
106
  get hasContent() {
409
107
  if (this.content !== '') {
410
108
  return true;
@@ -414,33 +112,12 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
414
112
  if (node.nodeType !== Node.ELEMENT_NODE) {
415
113
  return node.textContent?.trim() !== '';
416
114
  }
417
- /* A wrapper is empty when it holds nothing; anything else draws.
418
- *
419
- * Not children-or-text for every element, which reads as careful and
420
- * refuses the exact case this getter's own paragraph calls the whole
421
- * point of slotting: `<helix-icon slot="content">` has no children and no
422
- * text, so `hasContent` would be false, `show()` would return early, and
423
- * a tooltip whose bubble is an icon could not be opened at all.
424
- *
425
- * The spec beside this one is the other half, and it is why the answer is
426
- * not simply "an element is content": `<span slot="content"> </span>`
427
- * is an element too and says nothing. In the light DOM those two are
428
- * identical, so nothing can separate them by shape and the tag is what
429
- * is left. A generic wrapper is asked to hold something; everything else,
430
- * an icon, an image, an svg, a component, draws on its own. */
431
115
  const element = node;
432
116
  return GENERIC.has(element.localName)
433
117
  ? element.childElementCount > 0 || element.textContent?.trim() !== ''
434
118
  : true;
435
119
  });
436
120
  }
437
- /**
438
- * The bubble's words as a plain string, for naming the host.
439
- *
440
- * The property when it is set, otherwise whatever was slotted, flattened.
441
- * Rich content collapses to its text, which is the right answer for a
442
- * description: `Save ⌘ S` is what a reader needs, not the markup.
443
- */
444
121
  get describedText() {
445
122
  if (this.content !== '') {
446
123
  return this.content;
@@ -452,7 +129,6 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
452
129
  .replace(/\s+/gu, ' ')
453
130
  .trim();
454
131
  }
455
- /** Shows the bubble, unless there is nothing to show. */
456
132
  show() {
457
133
  clearTimeout(this.timer);
458
134
  if (this.disabled || !this.hasContent || this.open) {
@@ -461,16 +137,7 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
461
137
  if (!this.requestOpen(true)) {
462
138
  return;
463
139
  }
464
- // Raised in `updated()`, which the state change has just scheduled: one
465
- // path whether the bubble was opened by a pointer or by declaring `open`.
466
140
  }
467
- /**
468
- * Closes the bubble, and cancels a show that had not happened yet.
469
- *
470
- * The timer matters as much as the state: a pointer that arrives and leaves
471
- * inside the delay left one queued, and the tooltip appeared over whatever
472
- * the reader had moved on to.
473
- */
474
141
  hide() {
475
142
  clearTimeout(this.timer);
476
143
  if (!this.open) {
@@ -481,22 +148,11 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
481
148
  }
482
149
  this.bubbleLayer.hide();
483
150
  }
484
- /** Hides only once nothing is holding the bubble up. */
485
151
  release() {
486
152
  if (this.holds.size === 0) {
487
153
  this.hide();
488
154
  }
489
155
  }
490
- /**
491
- * Puts the listeners on whatever the trigger is.
492
- *
493
- * The host when it wraps, since the trigger is inside it and both `focusin`
494
- * and `focusout` bubble. The named element when `for` is set, because then the
495
- * host wraps nothing and `pointerenter` does not bubble at all.
496
- *
497
- * Client only, and after the first render: `for` names an element that may not
498
- * have upgraded, or existed, when this one connected.
499
- */
500
156
  bind() {
501
157
  const target = this.named ?? this;
502
158
  if (this.bound === target) {
@@ -505,8 +161,6 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
505
161
  this.unbind();
506
162
  target.addEventListener('pointerenter', this.showLater);
507
163
  target.addEventListener('pointerleave', this.hideLater);
508
- // Focus opens at once: a keyboard reader landing on a control has already
509
- // committed to it, where a pointer crossing one has not.
510
164
  target.addEventListener('focusin', this.showNow);
511
165
  target.addEventListener('focusout', this.releaseFocus);
512
166
  target.addEventListener('click', this.handleActivate);
@@ -520,44 +174,11 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
520
174
  this.bound?.removeEventListener('focusout', this.releaseFocus);
521
175
  this.bound = undefined;
522
176
  }
523
- /**
524
- * Checks that the trigger actually points back, and says so once if not.
525
- *
526
- * `for` is only half a link: this element finds the trigger, and the trigger
527
- * has to name this element for a screen reader to read the two together. That
528
- * second half is the consumer's line to write, deliberately, because writing
529
- * it from here means `setAttribute` on markup they server-rendered. So it is
530
- * theirs to forget, and a silent accessibility gap is the worst kind: the
531
- * tooltip looks completely correct.
532
- *
533
- * On first open rather than on connect, because a framework may render the
534
- * attribute after this element upgrades, and because a tooltip that never
535
- * opens has described nothing either way.
536
- *
537
- * Either attribute counts. WebAwesome uses `aria-labelledby` over
538
- * `aria-describedby` on purpose, their reasoning being that some screen
539
- * readers want the description on the focusable element itself while a label
540
- * reads on first focus regardless, and that is a live enough question that
541
- * this should not be the thing to settle it.
542
- */
543
177
  auditDescription() {
544
178
  if (this.audited || !this.hasContent) {
545
179
  return;
546
180
  }
547
181
  this.audited = true;
548
- /* **The unlinked form is the one that describes nothing, so it is the one
549
- warned about.** This returned on `for === ''`, which made the guard the
550
- exact inverse of the risk: the wrapping form is never linked, so it was
551
- the single form that could never be reported.
552
-
553
- Measured in Chrome's accessibility tree,
554
- `<helix-tooltip content="..."><helix-button>Save</helix-button></helix-tooltip>`
555
- exposes `button name="Save" desc=""`. All three routes to fixing it are
556
- closed: an `aria-describedby` IDREF resolves only within one tree,
557
- `ariaDescribedByElements` is valid outward and this points inward, and
558
- `aria-description` is the hydration mismatch 322 closed the library
559
- against. So the wrapping form is no longer the documented API, and this
560
- says so once, where somebody using it will see it. */
561
182
  if (this.for === '') {
562
183
  console.warn(`<${this.localName}> is wrapping its trigger rather than naming one, so its text ` +
563
184
  "reaches no screen reader: a description in this element's shadow root cannot be " +
@@ -600,10 +221,6 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
600
221
  }
601
222
  updated(changed) {
602
223
  super.updated(changed);
603
- /* Raised and lowered from the resolved state, so a bubble opened by
604
- declaring `open` takes the same path as one opened by a pointer. Here
605
- rather than a microtask later: the render has been written, so the bubble
606
- is no longer `display: none` and `showPopover` is safe on it. */
607
224
  if (changed.has('for')) {
608
225
  this.bind();
609
226
  }
@@ -620,52 +237,8 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
620
237
  if (!bubble) {
621
238
  return;
622
239
  }
623
- /* The reference goes on the host, and the host is not what a reader is on.
624
- *
625
- * The host is `display: contents` and carries no role; the thing that takes
626
- * focus is the consumer's trigger inside it, and a description on an
627
- * ancestor is not announced with it. So the trigger has to be described
628
- * too, and it cannot be described by reference: measured, assigning
629
- * `trigger.ariaDescribedByElements = [bubble]` reads back as an empty array,
630
- * a reference being valid only into the same tree or a shadow-including
631
- * ancestor of it. The bubble's tree is neither, it is inward.
632
- *
633
- * A string would cross, and `aria-description` on the trigger was tried:
634
- * it works, and it is a hydration mismatch. The trigger is the consumer's
635
- * own element, React rendered it on the server without that attribute, and
636
- * writing it at upgrade is exactly what this library declines to do
637
- * everywhere else. Measured against the site's own header: React logged the
638
- * mismatch on every tooltip on the page and said it would not patch it up.
639
- *
640
- * So the description stays on the host and the gap is recorded rather than
641
- * papered over. There is no third option: a description has to live on the
642
- * element being described, that element belongs to the consumer, and the
643
- * bubble is in a tree no reference from there can name. Closing it means
644
- * either writing to the consumer's markup or moving the bubble into the
645
- * light DOM, and both are decisions rather than fixes. */
646
240
  hostAria(this.internals, {
647
241
  describedBy: this.hasContent ? [bubble] : [],
648
- /* **And the host names itself, which is what makes `for` work when the
649
- bubble is down.**
650
- *
651
- * A reference resolves to the referenced element's accessible *name*, and
652
- * that name comes from `aria-labelledby`, then `aria-label`, then the
653
- * element's contents, in that order. Only the third can be defeated by
654
- * the contents being `display: none`, which is exactly what a closed
655
- * popover is. So without a name of its own the host describes nothing
656
- * until the bubble is already up, and a screen reader computes a
657
- * description when focus lands, which is a frame or two before that.
658
- *
659
- * Measured, one page, two identical tooltips:
660
- *
661
- * CLOSED bare host (none)
662
- * CLOSED labeled host Description B
663
- * OPEN bare host Description A
664
- * OPEN labeled host Description B
665
- *
666
- * Only when `for` is set, because only then is this host the thing being
667
- * referenced. Wrapping, it is an ancestor of the trigger and naming it
668
- * would put a name on a box that is `display: contents`. */
669
242
  label: this.for === '' ? '' : this.describedText,
670
243
  });
671
244
  }