@fusion.dev/helix-headless 0.165.0 → 0.167.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 +3235 -534
- package/dist/callout/root/callout-root.d.ts +8 -2
- package/dist/callout/root/callout-root.js +14 -1
- package/dist/canvas/arrange.d.ts +32 -0
- package/dist/canvas/arrange.js +144 -0
- package/dist/canvas/paste.d.ts +25 -0
- package/dist/canvas/paste.js +58 -0
- package/dist/canvas/root/canvas-root.d.ts +60 -3
- package/dist/canvas/root/canvas-root.js +166 -2
- package/dist/chat/composer/chat-composer.js +5 -1
- package/dist/code-block/root/code-block-root.d.ts +63 -0
- package/dist/code-block/root/code-block-root.js +117 -6
- package/dist/combobox/root/combobox-root.d.ts +6 -1
- package/dist/combobox/root/combobox-root.js +2 -2
- package/dist/date-input/root/date-input-root.d.ts +1 -1
- package/dist/element-events.generated.js +17 -2
- package/dist/file-input/root/file-input-root.d.ts +1 -1
- package/dist/file-input/root/file-input-root.js +1 -1
- package/dist/filter-input/menu/filter-menu.d.ts +1 -0
- package/dist/filter-input/menu/filter-menu.js +1 -1
- package/dist/format-date/root/format-date-root.d.ts +10 -3
- package/dist/format-date/root/format-date-root.js +6 -1
- package/dist/listbox/root/listbox-root.d.ts +12 -1
- package/dist/listbox/root/listbox-root.js +15 -6
- package/dist/menu/item/menu-item.d.ts +20 -0
- package/dist/menu/item/menu-item.js +53 -20
- package/dist/menu/root/menu-root.d.ts +84 -1
- package/dist/menu/root/menu-root.js +187 -44
- package/dist/object-inspector/root/object-inspector-root.d.ts +5 -0
- package/dist/object-inspector/root/object-inspector-root.js +5 -0
- package/dist/select/root/select-root.js +1 -1
- package/dist/steps/root/steps-root.d.ts +7 -0
- package/dist/steps/root/steps-root.js +7 -0
- package/dist/table/grid-controller.d.ts +10 -0
- package/dist/table/grid-controller.js +7 -0
- package/dist/table/root/table-root.d.ts +71 -1
- package/dist/table/root/table-root.js +122 -7
- package/dist/table/row/table-row.d.ts +17 -0
- package/dist/table/row/table-row.js +23 -0
- package/dist/time-input/root/time-input-root.d.ts +2 -1
- package/dist/time-input/root/time-input-root.js +1 -1
- package/dist/time-picker/root/time-picker-root.d.ts +48 -1
- package/dist/time-picker/root/time-picker-root.js +79 -21
- package/dist/video/root/video-root.d.ts +2 -0
- package/dist/video/root/video-root.js +4 -2
- package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
- package/dist/video-playlist/root/video-playlist-root.js +15 -1
- package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
- package/dist/virtual-list/root/virtual-list-root.js +216 -32
- package/dist/virtual-list/sections.d.ts +27 -0
- package/dist/virtual-list/sections.js +43 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +716 -96
- package/types/vue.d.ts +148 -12
- package/types/words.d.ts +45 -0
|
@@ -17,6 +17,7 @@ registerElementEvents({
|
|
|
17
17
|
'helix-headless-callout': ['helix-dismiss'],
|
|
18
18
|
'helix-headless-canvas': [
|
|
19
19
|
'helix-before-move',
|
|
20
|
+
'helix-before-paste',
|
|
20
21
|
'helix-before-reconnect',
|
|
21
22
|
'helix-before-relabel',
|
|
22
23
|
'helix-canvas-edit',
|
|
@@ -25,6 +26,7 @@ registerElementEvents({
|
|
|
25
26
|
'helix-canvas-zoom',
|
|
26
27
|
'helix-connect',
|
|
27
28
|
'helix-move',
|
|
29
|
+
'helix-pasted',
|
|
28
30
|
'helix-reconnect',
|
|
29
31
|
'helix-relabel',
|
|
30
32
|
'helix-remove',
|
|
@@ -36,7 +38,14 @@ registerElementEvents({
|
|
|
36
38
|
'helix-headless-chat-message': ['helix-send'],
|
|
37
39
|
'helix-headless-checkbox': ['change', 'helix-before-toggle', 'input'],
|
|
38
40
|
'helix-headless-checkbox-group': ['change', 'helix-before-change', 'input'],
|
|
39
|
-
'helix-headless-code-block': [
|
|
41
|
+
'helix-headless-code-block': [
|
|
42
|
+
'change',
|
|
43
|
+
'helix-before-change',
|
|
44
|
+
'helix-before-toggle',
|
|
45
|
+
'helix-copy',
|
|
46
|
+
'helix-copy-error',
|
|
47
|
+
'helix-toggle',
|
|
48
|
+
],
|
|
40
49
|
'helix-headless-color-input': [
|
|
41
50
|
'change',
|
|
42
51
|
'helix-before-change',
|
|
@@ -186,7 +195,13 @@ registerElementEvents({
|
|
|
186
195
|
'helix-headless-map-route': ['helix-map-part-change'],
|
|
187
196
|
'helix-headless-map-terrain': ['helix-map-part-change'],
|
|
188
197
|
'helix-headless-megamenu': ['helix-before-close', 'helix-before-open', 'toggle'],
|
|
189
|
-
'helix-headless-menu': [
|
|
198
|
+
'helix-headless-menu': [
|
|
199
|
+
'helix-before-close',
|
|
200
|
+
'helix-before-open',
|
|
201
|
+
'helix-filter',
|
|
202
|
+
'helix-select',
|
|
203
|
+
'toggle',
|
|
204
|
+
],
|
|
190
205
|
'helix-headless-menu-item': ['helix-before-toggle', 'helix-select'],
|
|
191
206
|
'helix-headless-meter': ['helix-band'],
|
|
192
207
|
'helix-headless-notification': ['helix-dismiss', 'helix-read'],
|
|
@@ -2,7 +2,7 @@ import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core'
|
|
|
2
2
|
import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
import { type FileStatus } from '../status.js';
|
|
4
4
|
export type { FileStatus } from '../status.js';
|
|
5
|
-
import '../../tag/index.js';
|
|
5
|
+
import '../../tag/root/index.js';
|
|
6
6
|
declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
7
7
|
/**
|
|
8
8
|
* @summary A field for choosing files from a picker or by dropping them on it.
|
|
@@ -11,7 +11,7 @@ import { property, state } from 'lit/decorators.js';
|
|
|
11
11
|
import { html } from 'lit/static-html.js';
|
|
12
12
|
import { screenFiles } from '../screen.js';
|
|
13
13
|
import { barOf, saysStatus } from '../status.js';
|
|
14
|
-
import '../../tag/index.js';
|
|
14
|
+
import '../../tag/root/index.js';
|
|
15
15
|
const size = (bytes, locale, inBytes) => {
|
|
16
16
|
const units = ['', 'KB', 'MB', 'GB', 'TB'];
|
|
17
17
|
let value = bytes;
|
|
@@ -82,6 +82,7 @@ import '../../menu/index.js';
|
|
|
82
82
|
* @csspart menu-header - The bar above a field's page, with the back button and the field's name.
|
|
83
83
|
* @csspart menu-live - The inner menu's `live` part: the region that screen readers announce the empty and loading messages from.
|
|
84
84
|
* @csspart menu-loading - Shown while the fields are loading.
|
|
85
|
+
* @csspart menu-search-field - The inner menu's filter field, drawn with `filterable`.
|
|
85
86
|
* @csspart menu-surface - The floating panel the menu is drawn on.
|
|
86
87
|
* @csspart menu-title - The name of the field whose page is showing.
|
|
87
88
|
*/
|
|
@@ -126,7 +126,7 @@ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement
|
|
|
126
126
|
return html `<${menu}
|
|
127
127
|
part="menu"
|
|
128
128
|
exportparts="back: menu-back, back-mark: menu-back-mark, empty: menu-empty, header: menu-header,
|
|
129
|
-
live: menu-live, loading: menu-loading, surface: menu-surface, title: menu-title"
|
|
129
|
+
live: menu-live, loading: menu-loading, search-field: menu-search-field, surface: menu-surface, title: menu-title"
|
|
130
130
|
label=${this.label}
|
|
131
131
|
@helix-select=${this.handleSelect}
|
|
132
132
|
>
|
|
@@ -84,7 +84,8 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
84
84
|
*/
|
|
85
85
|
calendar?: DateCalendar | MonthCalendar;
|
|
86
86
|
/**
|
|
87
|
-
* The timestamp,
|
|
87
|
+
* The timestamp: a `Date`, milliseconds since the epoch, or a string in any
|
|
88
|
+
* format `Date` can parse. The attribute is the string.
|
|
88
89
|
*
|
|
89
90
|
* An ISO string is what an API sends and what `<time datetime>` takes, so it
|
|
90
91
|
* is the shape that needs no translation at either end. Left unset this
|
|
@@ -100,7 +101,7 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
100
101
|
* it. An element rendered on a server needs the attribute; one rendered in a
|
|
101
102
|
* browser may use either.
|
|
102
103
|
*/
|
|
103
|
-
date: string;
|
|
104
|
+
date: Date | number | string;
|
|
104
105
|
/**
|
|
105
106
|
* The locale's own pattern for the date: `full`, `long`, `medium`, or
|
|
106
107
|
* `short`. Set, it stands in for `weekday`, `day`, `month`, `year` and `era`.
|
|
@@ -259,7 +260,13 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
259
260
|
private get dateOnly();
|
|
260
261
|
/** The timestamp, or nothing where there is not one to read. */
|
|
261
262
|
private get moment();
|
|
262
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* The timestamp as written, in the property, the attribute or the light DOM.
|
|
265
|
+
*
|
|
266
|
+
* A `Date` or a number is written as its ISO string, so it is read the way an
|
|
267
|
+
* attribute is. Measured before this: assigning `new Date()` threw, since
|
|
268
|
+
* only a string has `trim`, where Shoelace and Web Awesome take a `Date`.
|
|
269
|
+
*/
|
|
263
270
|
private get source();
|
|
264
271
|
/** Whether the reading is how long ago rather than the date. */
|
|
265
272
|
private get reading();
|
|
@@ -44,7 +44,12 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
44
44
|
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
45
45
|
}
|
|
46
46
|
get source() {
|
|
47
|
-
|
|
47
|
+
const date = this.date;
|
|
48
|
+
if (typeof date === 'string') {
|
|
49
|
+
return (date || this.written).trim();
|
|
50
|
+
}
|
|
51
|
+
const at = new Date(date);
|
|
52
|
+
return Number.isNaN(at.getTime()) ? '' : at.toISOString();
|
|
48
53
|
}
|
|
49
54
|
get reading() {
|
|
50
55
|
return this.elapsed || this.relative !== undefined;
|
|
@@ -542,7 +542,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
|
|
|
542
542
|
*/
|
|
543
543
|
navigate(event: KeyboardEvent): void;
|
|
544
544
|
/**
|
|
545
|
-
* Scrolls the chosen option to the middle of the list
|
|
545
|
+
* Scrolls the chosen option to the middle of the list, or the highlighted one
|
|
546
|
+
* where nothing is chosen.
|
|
546
547
|
*
|
|
547
548
|
* A method rather than something this does on its own, because a listbox
|
|
548
549
|
* opening on its answer usually wants it merely *visible* and the browser
|
|
@@ -554,6 +555,16 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
|
|
|
554
555
|
* arithmetic, since the chosen option may not be drawn.
|
|
555
556
|
*/
|
|
556
557
|
reveal(): void;
|
|
558
|
+
/**
|
|
559
|
+
* Puts the highlight on the option holding `value`, without choosing it, so
|
|
560
|
+
* the arrow keys move from there. Nothing changes where no option a reader
|
|
561
|
+
* can reach holds `value`.
|
|
562
|
+
*
|
|
563
|
+
* Time Picker calls it for a time between two of its steps, which no option
|
|
564
|
+
* holds: without it the first arrow moved from the top of the list, so Down
|
|
565
|
+
* from 14:07 went to midnight.
|
|
566
|
+
*/
|
|
567
|
+
highlightValue(value: string): void;
|
|
557
568
|
/** Hides what the filter excludes, and writes the rest onto the options. */
|
|
558
569
|
private place;
|
|
559
570
|
/**
|
|
@@ -4,7 +4,7 @@ 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, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, filterKeeps, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { magnifyingGlass } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -391,21 +391,30 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
|
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
393
|
const items = this.partNamed('items');
|
|
394
|
-
const chosen = this.options.find((option) => option.selected && !option.hidden)
|
|
394
|
+
const chosen = this.options.find((option) => option.selected && !option.hidden) ??
|
|
395
|
+
this.options.find((option) => option.value === this.activeValue && !option.hidden);
|
|
395
396
|
if (items && chosen) {
|
|
396
397
|
centerInView(items, chosen);
|
|
397
398
|
}
|
|
398
399
|
}
|
|
400
|
+
highlightValue(value) {
|
|
401
|
+
if (!this.shown.some((option) => !option.disabled && option.value === value)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.typeahead.reset();
|
|
405
|
+
this.parked = false;
|
|
406
|
+
this.activeValue = value;
|
|
407
|
+
if (this.childrenKnown) {
|
|
408
|
+
this.place();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
399
411
|
place() {
|
|
400
412
|
this.forgetRemoved();
|
|
401
413
|
const typed = this.filter.trim();
|
|
402
414
|
const needle = typed.toLowerCase();
|
|
403
|
-
const match = this.filterMatch;
|
|
404
415
|
const chosen = new Set(this.values);
|
|
405
416
|
for (const option of this.entries) {
|
|
406
|
-
option.hidden =
|
|
407
|
-
needle !== '' &&
|
|
408
|
-
!(match ? match(option.text, typed) : option.text.toLowerCase().includes(needle));
|
|
417
|
+
option.hidden = !filterKeeps(option.text, typed, this.filterMatch);
|
|
409
418
|
option.selected = chosen.has(option.value);
|
|
410
419
|
}
|
|
411
420
|
for (const group of this.groups) {
|
|
@@ -47,6 +47,12 @@ import '../../kbd/index.js';
|
|
|
47
47
|
* slots, so a mark of your own replaces either by being slotted. Neither is
|
|
48
48
|
* read as part of the row's name.
|
|
49
49
|
*
|
|
50
|
+
* In a menu with `filterable`, focus stays in the menu's filter field and the
|
|
51
|
+
* row the arrow keys reach is highlighted: the field names it with
|
|
52
|
+
* `aria-activedescendant`, the row carries its role on its host so the field
|
|
53
|
+
* can point at it, and `:state(highlighted)` says which row it is. A row with
|
|
54
|
+
* an `href` then follows its link without an anchor, as `Option` does.
|
|
55
|
+
*
|
|
50
56
|
* @since 0.1.0
|
|
51
57
|
* @status stable
|
|
52
58
|
* @aliases Menu option, Menu row
|
|
@@ -75,6 +81,7 @@ import '../../kbd/index.js';
|
|
|
75
81
|
* @csspart surface - The panel a submenu opens in beside the row, under the menu's `submenus="flyout"`.
|
|
76
82
|
*
|
|
77
83
|
* @cssstate checked - The row is checked.
|
|
84
|
+
* @cssstate highlighted - In a menu with `filterable`, the arrow keys or the pointer reached the row while focus stays in the filter field.
|
|
78
85
|
* @cssstate submenu-mark - Under `submenus="flyout"`, a row in this row's submenu can be ticked or opens a submenu, so the submenu keeps its trailing column.
|
|
79
86
|
* @cssstate submenu-start - Under `submenus="flyout"`, a row in this row's submenu has content before its label, so the submenu keeps its leading column.
|
|
80
87
|
*/
|
|
@@ -144,6 +151,11 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
|
|
|
144
151
|
* `submenus="flyout"`. Written by the menu.
|
|
145
152
|
*/
|
|
146
153
|
flyout: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* @internal Whether the row is the one the keys reached in a menu that keeps
|
|
156
|
+
* focus in its filter field. Written by the menu.
|
|
157
|
+
*/
|
|
158
|
+
highlighted: boolean;
|
|
147
159
|
/**
|
|
148
160
|
* The URL the row links to. Setting it renders the row as a link.
|
|
149
161
|
*
|
|
@@ -200,6 +212,12 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
|
|
|
200
212
|
* The row's value, sent in `helix-select` when the row is chosen.
|
|
201
213
|
*/
|
|
202
214
|
value: string;
|
|
215
|
+
/**
|
|
216
|
+
* @internal Whether the menu keeps focus in its filter field, so the row is
|
|
217
|
+
* highlighted rather than focused and says what it is from its host, where
|
|
218
|
+
* the field's `aria-activedescendant` can reach it. Written by the menu.
|
|
219
|
+
*/
|
|
220
|
+
virtual: boolean;
|
|
203
221
|
/**
|
|
204
222
|
* @internal Read by a parent to identify its children across the light DOM,
|
|
205
223
|
* which is cheaper than `instanceof` and is why this is public rather than
|
|
@@ -414,6 +432,8 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
|
|
|
414
432
|
* still line up without every row carrying an empty box to line them up with.
|
|
415
433
|
*/
|
|
416
434
|
protected renderMark(submenu: boolean): TemplateResult | typeof nothing;
|
|
435
|
+
/** Whether the row says what it is from its host, for a field's `aria-activedescendant`. */
|
|
436
|
+
private get onHost();
|
|
417
437
|
protected render(): TemplateResult | typeof nothing;
|
|
418
438
|
}
|
|
419
439
|
declare global {
|
|
@@ -4,7 +4,7 @@ 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 { ariaChord, auditName, customElement, deepActiveElement, HelixBeforeToggleEvent, HelixElement, HelixSelectEvent, isRtl, onApple, onUnlessFalse, PopoverController, requestChange, slotFilled, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { ariaChord, auditName, customElement, deepActiveElement, followLink, HelixBeforeToggleEvent, HelixElement, HelixSelectEvent, hostAria, isRtl, onApple, onUnlessFalse, PopoverController, requestChange, slotFilled, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { caretRight, check } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -21,6 +21,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
21
21
|
this.exclusive = false;
|
|
22
22
|
this.expanded = false;
|
|
23
23
|
this.flyout = false;
|
|
24
|
+
this.highlighted = false;
|
|
24
25
|
this.href = '';
|
|
25
26
|
this.onPage = true;
|
|
26
27
|
this.rel = '';
|
|
@@ -28,6 +29,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
28
29
|
this.showingSubmenu = false;
|
|
29
30
|
this.target = '';
|
|
30
31
|
this.value = '';
|
|
32
|
+
this.virtual = false;
|
|
31
33
|
this.isMenuItem = true;
|
|
32
34
|
this.keepsOpen = false;
|
|
33
35
|
this.submenu = false;
|
|
@@ -90,6 +92,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
90
92
|
willUpdate(changed) {
|
|
91
93
|
super.willUpdate(changed);
|
|
92
94
|
this.toggleState('checked', this.checked);
|
|
95
|
+
this.toggleState('highlighted', this.virtual && this.highlighted);
|
|
93
96
|
if (!this.childrenKnown) {
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
@@ -109,6 +112,11 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
109
112
|
}
|
|
110
113
|
updated(changed) {
|
|
111
114
|
super.updated(changed);
|
|
115
|
+
hostAria(this.internals, {
|
|
116
|
+
describedBy: this.onHost
|
|
117
|
+
? [...this.renderRoot.querySelectorAll('#description, #end-words')].filter((slot) => slot.id === 'description' ? this.description : this.end)
|
|
118
|
+
: [],
|
|
119
|
+
});
|
|
112
120
|
if (this.partNamed('item')) {
|
|
113
121
|
auditName(this, () => this.partNamed('item') ?? undefined, 'Give the row words in its default slot, or a label on the icon it holds.');
|
|
114
122
|
}
|
|
@@ -126,7 +134,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
126
134
|
const ids = [this.description ? 'description' : '', this.end ? 'end-words' : '']
|
|
127
135
|
.filter((id) => id !== '')
|
|
128
136
|
.join(' ');
|
|
129
|
-
return ids === '' ?
|
|
137
|
+
return ids === '' ? null : ids;
|
|
130
138
|
}
|
|
131
139
|
renderEnd() {
|
|
132
140
|
const kbd = this.tag('kbd');
|
|
@@ -151,6 +159,9 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
151
159
|
this.choose();
|
|
152
160
|
}
|
|
153
161
|
get active() {
|
|
162
|
+
if (this.virtual) {
|
|
163
|
+
return this.highlighted;
|
|
164
|
+
}
|
|
154
165
|
const active = deepActiveElement();
|
|
155
166
|
return active !== null && active.getRootNode() === this.renderRoot;
|
|
156
167
|
}
|
|
@@ -201,7 +212,11 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
201
212
|
const spaceKeeps = space && this.choice && !this.hasSubmenu;
|
|
202
213
|
this.keepsOpen = this.closeOnSelect === undefined ? spaceKeeps : !this.closeOnSelect;
|
|
203
214
|
this.tick();
|
|
204
|
-
this.dispatchEvent(new HelixSelectEvent({ value: this.value }))
|
|
215
|
+
if (this.dispatchEvent(new HelixSelectEvent({ value: this.value })) &&
|
|
216
|
+
this.virtual &&
|
|
217
|
+
this.href !== '') {
|
|
218
|
+
followLink(this.ownerDocument, this);
|
|
219
|
+
}
|
|
205
220
|
}
|
|
206
221
|
renderMark(submenu) {
|
|
207
222
|
if (submenu) {
|
|
@@ -220,19 +235,31 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
220
235
|
}
|
|
221
236
|
return nothing;
|
|
222
237
|
}
|
|
238
|
+
get onHost() {
|
|
239
|
+
return this.virtual && this.onPage && !this.showingSubmenu;
|
|
240
|
+
}
|
|
223
241
|
render() {
|
|
242
|
+
const submenu = this.hasSubmenu;
|
|
243
|
+
const ticks = this.choice;
|
|
244
|
+
const role = ticks ? (this.exclusive ? 'menuitemradio' : 'menuitemcheckbox') : 'menuitem';
|
|
245
|
+
const checkedState = ticks ? (this.checked ? 'true' : 'false') : null;
|
|
246
|
+
const keys = this.shortcut === '' ? null : ariaChord(this.shortcut, !this.hydrating && onApple());
|
|
247
|
+
const host = this.onHost;
|
|
248
|
+
const internals = this.internals;
|
|
249
|
+
internals.role = host ? role : null;
|
|
250
|
+
internals.ariaChecked = host ? checkedState : null;
|
|
251
|
+
internals.ariaDisabled = host && this.disabled ? 'true' : null;
|
|
252
|
+
internals.ariaHasPopup = host && submenu ? 'menu' : null;
|
|
253
|
+
internals.ariaExpanded = host && this.flyout && submenu ? String(this.expanded) : null;
|
|
254
|
+
internals.ariaKeyShortcuts = host ? keys : null;
|
|
255
|
+
const own = (value) => (host || value === null ? nothing : value);
|
|
224
256
|
if (this.showingSubmenu) {
|
|
225
257
|
return html `<slot name="submenu" @slotchange=${this.readSubmenu}></slot>`;
|
|
226
258
|
}
|
|
227
259
|
if (!this.onPage) {
|
|
228
260
|
return nothing;
|
|
229
261
|
}
|
|
230
|
-
const
|
|
231
|
-
const linked = this.href !== '' && !this.disabled;
|
|
232
|
-
const ticks = this.choice;
|
|
233
|
-
const role = ticks ? (this.exclusive ? 'menuitemradio' : 'menuitemcheckbox') : 'menuitem';
|
|
234
|
-
const checkedState = ticks ? (this.checked ? 'true' : 'false') : nothing;
|
|
235
|
-
const keys = this.shortcut === '' ? nothing : ariaChord(this.shortcut, !this.hydrating && onApple());
|
|
262
|
+
const linked = this.href !== '' && !this.disabled && !this.virtual;
|
|
236
263
|
const inside = html `<span part="start" ?data-empty=${!this.start}
|
|
237
264
|
><slot name="start" @slotchange=${this.readStart}></slot
|
|
238
265
|
></span>
|
|
@@ -249,11 +276,11 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
249
276
|
rel=${this.rel || nothing}
|
|
250
277
|
?download=${this.download}
|
|
251
278
|
role=${role}
|
|
252
|
-
aria-checked=${checkedState}
|
|
253
|
-
aria-describedby=${this.describedBy}
|
|
279
|
+
aria-checked=${own(checkedState)}
|
|
280
|
+
aria-describedby=${own(this.describedBy)}
|
|
254
281
|
aria-haspopup=${submenu ? 'menu' : nothing}
|
|
255
282
|
aria-expanded=${this.flyout && submenu ? String(this.expanded) : nothing}
|
|
256
|
-
aria-keyshortcuts=${keys}
|
|
283
|
+
aria-keyshortcuts=${own(keys)}
|
|
257
284
|
tabindex="-1"
|
|
258
285
|
@click=${this.handleClick}
|
|
259
286
|
@keydown=${this.handleKeydown}
|
|
@@ -261,14 +288,14 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
261
288
|
: html `<div
|
|
262
289
|
part="item"
|
|
263
290
|
id=${this.flyout ? 'row' : nothing}
|
|
264
|
-
role=${role}
|
|
265
|
-
aria-checked=${checkedState}
|
|
266
|
-
aria-describedby=${this.describedBy}
|
|
267
|
-
aria-disabled=${this.disabled ? 'true' :
|
|
268
|
-
aria-haspopup=${submenu ? 'menu' :
|
|
269
|
-
aria-expanded=${this.flyout && submenu ? String(this.expanded) :
|
|
270
|
-
aria-keyshortcuts=${keys}
|
|
271
|
-
tabindex=${this.disabled ?
|
|
291
|
+
role=${own(role)}
|
|
292
|
+
aria-checked=${own(checkedState)}
|
|
293
|
+
aria-describedby=${own(this.describedBy)}
|
|
294
|
+
aria-disabled=${own(this.disabled ? 'true' : null)}
|
|
295
|
+
aria-haspopup=${own(submenu ? 'menu' : null)}
|
|
296
|
+
aria-expanded=${own(this.flyout && submenu ? String(this.expanded) : null)}
|
|
297
|
+
aria-keyshortcuts=${own(keys)}
|
|
298
|
+
tabindex=${own(this.disabled ? null : '-1')}
|
|
272
299
|
@click=${this.handleClick}
|
|
273
300
|
@keydown=${this.handleKeydown}
|
|
274
301
|
>${inside}</div>`}
|
|
@@ -307,6 +334,9 @@ __decorate([
|
|
|
307
334
|
__decorate([
|
|
308
335
|
property({ attribute: false })
|
|
309
336
|
], HelixHeadlessMenuItem.prototype, "flyout", void 0);
|
|
337
|
+
__decorate([
|
|
338
|
+
property({ attribute: false })
|
|
339
|
+
], HelixHeadlessMenuItem.prototype, "highlighted", void 0);
|
|
310
340
|
__decorate([
|
|
311
341
|
property()
|
|
312
342
|
], HelixHeadlessMenuItem.prototype, "href", void 0);
|
|
@@ -328,6 +358,9 @@ __decorate([
|
|
|
328
358
|
__decorate([
|
|
329
359
|
property()
|
|
330
360
|
], HelixHeadlessMenuItem.prototype, "value", void 0);
|
|
361
|
+
__decorate([
|
|
362
|
+
property({ attribute: false })
|
|
363
|
+
], HelixHeadlessMenuItem.prototype, "virtual", void 0);
|
|
331
364
|
__decorate([
|
|
332
365
|
state()
|
|
333
366
|
], HelixHeadlessMenuItem.prototype, "submenu", void 0);
|
|
@@ -65,6 +65,21 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
65
65
|
* opens it from the keyboard, and the trigger is described by
|
|
66
66
|
* `long-press-label`, which says so.
|
|
67
67
|
*
|
|
68
|
+
* **A filter field.** With `filterable`, the menu opens with focus in a field
|
|
69
|
+
* above its rows, and typing narrows the rows of the page the keys move in,
|
|
70
|
+
* as React Aria's `Autocomplete` around a `Menu` does. A row is kept where its
|
|
71
|
+
* words hold the filter, in any case, or where `filterMatch` says so. Each
|
|
72
|
+
* page starts unfiltered, and the filter is cleared as the menu closes. Where
|
|
73
|
+
* typing keeps no row, the menu shows `no-results-text`. Focus stays in the
|
|
74
|
+
* field: Up and Down Arrow move the highlight through the rows that are left,
|
|
75
|
+
* Enter chooses the highlighted row, Right Arrow opens its submenu, Left Arrow
|
|
76
|
+
* on a submenu goes back, and Escape clears the filter and then closes the
|
|
77
|
+
* menu. Home, End and the other keys move the caret and type. The field names
|
|
78
|
+
* the highlighted row with `aria-activedescendant`, and the rows are a `menu`
|
|
79
|
+
* after the field. Cancel `helix-filter` to keep every row
|
|
80
|
+
* showing and narrow the rows yourself, such as by replacing them with rows
|
|
81
|
+
* from a server.
|
|
82
|
+
*
|
|
68
83
|
* @since 0.1.0
|
|
69
84
|
* @status stable
|
|
70
85
|
* @category overlays
|
|
@@ -72,10 +87,12 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
72
87
|
* @documentation https://fusion.dev/helix/components/menu
|
|
73
88
|
*
|
|
74
89
|
* @slot - The items.
|
|
90
|
+
* @slot no-results - What the menu shows when the filter keeps no rows, for content richer than `no-results-text`.
|
|
75
91
|
* @slot trigger - The control that opens the menu, or with `context` the area a context menu opens over.
|
|
76
92
|
*
|
|
77
93
|
* @event {HelixBeforeCloseEvent} helix-before-close - The menu is about to close. Cancelable: cancel it to keep it open.
|
|
78
94
|
* @event {HelixBeforeOpenEvent} helix-before-open - The menu is about to open. Cancelable: cancel it to keep it closed.
|
|
95
|
+
* @event {HelixFilterEvent} helix-filter - The text in the filter field changed, with the new text in `detail.value`. Cancelable: cancel it to keep every row showing and narrow the rows yourself.
|
|
79
96
|
* @event {HelixSelectEvent} helix-select - An item was chosen. Cancelable: cancel it to keep the menu open.
|
|
80
97
|
* @event {ToggleEvent} toggle - The menu opened or closed. The event carries `newState` and `oldState`.
|
|
81
98
|
*
|
|
@@ -85,7 +102,8 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
85
102
|
* @csspart header - The bar above a submenu, holding the way back and its name.
|
|
86
103
|
* @csspart live - A live region that repeats the empty and loading text, so screen readers announce it when it changes. Visually hidden in the styled tier.
|
|
87
104
|
* @csspart loading - The text shown while the items are loading.
|
|
88
|
-
* @csspart
|
|
105
|
+
* @csspart search-field - With `filterable`, the field that narrows the rows.
|
|
106
|
+
* @csspart surface - The floating panel the menu is drawn on. It is the `menu` itself, and with `filterable` it holds the filter field and then the `menu`.
|
|
89
107
|
* @csspart title - The name of the submenu that is showing.
|
|
90
108
|
*
|
|
91
109
|
* @cssproperty --floating-anchor-inline-size - The trigger's width, written on the panel while it is open, as in `min-inline-size: var(--floating-anchor-inline-size)`. Read it; a value set here is written over.
|
|
@@ -123,6 +141,31 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
123
141
|
* asks for one, with a right-click, a long press, Shift+F10 or the menu key.
|
|
124
142
|
*/
|
|
125
143
|
context: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* The text the rows are filtered by, which the filter field holds.
|
|
146
|
+
*
|
|
147
|
+
* Cleared as the menu closes and as a submenu opens or closes, so each page
|
|
148
|
+
* starts with every row showing.
|
|
149
|
+
*/
|
|
150
|
+
filter: string;
|
|
151
|
+
/**
|
|
152
|
+
* Whether the menu shows a field above its rows that narrows them as the
|
|
153
|
+
* reader types, with focus staying in the field.
|
|
154
|
+
*/
|
|
155
|
+
filterable: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* The filter field's name and placeholder.
|
|
158
|
+
*/
|
|
159
|
+
filterLabel: string;
|
|
160
|
+
/**
|
|
161
|
+
* Whether a row is kept by the filter: a function given the row's words and
|
|
162
|
+
* the filter, trimmed, that returns `true` to keep it showing, as React
|
|
163
|
+
* Aria's `Autocomplete` takes `filter`. See Listbox's `filterMatch`.
|
|
164
|
+
*
|
|
165
|
+
* Absent, a row is kept where its words hold the filter, in any case. A
|
|
166
|
+
* property only, there being no way to write a function in markup.
|
|
167
|
+
*/
|
|
168
|
+
filterMatch?: ((text: string, filter: string) => boolean) | undefined;
|
|
126
169
|
/**
|
|
127
170
|
* Accessible name for the surface.
|
|
128
171
|
*
|
|
@@ -153,6 +196,10 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
153
196
|
* to open the menu. Translate it with the rest of your interface.
|
|
154
197
|
*/
|
|
155
198
|
longPressLabel: string;
|
|
199
|
+
/**
|
|
200
|
+
* The text shown when the filter keeps no rows.
|
|
201
|
+
*/
|
|
202
|
+
noResultsText: string;
|
|
156
203
|
/**
|
|
157
204
|
* Space between the trigger and the surface, in pixels.
|
|
158
205
|
*/
|
|
@@ -234,6 +281,35 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
234
281
|
private get trigger();
|
|
235
282
|
/** The items on the page that is showing, which is what the keys move over. */
|
|
236
283
|
private get items();
|
|
284
|
+
/** The element that is the `menu`: the surface, or with `filterable` the box after the field. */
|
|
285
|
+
private get menuBox();
|
|
286
|
+
/** The filter field, with `filterable`. */
|
|
287
|
+
private get field();
|
|
288
|
+
/** The submenu the filter narrows, or null for the top page. */
|
|
289
|
+
private filterPage;
|
|
290
|
+
/** The filter a listener refused, which narrows nothing. */
|
|
291
|
+
private refused;
|
|
292
|
+
/** Whether the filter is narrowing the rows. */
|
|
293
|
+
private get filtering();
|
|
294
|
+
/** Whether the filter keeps a row: one of another page, or one whose words it matches. */
|
|
295
|
+
private kept;
|
|
296
|
+
/**
|
|
297
|
+
* Asks to filter by new text, which the field and `filter` then hold. A
|
|
298
|
+
* refusal keeps every row showing, for a page that narrows them itself.
|
|
299
|
+
*/
|
|
300
|
+
private requestFilter;
|
|
301
|
+
private readonly handleFilterInput;
|
|
302
|
+
/** Filters by what the reader typed, and highlights the first row kept. */
|
|
303
|
+
private filterBy;
|
|
304
|
+
/** The row the keys reached while focus stays in the filter field. */
|
|
305
|
+
private highlighted;
|
|
306
|
+
/**
|
|
307
|
+
* Moves the reader to a row: focus, or with `filterable` the highlight,
|
|
308
|
+
* which the field names with `aria-activedescendant`. An element reference,
|
|
309
|
+
* because the field is in this shadow root and the row in the tree around
|
|
310
|
+
* it, the one direction a reference crosses.
|
|
311
|
+
*/
|
|
312
|
+
private reach;
|
|
237
313
|
/** Opens the menu. */
|
|
238
314
|
show(): void;
|
|
239
315
|
/** Opens the menu with focus on its first or its last row. */
|
|
@@ -260,6 +336,8 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
260
336
|
* below the fold of a scrolled panel.
|
|
261
337
|
*/
|
|
262
338
|
private readonly handleFocusIn;
|
|
339
|
+
/** Scrolls the panel that holds a row to show it. */
|
|
340
|
+
private revealRow;
|
|
263
341
|
private readonly handleKeydown;
|
|
264
342
|
/**
|
|
265
343
|
* Down and Up on the trigger open the menu on its first and its last row.
|
|
@@ -276,6 +354,11 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
276
354
|
* `open-on="long-press"`, or a touch or a pen held on a context menu's area.
|
|
277
355
|
*/
|
|
278
356
|
private readonly handlePointerDown;
|
|
357
|
+
/**
|
|
358
|
+
* Keeps focus in the filter field through a press on a row or the way
|
|
359
|
+
* back, as React Aria's virtual focus does: the press still chooses.
|
|
360
|
+
*/
|
|
361
|
+
private readonly handleMouseDown;
|
|
279
362
|
private readonly handlePointerMove;
|
|
280
363
|
private readonly handlePointerUp;
|
|
281
364
|
private readonly handlePointerCancel;
|