@fusion.dev/helix-headless 0.1.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,72 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { CommandController, customElement, HelixElement, hostAria, MixinControllerOpen, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary A modal window, on the platform's own dialog.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* A real `<dialog>` opened with `showModal()`, not a div with a role and a
|
|
15
|
-
* hand-built trap. The platform gives four things that are each a day's work to
|
|
16
|
-
* reimplement and easy to get subtly wrong: the top layer, so the dialog is
|
|
17
|
-
* above everything regardless of what `z-index` anyone else wrote; a real
|
|
18
|
-
* backdrop; Escape; and inert-behind, so the page underneath is not reachable
|
|
19
|
-
* by pointer, keyboard, or a screen reader's virtual cursor.
|
|
20
|
-
*
|
|
21
|
-
* **`open` drives `showModal()`, and every way it can close comes back through
|
|
22
|
-
* `requestOpen`.** A native dialog can close from three directions: the
|
|
23
|
-
* property, Escape, and a `<form method="dialog">` submit inside it. Left
|
|
24
|
-
* alone, two of those would shut the dialog while the consumer's state still
|
|
25
|
-
* said open, which is the bug that makes a modal impossible to reopen. Escape
|
|
26
|
-
* is intercepted and routed through the request, so `preventDefault()` on the
|
|
27
|
-
* `helix-before-close` refuses it; a close that happens anyway is reflected back.
|
|
28
|
-
*
|
|
29
|
-
* **Nothing here traps focus.** `showModal()` does it, and better: `inert` on
|
|
30
|
-
* the rest of the document is something no JavaScript trap can achieve.
|
|
31
|
-
* `FocusTrapController` stays for the surfaces that are not modal.
|
|
32
|
-
*
|
|
33
|
-
* The regions are bare slots rather than wrappers, for the reason the card's
|
|
34
|
-
* are: an unassigned `<slot>` is `display: contents` and generates no box, so a
|
|
35
|
-
* dialog with no footer has no empty band where one would be.
|
|
36
|
-
*
|
|
37
|
-
* Opened declaratively with the platform's invoker attributes:
|
|
38
|
-
*
|
|
39
|
-
* ```html
|
|
40
|
-
* <helix-button commandfor="confirm" command="--show">Delete…</helix-button>
|
|
41
|
-
* <helix-dialog id="confirm">…</helix-dialog>
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* `--show`, `--close` and `--toggle`. Custom commands rather than the built-in
|
|
45
|
-
* `show-modal`, because those are defined against native elements and do
|
|
46
|
-
* nothing when pointed at a custom one.
|
|
47
|
-
*
|
|
48
|
-
* @since 0.1.0
|
|
49
|
-
* @status stable
|
|
50
|
-
* @category overlays
|
|
51
|
-
* @aliases Modal, Lightbox, Sheet
|
|
52
|
-
* @documentation https://helix.fusion.dev/components/dialog
|
|
53
|
-
*
|
|
54
|
-
* @slot - The dialog's content.
|
|
55
|
-
* @slot footer - The actions.
|
|
56
|
-
* @slot header - The title, and anything beside it.
|
|
57
|
-
* @slot icon - A single mark above the title, for a dialog that warns.
|
|
58
|
-
*
|
|
59
|
-
* @event {Event} close - Relayed, because the platform's own does not escape.
|
|
60
|
-
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes, and cancelable.
|
|
61
|
-
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before it opens, and cancelable.
|
|
62
|
-
* @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
|
|
63
|
-
*
|
|
64
|
-
* @csspart dialog - The native dialog element.
|
|
65
|
-
*/
|
|
66
10
|
let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(HelixElement) {
|
|
67
11
|
constructor() {
|
|
68
12
|
super(...arguments);
|
|
69
|
-
/**
|
|
70
|
-
* Accessible name for the dialog.
|
|
71
|
-
*/
|
|
72
13
|
this.label = '';
|
|
73
|
-
/* Opened and closed from anywhere on the page, without a line of script:
|
|
74
|
-
`<helix-button commandfor="…" command="--show">`. See `CommandController`
|
|
75
|
-
for why this is the platform's invoker rather than a `data-` recipe. */
|
|
76
14
|
this.commands = new CommandController(this, {
|
|
77
15
|
commands: {
|
|
78
16
|
'--close': () => this.close(),
|
|
@@ -80,28 +18,8 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
80
18
|
'--toggle': () => this.toggleOpen(),
|
|
81
19
|
},
|
|
82
20
|
});
|
|
83
|
-
/**
|
|
84
|
-
* Whether Escape or a press on the backdrop can close it.
|
|
85
|
-
*
|
|
86
|
-
* Off for a dialog that must be answered. It does not disable the close
|
|
87
|
-
* control a consumer slots in: refusing to be dismissed is not the same as
|
|
88
|
-
* refusing to be closed, and a dialog with no way out at all is a trap.
|
|
89
|
-
*
|
|
90
|
-
* A persistent dialog does not ignore that press. It answers it: see
|
|
91
|
-
* `refuse`, which is the only way a dialog that will not be dismissed can say
|
|
92
|
-
* so at the moment a reader asks.
|
|
93
|
-
*/
|
|
94
21
|
this.persistent = false;
|
|
95
|
-
/** What a value set before the element existed is kept in. */
|
|
96
22
|
this.returned = '';
|
|
97
|
-
/**
|
|
98
|
-
* Escape, intercepted so it is a request rather than an act.
|
|
99
|
-
*
|
|
100
|
-
* The native `cancel` event fires before the dialog closes, so preventing it
|
|
101
|
-
* leaves the dialog open and lets `requestOpen` decide. Without this a reader
|
|
102
|
-
* pressing Escape would close a dialog its consumer had said was open, and
|
|
103
|
-
* the two would disagree until something else moved.
|
|
104
|
-
*/
|
|
105
23
|
this.handleCancel = (event) => {
|
|
106
24
|
event.preventDefault();
|
|
107
25
|
if (this.persistent) {
|
|
@@ -109,39 +27,12 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
109
27
|
}
|
|
110
28
|
this.requestOpen(false);
|
|
111
29
|
};
|
|
112
|
-
/**
|
|
113
|
-
* A close that happened anyway, reflected back.
|
|
114
|
-
*
|
|
115
|
-
* `<form method="dialog">` closes the dialog directly and there is no event
|
|
116
|
-
* to prevent. Rather than fight it, the state follows: the dialog is shut, so
|
|
117
|
-
* saying otherwise would be a lie the next `showModal()` would trip over.
|
|
118
|
-
*/
|
|
119
30
|
this.handleClose = (event) => {
|
|
120
31
|
if (this.open) {
|
|
121
32
|
this.requestOpen(false);
|
|
122
33
|
}
|
|
123
|
-
/* And under the platform's own name, from the host. `close` on a `<dialog>`
|
|
124
|
-
neither bubbles nor is composed, so the one the browser just fired reaches
|
|
125
|
-
nobody outside this shadow root, which is the same reason `Avatar` relays
|
|
126
|
-
`load`. Announced after the state has moved, so a consumer binding
|
|
127
|
-
`onClose` reads a dialog that is already shut.
|
|
128
|
-
|
|
129
|
-
`cancel` is deliberately not relayed. `handleCancel` prevents it so that
|
|
130
|
-
Escape is a request rather than an act, so the native one never completes:
|
|
131
|
-
relaying it would announce a dismissal that was refused. */
|
|
132
34
|
this.relayNativeEvent(event);
|
|
133
35
|
};
|
|
134
|
-
/**
|
|
135
|
-
* A press on the backdrop, which lands on the dialog element itself.
|
|
136
|
-
*
|
|
137
|
-
* **The target alone is not enough here, where it is for `Drawer`.** A modal
|
|
138
|
-
* `<dialog>` attributes backdrop clicks to itself, which is the usual trick,
|
|
139
|
-
* and this panel also carries `--dialog-padding` of padding: a press in that
|
|
140
|
-
* padding has the same target as a press on the backdrop, so the trick alone
|
|
141
|
-
* closes a dialog when a reader clicks just inside its own edge. The drawer
|
|
142
|
-
* escapes that because its panel is flush against the window and its content
|
|
143
|
-
* fills it. So the point is compared against the panel's box.
|
|
144
|
-
*/
|
|
145
36
|
this.handleBackdrop = (event) => {
|
|
146
37
|
const panel = this.dialog;
|
|
147
38
|
if (!panel || event.target !== panel) {
|
|
@@ -161,18 +52,6 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
161
52
|
this.requestOpen(false);
|
|
162
53
|
};
|
|
163
54
|
}
|
|
164
|
-
/**
|
|
165
|
-
* What the dialog closed with, the way `<dialog>` reports it.
|
|
166
|
-
*
|
|
167
|
-
* **The whole point of a `<dialog>` returning a value is lost behind a shadow
|
|
168
|
-
* boundary**, which is where this one is: the platform's contract is
|
|
169
|
-
* `close(value)` then read `returnValue`, and `querySelector` does not reach
|
|
170
|
-
* the element to do either. A confirmation asking which button was pressed is
|
|
171
|
-
* the ordinary case.
|
|
172
|
-
*
|
|
173
|
-
* Held here as well as on the element, so a value set before the first render
|
|
174
|
-
* survives to it and a consumer reading one back gets what they wrote.
|
|
175
|
-
*/
|
|
176
55
|
get returnValue() {
|
|
177
56
|
return this.dialog?.returnValue ?? this.returned;
|
|
178
57
|
}
|
|
@@ -183,25 +62,12 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
183
62
|
dialog.returnValue = value;
|
|
184
63
|
}
|
|
185
64
|
}
|
|
186
|
-
/** The native dialog, or nothing where there is no DOM to hold one. */
|
|
187
65
|
get dialog() {
|
|
188
66
|
return this.renderRoot?.querySelector('dialog') ?? null;
|
|
189
67
|
}
|
|
190
|
-
/** Moves focus into the dialog and makes the rest of the page inert. */
|
|
191
68
|
show() {
|
|
192
69
|
this.requestOpen(true);
|
|
193
70
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Asks to close, which a consumer may refuse.
|
|
196
|
-
*
|
|
197
|
-
* Takes what the dialog closed with, the way `<dialog>.close(value)` does, so
|
|
198
|
-
* the button that dismissed it can say which one it was. Set before the
|
|
199
|
-
* request rather than after, because a `close` listener reads `returnValue`
|
|
200
|
-
* and would otherwise read the previous one.
|
|
201
|
-
*
|
|
202
|
-
* Not set where the request is refused, since nothing closed and a value
|
|
203
|
-
* saying it did would outlive the refusal.
|
|
204
|
-
*/
|
|
205
71
|
close(returnValue) {
|
|
206
72
|
const previous = this.returnValue;
|
|
207
73
|
if (returnValue !== undefined) {
|
|
@@ -217,78 +83,25 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
217
83
|
}
|
|
218
84
|
updated(changed) {
|
|
219
85
|
super.updated(changed);
|
|
220
|
-
/* After render, because the element has to exist to be opened, and only on
|
|
221
|
-
a difference: `showModal()` on an open dialog throws, and `close()` on a
|
|
222
|
-
shut one fires a second `close` event. */
|
|
223
86
|
const dialog = this.dialog;
|
|
224
87
|
if (!dialog) {
|
|
225
88
|
return;
|
|
226
89
|
}
|
|
227
90
|
if (this.open && !dialog.open) {
|
|
228
|
-
/* **After the slotted elements have rendered, or the modal focuses
|
|
229
|
-
itself.** `showModal()` moves focus to the first focusable descendant,
|
|
230
|
-
and a custom element that has not rendered its shadow yet has none: the
|
|
231
|
-
browser falls back to the dialog. Markup never showed this, the parser
|
|
232
|
-
having upgraded everything before this element's first update, and
|
|
233
|
-
React does: it creates and connects the dialog before the chat inside
|
|
234
|
-
it has rendered, so a `default-open` dialog opened with focus on the
|
|
235
|
-
dialog rather than on the field, measured against the markup twin.
|
|
236
|
-
|
|
237
|
-
Awaited only for children that are Lit elements and only on the way
|
|
238
|
-
open; a dialog with no such children resolves on the same microtask and
|
|
239
|
-
is unchanged. */
|
|
240
91
|
void this.openWhenSlotted(dialog);
|
|
241
92
|
}
|
|
242
93
|
else if (!this.open && dialog.open) {
|
|
243
94
|
dialog.close();
|
|
244
95
|
}
|
|
245
96
|
}
|
|
246
|
-
/** Opens once anything slotted has had its first render. See `updated`. */
|
|
247
97
|
async openWhenSlotted(dialog) {
|
|
248
98
|
await Promise.all([...this.children]
|
|
249
99
|
.map((one) => one.updateComplete)
|
|
250
100
|
.filter((one) => one instanceof Promise));
|
|
251
|
-
/* Re-checked, because both facts can have moved while that resolved. */
|
|
252
101
|
if (this.open && !dialog.open && this.isConnected) {
|
|
253
102
|
dialog.showModal();
|
|
254
103
|
}
|
|
255
104
|
}
|
|
256
|
-
/**
|
|
257
|
-
* Says no to a dismissal, by moving.
|
|
258
|
-
*
|
|
259
|
-
* A persistent dialog that ignores a press on the backdrop is
|
|
260
|
-
* indistinguishable from one that did not notice, and a reader's next move is
|
|
261
|
-
* to press again harder. A short swell and settle is the smallest thing that
|
|
262
|
-
* reads as "I heard you, and no": it is the same gesture a native alert makes
|
|
263
|
-
* on macOS and the one every reader has already been taught.
|
|
264
|
-
*
|
|
265
|
-
* An attribute for the length of the animation rather than a class, cleared
|
|
266
|
-
* on `animationend`, so the styled tier decides what refusing looks like and
|
|
267
|
-
* this decides only when it happens. Removed first so a second press
|
|
268
|
-
* restarts it: without that, pressing twice does nothing the second time,
|
|
269
|
-
* which is the impression the whole thing exists to avoid.
|
|
270
|
-
*
|
|
271
|
-
* **`once: true` is not a release when the event cannot arrive.** Under
|
|
272
|
-
* `prefers-reduced-motion: reduce` the styled tier sets `animation: none`, so
|
|
273
|
-
* no animation runs, `animationend` never fires, and every refused press left
|
|
274
|
-
* a listener that could not be removed and an attribute that stayed on. The
|
|
275
|
-
* controller bounds it to one listener, aborted before the next is added.
|
|
276
|
-
*
|
|
277
|
-
* **And the attribute is cleared at once when no animation is running**, asked
|
|
278
|
-
* of the platform rather than of a media query: this tier does not know what
|
|
279
|
-
* refusing looks like, and the same silence arrives from a consumer's
|
|
280
|
-
* stylesheet, from reduced motion, and from no styled tier being loaded at
|
|
281
|
-
* all.
|
|
282
|
-
*
|
|
283
|
-
* **`CSSAnimation` specifically, because `getAnimations` returns transitions
|
|
284
|
-
* too.** A panel that is still running its entrance transition answers the
|
|
285
|
-
* bare `length` check with a truthy number while no animation exists to end,
|
|
286
|
-
* so a refusal landing inside that window took the listener branch and waited
|
|
287
|
-
* for an `animationend` that was never coming: the attribute stayed on until
|
|
288
|
-
* the next refused press. That is the same bug this method exists to close,
|
|
289
|
-
* one window narrower. `getAnimations` flushes pending style itself, so no
|
|
290
|
-
* reflow is forced for it.
|
|
291
|
-
*/
|
|
292
105
|
refuse() {
|
|
293
106
|
const panel = this.dialog;
|
|
294
107
|
if (!panel) {
|
|
@@ -296,8 +109,6 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
296
109
|
}
|
|
297
110
|
this.refusal?.abort();
|
|
298
111
|
panel.removeAttribute('data-refused');
|
|
299
|
-
// Read forces the removal to land before the attribute goes back on, or the
|
|
300
|
-
// two happen in one frame and the animation never restarts.
|
|
301
112
|
void panel.offsetWidth;
|
|
302
113
|
panel.setAttribute('data-refused', '');
|
|
303
114
|
const animating = panel.getAnimations().some((one) => one instanceof CSSAnimation);
|
|
@@ -7,70 +7,15 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, slotFilled } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary Separates content into groups with a rule.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* The separator role lives on the rule inside the shadow root rather than on
|
|
15
|
-
* the host. A role does not need to cross the shadow boundary the way an IDREF
|
|
16
|
-
* does, and putting it on the rendered element means the semantics are in the
|
|
17
|
-
* server's markup with no attribute written during a lifecycle the server never
|
|
18
|
-
* runs.
|
|
19
|
-
*
|
|
20
|
-
* Use `orientation="vertical"` between items on one line, such as inside a
|
|
21
|
-
* toolbar. A vertical divider stretches to its flex parent rather than setting
|
|
22
|
-
* a height of its own.
|
|
23
|
-
*
|
|
24
|
-
* @since 0.1.0
|
|
25
|
-
* @status stable
|
|
26
|
-
* @category layout
|
|
27
|
-
* @aliases Separator, Rule
|
|
28
|
-
* @documentation https://helix.fusion.dev/components/divider
|
|
29
|
-
*
|
|
30
|
-
* @slot - Content set into the rule, such as the word between two sections.
|
|
31
|
-
*
|
|
32
|
-
* @csspart content - What is set into it.
|
|
33
|
-
* @csspart line - The rule itself, or each half of it where something is set in.
|
|
34
|
-
*/
|
|
35
10
|
let HelixHeadlessDivider = class HelixHeadlessDivider extends HelixElement {
|
|
36
11
|
constructor() {
|
|
37
12
|
super(...arguments);
|
|
38
|
-
/**
|
|
39
|
-
* Whether the rule is announced as a separator.
|
|
40
|
-
*
|
|
41
|
-
* Off by default, and out of the accessibility tree. Most rules on a page
|
|
42
|
-
* separate nothing: they underline a heading or close a section that space
|
|
43
|
-
* had already closed, and announcing every one as a separator is noise a
|
|
44
|
-
* screen-reader user has to walk past.
|
|
45
|
-
*
|
|
46
|
-
* An opt-in, not a `decorative` opt-out, because a boolean attribute cannot
|
|
47
|
-
* be turned off: `decorative="false"` parses as `true`, which would put the
|
|
48
|
-
* quiet default out of reach from HTML and leave it settable only as a
|
|
49
|
-
* property. An opt-in is absent by default, which every reflected property
|
|
50
|
-
* here has to be.
|
|
51
|
-
*/
|
|
52
13
|
this.announced = false;
|
|
53
14
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Marks the middle region when nothing has been set into it.
|
|
56
|
-
*
|
|
57
|
-
* A divider with no content has to be one continuous line, and the space
|
|
58
|
-
* either side of the content is what would break it. `:empty` cannot answer
|
|
59
|
-
* this and neither can `:host(:has(...))`, which is invalid CSS dropped at
|
|
60
|
-
* parse time.
|
|
61
|
-
*
|
|
62
|
-
* On the region inside the shadow root rather than on the host, because an
|
|
63
|
-
* attribute written onto the host is a change to markup the consumer rendered.
|
|
64
|
-
*/
|
|
65
15
|
markEmpty() {
|
|
66
16
|
const region = this.renderRoot?.querySelector('[part="content"]');
|
|
67
17
|
const empty = !slotFilled(this.renderRoot?.querySelector('slot'));
|
|
68
18
|
region?.toggleAttribute('data-empty', empty);
|
|
69
|
-
/* `hidden` beside the mirror, because `data-empty` is a hook the styled
|
|
70
|
-
sheet reads and this tier has no sheet: an empty region left in the box
|
|
71
|
-
tree is a gap a consumer cannot close without knowing to look for it. The
|
|
72
|
-
styled rule stays, because that sheet sets `display` on this part and so
|
|
73
|
-
outranks the UA one. */
|
|
74
19
|
region?.toggleAttribute('hidden', empty);
|
|
75
20
|
}
|
|
76
21
|
updated(changed) {
|
|
@@ -78,22 +23,6 @@ let HelixHeadlessDivider = class HelixHeadlessDivider extends HelixElement {
|
|
|
78
23
|
this.markEmpty();
|
|
79
24
|
}
|
|
80
25
|
render() {
|
|
81
|
-
/* Two rules and a slot between them, always.
|
|
82
|
-
*
|
|
83
|
-
* `role="presentation"` is the honest default; `announced` is for the rule
|
|
84
|
-
* that genuinely divides one thing from another rather than just drawing a
|
|
85
|
-
* line under it.
|
|
86
|
-
*
|
|
87
|
-
* **Both halves are always rendered and the semantics stay on the first.**
|
|
88
|
-
* A rule with nothing set into it is one line with a zero-width slot and a
|
|
89
|
-
* second line of no length, which costs a reader nothing and costs the
|
|
90
|
-
* component the branch that would otherwise decide between two markups. It
|
|
91
|
-
* also means a consumer who slots something in later gets it without the
|
|
92
|
-
* element re-rendering into a different shape.
|
|
93
|
-
*
|
|
94
|
-
* The role is not repeated on the second half. Two separators where a reader
|
|
95
|
-
* crossed one line is the announcement saying twice what happened once, and
|
|
96
|
-
* the content between them is not a boundary of its own. */
|
|
97
26
|
return html `<div
|
|
98
27
|
part="line"
|
|
99
28
|
role=${this.announced ? 'separator' : 'presentation'}
|
|
@@ -8,134 +8,27 @@ import { customElement, drawTooltip, HelixElement, MixinChartSource, seriesIndex
|
|
|
8
8
|
import { html, nothing, svg } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import '../../tooltip/index.js';
|
|
11
|
-
/** The drawing's own coordinates, square because the shape has to stay round. */
|
|
12
11
|
const BOX = 100;
|
|
13
12
|
const CENTER = BOX / 2;
|
|
14
|
-
/** The outer edge of the ring, whatever its thickness. */
|
|
15
13
|
const EDGE = 40;
|
|
16
|
-
/**
|
|
17
|
-
* The dash gap, long enough that the pattern cannot repeat.
|
|
18
|
-
*
|
|
19
|
-
* A dash array tiles. Set to `circumference - length` the pattern fills the
|
|
20
|
-
* circle exactly once and is right at rest, and wrong the moment it animates:
|
|
21
|
-
* both numbers interpolate, and while the gap is shorter than the path still to
|
|
22
|
-
* cover, a second and third copy of the slice sweep round the ring and vanish
|
|
23
|
-
* inside one transition. A constant far larger than any circumference has no
|
|
24
|
-
* second tile to draw at any point in the interpolation. It is the number
|
|
25
|
-
* `@starting-style` already uses for the same reason.
|
|
26
|
-
*/
|
|
27
14
|
const GAP = 9999;
|
|
28
|
-
/**
|
|
29
|
-
* How much of the radius the ring takes, by name.
|
|
30
|
-
*
|
|
31
|
-
* **The outer edge is fixed and the radius follows the thickness**, which is
|
|
32
|
-
* what makes `none` a pie. The stroke sits astride its own radius, so a ring
|
|
33
|
-
* drawn at a fixed `r` grows outward as well as inward: against a 100 box, a
|
|
34
|
-
* fixed `r=40` has an unstated ceiling at a thickness of 20, and anything past
|
|
35
|
-
* it crosses the view box and is cut with nothing to say so.
|
|
36
|
-
*
|
|
37
|
-
* Derived instead, `r = EDGE - thickness / 2`, the outer edge lands on `EDGE`
|
|
38
|
-
* for every value and the inner edge is `EDGE - thickness`. A pie is where the
|
|
39
|
-
* inner edge reaches the center, which solves to `thickness = EDGE`: the ring
|
|
40
|
-
* is then drawn at `r = EDGE / 2` and its stroke spans the whole radius.
|
|
41
|
-
*
|
|
42
|
-
* `EDGE * 2` was the first answer and is the trap in this arithmetic. It gives
|
|
43
|
-
* `r = 0`, and a circle of radius zero draws nothing at all, so a pie came out
|
|
44
|
-
* as an empty box. The stroke reaches `thickness / 2` either side of `r`, not
|
|
45
|
-
* `thickness`.
|
|
46
|
-
*/
|
|
47
15
|
const THICKNESS = {
|
|
48
16
|
large: 8,
|
|
49
17
|
medium: 16,
|
|
50
18
|
none: EDGE,
|
|
51
19
|
small: 26,
|
|
52
20
|
};
|
|
53
|
-
/**
|
|
54
|
-
* @summary Parts of a whole as a ring, read from a table.
|
|
55
|
-
*
|
|
56
|
-
* @description
|
|
57
|
-
* The chart with the narrowest honest use, so the component is narrow too.
|
|
58
|
-
*
|
|
59
|
-
* **It reads one column and treats the rows as the parts.** Every other chart
|
|
60
|
-
* here takes a column per series; a ring has one whole to divide, and a second
|
|
61
|
-
* ring inside the first is two charts sharing a picture in the most literal
|
|
62
|
-
* way. So the first column of numbers is the series and each row is a slice.
|
|
63
|
-
*
|
|
64
|
-
* **It refuses negative values rather than drawing them.** A share of a whole
|
|
65
|
-
* cannot be less than nothing, and an arc for a negative number either runs
|
|
66
|
-
* backwards over its neighbors or is silently dropped. Those rows are skipped
|
|
67
|
-
* and the total is of what is left, which is the only reading that stays true.
|
|
68
|
-
*
|
|
69
|
-
* **The shape is drawn with a dashed stroke, not with arc commands.** One
|
|
70
|
-
* `<circle>` per slice, its dash set to the slice's share of the circumference
|
|
71
|
-
* and its offset to where the slice begins. Arc paths need the sweep flag and a
|
|
72
|
-
* large-arc flag, both of which flip at values a reader never sees coming, and
|
|
73
|
-
* a slice of exactly one whole becomes a path with no length at all.
|
|
74
|
-
*
|
|
75
|
-
* **And it keeps its aspect ratio**, alone among the charts here. The others
|
|
76
|
-
* set `preserveAspectRatio="none"` so one drawing fills any box and rescue
|
|
77
|
-
* their strokes with `vector-effect`; that rescues strokes and not geometry, so
|
|
78
|
-
* a ring drawn the same way is an ellipse in anything but a square.
|
|
79
|
-
*
|
|
80
|
-
* The table is the accessible representation and this drawing is `aria-hidden`.
|
|
81
|
-
*
|
|
82
|
-
* The properties below are written by this component and read by whoever draws
|
|
83
|
-
* it. A consumer sets none of them: they carry a number this element computed
|
|
84
|
-
* and CSS cannot, which is what lets a stylesheet on this tier draw the thing
|
|
85
|
-
* without measuring anything itself.
|
|
86
|
-
*
|
|
87
|
-
* @since 0.1.0
|
|
88
|
-
* @status experimental
|
|
89
|
-
* @category charts
|
|
90
|
-
* @aliases Pie Chart, Ring Chart, Share Chart
|
|
91
|
-
* @documentation https://helix.fusion.dev/components/donut-chart
|
|
92
|
-
*
|
|
93
|
-
* @slot - Goes in the middle of the ring. A total, usually.
|
|
94
|
-
*
|
|
95
|
-
* @csspart center - The middle of the ring, holding whatever is slotted.
|
|
96
|
-
* @csspart mark - The invisible point the tooltip is anchored to.
|
|
97
|
-
* @csspart plot - The drawing.
|
|
98
|
-
* @csspart segment - One slice.
|
|
99
|
-
* @csspart segment-edge - One slice as a closed shape, for a boundary to trace.
|
|
100
|
-
* @csspart tooltip - The bubble, forwarded from `Tooltip`.
|
|
101
|
-
* @csspart tooltip-anchor - The `Tooltip` element itself.
|
|
102
|
-
* @csspart tooltip-cell - One column's name and value inside the tooltip.
|
|
103
|
-
* @csspart tooltip-label - The slice's own name inside the tooltip.
|
|
104
|
-
* @csspart tooltip-name - A column's name inside the tooltip.
|
|
105
|
-
* @csspart tooltip-value - A column's value inside the tooltip.
|
|
106
|
-
* @csspart track - The ring the slices are drawn on.
|
|
107
|
-
*
|
|
108
|
-
* @cssproperty --donut-chart-stroke - How thick the ring is drawn, derived from `hole`.
|
|
109
|
-
*/
|
|
110
21
|
let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSource(HelixElement) {
|
|
111
|
-
/**
|
|
112
|
-
* The pointer is not mapped across this one.
|
|
113
|
-
*
|
|
114
|
-
* Every other chart lays its readings out left to right, so the nearest is
|
|
115
|
-
* arithmetic on x. A ring's slices are real elements at every angle, and x
|
|
116
|
-
* alone would answer with whichever slice shared a horizontal position with
|
|
117
|
-
* the pointer: at three o'clock and at nine o'clock it would give the same
|
|
118
|
-
* reading for opposite sides of the chart. So the slices say which one they
|
|
119
|
-
* are and this only holds the answer.
|
|
120
|
-
*
|
|
121
|
-
* Set here rather than in a constructor, and it must not move there: the
|
|
122
|
-
* analyzer reads `this.pointer.axis = 'none'` in a constructor body as the
|
|
123
|
-
* declaration of a public field, and fails the build demanding a description
|
|
124
|
-
* for a member this class does not have.
|
|
125
|
-
*/
|
|
126
22
|
connectedCallback() {
|
|
127
23
|
super.connectedCallback();
|
|
128
24
|
this.pointer.axis = 'none';
|
|
129
25
|
}
|
|
130
|
-
/** The fraction of the circle the shares are spread over. */
|
|
131
26
|
get span() {
|
|
132
27
|
return this.sweep === 'half' ? 0.5 : 1;
|
|
133
28
|
}
|
|
134
|
-
/** How thick the ring is drawn, in the drawing's own units. */
|
|
135
29
|
get thickness() {
|
|
136
30
|
return THICKNESS[this.hole ?? 'medium'] ?? THICKNESS.medium ?? 0;
|
|
137
31
|
}
|
|
138
|
-
/** The ring's radius, which follows its thickness so the outer edge holds. */
|
|
139
32
|
get radius() {
|
|
140
33
|
return EDGE - this.thickness / 2;
|
|
141
34
|
}
|
|
@@ -158,24 +51,10 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
|
|
|
158
51
|
}
|
|
159
52
|
render() {
|
|
160
53
|
const shares = this.shares;
|
|
161
|
-
/* No middle where there is no hole. A `center` part over a pie would put
|
|
162
|
-
slotted content on top of the slices, and a slot nobody can see is worse
|
|
163
|
-
than one that is not there: a consumer fills it and nothing happens. */
|
|
164
54
|
return html `${this.renderPlot(shares)}
|
|
165
55
|
`;
|
|
166
56
|
}
|
|
167
|
-
/**
|
|
168
|
-
* The ring itself, behind a seam the styled tier can move.
|
|
169
|
-
*
|
|
170
|
-
* `aria-hidden`, because the table it was read from is the readable version
|
|
171
|
-
* and a screen reader hearing both hears the same numbers twice.
|
|
172
|
-
*/
|
|
173
57
|
renderPlot(shares) {
|
|
174
|
-
/* The thickness is declared here, on the box that holds both the ring and
|
|
175
|
-
the middle, and never on the slices. A custom property inherits down and
|
|
176
|
-
never sideways, so `[part="center"]` is a sibling of the circles and would
|
|
177
|
-
only ever read the fallback, sizing the hole as if every ring were the
|
|
178
|
-
resting thickness. */
|
|
179
58
|
return html `<div
|
|
180
59
|
aria-hidden="true"
|
|
181
60
|
id="helix-chart-plot"
|
|
@@ -198,30 +77,15 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
|
|
|
198
77
|
${this.renderTooltip()}
|
|
199
78
|
</div>`;
|
|
200
79
|
}
|
|
201
|
-
/**
|
|
202
|
-
* The view box, cropped to what is actually drawn.
|
|
203
|
-
*
|
|
204
|
-
* A full turn uses the square. A half arc uses the band from the top of the
|
|
205
|
-
* ring down to the center line, plus the half stroke its flat ends hang
|
|
206
|
-
* below that, and cropping to it is the difference between a semi-donut and a
|
|
207
|
-
* semi-donut with an empty quarter of the box under it. The outer edge is
|
|
208
|
-
* `EDGE` from the center whatever the thickness, which is what makes this two
|
|
209
|
-
* numbers rather than a measurement.
|
|
210
|
-
*/
|
|
211
80
|
get frame() {
|
|
212
81
|
if (this.sweep !== 'half') {
|
|
213
82
|
return `0 0 ${BOX} ${BOX}`;
|
|
214
83
|
}
|
|
215
84
|
return `0 ${CENTER - EDGE} ${BOX} ${EDGE + this.thickness / 2}`;
|
|
216
85
|
}
|
|
217
|
-
/** One slice, as a dash the length of its share. */
|
|
218
86
|
renderSegment(share, index) {
|
|
219
87
|
const circumference = 2 * Math.PI * this.radius;
|
|
220
88
|
const length = share.fraction * circumference * this.span;
|
|
221
|
-
/* Negative, because `stroke-dashoffset` counts backwards along the path:
|
|
222
|
-
a positive offset would run the slices anticlockwise from twelve. The
|
|
223
|
-
rotation that puts zero at the top is the stylesheet's, since it is a
|
|
224
|
-
fact about how the picture is presented rather than about the numbers. */
|
|
225
89
|
return svg `<circle
|
|
226
90
|
cx=${CENTER}
|
|
227
91
|
cy=${CENTER}
|
|
@@ -237,36 +101,6 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
|
|
|
237
101
|
/>
|
|
238
102
|
${this.renderEdge(share, index)}`;
|
|
239
103
|
}
|
|
240
|
-
/**
|
|
241
|
-
* The same slice again, as a shape with an inside.
|
|
242
|
-
*
|
|
243
|
-
* **A stroked arc has no boundary to draw, which is why this exists.** The
|
|
244
|
-
* segment above is a dash on a circle: the mark *is* the edge, so there is
|
|
245
|
-
* nothing for an outline to trace and nothing within it to fade. Anything
|
|
246
|
-
* wanting those two separately needs the slice as a closed path, and this is
|
|
247
|
-
* it: the outer arc, a step inward, the inner arc back, closed.
|
|
248
|
-
*
|
|
249
|
-
* **Rendered always and painted only sometimes**, which is what keeps this on
|
|
250
|
-
* this tier. Whether a boundary is drawn is a styled question and the answer
|
|
251
|
-
* lives in a stylesheet; whether the geometry a boundary would need exists at
|
|
252
|
-
* all is structural, and a component cannot conjure a shape from CSS. So the
|
|
253
|
-
* path is here for every donut and is `fill: none; stroke: none` until
|
|
254
|
-
* something asks for it.
|
|
255
|
-
*
|
|
256
|
-
* **Not a replacement for the dash.** The dash carries three jobs and only
|
|
257
|
-
* one of them is the shape: a share growing is a `stroke-dasharray`
|
|
258
|
-
* interpolating with no reflow, and `@starting-style` on that same property is
|
|
259
|
-
* the whole reason the ring sweeps in rather than appearing. A `d` cannot do
|
|
260
|
-
* either. CSS interpolates one only between paths of identical command
|
|
261
|
-
* structure, and the large-arc flag flips the moment a slice crosses a half
|
|
262
|
-
* turn, so a growing slice would pop partway through the transition it was
|
|
263
|
-
* meant to smooth. Both animations stay on the dash and this path follows it.
|
|
264
|
-
*
|
|
265
|
-
* The turn to twelve o'clock is arithmetic here rather than the rotation the
|
|
266
|
-
* stylesheet gives the circles. That rule uses `transform-box: fill-box`, so
|
|
267
|
-
* it spins each element about its own bounding box, which is the ring's center
|
|
268
|
-
* for a circle and is not for a sector.
|
|
269
|
-
*/
|
|
270
104
|
renderEdge(share, index) {
|
|
271
105
|
const turn = 2 * Math.PI * this.span;
|
|
272
106
|
const from = share.start * turn - Math.PI / 2;
|
|
@@ -275,45 +109,17 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
|
|
|
275
109
|
const inner = EDGE - this.thickness;
|
|
276
110
|
const at = (radius, angle) => `${(CENTER + Math.cos(angle) * radius).toFixed(3)} ${(CENTER + Math.sin(angle) * radius).toFixed(3)}`;
|
|
277
111
|
const wide = to - from > Math.PI ? 1 : 0;
|
|
278
|
-
/* A slice that is the whole ring starts and ends at one point, and an arc
|
|
279
|
-
between a point and itself draws nothing. Split at the halfway angle so
|
|
280
|
-
each half is a real arc. */
|
|
281
112
|
const full = to - from >= 2 * Math.PI - 1e-6;
|
|
282
113
|
const mid = from + (to - from) / 2;
|
|
283
114
|
const outward = full
|
|
284
115
|
? `A ${outer} ${outer} 0 0 1 ${at(outer, mid)} A ${outer} ${outer} 0 0 1 ${at(outer, to)}`
|
|
285
116
|
: `A ${outer} ${outer} 0 ${wide} 1 ${at(outer, to)}`;
|
|
286
|
-
/* A pie has no inner arc: the radius solves to zero and the two ends meet at
|
|
287
|
-
the middle, so the step back is a line to the center and not an arc of no
|
|
288
|
-
size. */
|
|
289
117
|
const homeward = inner <= 0
|
|
290
118
|
? `L ${CENTER} ${CENTER}`
|
|
291
119
|
: full
|
|
292
120
|
? `L ${at(inner, to)} A ${inner} ${inner} 0 0 0 ${at(inner, mid)} A ${inner} ${inner} 0 0 0 ${at(inner, from)}`
|
|
293
121
|
: `L ${at(inner, to)} A ${inner} ${inner} 0 ${wide} 0 ${at(inner, from)}`;
|
|
294
122
|
const shape = `M ${at(outer, from)} ${outward} ${homeward} Z`;
|
|
295
|
-
/* **Clipped to itself, because a stroke straddles the line it is drawn on.**
|
|
296
|
-
Half of it falls outside the wedge: past the fill on both arcs, and across
|
|
297
|
-
the seam into the neighbor on both radial sides, where that neighbor is
|
|
298
|
-
drawing the same line from the other direction and the two overlap. Seen
|
|
299
|
-
as a doubled edge at every join and a halo around the ring.
|
|
300
|
-
|
|
301
|
-
Clipping to the same path keeps the inner half and drops the outer, so an
|
|
302
|
-
edge stays inside the slice it belongs to and two adjacent edges meet
|
|
303
|
-
exactly. The clip is the shape itself, so it costs no second geometry and
|
|
304
|
-
cannot drift from it.
|
|
305
|
-
|
|
306
|
-
Ids are scoped to the shadow root, so an index is enough to keep them
|
|
307
|
-
apart, and two donuts on a page cannot collide. */
|
|
308
|
-
/* **`fill="none"` as an attribute, not a rule, because this tier has
|
|
309
|
-
no rules.** An SVG path fills black by default, and the styled tier's
|
|
310
|
-
own `fill: none` covers only that tier: everywhere else these closed
|
|
311
|
-
shapes paint over the ring.
|
|
312
|
-
|
|
313
|
-
A presentation attribute loses to any CSS rule, so the styled tier
|
|
314
|
-
still decides and a consumer can still fill this if they want one.
|
|
315
|
-
What it cannot do is fill by accident on the tier whose whole premise
|
|
316
|
-
is that it ships no CSS. */
|
|
317
123
|
const clip = `helix-donut-edge-${index}`;
|
|
318
124
|
return svg `<clipPath id=${clip}><path d=${shape} /></clipPath>
|
|
319
125
|
<path
|
|
@@ -326,37 +132,17 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
|
|
|
326
132
|
style="--chart-series: var(--chart-series-${seriesIndex(index)})"
|
|
327
133
|
/>`;
|
|
328
134
|
}
|
|
329
|
-
/**
|
|
330
|
-
* What a pointer resting on a slice shows.
|
|
331
|
-
*
|
|
332
|
-
* Placed in the middle rather than along an axis, a ring having none, and the
|
|
333
|
-
* middle is where a reader is already looking: the hole usually holds a
|
|
334
|
-
* total, so a tooltip over it replaces one figure with the one being asked
|
|
335
|
-
* about rather than covering something else.
|
|
336
|
-
*/
|
|
337
135
|
renderTooltip() {
|
|
338
136
|
const at = this.pointer.at;
|
|
339
137
|
const share = this.shares[at];
|
|
340
138
|
if (!share) {
|
|
341
139
|
return drawTooltip(this.sourceRows, at, this.tag('tooltip'));
|
|
342
140
|
}
|
|
343
|
-
/* **Outside the ring, on the slice's own bearing**, rather than centered in
|
|
344
|
-
the hole: centered covers the data on a pie, and in neither shape does it
|
|
345
|
-
say *which* slice it means. Out here one rule serves both.
|
|
346
|
-
|
|
347
|
-
The mid-angle, less a quarter turn because the ring is rotated to start
|
|
348
|
-
at twelve o'clock and this arithmetic starts at three. The point is the
|
|
349
|
-
outer edge, and the box is pushed a little further along the same bearing
|
|
350
|
-
so its arrow rests on the ring rather than in it. */
|
|
351
141
|
const angle = (share.start + share.fraction / 2) * 2 * Math.PI - Math.PI / 2;
|
|
352
142
|
const edge = EDGE;
|
|
353
143
|
const x = CENTER + Math.cos(angle) * edge;
|
|
354
144
|
const y = CENTER + Math.sin(angle) * edge;
|
|
355
145
|
return drawTooltip(this.sourceRows, at, this.tag('tooltip'), {
|
|
356
|
-
/* The preferred side from the bearing's dominant axis, and
|
|
357
|
-
`FloatingController` moves it if that side does not fit. A fixed side
|
|
358
|
-
cannot express a bearing, so the arrow misses the ring; a preference
|
|
359
|
-
plus flipping can. */
|
|
360
146
|
placement: Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))
|
|
361
147
|
? Math.cos(angle) > 0
|
|
362
148
|
? 'right'
|