@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,19 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, HelixSelectEvent, MixinControllerOpen, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { property, state } from 'lit/decorators.js';
|
|
9
9
|
import { html } from 'lit/static-html.js';
|
|
10
|
-
/* Side-effect imports, because this renders both in its own shadow root and a
|
|
11
|
-
custom element registers on import. The paths are relative, so this registers
|
|
12
|
-
the base pair and the styled file's identical imports register the styled
|
|
13
|
-
pair; `tier` picks which names the template asks for. */
|
|
14
10
|
import '../../dialog/index.js';
|
|
15
11
|
import '../../listbox/index.js';
|
|
16
|
-
/**
|
|
17
|
-
* A hotkey, as the pieces a `keydown` can be compared against.
|
|
18
|
-
*
|
|
19
|
-
* `mod` is the platform's own: Command on a Mac and Control everywhere else,
|
|
20
|
-
* which is what every application means by it and what a consumer would
|
|
21
|
-
* otherwise have to detect and write twice.
|
|
22
|
-
*/
|
|
23
12
|
const parse = (hotkey) => {
|
|
24
13
|
const parts = hotkey.toLowerCase().split('+');
|
|
25
14
|
return {
|
|
@@ -29,105 +18,14 @@ const parse = (hotkey) => {
|
|
|
29
18
|
shift: parts.includes('shift'),
|
|
30
19
|
};
|
|
31
20
|
};
|
|
32
|
-
/**
|
|
33
|
-
* @summary A list of commands, opened from anywhere and filtered by typing.
|
|
34
|
-
*
|
|
35
|
-
* @description
|
|
36
|
-
* **The list is `helix-listbox`, filtering and all.** Listbox already owns
|
|
37
|
-
* narrowing, the empty state, the highlight over what matched,
|
|
38
|
-
* `aria-activedescendant`, scrolling an option into view and the keyboard that
|
|
39
|
-
* moves between them. Its own note says a command palette is that list again
|
|
40
|
-
* with something different opening it, and this is that something: a dialog,
|
|
41
|
-
* and a key that summons it.
|
|
42
|
-
*
|
|
43
|
-
* **The value is not kept.** A palette runs a command and forgets it: nothing
|
|
44
|
-
* here is a form control and nothing is submitted, so what it reports is that
|
|
45
|
-
* an option was chosen, once, with `helix-select`. A consumer that wants the
|
|
46
|
-
* last command remembered is a consumer with somewhere better to keep it.
|
|
47
|
-
*
|
|
48
|
-
* **The hotkey is global and the element does not have to be open to hear it.**
|
|
49
|
-
* The listener is on the document, because the point of a palette is that it
|
|
50
|
-
* answers from wherever the reader is, including from inside another control's
|
|
51
|
-
* text field. `mod` is Command on a Mac and Control elsewhere, resolved here so
|
|
52
|
-
* a consumer writes one thing.
|
|
53
|
-
*
|
|
54
|
-
* The filter is emptied as the palette closes, for the reason Combobox and
|
|
55
|
-
* Select both record: a control reopened still narrowed by the last visit's
|
|
56
|
-
* search is one whose second use starts from the first one's leftovers, and a
|
|
57
|
-
* palette is used far more often than either.
|
|
58
|
-
*
|
|
59
|
-
* @since 0.1.0
|
|
60
|
-
* @status stable
|
|
61
|
-
* @category navigation
|
|
62
|
-
* @aliases Command palette, Quick open, Spotlight
|
|
63
|
-
* @documentation https://helix.fusion.dev/components/command
|
|
64
|
-
*
|
|
65
|
-
* @slot - The commands, which are `helix-option` elements and their groups.
|
|
66
|
-
* @slot footer - Under the list, for the keys a reader can press.
|
|
67
|
-
*
|
|
68
|
-
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes. Cancelable: refusing it takes the decision and nothing moves.
|
|
69
|
-
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the palette opens. Cancelable: refusing it takes the decision and nothing moves.
|
|
70
|
-
* @event {HelixSelectEvent} helix-select - Fired when a command is chosen. Cancelable: refusing it takes the decision and nothing moves.
|
|
71
|
-
* @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
|
|
72
|
-
*
|
|
73
|
-
* @csspart dialog - The overlay's own box, forwarded from the dialog.
|
|
74
|
-
* @csspart footer - The row under the list.
|
|
75
|
-
* @csspart listbox - The list inside it.
|
|
76
|
-
* @csspart listbox-empty - Shown when a filter has hidden everything.
|
|
77
|
-
* @csspart listbox-feedback - What is wrong with it.
|
|
78
|
-
* @csspart listbox-filter-icon - The mark beside that field.
|
|
79
|
-
* @csspart listbox-filter-mark - The glyph beside the filter field, where none is slotted.
|
|
80
|
-
* @csspart listbox-hint - Help under the list.
|
|
81
|
-
* @csspart listbox-items - The list itself.
|
|
82
|
-
* @csspart listbox-label - The question, over the list.
|
|
83
|
-
* @csspart listbox-loading - Shown while the options are still on their way.
|
|
84
|
-
* @csspart listbox-search - The region holding the field that narrows the list.
|
|
85
|
-
* @csspart listbox-search-field - The field itself.
|
|
86
|
-
*/
|
|
87
21
|
let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpen(HelixElement) {
|
|
88
22
|
constructor() {
|
|
89
23
|
super(...arguments);
|
|
90
|
-
/**
|
|
91
|
-
* Words shown where the filter has matched nothing.
|
|
92
|
-
*/
|
|
93
24
|
this.emptyText = 'No commands';
|
|
94
|
-
/**
|
|
95
|
-
* The key that opens it, as `mod+k` or `shift+p` or plain `k`.
|
|
96
|
-
*
|
|
97
|
-
* Empty turns it off, for a consumer who opens the palette from a button or
|
|
98
|
-
* from their own shortcut handler. There is no list of defaults to fight
|
|
99
|
-
* with: one key, said once.
|
|
100
|
-
*/
|
|
101
25
|
this.hotkey = 'mod+k';
|
|
102
|
-
/**
|
|
103
|
-
* Accessible name for the overlay, which has no visible heading.
|
|
104
|
-
*/
|
|
105
26
|
this.label = 'Commands';
|
|
106
|
-
/**
|
|
107
|
-
* Shown in the field before anything is typed.
|
|
108
|
-
*/
|
|
109
27
|
this.placeholder = 'Search commands';
|
|
110
|
-
/**
|
|
111
|
-
* Whether the consumer put anything under the list.
|
|
112
|
-
*
|
|
113
|
-
* Asked of the slot rather than of CSS. A wrapper holding a slot always has a
|
|
114
|
-
* child element, which is the slot, so `:not(:has(*))` on it is never true
|
|
115
|
-
* and an empty footer would draw its rule and its padding over nothing. Steps
|
|
116
|
-
* was caught by the same thing.
|
|
117
|
-
*/
|
|
118
28
|
this.footed = false;
|
|
119
|
-
/**
|
|
120
|
-
* Answers the hotkey from wherever the reader is.
|
|
121
|
-
*
|
|
122
|
-
* On the document rather than on this element, which is never the thing with
|
|
123
|
-
* focus: a palette that only answered when it already had focus would be a
|
|
124
|
-
* palette nobody could open.
|
|
125
|
-
*
|
|
126
|
-
* `preventDefault` because every key worth using is already the browser's.
|
|
127
|
-
* Command-K focuses the address bar in some browsers and opens a search in
|
|
128
|
-
* others, and a palette that opened underneath one of those would be a
|
|
129
|
-
* palette the reader never sees.
|
|
130
|
-
*/
|
|
131
29
|
this.handleHotkey = (event) => {
|
|
132
30
|
if (this.hotkey === '') {
|
|
133
31
|
return;
|
|
@@ -143,14 +41,6 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
143
41
|
event.preventDefault();
|
|
144
42
|
this.requestOpen(!this.open);
|
|
145
43
|
};
|
|
146
|
-
/**
|
|
147
|
-
* Takes what the list announced and says it under this element's name.
|
|
148
|
-
*
|
|
149
|
-
* The listbox says `helix-before-change`, which is the right word for a control that
|
|
150
|
-
* holds a value and the wrong one for a palette: nothing here changed, a
|
|
151
|
-
* command ran. It is stopped and re-announced rather than relayed, so a
|
|
152
|
-
* consumer hears one event naming the element they put on the page.
|
|
153
|
-
*/
|
|
154
44
|
this.handleChange = (event) => {
|
|
155
45
|
event.stopPropagation();
|
|
156
46
|
const { value } = event.detail;
|
|
@@ -159,26 +49,6 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
159
49
|
}
|
|
160
50
|
this.requestOpen(false);
|
|
161
51
|
};
|
|
162
|
-
/**
|
|
163
|
-
* Takes the row's second destination, where the reader asked for it.
|
|
164
|
-
*
|
|
165
|
-
* **Command intercepts rather than Listbox reporting, and that is the whole
|
|
166
|
-
* design.** Enter belongs to the listbox: it owns the active row, it decides
|
|
167
|
-
* what choosing means, and it announces `helix-before-change`. Teaching it about a
|
|
168
|
-
* modifier would put a palette's idea of a second destination inside a
|
|
169
|
-
* control that is also a select and a combobox, neither of which has one.
|
|
170
|
-
*
|
|
171
|
-
* So this listens in the capture phase, before the listbox sees the key, and
|
|
172
|
-
* only for the one combination that means something here. Anything else falls
|
|
173
|
-
* straight through and the listbox behaves exactly as it always did.
|
|
174
|
-
*
|
|
175
|
-
* `mod` is Command on a Mac and Control elsewhere, which is what `hotkey`
|
|
176
|
-
* already means by it two methods up.
|
|
177
|
-
*
|
|
178
|
-
* The pointer takes the same path for the same reason: a modifier-click on a
|
|
179
|
-
* row is the same intent as a modifier-Enter on it, and both have to be
|
|
180
|
-
* stopped before the row announces its ordinary selection.
|
|
181
|
-
*/
|
|
182
52
|
this.handleAlternate = (event) => {
|
|
183
53
|
if (!(event.metaKey || event.ctrlKey) || !this.open) {
|
|
184
54
|
return;
|
|
@@ -199,21 +69,6 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
199
69
|
this.requestOpen(false);
|
|
200
70
|
}
|
|
201
71
|
};
|
|
202
|
-
/*
|
|
203
|
-
* Keeps an option's own announcement inside the palette.
|
|
204
|
-
*
|
|
205
|
-
* Every option says `helix-select` when it is chosen, which is how the list
|
|
206
|
-
* hears it, and the options are light DOM children of this element, so it
|
|
207
|
-
* carries straight on out to the consumer: two selections for one command
|
|
208
|
-
* under one event name, told apart only by the target. The one this element
|
|
209
|
-
* dispatches has itself as the target and is let past.
|
|
210
|
-
*
|
|
211
|
-
* `stopImmediatePropagation`, not `stopPropagation`. The consumer's listener
|
|
212
|
-
* is on this same element, and stopping propagation only keeps an event off
|
|
213
|
-
* the *next* node, so every other listener already bound here still runs. The
|
|
214
|
-
* listbox hears it well before this, from inside the shadow root, so nothing
|
|
215
|
-
* this element needs is lost by ending it here.
|
|
216
|
-
*/
|
|
217
72
|
this.swallowOptionSelect = (event) => {
|
|
218
73
|
if (event.target !== this) {
|
|
219
74
|
event.stopImmediatePropagation();
|
|
@@ -223,21 +78,12 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
223
78
|
get listbox() {
|
|
224
79
|
return this.renderRoot?.querySelector('[part="listbox"]') ?? null;
|
|
225
80
|
}
|
|
226
|
-
/**
|
|
227
|
-
* The row under the keyboard, read from the attribute the listbox writes.
|
|
228
|
-
*
|
|
229
|
-
* `data-active` rather than a reference the listbox hands over, because the
|
|
230
|
-
* options are this element's own light DOM children and the attribute is
|
|
231
|
-
* already published: asking the listbox would mean a second channel saying
|
|
232
|
-
* what the markup says.
|
|
233
|
-
*/
|
|
234
81
|
get activeOption() {
|
|
235
82
|
return this.querySelector('[data-active]');
|
|
236
83
|
}
|
|
237
84
|
connectedCallback() {
|
|
238
85
|
super.connectedCallback();
|
|
239
86
|
this.addEventListener('helix-select', this.swallowOptionSelect);
|
|
240
|
-
/* Capture, so both reach this before the listbox and the row do. */
|
|
241
87
|
this.addEventListener('keydown', this.handleAlternate, { capture: true });
|
|
242
88
|
this.addEventListener('click', this.handleAlternate, { capture: true });
|
|
243
89
|
document.addEventListener('keydown', this.handleHotkey);
|
|
@@ -251,18 +97,10 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
251
97
|
}
|
|
252
98
|
updated(changed) {
|
|
253
99
|
super.updated?.(changed);
|
|
254
|
-
/* On the change rather than on every update. Both of these reach into the
|
|
255
|
-
listbox, and asking it to filter on a render it did not cause is how a
|
|
256
|
-
render becomes a loop: the list updates, something it owns comes back
|
|
257
|
-
through this element, and the next update asks again. Only the moment the
|
|
258
|
-
palette opens or shuts is either of them true. */
|
|
259
100
|
if (!changed.has('open')) {
|
|
260
101
|
return;
|
|
261
102
|
}
|
|
262
103
|
if (this.open) {
|
|
263
|
-
/* The field takes focus as the palette arrives. A reader who pressed a
|
|
264
|
-
key to summon a search expects to be typing in it, and the dialog would
|
|
265
|
-
otherwise put focus on itself. */
|
|
266
104
|
this.renderRoot
|
|
267
105
|
.querySelector('[part="listbox"]')
|
|
268
106
|
?.renderRoot?.querySelector('[part="search-field"]')
|
|
@@ -272,16 +110,6 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
272
110
|
this.listbox?.requestFilter('');
|
|
273
111
|
}
|
|
274
112
|
}
|
|
275
|
-
/**
|
|
276
|
-
* The surface the palette opens in, behind a seam the styled tier can move.
|
|
277
|
-
*
|
|
278
|
-
* A method rather than a fragment inline, so a tier above can render its own,
|
|
279
|
-
* wrap this one, pass it a property the base does not know about, or leave it
|
|
280
|
-
* out, without reimplementing the whole template to do any of them. The tag is
|
|
281
|
-
* interpolated rather than written, so `tier` resolves it: this renders the
|
|
282
|
-
* headless element, and a styled subclass that sets `tier` renders the styled one
|
|
283
|
-
* from the same template.
|
|
284
|
-
*/
|
|
285
113
|
renderDialog() {
|
|
286
114
|
return html `<${this.tag('dialog')}
|
|
287
115
|
exportparts="dialog"
|
|
@@ -320,11 +148,6 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
320
148
|
</${this.tag('dialog')}>`;
|
|
321
149
|
}
|
|
322
150
|
render() {
|
|
323
|
-
/* No part of its own on the host, and the dialog's own box forwarded under
|
|
324
|
-
its own name: `::part(dialog)` then reaches the thing with the shadow and
|
|
325
|
-
the padding, rather than a host that is only a wrapper. `align` is not set
|
|
326
|
-
here either, being a styled-tier property: where the panel sits is
|
|
327
|
-
presentation, and this tier renders no CSS. */
|
|
328
151
|
return html `${this.renderDialog()}`;
|
|
329
152
|
}
|
|
330
153
|
};
|
|
@@ -13,97 +13,13 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
13
13
|
import { html } from 'lit/static-html.js';
|
|
14
14
|
import { HelixHeadlessIconButton } from '../../icon-button/root/icon-button-root.js';
|
|
15
15
|
import '../../tooltip/index.js';
|
|
16
|
-
/** How long the confirmation stands before the button goes back to itself. */
|
|
17
16
|
const CONFIRM = 1600;
|
|
18
|
-
/**
|
|
19
|
-
* @summary A button that puts a string on the clipboard and says it did.
|
|
20
|
-
*
|
|
21
|
-
* @description
|
|
22
|
-
* **The confirmation is the component.** Writing to the clipboard is three
|
|
23
|
-
* lines; what makes this worth an element is that the platform gives a reader
|
|
24
|
-
* no feedback at all. A press that copies and looks identical to a press that
|
|
25
|
-
* failed is a control nobody trusts, so they press it again, and the second
|
|
26
|
-
* press is indistinguishable from the first.
|
|
27
|
-
*
|
|
28
|
-
* **Announced after the write, never before.** A clipboard write is a
|
|
29
|
-
* permission the browser may refuse, and it is refused for real reasons: an
|
|
30
|
-
* insecure origin, a document that is not focused, a user agent that requires a
|
|
31
|
-
* gesture this press did not qualify as. So the confirmation and the event both
|
|
32
|
-
* wait for the promise, and a refusal leaves the button exactly as it was.
|
|
33
|
-
* `HelixCopyEvent` is not cancelable for the same reason: by the time there is
|
|
34
|
-
* anything to say, it has already happened.
|
|
35
|
-
*
|
|
36
|
-
* **An icon button, extending IconButton.** Both of this button's faces are
|
|
37
|
-
* icons and it never carries a word, so it is the square control rather than a
|
|
38
|
-
* text button that happens to hold a glyph: it inherits the square box, and
|
|
39
|
-
* with it the rule that an icon-only control must be named. `label` is not
|
|
40
|
-
* optional here for the same reason it is not optional there, and the two
|
|
41
|
-
* faces make it sharper: the accessible name has to describe the press, not
|
|
42
|
-
* whichever picture is showing when a reader arrives.
|
|
43
|
-
*
|
|
44
|
-
* `data-copied`, not a reflected `copied`. State never reflects here: a
|
|
45
|
-
* serialized `copied` attribute would come back from a paste as a button
|
|
46
|
-
* claiming to have copied something it never held.
|
|
47
|
-
*
|
|
48
|
-
* @since 0.1.0
|
|
49
|
-
* @status experimental
|
|
50
|
-
* @category actions
|
|
51
|
-
* @aliases Copy, Copy to clipboard
|
|
52
|
-
* @documentation https://helix.fusion.dev/components/copy-button
|
|
53
|
-
*
|
|
54
|
-
* @slot - The resting icon. Falls back to a clipboard.
|
|
55
|
-
* @slot copied - What replaces it once the write succeeds. Falls back to a check.
|
|
56
|
-
*
|
|
57
|
-
* @event {HelixCopyEvent} helix-copy - Fired after the clipboard write succeeds.
|
|
58
|
-
*
|
|
59
|
-
* @csspart button - The inner button.
|
|
60
|
-
* @csspart mark - The clipboard glyph, and the confirmation that replaces it.
|
|
61
|
-
* @csspart tooltip - The bubble, forwarded from the tooltip inside.
|
|
62
|
-
* @csspart tooltip-arrow - The point, resting against the trigger.
|
|
63
|
-
*/
|
|
64
17
|
let HelixHeadlessCopyButton = HelixHeadlessCopyButton_1 = class HelixHeadlessCopyButton extends HelixHeadlessIconButton {
|
|
65
18
|
constructor() {
|
|
66
19
|
super(...arguments);
|
|
67
|
-
/**
|
|
68
|
-
* What it is called, and says, after one.
|
|
69
|
-
*
|
|
70
|
-
* `attribute` written out, because Lit lowercases a property name rather than
|
|
71
|
-
* dash-casing it, and `copiedlabel` is not what anyone would type.
|
|
72
|
-
*/
|
|
73
20
|
this.copiedLabel = 'Copied';
|
|
74
|
-
/**
|
|
75
|
-
* What it is called, and says, before a press.
|
|
76
|
-
*
|
|
77
|
-
* Defaulted rather than left empty. An icon-only control must be named, and
|
|
78
|
-
* the name of this one is not a judgment call: every copy button in the
|
|
79
|
-
* world is called Copy. A consumer with a better word still sets it.
|
|
80
|
-
*/
|
|
81
21
|
this.label = 'Copy';
|
|
82
|
-
/**
|
|
83
|
-
* What goes on the clipboard.
|
|
84
|
-
*
|
|
85
|
-
* **Narrows `Button.value`, which is the submit value, and the two agree.**
|
|
86
|
-
* A copy button is `type="button"` and never submits, so the inherited
|
|
87
|
-
* meaning has nothing to act on; where a consumer changes the type, what it
|
|
88
|
-
* submits is what it copies, which is the reading anybody would expect of one
|
|
89
|
-
* property called `value`. Declared here rather than inherited because the
|
|
90
|
-
* clipboard is what this component is about and its own page should say so.
|
|
91
|
-
*/
|
|
92
22
|
this.value = '';
|
|
93
|
-
/**
|
|
94
|
-
* The write, the confirmation, the timer and the event, all of them shared.
|
|
95
|
-
*
|
|
96
|
-
* **Never reach for `navigator.clipboard.writeText` directly.** It is inert in
|
|
97
|
-
* a sandboxed iframe without `allow-same-origin`, which is the sandbox every
|
|
98
|
-
* example on the docs site runs in, and guarding on the API existing does not
|
|
99
|
-
* help there: the object exists and the write rejects. A copy button that does
|
|
100
|
-
* nothing is indistinguishable from a broken one.
|
|
101
|
-
*
|
|
102
|
-
* The controller carries the `execCommand` fallback, checks the host is still
|
|
103
|
-
* connected after the await (writing is asynchronous, so `hostDisconnected`
|
|
104
|
-
* can run before there is a timer to clear), and registers itself so the timer
|
|
105
|
-
* is released without an override here.
|
|
106
|
-
*/
|
|
107
23
|
this.clipboard = new CopyController(this, { after: CONFIRM });
|
|
108
24
|
this.handleCopy = async () => {
|
|
109
25
|
if (this.isDisabled) {
|
|
@@ -112,32 +28,12 @@ let HelixHeadlessCopyButton = HelixHeadlessCopyButton_1 = class HelixHeadlessCop
|
|
|
112
28
|
await this.clipboard.copy(this.value);
|
|
113
29
|
};
|
|
114
30
|
}
|
|
115
|
-
/**
|
|
116
|
-
* The words for the state it is in, used twice on purpose.
|
|
117
|
-
*
|
|
118
|
-
* The tooltip is the visible confirmation and the accessible name is the
|
|
119
|
-
* spoken one, and they have to be the same words. A tooltip that changes to
|
|
120
|
-
* Copied while the accessible name stays Copy tells a sighted reader it
|
|
121
|
-
* worked and leaves a screen reader user pressing a button that never
|
|
122
|
-
* acknowledges anything. Changing the name of the focused element is what
|
|
123
|
-
* makes the announcement happen at all.
|
|
124
|
-
*/
|
|
125
31
|
get says() {
|
|
126
32
|
return this.copied ? this.copiedLabel : this.label;
|
|
127
33
|
}
|
|
128
34
|
get copied() {
|
|
129
35
|
return this.clipboard.copied;
|
|
130
36
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Both faces, drawn rather than demanded.
|
|
133
|
-
*
|
|
134
|
-
* Slot fallback, which is this library's answer for content a component has a
|
|
135
|
-
* sensible default for: a copy button with no icons is not a copy button, and
|
|
136
|
-
* making every consumer slot the same clipboard is asking them to supply the
|
|
137
|
-
* one thing the component is sure about. Slotting anything replaces it, at
|
|
138
|
-
* assignment time, with no property to set and nothing for the server and the
|
|
139
|
-
* client to disagree about.
|
|
140
|
-
*/
|
|
141
37
|
static icon(glyph) {
|
|
142
38
|
return html `<svg part="mark" fill="currentColor" viewBox=${glyph.viewBox} aria-hidden="true">
|
|
143
39
|
${unsafeSVG(glyph.content)}
|
|
@@ -155,45 +51,15 @@ let HelixHeadlessCopyButton = HelixHeadlessCopyButton_1 = class HelixHeadlessCop
|
|
|
155
51
|
${this.renderCopiedIcon()} ${this.renderCopyIcon()}
|
|
156
52
|
</button>`);
|
|
157
53
|
}
|
|
158
|
-
/**
|
|
159
|
-
* The mark for the resting state, behind a seam a tier can move.
|
|
160
|
-
*
|
|
161
|
-
* Two seams and not one taking the state, because the two marks are two
|
|
162
|
-
* decisions: a tier changing the clipboard for a house glyph has no reason to
|
|
163
|
-
* touch the confirmation, and the stylesheet already treats them separately,
|
|
164
|
-
* the check holding its green through a hover where this one does not.
|
|
165
|
-
*/
|
|
166
54
|
renderCopyIcon() {
|
|
167
55
|
return html `<slot ?hidden=${this.copied}>${HelixHeadlessCopyButton_1.icon(copy)}</slot>`;
|
|
168
56
|
}
|
|
169
|
-
/**
|
|
170
|
-
* The mark for the moment after a press, behind a seam of its own.
|
|
171
|
-
*
|
|
172
|
-
* Both slots render and CSS shows one, which is why this is a slot rather
|
|
173
|
-
* than a branch: swapping which slot exists would unassign the consumer's
|
|
174
|
-
* content on every press, and an element that leaves and re-enters the tree
|
|
175
|
-
* loses focus with it, so the button a reader just pressed would stop being
|
|
176
|
-
* the focused element mid-press.
|
|
177
|
-
*/
|
|
178
57
|
renderCopiedIcon() {
|
|
179
58
|
return html `<slot name="copied" ?hidden=${!this.copied}
|
|
180
59
|
>${HelixHeadlessCopyButton_1.icon(check)}</slot
|
|
181
60
|
>`;
|
|
182
61
|
}
|
|
183
|
-
/**
|
|
184
|
-
* The bubble the control sits inside, behind a seam.
|
|
185
|
-
*
|
|
186
|
-
* It takes the control rather than rendering it, because this one wraps: a
|
|
187
|
-
* tier above overriding it can drop the tooltip entirely and return the
|
|
188
|
-
* button, put something else around it, or keep the tooltip and change only
|
|
189
|
-
* how it is anchored, and every one of those is a normal thing to want.
|
|
190
|
-
* Rendering the button here as well would make all three a rewrite.
|
|
191
|
-
*/
|
|
192
62
|
renderTooltip(control) {
|
|
193
|
-
/* `persist`, because this bubble is the confirmation rather than a hint
|
|
194
|
-
about it. Without it the tooltip hid on the same click that swapped its
|
|
195
|
-
content to Copied, so the words this component takes such care over were
|
|
196
|
-
written and then taken off screen in the same frame. */
|
|
197
63
|
return html `<${this.tag('tooltip')} content=${this.says} persist
|
|
198
64
|
exportparts="arrow: tooltip-arrow, tooltip">
|
|
199
65
|
${control}
|