@fusion.dev/helix-headless 0.156.0 → 0.157.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 +909 -46
- package/dist/drilldown/item/drilldown-item.d.ts +49 -1
- package/dist/drilldown/item/drilldown-item.js +77 -17
- package/dist/drilldown/root/drilldown-root.d.ts +45 -5
- package/dist/drilldown/root/drilldown-root.js +51 -5
- package/dist/element-events.generated.js +1 -1
- package/dist/filter-input/menu/filter-menu.d.ts +1 -0
- package/dist/filter-input/menu/filter-menu.js +4 -4
- package/dist/megamenu/root/megamenu-root.d.ts +50 -13
- package/dist/megamenu/root/megamenu-root.js +44 -6
- package/dist/menu/item/menu-item.d.ts +24 -1
- package/dist/menu/item/menu-item.js +42 -2
- package/dist/menu/root/menu-root.d.ts +120 -9
- package/dist/menu/root/menu-root.js +329 -26
- package/dist/notifications/root/notifications-root.d.ts +20 -0
- package/dist/notifications/root/notifications-root.js +32 -1
- package/dist/toast/item/toast-item.d.ts +26 -0
- package/dist/toast/item/toast-item.js +55 -3
- package/dist/toast/root/toast-root.d.ts +32 -0
- package/dist/toast/root/toast-root.js +47 -2
- package/dist/video/root/video-root.d.ts +1 -0
- package/dist/video/root/video-root.js +9 -9
- package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +445 -34
- package/types/vue.d.ts +86 -8
- package/types/words.d.ts +14 -0
|
@@ -4,11 +4,28 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, HelixElement, inlineStep, KeyboardNavigationController, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, slottedWords, tellDisclosure, typeaheadBy, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { caretLeft } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
+
import { LongPress } from '../../speed-dial/long-press.js';
|
|
13
|
+
const isItem = (element) => element !== null &&
|
|
14
|
+
typeof element === 'object' &&
|
|
15
|
+
'isMenuItem' in element &&
|
|
16
|
+
element.isMenuItem === true;
|
|
17
|
+
const HOVER_DELAY = 100;
|
|
18
|
+
const GRACE = 300;
|
|
19
|
+
const inTriangle = (x, y, [a, b, c]) => {
|
|
20
|
+
if (!a || !b || !c) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const side = (p, q) => (q[0] - p[0]) * (y - p[1]) - (q[1] - p[1]) * (x - p[0]);
|
|
24
|
+
const one = side(a, b);
|
|
25
|
+
const two = side(b, c);
|
|
26
|
+
const three = side(c, a);
|
|
27
|
+
return (one >= 0 && two >= 0 && three >= 0) || (one <= 0 && two <= 0 && three <= 0);
|
|
28
|
+
};
|
|
12
29
|
const isSection = (element) => 'isMenuSection' in element && element.isMenuSection === true;
|
|
13
30
|
let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinControllerOpen(HelixElement)) {
|
|
14
31
|
constructor() {
|
|
@@ -16,8 +33,25 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
16
33
|
this.backLabel = 'Back';
|
|
17
34
|
this.context = false;
|
|
18
35
|
this.label = '';
|
|
36
|
+
this.longPressLabel = 'Long press or press Alt+Down to show the menu';
|
|
19
37
|
this.offset = 4;
|
|
38
|
+
this.longPress = new LongPress(this, {
|
|
39
|
+
onLongPress: () => {
|
|
40
|
+
if (this.context) {
|
|
41
|
+
const at = this.heldAt;
|
|
42
|
+
if (at) {
|
|
43
|
+
this.openContextAt(at);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!this.open) {
|
|
48
|
+
this.show();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
this.openOn = 'click';
|
|
20
53
|
this.placement = 'bottom-start';
|
|
54
|
+
this.submenus = 'drill';
|
|
21
55
|
this.drilldown = new DrilldownController(this, {
|
|
22
56
|
isRow: (element) => 'isMenuItem' in element && element.isMenuItem === true,
|
|
23
57
|
root: () => [...this.children]
|
|
@@ -46,6 +80,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
46
80
|
placement: () => this.placement,
|
|
47
81
|
surface: () => this.partNamed('surface'),
|
|
48
82
|
});
|
|
83
|
+
this.settledOnce = false;
|
|
49
84
|
this.handleContextMenu = (event) => {
|
|
50
85
|
if (!this.context) {
|
|
51
86
|
return;
|
|
@@ -56,21 +91,19 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
56
91
|
return;
|
|
57
92
|
}
|
|
58
93
|
event.preventDefault();
|
|
59
|
-
|
|
60
|
-
const keyed = event.clientX === 0 && event.clientY === 0;
|
|
61
|
-
this.point = keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY };
|
|
62
|
-
if (this.open) {
|
|
63
|
-
this.requestUpdate();
|
|
64
|
-
this.updateComplete.then(() => this.panel.reposition());
|
|
94
|
+
if (this.open && this.longPress.takeClick()) {
|
|
65
95
|
return;
|
|
66
96
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
this.
|
|
97
|
+
const box = this.trigger.getBoundingClientRect();
|
|
98
|
+
const keyed = event.clientX === 0 && event.clientY === 0;
|
|
99
|
+
this.openContextAt(keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY });
|
|
70
100
|
};
|
|
71
101
|
this.handleSelect = (event) => {
|
|
72
102
|
const item = event.target;
|
|
73
|
-
|
|
103
|
+
const reachable = this.flies
|
|
104
|
+
? isItem(item) && item.onPage
|
|
105
|
+
: this.items.includes(item);
|
|
106
|
+
if (!reachable) {
|
|
74
107
|
return;
|
|
75
108
|
}
|
|
76
109
|
const chosen = item;
|
|
@@ -90,7 +123,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
90
123
|
if (!item) {
|
|
91
124
|
return;
|
|
92
125
|
}
|
|
93
|
-
const
|
|
126
|
+
const holder = this.flies ? this.holderOf(item) : null;
|
|
127
|
+
const surface = holder
|
|
128
|
+
? holder.shadowRoot?.querySelector('[part="surface"]')
|
|
129
|
+
: this.partNamed('surface');
|
|
94
130
|
scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
|
|
95
131
|
};
|
|
96
132
|
this.handleKeydown = (event) => {
|
|
@@ -109,6 +145,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
109
145
|
this.enter(items[at]);
|
|
110
146
|
}
|
|
111
147
|
else if (along === -1) {
|
|
148
|
+
if (this.flies && this.keyPage === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
112
151
|
this.leave();
|
|
113
152
|
}
|
|
114
153
|
else if (event.key === 'Tab') {
|
|
@@ -127,21 +166,166 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
127
166
|
if (this.open) {
|
|
128
167
|
this.hide();
|
|
129
168
|
}
|
|
130
|
-
else {
|
|
169
|
+
else if (!this.pressHeld) {
|
|
131
170
|
this.show();
|
|
132
171
|
}
|
|
133
172
|
};
|
|
173
|
+
this.handlePointerDown = (event) => {
|
|
174
|
+
if (this.open || !this.fromTrigger(event)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (this.pressHeld || (this.context && event.pointerType !== 'mouse')) {
|
|
178
|
+
this.heldAt = { x: event.clientX, y: event.clientY };
|
|
179
|
+
this.longPress.down(event);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
this.handlePointerMove = (event) => {
|
|
183
|
+
this.longPress.move(event);
|
|
184
|
+
};
|
|
185
|
+
this.handlePointerUp = () => {
|
|
186
|
+
this.longPress.up();
|
|
187
|
+
};
|
|
188
|
+
this.handlePointerCancel = () => {
|
|
189
|
+
this.longPress.cancel();
|
|
190
|
+
};
|
|
191
|
+
this.handleHeldClick = (event) => {
|
|
192
|
+
if (this.pressHeld && this.longPress.takeClick()) {
|
|
193
|
+
event.stopImmediatePropagation();
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
this.handleTriggerFocusIn = (event) => {
|
|
198
|
+
if (this.pressHeld && this.fromTrigger(event)) {
|
|
199
|
+
this.describeTrigger(true);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
this.handleHover = (event) => {
|
|
203
|
+
if (!this.flies || !this.open || event.pointerType !== 'mouse') {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const row = event
|
|
207
|
+
.composedPath()
|
|
208
|
+
.find((node) => isItem(node));
|
|
209
|
+
if (!row?.onPage || row === this.hovered || this.inGrace(event)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.hovered = row;
|
|
213
|
+
this.grace = undefined;
|
|
214
|
+
if (!row.disabled) {
|
|
215
|
+
row.focus();
|
|
216
|
+
}
|
|
217
|
+
clearTimeout(this.hoverTimer);
|
|
218
|
+
this.hoverTimer = setTimeout(() => {
|
|
219
|
+
if (!this.open || this.hovered !== row) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (row.hasSubmenu && !row.disabled) {
|
|
223
|
+
if (this.drilldown.page !== row) {
|
|
224
|
+
this.drilldown.enter(row);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const holder = this.holderOf(row);
|
|
229
|
+
if (this.drilldown.page !== holder) {
|
|
230
|
+
this.openPage(holder);
|
|
231
|
+
}
|
|
232
|
+
}, HOVER_DELAY);
|
|
233
|
+
};
|
|
234
|
+
this.handleRowOut = (event) => {
|
|
235
|
+
if (!this.flies || event.pointerType !== 'mouse') {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const row = event
|
|
239
|
+
.composedPath()
|
|
240
|
+
.find((node) => isItem(node));
|
|
241
|
+
const panel = row?.expanded
|
|
242
|
+
? row.shadowRoot?.querySelector('[part="surface"]')
|
|
243
|
+
: undefined;
|
|
244
|
+
if (!panel) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const box = panel.getBoundingClientRect();
|
|
248
|
+
const edge = box.left >= event.clientX ? box.left : box.right;
|
|
249
|
+
this.grace = {
|
|
250
|
+
points: [
|
|
251
|
+
[event.clientX, event.clientY],
|
|
252
|
+
[edge, box.top],
|
|
253
|
+
[edge, box.bottom],
|
|
254
|
+
],
|
|
255
|
+
until: Date.now() + GRACE,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
134
258
|
}
|
|
135
259
|
static { this.words = 'helix-menu'; }
|
|
260
|
+
get flies() {
|
|
261
|
+
return this.submenus === 'flyout';
|
|
262
|
+
}
|
|
263
|
+
get topRows() {
|
|
264
|
+
return [...this.children]
|
|
265
|
+
.filter((child) => child.slot !== 'trigger')
|
|
266
|
+
.flatMap((child) => (isSection(child) ? [...child.children] : [child]))
|
|
267
|
+
.filter(isItem);
|
|
268
|
+
}
|
|
269
|
+
rowsOf(holder) {
|
|
270
|
+
return holder
|
|
271
|
+
? [...holder.children].filter((child) => child.slot === 'submenu' && isItem(child))
|
|
272
|
+
: this.topRows;
|
|
273
|
+
}
|
|
274
|
+
holderOf(row) {
|
|
275
|
+
const parent = row.parentElement;
|
|
276
|
+
return row.slot === 'submenu' && parent && isItem(parent) ? parent : null;
|
|
277
|
+
}
|
|
278
|
+
get openPages() {
|
|
279
|
+
const pages = [];
|
|
280
|
+
for (let page = this.drilldown.page; page; page = this.holderOf(page)) {
|
|
281
|
+
pages.unshift(page);
|
|
282
|
+
}
|
|
283
|
+
return [null, ...pages];
|
|
284
|
+
}
|
|
285
|
+
get keyPage() {
|
|
286
|
+
for (const page of this.openPages) {
|
|
287
|
+
const active = this.rowsOf(page).find((row) => row.active);
|
|
288
|
+
if (active) {
|
|
289
|
+
return page;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return this.drilldown.page;
|
|
293
|
+
}
|
|
294
|
+
openPage(holder) {
|
|
295
|
+
if (holder) {
|
|
296
|
+
this.drilldown.enter(holder);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
this.drilldown.reset();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
get pressHeld() {
|
|
303
|
+
return !this.context && this.openOn === 'long-press';
|
|
304
|
+
}
|
|
136
305
|
tellTrigger() {
|
|
137
306
|
const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
|
|
138
|
-
|
|
307
|
+
const trigger = slot?.assignedElements({ flatten: true })[0];
|
|
308
|
+
if (!trigger || trigger.localName.includes('-')) {
|
|
309
|
+
tellDisclosure(trigger, this.open, 'menu');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (trigger !== this.told) {
|
|
313
|
+
if (!this.settledOnce || trigger.hasAttribute('aria-expanded')) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.told = trigger;
|
|
317
|
+
if (!trigger.hasAttribute('aria-haspopup')) {
|
|
318
|
+
trigger.setAttribute('aria-haspopup', 'menu');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
trigger.setAttribute('aria-expanded', String(this.open));
|
|
139
322
|
}
|
|
140
323
|
get trigger() {
|
|
141
324
|
return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this);
|
|
142
325
|
}
|
|
143
326
|
get items() {
|
|
144
|
-
|
|
327
|
+
const rows = this.flies ? this.rowsOf(this.keyPage) : this.drilldown.rows;
|
|
328
|
+
return rows.filter((row) => !row.disabled);
|
|
145
329
|
}
|
|
146
330
|
show() {
|
|
147
331
|
this.openAt('first');
|
|
@@ -161,6 +345,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
161
345
|
}
|
|
162
346
|
this.panel.hide();
|
|
163
347
|
this.drilldown.reset();
|
|
348
|
+
this.stopHover();
|
|
164
349
|
if (deepContains(this, deepActiveElement())) {
|
|
165
350
|
;
|
|
166
351
|
(this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
|
|
@@ -168,13 +353,43 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
168
353
|
this.point = undefined;
|
|
169
354
|
this.returnTo = undefined;
|
|
170
355
|
}
|
|
356
|
+
openContextAt(point) {
|
|
357
|
+
this.point = point;
|
|
358
|
+
if (this.open) {
|
|
359
|
+
this.requestUpdate();
|
|
360
|
+
this.updateComplete.then(() => this.panel.reposition());
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const active = deepActiveElement();
|
|
364
|
+
this.returnTo = active instanceof HTMLElement ? active : undefined;
|
|
365
|
+
this.show();
|
|
366
|
+
}
|
|
171
367
|
enter(item) {
|
|
172
368
|
if (!this.drilldown.enter(item)) {
|
|
173
369
|
return;
|
|
174
370
|
}
|
|
175
|
-
this.updateComplete.then(() =>
|
|
371
|
+
void this.updateComplete.then(async () => {
|
|
372
|
+
if (!this.flies) {
|
|
373
|
+
this.items[0]?.focusItem();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
await item.updateComplete;
|
|
377
|
+
const first = this.rowsOf(item).find((row) => !row.disabled);
|
|
378
|
+
await first?.updateComplete;
|
|
379
|
+
first?.focusItem();
|
|
380
|
+
});
|
|
176
381
|
}
|
|
177
382
|
leave() {
|
|
383
|
+
if (this.flies) {
|
|
384
|
+
const holder = this.keyPage;
|
|
385
|
+
if (!holder) {
|
|
386
|
+
this.hide();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.openPage(this.holderOf(holder));
|
|
390
|
+
this.updateComplete.then(() => holder.focus());
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
178
393
|
if (this.drilldown.atRoot) {
|
|
179
394
|
this.hide();
|
|
180
395
|
return;
|
|
@@ -184,42 +399,107 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
184
399
|
this.updateComplete.then(() => leaving?.focus());
|
|
185
400
|
}
|
|
186
401
|
handleClosedKeydown(event) {
|
|
402
|
+
if (this.context && event.shiftKey && event.key === 'F10' && this.fromTrigger(event)) {
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
const box = this.trigger.getBoundingClientRect();
|
|
405
|
+
this.openContextAt({ x: box.left, y: box.top });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
187
408
|
if (this.context || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) {
|
|
188
409
|
return;
|
|
189
410
|
}
|
|
190
|
-
|
|
191
|
-
const triggers = slot?.assignedElements({ flatten: true }) ?? [];
|
|
192
|
-
if (!event.composedPath().some((node) => triggers.includes(node))) {
|
|
411
|
+
if (this.pressHeld !== event.altKey || !this.fromTrigger(event)) {
|
|
193
412
|
return;
|
|
194
413
|
}
|
|
195
414
|
event.preventDefault();
|
|
196
415
|
this.openAt(event.key === 'ArrowDown' ? 'first' : 'last');
|
|
197
416
|
}
|
|
417
|
+
fromTrigger(event) {
|
|
418
|
+
const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
|
|
419
|
+
const triggers = slot?.assignedElements({ flatten: true }) ?? [];
|
|
420
|
+
return event.composedPath().some((node) => triggers.includes(node));
|
|
421
|
+
}
|
|
422
|
+
describeTrigger(light = false) {
|
|
423
|
+
const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
|
|
424
|
+
const trigger = slot?.assignedElements({ flatten: true })[0];
|
|
425
|
+
const control = this.pressHeld && trigger instanceof HTMLElement ? focusTarget(trigger) : undefined;
|
|
426
|
+
if (this.described && this.described !== control) {
|
|
427
|
+
unlinkDescription(this.described, this);
|
|
428
|
+
this.described = undefined;
|
|
429
|
+
}
|
|
430
|
+
if (control && control !== this.described && linkDescription(control, this, light)) {
|
|
431
|
+
this.described = control;
|
|
432
|
+
}
|
|
433
|
+
this.internals.ariaLabel = this.described ? this.longPressLabel : null;
|
|
434
|
+
}
|
|
435
|
+
stopHover() {
|
|
436
|
+
clearTimeout(this.hoverTimer);
|
|
437
|
+
this.hoverTimer = undefined;
|
|
438
|
+
this.hovered = undefined;
|
|
439
|
+
this.grace = undefined;
|
|
440
|
+
}
|
|
441
|
+
inGrace(event) {
|
|
442
|
+
const grace = this.grace;
|
|
443
|
+
return (grace !== undefined &&
|
|
444
|
+
Date.now() < grace.until &&
|
|
445
|
+
inTriangle(event.clientX, event.clientY, grace.points));
|
|
446
|
+
}
|
|
198
447
|
connectedCallback() {
|
|
199
448
|
super.connectedCallback();
|
|
449
|
+
this.addEventListener('pointermove', this.handleHover);
|
|
450
|
+
this.addEventListener('pointerout', this.handleRowOut);
|
|
200
451
|
this.addEventListener('helix-select', this.handleSelect, true);
|
|
201
452
|
this.addEventListener('keydown', this.handleKeydown);
|
|
202
453
|
this.addEventListener('focusin', this.handleFocusIn);
|
|
203
454
|
this.addEventListener('contextmenu', this.handleContextMenu);
|
|
455
|
+
this.addEventListener('pointerdown', this.handlePointerDown);
|
|
456
|
+
this.addEventListener('pointermove', this.handlePointerMove);
|
|
457
|
+
this.addEventListener('pointerup', this.handlePointerUp);
|
|
458
|
+
this.addEventListener('pointercancel', this.handlePointerCancel);
|
|
459
|
+
this.addEventListener('click', this.handleHeldClick, true);
|
|
460
|
+
this.addEventListener('focusin', this.handleTriggerFocusIn);
|
|
461
|
+
void this.whenSettled.then(() => {
|
|
462
|
+
this.settledOnce = true;
|
|
463
|
+
if (!this.context) {
|
|
464
|
+
this.tellTrigger();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
204
467
|
}
|
|
205
468
|
disconnectedCallback() {
|
|
206
469
|
super.disconnectedCallback();
|
|
470
|
+
this.removeEventListener('pointermove', this.handleHover);
|
|
471
|
+
this.removeEventListener('pointerout', this.handleRowOut);
|
|
472
|
+
this.stopHover();
|
|
207
473
|
this.removeEventListener('helix-select', this.handleSelect, true);
|
|
208
474
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
209
475
|
this.removeEventListener('focusin', this.handleFocusIn);
|
|
210
476
|
this.removeEventListener('contextmenu', this.handleContextMenu);
|
|
477
|
+
this.removeEventListener('pointerdown', this.handlePointerDown);
|
|
478
|
+
this.removeEventListener('pointermove', this.handlePointerMove);
|
|
479
|
+
this.removeEventListener('pointerup', this.handlePointerUp);
|
|
480
|
+
this.removeEventListener('pointercancel', this.handlePointerCancel);
|
|
481
|
+
this.removeEventListener('click', this.handleHeldClick, true);
|
|
482
|
+
this.removeEventListener('focusin', this.handleTriggerFocusIn);
|
|
483
|
+
if (this.described) {
|
|
484
|
+
unlinkDescription(this.described, this);
|
|
485
|
+
this.described = undefined;
|
|
486
|
+
}
|
|
211
487
|
}
|
|
212
488
|
updated(changed) {
|
|
213
489
|
super.updated(changed);
|
|
214
490
|
if (!this.context) {
|
|
215
491
|
this.tellTrigger();
|
|
216
492
|
}
|
|
493
|
+
if (changed.has('openOn') || changed.has('context') || this.described) {
|
|
494
|
+
this.describeTrigger();
|
|
495
|
+
}
|
|
496
|
+
const top = this.flies || this.drilldown.atRoot;
|
|
217
497
|
for (const child of this.children) {
|
|
218
498
|
if (isSection(child)) {
|
|
219
|
-
child.onPage =
|
|
499
|
+
child.onPage = top;
|
|
220
500
|
for (const row of child.children) {
|
|
221
501
|
if (row.isMenuItem === true) {
|
|
222
|
-
this.mark(row,
|
|
502
|
+
this.mark(row, top);
|
|
223
503
|
}
|
|
224
504
|
}
|
|
225
505
|
continue;
|
|
@@ -228,9 +508,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
228
508
|
if (item.isMenuItem !== true) {
|
|
229
509
|
continue;
|
|
230
510
|
}
|
|
231
|
-
this.mark(item,
|
|
511
|
+
this.mark(item, top);
|
|
232
512
|
}
|
|
233
|
-
this.toggleState('drilled', !this.drilldown.atRoot);
|
|
513
|
+
this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
|
|
234
514
|
this.markColumns();
|
|
235
515
|
this.nameSurface();
|
|
236
516
|
}
|
|
@@ -249,13 +529,26 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
249
529
|
surface.ariaLabelledByElements = trigger && !named ? [trigger] : null;
|
|
250
530
|
}
|
|
251
531
|
markColumns() {
|
|
252
|
-
const rows = this.drilldown.rows;
|
|
532
|
+
const rows = this.flies ? this.topRows : this.drilldown.rows;
|
|
253
533
|
this.toggleState('start', rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
|
|
254
534
|
this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
|
|
255
535
|
}
|
|
256
536
|
mark(item, onPage) {
|
|
257
537
|
const page = this.drilldown.page;
|
|
258
538
|
item.onPage = onPage;
|
|
539
|
+
const holds = page !== null && item.contains(page);
|
|
540
|
+
item.flyout = this.flies;
|
|
541
|
+
item.expanded = this.flies && holds;
|
|
542
|
+
if (this.flies) {
|
|
543
|
+
item.showingSubmenu = false;
|
|
544
|
+
for (const child of item.children) {
|
|
545
|
+
const nested = child;
|
|
546
|
+
if (nested.isMenuItem === true && child.slot === 'submenu') {
|
|
547
|
+
this.mark(nested, onPage && holds);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
259
552
|
item.showingSubmenu = page !== null && item.contains(page);
|
|
260
553
|
for (const child of item.children) {
|
|
261
554
|
const nested = child;
|
|
@@ -279,10 +572,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
279
572
|
role="menu"
|
|
280
573
|
aria-busy=${this.loading ? 'true' : nothing}
|
|
281
574
|
aria-label=${this.label || nothing}
|
|
282
|
-
data-drilled=${this.drilldown.atRoot ? nothing : ''}
|
|
575
|
+
data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
|
|
283
576
|
?hidden=${!this.open}
|
|
284
577
|
>
|
|
285
|
-
${this.drilldown.atRoot
|
|
578
|
+
${this.flies || this.drilldown.atRoot
|
|
286
579
|
? nothing
|
|
287
580
|
: html `<div part="header">
|
|
288
581
|
<button
|
|
@@ -314,12 +607,22 @@ __decorate([
|
|
|
314
607
|
__decorate([
|
|
315
608
|
property()
|
|
316
609
|
], HelixHeadlessMenu.prototype, "label", void 0);
|
|
610
|
+
__decorate([
|
|
611
|
+
words,
|
|
612
|
+
property({ attribute: 'long-press-label' })
|
|
613
|
+
], HelixHeadlessMenu.prototype, "longPressLabel", void 0);
|
|
317
614
|
__decorate([
|
|
318
615
|
property({ type: Number })
|
|
319
616
|
], HelixHeadlessMenu.prototype, "offset", void 0);
|
|
617
|
+
__decorate([
|
|
618
|
+
property({ attribute: 'open-on' })
|
|
619
|
+
], HelixHeadlessMenu.prototype, "openOn", void 0);
|
|
320
620
|
__decorate([
|
|
321
621
|
property()
|
|
322
622
|
], HelixHeadlessMenu.prototype, "placement", void 0);
|
|
623
|
+
__decorate([
|
|
624
|
+
property()
|
|
625
|
+
], HelixHeadlessMenu.prototype, "submenus", void 0);
|
|
323
626
|
HelixHeadlessMenu = __decorate([
|
|
324
627
|
customElement('helix-headless-menu')
|
|
325
628
|
], HelixHeadlessMenu);
|
|
@@ -40,6 +40,11 @@ declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("
|
|
|
40
40
|
* | Control+Home | From anywhere in the feed, moves to the nearest control before it on the page. |
|
|
41
41
|
* | Tab | Moves through the controls in a row, and on out of the feed. |
|
|
42
42
|
*
|
|
43
|
+
* **A notification that arrives says so.** Unread rows added above the rows
|
|
44
|
+
* there were are counted in the live region,
|
|
45
|
+
* "1 new notification", in `arrived-label`. Rows that arrive while `loading`
|
|
46
|
+
* is set say nothing, so set it while a page is on its way.
|
|
47
|
+
*
|
|
43
48
|
* **`load-more` adds a button after the list** that sends `helix-load-more`,
|
|
44
49
|
* for a feed that holds one page of a longer list. Append the next page, set
|
|
45
50
|
* `loading` while it is on its way, and take `load-more` off once there is
|
|
@@ -100,6 +105,11 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
|
|
|
100
105
|
* A notification's own words are under `helix-notification`.
|
|
101
106
|
*/
|
|
102
107
|
static words: string;
|
|
108
|
+
/**
|
|
109
|
+
* What the feed says when unread notifications arrive, with `{count}` the
|
|
110
|
+
* number of them. Empty it to say nothing.
|
|
111
|
+
*/
|
|
112
|
+
arrivedLabel: string;
|
|
103
113
|
/** The region's name where `label` is empty. */
|
|
104
114
|
fallbackLabel: string;
|
|
105
115
|
/**
|
|
@@ -272,6 +282,16 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
|
|
|
272
282
|
private rove;
|
|
273
283
|
connectedCallback(): void;
|
|
274
284
|
disconnectedCallback(): void;
|
|
285
|
+
/** The rows the feed has met, so a row that arrives later is told apart. */
|
|
286
|
+
private seen;
|
|
287
|
+
/** Empties the arrival a moment after it is said, so the next one is news. */
|
|
288
|
+
private readonly hush;
|
|
289
|
+
/**
|
|
290
|
+
* Counts the unread rows that arrived above the rows there were, and says
|
|
291
|
+
* how many. Rows that fill an empty feed are a page, which says nothing. The rows there are at the first update are
|
|
292
|
+
* the feed's to begin with, and say nothing.
|
|
293
|
+
*/
|
|
294
|
+
private noteArrivals;
|
|
275
295
|
protected willUpdate(changed: PropertyValues): void;
|
|
276
296
|
/**
|
|
277
297
|
* The error, the empty or the loading line, each around a slot of the same
|
|
@@ -4,13 +4,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { assignSlots, ChildrenController, customElement, deepContains, focusableElements, HelixElement, HelixLoadMoreEvent, HelixReadEvent, hostAria, MixinCollection, markRegion, rovingStop, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { assignSlots, ChildrenController, customElement, DelayController, deepContains, focusableElements, formatWords, HelixElement, HelixLoadMoreEvent, HelixReadEvent, hostAria, MixinCollection, markRegion, rovingStop, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { feedStep } from '../feed-keys.js';
|
|
11
11
|
let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinCollection(HelixElement) {
|
|
12
12
|
constructor() {
|
|
13
13
|
super(...arguments);
|
|
14
|
+
this.arrivedLabel = '{count, plural, one {# new notification} other {# new notifications}}';
|
|
14
15
|
this.fallbackLabel = 'Notifications';
|
|
15
16
|
this.label = '';
|
|
16
17
|
this.loadError = '';
|
|
@@ -60,6 +61,7 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
|
|
|
60
61
|
rowBox(next)?.focus();
|
|
61
62
|
}
|
|
62
63
|
};
|
|
64
|
+
this.hush = new DelayController(this, { renders: false });
|
|
63
65
|
this.askForMore = () => {
|
|
64
66
|
if (!this.loading) {
|
|
65
67
|
this.dispatchEvent(new HelixLoadMoreEvent());
|
|
@@ -196,8 +198,33 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
|
|
|
196
198
|
this.removeEventListener('focusin', this.handleFocusIn);
|
|
197
199
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
198
200
|
}
|
|
201
|
+
noteArrivals() {
|
|
202
|
+
if (!this.childrenKnown) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const items = this.items;
|
|
206
|
+
const seen = this.seen;
|
|
207
|
+
if (!seen) {
|
|
208
|
+
this.seen = new WeakSet(items);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const firstKnown = items.findIndex((item) => seen.has(item));
|
|
212
|
+
const fresh = items.filter((item, at) => !seen.has(item) && at < firstKnown && !item.read);
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
seen.add(item);
|
|
215
|
+
}
|
|
216
|
+
if (fresh.length === 0 || this.loading || this.arrivedLabel === '') {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.announced = formatWords(this, 'arrivedLabel', { count: fresh.length });
|
|
220
|
+
this.hush.start(1000, () => {
|
|
221
|
+
this.announced = '';
|
|
222
|
+
this.requestUpdate();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
199
225
|
willUpdate(changed) {
|
|
200
226
|
super.willUpdate(changed);
|
|
227
|
+
this.noteArrivals();
|
|
201
228
|
hostAria(this.internals, { label: this.label || this.fallbackLabel, role: 'region' });
|
|
202
229
|
const failed = this.failed;
|
|
203
230
|
this.toggleState('error', failed);
|
|
@@ -284,6 +311,10 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
|
|
|
284
311
|
<div part="footer"><slot name="footer"></slot></div>`;
|
|
285
312
|
}
|
|
286
313
|
};
|
|
314
|
+
__decorate([
|
|
315
|
+
words,
|
|
316
|
+
property({ attribute: 'arrived-label' })
|
|
317
|
+
], HelixHeadlessNotifications.prototype, "arrivedLabel", void 0);
|
|
287
318
|
__decorate([
|
|
288
319
|
words,
|
|
289
320
|
property({ attribute: 'fallback-label' })
|
|
@@ -25,6 +25,9 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
|
|
|
25
25
|
* control. While it moves, the distance is on the `item` part as
|
|
26
26
|
* `--toast-swipe-x`.
|
|
27
27
|
*
|
|
28
|
+
* **Escape dismisses the message holding focus**, through the same
|
|
29
|
+
* `helix-dismiss`, and focus moves to the next message.
|
|
30
|
+
*
|
|
28
31
|
* **Messages are inverted by default.** The item flips `color-scheme`, so a new
|
|
29
32
|
* message stands out from the page without using a color that carries meaning.
|
|
30
33
|
*
|
|
@@ -57,6 +60,10 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
|
|
|
57
60
|
*
|
|
58
61
|
* @cssproperty --toast-duration - How long this message stays, in milliseconds, for a progress hint.
|
|
59
62
|
* @cssproperty --toast-swipe-x - How far a swipe has moved the message across, in pixels, toward the right.
|
|
63
|
+
*
|
|
64
|
+
* @cssstate closed - The message has been dismissed and is hidden, or leaving.
|
|
65
|
+
* @cssstate paused - The dismiss timer is paused, while a pointer or focus is on the message, or while the page is hidden.
|
|
66
|
+
* @cssstate waiting - The region holds the message back, past its `max-visible`, until a message before it goes.
|
|
60
67
|
*/
|
|
61
68
|
export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base {
|
|
62
69
|
/**
|
|
@@ -110,6 +117,18 @@ export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base
|
|
|
110
117
|
* A subset of the tone axis: a toast reports a status and never a brand role.
|
|
111
118
|
*/
|
|
112
119
|
tone?: 'danger' | 'info' | 'success' | 'warning';
|
|
120
|
+
/**
|
|
121
|
+
* Whether the region holds the message back, because more messages are
|
|
122
|
+
* showing than its `max-visible`. The region sets it. A waiting message is
|
|
123
|
+
* hidden, its timer has not started, and it is said once it shows.
|
|
124
|
+
*/
|
|
125
|
+
waiting: boolean;
|
|
126
|
+
/** Whether the dismiss timer has started, which it does once the message first shows. */
|
|
127
|
+
private started;
|
|
128
|
+
/** Whether the timer is held for `waiting`, so the hold is let go once. */
|
|
129
|
+
private holding;
|
|
130
|
+
/** Starts the timer when the message first shows, and holds it while it waits again. */
|
|
131
|
+
private wait;
|
|
113
132
|
/** Whether it is showing, whoever said so, and `undefined` until somebody has. */
|
|
114
133
|
private openState;
|
|
115
134
|
/**
|
|
@@ -128,6 +147,13 @@ export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base
|
|
|
128
147
|
private readonly handlePause;
|
|
129
148
|
private readonly handleResume;
|
|
130
149
|
connectedCallback(): void;
|
|
150
|
+
disconnectedCallback(): void;
|
|
151
|
+
/**
|
|
152
|
+
* Escape dismisses the message holding focus, as Radix's toast does. Taken
|
|
153
|
+
* only when the dismissal goes ahead, so a refused one leaves the key to
|
|
154
|
+
* the page.
|
|
155
|
+
*/
|
|
156
|
+
private readonly handleKeydown;
|
|
131
157
|
protected willUpdate(changed: PropertyValues): void;
|
|
132
158
|
protected updated(changed: PropertyValues): void;
|
|
133
159
|
/** The elements assigned to one of the message's named slots. */
|