@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.
- package/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- 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
|
}
|