@fusion.dev/helix-headless 0.156.0 → 0.158.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 +1085 -66
- package/dist/command/root/command-root.d.ts +19 -5
- package/dist/command/root/command-root.js +14 -4
- package/dist/data-table/root/data-table-root.js +5 -2
- 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/list/item/list-item.d.ts +15 -1
- package/dist/list/item/list-item.js +20 -1
- package/dist/list/root/list-root.d.ts +29 -1
- package/dist/list/root/list-root.js +44 -10
- package/dist/listbox/root/listbox-root.d.ts +19 -2
- package/dist/listbox/root/listbox-root.js +16 -8
- package/dist/map/root/map-root.d.ts +4 -0
- package/dist/map/root/map-root.js +1 -1
- 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 +126 -11
- package/dist/menu/root/menu-root.js +336 -26
- package/dist/notifications/root/notifications-root.d.ts +20 -0
- package/dist/notifications/root/notifications-root.js +32 -1
- package/dist/number-input/root/number-input-root.d.ts +7 -4
- package/dist/number-input/root/number-input-root.js +3 -3
- package/dist/progress/root/progress-root.js +1 -1
- package/dist/select/root/select-root.d.ts +8 -1
- package/dist/select/root/select-root.js +8 -5
- package/dist/skeleton/root/skeleton-root.d.ts +2 -1
- package/dist/skeleton/root/skeleton-root.js +1 -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 +653 -74
- package/types/vue.d.ts +133 -16
- 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, sideOf, 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]
|
|
@@ -42,10 +76,18 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
42
76
|
boundary: () => this,
|
|
43
77
|
offset: () => this.offset,
|
|
44
78
|
onDismiss: () => this.hide(),
|
|
79
|
+
onPlace: (placement) => {
|
|
80
|
+
const side = sideOf(placement);
|
|
81
|
+
this.toggleState('placed-bottom', side === 'bottom');
|
|
82
|
+
this.toggleState('placed-left', side === 'left');
|
|
83
|
+
this.toggleState('placed-right', side === 'right');
|
|
84
|
+
this.toggleState('placed-top', side === 'top');
|
|
85
|
+
},
|
|
45
86
|
open: () => this.open,
|
|
46
87
|
placement: () => this.placement,
|
|
47
88
|
surface: () => this.partNamed('surface'),
|
|
48
89
|
});
|
|
90
|
+
this.settledOnce = false;
|
|
49
91
|
this.handleContextMenu = (event) => {
|
|
50
92
|
if (!this.context) {
|
|
51
93
|
return;
|
|
@@ -56,21 +98,19 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
56
98
|
return;
|
|
57
99
|
}
|
|
58
100
|
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());
|
|
101
|
+
if (this.open && this.longPress.takeClick()) {
|
|
65
102
|
return;
|
|
66
103
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
this.
|
|
104
|
+
const box = this.trigger.getBoundingClientRect();
|
|
105
|
+
const keyed = event.clientX === 0 && event.clientY === 0;
|
|
106
|
+
this.openContextAt(keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY });
|
|
70
107
|
};
|
|
71
108
|
this.handleSelect = (event) => {
|
|
72
109
|
const item = event.target;
|
|
73
|
-
|
|
110
|
+
const reachable = this.flies
|
|
111
|
+
? isItem(item) && item.onPage
|
|
112
|
+
: this.items.includes(item);
|
|
113
|
+
if (!reachable) {
|
|
74
114
|
return;
|
|
75
115
|
}
|
|
76
116
|
const chosen = item;
|
|
@@ -90,7 +130,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
90
130
|
if (!item) {
|
|
91
131
|
return;
|
|
92
132
|
}
|
|
93
|
-
const
|
|
133
|
+
const holder = this.flies ? this.holderOf(item) : null;
|
|
134
|
+
const surface = holder
|
|
135
|
+
? holder.shadowRoot?.querySelector('[part="surface"]')
|
|
136
|
+
: this.partNamed('surface');
|
|
94
137
|
scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
|
|
95
138
|
};
|
|
96
139
|
this.handleKeydown = (event) => {
|
|
@@ -109,6 +152,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
109
152
|
this.enter(items[at]);
|
|
110
153
|
}
|
|
111
154
|
else if (along === -1) {
|
|
155
|
+
if (this.flies ? this.keyPage === null : this.drilldown.atRoot) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
112
158
|
this.leave();
|
|
113
159
|
}
|
|
114
160
|
else if (event.key === 'Tab') {
|
|
@@ -127,21 +173,166 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
127
173
|
if (this.open) {
|
|
128
174
|
this.hide();
|
|
129
175
|
}
|
|
130
|
-
else {
|
|
176
|
+
else if (!this.pressHeld) {
|
|
131
177
|
this.show();
|
|
132
178
|
}
|
|
133
179
|
};
|
|
180
|
+
this.handlePointerDown = (event) => {
|
|
181
|
+
if (this.open || !this.fromTrigger(event)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (this.pressHeld || (this.context && event.pointerType !== 'mouse')) {
|
|
185
|
+
this.heldAt = { x: event.clientX, y: event.clientY };
|
|
186
|
+
this.longPress.down(event);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
this.handlePointerMove = (event) => {
|
|
190
|
+
this.longPress.move(event);
|
|
191
|
+
};
|
|
192
|
+
this.handlePointerUp = () => {
|
|
193
|
+
this.longPress.up();
|
|
194
|
+
};
|
|
195
|
+
this.handlePointerCancel = () => {
|
|
196
|
+
this.longPress.cancel();
|
|
197
|
+
};
|
|
198
|
+
this.handleHeldClick = (event) => {
|
|
199
|
+
if (this.pressHeld && this.longPress.takeClick()) {
|
|
200
|
+
event.stopImmediatePropagation();
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
this.handleTriggerFocusIn = (event) => {
|
|
205
|
+
if (this.pressHeld && this.fromTrigger(event)) {
|
|
206
|
+
this.describeTrigger(true);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
this.handleHover = (event) => {
|
|
210
|
+
if (!this.flies || !this.open || event.pointerType !== 'mouse') {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const row = event
|
|
214
|
+
.composedPath()
|
|
215
|
+
.find((node) => isItem(node));
|
|
216
|
+
if (!row?.onPage || row === this.hovered || this.inGrace(event)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.hovered = row;
|
|
220
|
+
this.grace = undefined;
|
|
221
|
+
if (!row.disabled) {
|
|
222
|
+
row.focus();
|
|
223
|
+
}
|
|
224
|
+
clearTimeout(this.hoverTimer);
|
|
225
|
+
this.hoverTimer = setTimeout(() => {
|
|
226
|
+
if (!this.open || this.hovered !== row) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (row.hasSubmenu && !row.disabled) {
|
|
230
|
+
if (this.drilldown.page !== row) {
|
|
231
|
+
this.drilldown.enter(row);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const holder = this.holderOf(row);
|
|
236
|
+
if (this.drilldown.page !== holder) {
|
|
237
|
+
this.openPage(holder);
|
|
238
|
+
}
|
|
239
|
+
}, HOVER_DELAY);
|
|
240
|
+
};
|
|
241
|
+
this.handleRowOut = (event) => {
|
|
242
|
+
if (!this.flies || event.pointerType !== 'mouse') {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const row = event
|
|
246
|
+
.composedPath()
|
|
247
|
+
.find((node) => isItem(node));
|
|
248
|
+
const panel = row?.expanded
|
|
249
|
+
? row.shadowRoot?.querySelector('[part="surface"]')
|
|
250
|
+
: undefined;
|
|
251
|
+
if (!panel) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const box = panel.getBoundingClientRect();
|
|
255
|
+
const edge = box.left >= event.clientX ? box.left : box.right;
|
|
256
|
+
this.grace = {
|
|
257
|
+
points: [
|
|
258
|
+
[event.clientX, event.clientY],
|
|
259
|
+
[edge, box.top],
|
|
260
|
+
[edge, box.bottom],
|
|
261
|
+
],
|
|
262
|
+
until: Date.now() + GRACE,
|
|
263
|
+
};
|
|
264
|
+
};
|
|
134
265
|
}
|
|
135
266
|
static { this.words = 'helix-menu'; }
|
|
267
|
+
get flies() {
|
|
268
|
+
return this.submenus === 'flyout';
|
|
269
|
+
}
|
|
270
|
+
get topRows() {
|
|
271
|
+
return [...this.children]
|
|
272
|
+
.filter((child) => child.slot !== 'trigger')
|
|
273
|
+
.flatMap((child) => (isSection(child) ? [...child.children] : [child]))
|
|
274
|
+
.filter(isItem);
|
|
275
|
+
}
|
|
276
|
+
rowsOf(holder) {
|
|
277
|
+
return holder
|
|
278
|
+
? [...holder.children].filter((child) => child.slot === 'submenu' && isItem(child))
|
|
279
|
+
: this.topRows;
|
|
280
|
+
}
|
|
281
|
+
holderOf(row) {
|
|
282
|
+
const parent = row.parentElement;
|
|
283
|
+
return row.slot === 'submenu' && parent && isItem(parent) ? parent : null;
|
|
284
|
+
}
|
|
285
|
+
get openPages() {
|
|
286
|
+
const pages = [];
|
|
287
|
+
for (let page = this.drilldown.page; page; page = this.holderOf(page)) {
|
|
288
|
+
pages.unshift(page);
|
|
289
|
+
}
|
|
290
|
+
return [null, ...pages];
|
|
291
|
+
}
|
|
292
|
+
get keyPage() {
|
|
293
|
+
for (const page of this.openPages) {
|
|
294
|
+
const active = this.rowsOf(page).find((row) => row.active);
|
|
295
|
+
if (active) {
|
|
296
|
+
return page;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return this.drilldown.page;
|
|
300
|
+
}
|
|
301
|
+
openPage(holder) {
|
|
302
|
+
if (holder) {
|
|
303
|
+
this.drilldown.enter(holder);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.drilldown.reset();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
get pressHeld() {
|
|
310
|
+
return !this.context && this.openOn === 'long-press';
|
|
311
|
+
}
|
|
136
312
|
tellTrigger() {
|
|
137
313
|
const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
|
|
138
|
-
|
|
314
|
+
const trigger = slot?.assignedElements({ flatten: true })[0];
|
|
315
|
+
if (!trigger || trigger.localName.includes('-')) {
|
|
316
|
+
tellDisclosure(trigger, this.open, 'menu');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (trigger !== this.told) {
|
|
320
|
+
if (!this.settledOnce || trigger.hasAttribute('aria-expanded')) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.told = trigger;
|
|
324
|
+
if (!trigger.hasAttribute('aria-haspopup')) {
|
|
325
|
+
trigger.setAttribute('aria-haspopup', 'menu');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
trigger.setAttribute('aria-expanded', String(this.open));
|
|
139
329
|
}
|
|
140
330
|
get trigger() {
|
|
141
331
|
return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this);
|
|
142
332
|
}
|
|
143
333
|
get items() {
|
|
144
|
-
|
|
334
|
+
const rows = this.flies ? this.rowsOf(this.keyPage) : this.drilldown.rows;
|
|
335
|
+
return rows.filter((row) => !row.disabled);
|
|
145
336
|
}
|
|
146
337
|
show() {
|
|
147
338
|
this.openAt('first');
|
|
@@ -161,6 +352,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
161
352
|
}
|
|
162
353
|
this.panel.hide();
|
|
163
354
|
this.drilldown.reset();
|
|
355
|
+
this.stopHover();
|
|
164
356
|
if (deepContains(this, deepActiveElement())) {
|
|
165
357
|
;
|
|
166
358
|
(this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
|
|
@@ -168,13 +360,43 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
168
360
|
this.point = undefined;
|
|
169
361
|
this.returnTo = undefined;
|
|
170
362
|
}
|
|
363
|
+
openContextAt(point) {
|
|
364
|
+
this.point = point;
|
|
365
|
+
if (this.open) {
|
|
366
|
+
this.requestUpdate();
|
|
367
|
+
this.updateComplete.then(() => this.panel.reposition());
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const active = deepActiveElement();
|
|
371
|
+
this.returnTo = active instanceof HTMLElement ? active : undefined;
|
|
372
|
+
this.show();
|
|
373
|
+
}
|
|
171
374
|
enter(item) {
|
|
172
375
|
if (!this.drilldown.enter(item)) {
|
|
173
376
|
return;
|
|
174
377
|
}
|
|
175
|
-
this.updateComplete.then(() =>
|
|
378
|
+
void this.updateComplete.then(async () => {
|
|
379
|
+
if (!this.flies) {
|
|
380
|
+
this.items[0]?.focusItem();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await item.updateComplete;
|
|
384
|
+
const first = this.rowsOf(item).find((row) => !row.disabled);
|
|
385
|
+
await first?.updateComplete;
|
|
386
|
+
first?.focusItem();
|
|
387
|
+
});
|
|
176
388
|
}
|
|
177
389
|
leave() {
|
|
390
|
+
if (this.flies) {
|
|
391
|
+
const holder = this.keyPage;
|
|
392
|
+
if (!holder) {
|
|
393
|
+
this.hide();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.openPage(this.holderOf(holder));
|
|
397
|
+
this.updateComplete.then(() => holder.focus());
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
178
400
|
if (this.drilldown.atRoot) {
|
|
179
401
|
this.hide();
|
|
180
402
|
return;
|
|
@@ -184,42 +406,107 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
184
406
|
this.updateComplete.then(() => leaving?.focus());
|
|
185
407
|
}
|
|
186
408
|
handleClosedKeydown(event) {
|
|
409
|
+
if (this.context && event.shiftKey && event.key === 'F10' && this.fromTrigger(event)) {
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
const box = this.trigger.getBoundingClientRect();
|
|
412
|
+
this.openContextAt({ x: box.left, y: box.top });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
187
415
|
if (this.context || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) {
|
|
188
416
|
return;
|
|
189
417
|
}
|
|
190
|
-
|
|
191
|
-
const triggers = slot?.assignedElements({ flatten: true }) ?? [];
|
|
192
|
-
if (!event.composedPath().some((node) => triggers.includes(node))) {
|
|
418
|
+
if (this.pressHeld !== event.altKey || !this.fromTrigger(event)) {
|
|
193
419
|
return;
|
|
194
420
|
}
|
|
195
421
|
event.preventDefault();
|
|
196
422
|
this.openAt(event.key === 'ArrowDown' ? 'first' : 'last');
|
|
197
423
|
}
|
|
424
|
+
fromTrigger(event) {
|
|
425
|
+
const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
|
|
426
|
+
const triggers = slot?.assignedElements({ flatten: true }) ?? [];
|
|
427
|
+
return event.composedPath().some((node) => triggers.includes(node));
|
|
428
|
+
}
|
|
429
|
+
describeTrigger(light = false) {
|
|
430
|
+
const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
|
|
431
|
+
const trigger = slot?.assignedElements({ flatten: true })[0];
|
|
432
|
+
const control = this.pressHeld && trigger instanceof HTMLElement ? focusTarget(trigger) : undefined;
|
|
433
|
+
if (this.described && this.described !== control) {
|
|
434
|
+
unlinkDescription(this.described, this);
|
|
435
|
+
this.described = undefined;
|
|
436
|
+
}
|
|
437
|
+
if (control && control !== this.described && linkDescription(control, this, light)) {
|
|
438
|
+
this.described = control;
|
|
439
|
+
}
|
|
440
|
+
this.internals.ariaLabel = this.described ? this.longPressLabel : null;
|
|
441
|
+
}
|
|
442
|
+
stopHover() {
|
|
443
|
+
clearTimeout(this.hoverTimer);
|
|
444
|
+
this.hoverTimer = undefined;
|
|
445
|
+
this.hovered = undefined;
|
|
446
|
+
this.grace = undefined;
|
|
447
|
+
}
|
|
448
|
+
inGrace(event) {
|
|
449
|
+
const grace = this.grace;
|
|
450
|
+
return (grace !== undefined &&
|
|
451
|
+
Date.now() < grace.until &&
|
|
452
|
+
inTriangle(event.clientX, event.clientY, grace.points));
|
|
453
|
+
}
|
|
198
454
|
connectedCallback() {
|
|
199
455
|
super.connectedCallback();
|
|
456
|
+
this.addEventListener('pointermove', this.handleHover);
|
|
457
|
+
this.addEventListener('pointerout', this.handleRowOut);
|
|
200
458
|
this.addEventListener('helix-select', this.handleSelect, true);
|
|
201
459
|
this.addEventListener('keydown', this.handleKeydown);
|
|
202
460
|
this.addEventListener('focusin', this.handleFocusIn);
|
|
203
461
|
this.addEventListener('contextmenu', this.handleContextMenu);
|
|
462
|
+
this.addEventListener('pointerdown', this.handlePointerDown);
|
|
463
|
+
this.addEventListener('pointermove', this.handlePointerMove);
|
|
464
|
+
this.addEventListener('pointerup', this.handlePointerUp);
|
|
465
|
+
this.addEventListener('pointercancel', this.handlePointerCancel);
|
|
466
|
+
this.addEventListener('click', this.handleHeldClick, true);
|
|
467
|
+
this.addEventListener('focusin', this.handleTriggerFocusIn);
|
|
468
|
+
void this.whenSettled.then(() => {
|
|
469
|
+
this.settledOnce = true;
|
|
470
|
+
if (!this.context) {
|
|
471
|
+
this.tellTrigger();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
204
474
|
}
|
|
205
475
|
disconnectedCallback() {
|
|
206
476
|
super.disconnectedCallback();
|
|
477
|
+
this.removeEventListener('pointermove', this.handleHover);
|
|
478
|
+
this.removeEventListener('pointerout', this.handleRowOut);
|
|
479
|
+
this.stopHover();
|
|
207
480
|
this.removeEventListener('helix-select', this.handleSelect, true);
|
|
208
481
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
209
482
|
this.removeEventListener('focusin', this.handleFocusIn);
|
|
210
483
|
this.removeEventListener('contextmenu', this.handleContextMenu);
|
|
484
|
+
this.removeEventListener('pointerdown', this.handlePointerDown);
|
|
485
|
+
this.removeEventListener('pointermove', this.handlePointerMove);
|
|
486
|
+
this.removeEventListener('pointerup', this.handlePointerUp);
|
|
487
|
+
this.removeEventListener('pointercancel', this.handlePointerCancel);
|
|
488
|
+
this.removeEventListener('click', this.handleHeldClick, true);
|
|
489
|
+
this.removeEventListener('focusin', this.handleTriggerFocusIn);
|
|
490
|
+
if (this.described) {
|
|
491
|
+
unlinkDescription(this.described, this);
|
|
492
|
+
this.described = undefined;
|
|
493
|
+
}
|
|
211
494
|
}
|
|
212
495
|
updated(changed) {
|
|
213
496
|
super.updated(changed);
|
|
214
497
|
if (!this.context) {
|
|
215
498
|
this.tellTrigger();
|
|
216
499
|
}
|
|
500
|
+
if (changed.has('openOn') || changed.has('context') || this.described) {
|
|
501
|
+
this.describeTrigger();
|
|
502
|
+
}
|
|
503
|
+
const top = this.flies || this.drilldown.atRoot;
|
|
217
504
|
for (const child of this.children) {
|
|
218
505
|
if (isSection(child)) {
|
|
219
|
-
child.onPage =
|
|
506
|
+
child.onPage = top;
|
|
220
507
|
for (const row of child.children) {
|
|
221
508
|
if (row.isMenuItem === true) {
|
|
222
|
-
this.mark(row,
|
|
509
|
+
this.mark(row, top);
|
|
223
510
|
}
|
|
224
511
|
}
|
|
225
512
|
continue;
|
|
@@ -228,9 +515,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
228
515
|
if (item.isMenuItem !== true) {
|
|
229
516
|
continue;
|
|
230
517
|
}
|
|
231
|
-
this.mark(item,
|
|
518
|
+
this.mark(item, top);
|
|
232
519
|
}
|
|
233
|
-
this.toggleState('drilled', !this.drilldown.atRoot);
|
|
520
|
+
this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
|
|
234
521
|
this.markColumns();
|
|
235
522
|
this.nameSurface();
|
|
236
523
|
}
|
|
@@ -249,13 +536,26 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
249
536
|
surface.ariaLabelledByElements = trigger && !named ? [trigger] : null;
|
|
250
537
|
}
|
|
251
538
|
markColumns() {
|
|
252
|
-
const rows = this.drilldown.rows;
|
|
539
|
+
const rows = this.flies ? this.topRows : this.drilldown.rows;
|
|
253
540
|
this.toggleState('start', rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
|
|
254
541
|
this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
|
|
255
542
|
}
|
|
256
543
|
mark(item, onPage) {
|
|
257
544
|
const page = this.drilldown.page;
|
|
258
545
|
item.onPage = onPage;
|
|
546
|
+
const holds = page !== null && item.contains(page);
|
|
547
|
+
item.flyout = this.flies;
|
|
548
|
+
item.expanded = this.flies && holds;
|
|
549
|
+
if (this.flies) {
|
|
550
|
+
item.showingSubmenu = false;
|
|
551
|
+
for (const child of item.children) {
|
|
552
|
+
const nested = child;
|
|
553
|
+
if (nested.isMenuItem === true && child.slot === 'submenu') {
|
|
554
|
+
this.mark(nested, onPage && holds);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
259
559
|
item.showingSubmenu = page !== null && item.contains(page);
|
|
260
560
|
for (const child of item.children) {
|
|
261
561
|
const nested = child;
|
|
@@ -279,10 +579,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
|
|
|
279
579
|
role="menu"
|
|
280
580
|
aria-busy=${this.loading ? 'true' : nothing}
|
|
281
581
|
aria-label=${this.label || nothing}
|
|
282
|
-
data-drilled=${this.drilldown.atRoot ? nothing : ''}
|
|
582
|
+
data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
|
|
283
583
|
?hidden=${!this.open}
|
|
284
584
|
>
|
|
285
|
-
${this.drilldown.atRoot
|
|
585
|
+
${this.flies || this.drilldown.atRoot
|
|
286
586
|
? nothing
|
|
287
587
|
: html `<div part="header">
|
|
288
588
|
<button
|
|
@@ -314,12 +614,22 @@ __decorate([
|
|
|
314
614
|
__decorate([
|
|
315
615
|
property()
|
|
316
616
|
], HelixHeadlessMenu.prototype, "label", void 0);
|
|
617
|
+
__decorate([
|
|
618
|
+
words,
|
|
619
|
+
property({ attribute: 'long-press-label' })
|
|
620
|
+
], HelixHeadlessMenu.prototype, "longPressLabel", void 0);
|
|
317
621
|
__decorate([
|
|
318
622
|
property({ type: Number })
|
|
319
623
|
], HelixHeadlessMenu.prototype, "offset", void 0);
|
|
624
|
+
__decorate([
|
|
625
|
+
property({ attribute: 'open-on' })
|
|
626
|
+
], HelixHeadlessMenu.prototype, "openOn", void 0);
|
|
320
627
|
__decorate([
|
|
321
628
|
property()
|
|
322
629
|
], HelixHeadlessMenu.prototype, "placement", void 0);
|
|
630
|
+
__decorate([
|
|
631
|
+
property()
|
|
632
|
+
], HelixHeadlessMenu.prototype, "submenus", void 0);
|
|
323
633
|
HelixHeadlessMenu = __decorate([
|
|
324
634
|
customElement('helix-headless-menu')
|
|
325
635
|
], 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,9 +25,9 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
25
25
|
* The locale's group separators are read in every field, so `1,234` is a
|
|
26
26
|
* number in `en-US`.
|
|
27
27
|
*
|
|
28
|
-
* The field and its two buttons sit in a `role="group"
|
|
29
|
-
* `role-label`, "number field" in English, as its
|
|
30
|
-
* React Aria's NumberField does. On iOS and iPadOS the field is a plain text
|
|
28
|
+
* The field and its two buttons sit in a `role="group"` named by the label,
|
|
29
|
+
* and the field carries `role-label`, "number field" in English, as its
|
|
30
|
+
* `aria-roledescription`, as React Aria's NumberField does. On iOS and iPadOS the field is a plain text
|
|
31
31
|
* field, with no `role="spinbutton"`, which VoiceOver reads as a slider you
|
|
32
32
|
* cannot type in, and no role description, so VoiceOver says the field is
|
|
33
33
|
* required. React Aria's NumberField does the same.
|
|
@@ -492,8 +492,11 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
492
492
|
protected renderIncrementIcon(): TemplateResult;
|
|
493
493
|
protected render(): TemplateResult;
|
|
494
494
|
/**
|
|
495
|
-
* The value as a number, or `NaN` while the control is empty
|
|
495
|
+
* The value as a number, or `NaN` while the control is empty or does not
|
|
496
|
+
* hold a number.
|
|
496
497
|
*
|
|
498
|
+
* Read in the locale and the format, as the form reads it, so `1,5` typed
|
|
499
|
+
* in a locale with a decimal comma is 1.5 here while the form holds 1.5.
|
|
497
500
|
* `NaN` and not zero, which is the trap the platform already avoids: `Number('')`
|
|
498
501
|
* is zero, so an empty control would report a number it does not hold and a
|
|
499
502
|
* form would submit it.
|