@fusion.dev/helix-headless 0.177.1 → 0.178.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 +557 -53
- package/dist/color-input/root/color-input-root.d.ts +0 -13
- package/dist/color-input/root/color-input-root.js +1 -19
- package/dist/date-input/root/date-input-root.d.ts +7 -1
- package/dist/date-input/root/date-input-root.js +6 -2
- package/dist/date-picker/root/date-picker-root.d.ts +6 -2
- package/dist/date-picker/root/date-picker-root.js +6 -2
- package/dist/filter-input/index.d.ts +1 -0
- package/dist/filter-input/root/filter-input-root.d.ts +92 -6
- package/dist/filter-input/root/filter-input-root.js +182 -10
- package/dist/filter-input/suggest.d.ts +47 -0
- package/dist/filter-input/suggest.js +17 -0
- package/dist/graph/root/graph-root.d.ts +101 -3
- package/dist/graph/root/graph-root.js +188 -9
- package/dist/time-picker/root/time-picker-root.d.ts +8 -3
- package/dist/time-picker/root/time-picker-root.js +2 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +324 -54
- package/types/vue.d.ts +79 -16
- package/types/words.d.ts +23 -4
|
@@ -7,6 +7,53 @@
|
|
|
7
7
|
* and after its colon where it has one, so `status:ope` narrows by `ope`.
|
|
8
8
|
*/
|
|
9
9
|
export declare const currentTerm: (text: string, caret?: number) => string;
|
|
10
|
+
/**
|
|
11
|
+
* The field the term at the caret is a value of: the word before its colon,
|
|
12
|
+
* without a leading `-`, so `-status:ope` is a value of `status`. Empty where
|
|
13
|
+
* the term has no colon, which is a key being typed.
|
|
14
|
+
*/
|
|
15
|
+
export declare const currentField: (text: string, caret?: number) => string;
|
|
16
|
+
/**
|
|
17
|
+
* The text with a suggestion written over the term at the caret, and where
|
|
18
|
+
* the caret goes after it.
|
|
19
|
+
*
|
|
20
|
+
* The word's field and its colon stay, and a leading `-`, so a value chosen
|
|
21
|
+
* for `-status:op` gives `-status:open`. A space follows, ready for the next
|
|
22
|
+
* term, unless the suggestion ends with a colon, which is a key whose value
|
|
23
|
+
* comes next.
|
|
24
|
+
*/
|
|
25
|
+
export declare const completeTerm: (text: string, caret: number, value: string) => {
|
|
26
|
+
caret: number;
|
|
27
|
+
text: string;
|
|
28
|
+
};
|
|
29
|
+
/** One suggestion a `source` gives: its words, or its value with the words a reader sees. */
|
|
30
|
+
export type FilterSuggestion = string | {
|
|
31
|
+
/** Shown, and skipped by the arrow keys. */
|
|
32
|
+
disabled?: boolean | undefined;
|
|
33
|
+
/** What a reader sees. The value where unset. */
|
|
34
|
+
label?: string | undefined;
|
|
35
|
+
/** What is written over the term when the suggestion is chosen. */
|
|
36
|
+
value: string;
|
|
37
|
+
};
|
|
38
|
+
/** What a `source` is asked for. */
|
|
39
|
+
export interface FilterSourceRequest {
|
|
40
|
+
/**
|
|
41
|
+
* The field the term is a value of, where the caret is after `field:`, as
|
|
42
|
+
* `currentField` reads it. Empty while a key is being typed.
|
|
43
|
+
*/
|
|
44
|
+
field: string;
|
|
45
|
+
/** Aborted once a later keystroke asks again, or the field leaves the page. */
|
|
46
|
+
signal: AbortSignal;
|
|
47
|
+
/** The term at the caret, after its colon, as `currentTerm` reads it. */
|
|
48
|
+
term: string;
|
|
49
|
+
/** The whole text. */
|
|
50
|
+
text: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Where the suggestions come from: given the term at the caret, the
|
|
54
|
+
* suggestions for it, or a promise of them.
|
|
55
|
+
*/
|
|
56
|
+
export type FilterSource = (request: FilterSourceRequest) => readonly FilterSuggestion[] | PromiseLike<readonly FilterSuggestion[]>;
|
|
10
57
|
/** Whether a suggestion's words hold the term, ignoring case and accents. */
|
|
11
58
|
export declare const suggests: (words: string, term: string) => boolean;
|
|
12
59
|
/** What a token says about the filter it shows, as `tokenEntry` reads it. */
|
|
@@ -4,6 +4,23 @@ export const currentTerm = (text, caret = text.length) => {
|
|
|
4
4
|
const colon = word.indexOf(':');
|
|
5
5
|
return (colon === -1 ? word : word.slice(colon + 1)).replace(/^-/u, '');
|
|
6
6
|
};
|
|
7
|
+
export const currentField = (text, caret = text.length) => {
|
|
8
|
+
const before = text.slice(0, caret);
|
|
9
|
+
const word = before.slice(before.search(/\S*$/u));
|
|
10
|
+
const colon = word.indexOf(':');
|
|
11
|
+
return colon === -1 ? '' : word.slice(0, colon).replace(/^-/u, '');
|
|
12
|
+
};
|
|
13
|
+
export const completeTerm = (text, caret, value) => {
|
|
14
|
+
const before = text.slice(0, caret);
|
|
15
|
+
const start = before.search(/\S*$/u);
|
|
16
|
+
const word = before.slice(start);
|
|
17
|
+
const colon = word.indexOf(':');
|
|
18
|
+
const kept = colon === -1 ? (word.startsWith('-') ? '-' : '') : word.slice(0, colon + 1);
|
|
19
|
+
const after = text.slice(caret).replace(/^\S*/u, '');
|
|
20
|
+
const space = value.endsWith(':') ? '' : ' ';
|
|
21
|
+
const written = `${text.slice(0, start)}${kept}${value}${space}`;
|
|
22
|
+
return { caret: written.length, text: `${written}${space ? after.replace(/^\s+/u, '') : after}` };
|
|
23
|
+
};
|
|
7
24
|
export const suggests = (words, term) => {
|
|
8
25
|
const plain = (text) => text.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
|
|
9
26
|
return term === '' || plain(words).includes(plain(term));
|
|
@@ -20,6 +20,23 @@ declare const HelixHeadlessGraph_base: (new (...args: any[]) => import("@fusion.
|
|
|
20
20
|
* Set `depth` to show the local graph: only the notes within that many links
|
|
21
21
|
* of the selected one.
|
|
22
22
|
*
|
|
23
|
+
* The search field is a combobox over the list. Down and Up move through the
|
|
24
|
+
* notes while the focus stays in the field, and Enter selects one.
|
|
25
|
+
*
|
|
26
|
+
* **Walk the links with Alt and the arrow keys**, in the list or in the
|
|
27
|
+
* search field once its keys are on a note. The note the keys are on is
|
|
28
|
+
* marked in the drawing.
|
|
29
|
+
*
|
|
30
|
+
* | Key | What it does |
|
|
31
|
+
* |---|---|
|
|
32
|
+
* | Alt+Right | Follows a link, to the first note the one the keys are on links to or from. |
|
|
33
|
+
* | Alt+Down, Alt+Up | Moves to the next or the previous note linked to the one the step came from, wrapping. |
|
|
34
|
+
* | Alt+Left | Goes back along the links walked. |
|
|
35
|
+
*
|
|
36
|
+
* Right and Left swap on a right-to-left page. Each step says the note, how it
|
|
37
|
+
* stands to the note it came from, and its place among that note's links,
|
|
38
|
+
* such as "Hiring plan. Linked from Roadmap. 2 of 3". Enter selects the note.
|
|
39
|
+
*
|
|
23
40
|
* Set `legend` to name the groups over the drawing, each a button that shows
|
|
24
41
|
* or hides its notes. `hiddenGroups` holds the groups turned off, and the
|
|
25
42
|
* list holds only what the drawing shows. The legend is a toolbar and one tab
|
|
@@ -59,7 +76,7 @@ declare const HelixHeadlessGraph_base: (new (...args: any[]) => import("@fusion.
|
|
|
59
76
|
* @event {HelixGraphFilterEvent} helix-graph-filter - A group in the legend was pressed, to show or hide its notes. The detail's `hidden` is every group that would be hidden. Cancelable: cancel it to keep the groups as they were, or to set `hiddenGroups` yourself.
|
|
60
77
|
* @event {HelixGraphLayoutEvent} helix-graph-layout - A layout settled, with where it put each note, sent by `laidOut()`. Keep the positions and pass them back as each note's `x` and `y`, and `z` where a renderer draws in three dimensions.
|
|
61
78
|
* @event {HelixGraphPanEvent} helix-graph-pan - A pan button or arrow key was pressed. `connectGraph` moves the drawing, then calls `moved()`.
|
|
62
|
-
* @event {HelixGraphPartChangeEvent} helix-graph-part-change - The notes, the links, the selection, the depth or the gestures changed. Used by `connectGraph`; pages do not need to listen for it.
|
|
79
|
+
* @event {HelixGraphPartChangeEvent} helix-graph-part-change - The notes, the links, the selection, the depth, the note the keys are on, or the gestures changed. Used by `connectGraph`; pages do not need to listen for it.
|
|
63
80
|
* @event {HelixGraphReadyEvent} helix-graph-ready - The graph has been drawn whole, laid out and settled. Sent each time `drawn()` says it is drawn after it was not.
|
|
64
81
|
* @event {HelixGraphSignalEvent} helix-graph-signal - Something passed between two notes, sent by `signal()`. Used by `connectGraph`; pages do not need to listen for it.
|
|
65
82
|
* @event {HelixGraphZoomEvent} helix-graph-zoom - A zoom button or key was pressed. `connectGraph` zooms the drawing, then calls `moved()`.
|
|
@@ -166,6 +183,12 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
166
183
|
legend: boolean;
|
|
167
184
|
/** The legend's name, read by screen readers. */
|
|
168
185
|
legendLabel: string;
|
|
186
|
+
/**
|
|
187
|
+
* What screen readers hear when a walk along the links reaches a note that
|
|
188
|
+
* has no link to follow, or goes back to a note: `{note}` for its name and
|
|
189
|
+
* `{count}` for how many notes the list holds that it links to or from.
|
|
190
|
+
*/
|
|
191
|
+
linkCountLabel: string;
|
|
169
192
|
/** How a note linked both to and from the selected one is described, with `{note}`. */
|
|
170
193
|
linkedBothLabel: string;
|
|
171
194
|
/** How a note the selected one links to is described, with `{note}`, the selected note's name. */
|
|
@@ -231,6 +254,13 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
231
254
|
twoFingerHintLabel: string;
|
|
232
255
|
/** The line shown while the graph, with `gestures="activate"`, has the gestures. */
|
|
233
256
|
usingLabel: string;
|
|
257
|
+
/**
|
|
258
|
+
* What screen readers hear when a walk along the links moves to a note:
|
|
259
|
+
* `{note}` for its name, `{relation}` for how it stands to the note the
|
|
260
|
+
* step came from, such as "Linked from Roadmap", and `{index}` and `{count}`
|
|
261
|
+
* for its place among that note's links.
|
|
262
|
+
*/
|
|
263
|
+
walkLabel: string;
|
|
234
264
|
/**
|
|
235
265
|
* The hint shown when the wheel scrolls past the graph with
|
|
236
266
|
* `gestures="cooperative"`. `{key}` is ⌘ on a Mac and Ctrl elsewhere.
|
|
@@ -246,6 +276,22 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
246
276
|
private cursor;
|
|
247
277
|
/** Whether the search and the list are open over the drawing, with `no-panel`. */
|
|
248
278
|
private panelOpen;
|
|
279
|
+
/**
|
|
280
|
+
* Whether the search field's arrow keys have moved onto a note, which the
|
|
281
|
+
* field then names as its `aria-activedescendant` while the focus stays in
|
|
282
|
+
* it. Typing, Left, Right, Escape, and leaving the field let it go.
|
|
283
|
+
*/
|
|
284
|
+
private fieldKeys;
|
|
285
|
+
/** Whether the list has the focus, so the drawing marks the note its keys are on. */
|
|
286
|
+
private listKeys;
|
|
287
|
+
/**
|
|
288
|
+
* The notes a walk along the links stepped from, by `id`, the last the one
|
|
289
|
+
* the note the keys are on was reached from. Empty when the keys moved some
|
|
290
|
+
* other way.
|
|
291
|
+
*/
|
|
292
|
+
private trail;
|
|
293
|
+
/** The note the drawing last heard the keys are on, so it hears each move once. */
|
|
294
|
+
private keyedSent;
|
|
249
295
|
/** Which notes link to and from each, read again when `links` changes. */
|
|
250
296
|
private adjacency;
|
|
251
297
|
/** The notes in the order of their names, read again when `nodes` changes. */
|
|
@@ -292,6 +338,18 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
292
338
|
* within `depth` of the selected note, or `undefined` for every note.
|
|
293
339
|
*/
|
|
294
340
|
get shown(): ReadonlySet<string> | undefined;
|
|
341
|
+
/**
|
|
342
|
+
* @internal The note the keys are on, while the list or the search field
|
|
343
|
+
* has them, which `connectGraph` marks in the drawing as it marks the note
|
|
344
|
+
* under the pointer. `walked` is true where a walk along the links reached
|
|
345
|
+
* it, which brings it into view.
|
|
346
|
+
*/
|
|
347
|
+
get keyed(): {
|
|
348
|
+
id: string;
|
|
349
|
+
walked: boolean;
|
|
350
|
+
} | undefined;
|
|
351
|
+
/** The `id` of the note the keys are on, or `undefined` where neither the list nor the field has them. */
|
|
352
|
+
private keyedId;
|
|
295
353
|
/** Whether the graph has been drawn whole. For code that runs after `helix-graph-ready`. */
|
|
296
354
|
get ready(): boolean;
|
|
297
355
|
/**
|
|
@@ -404,6 +462,33 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
404
462
|
* together, and a repeated letter steps through the names that start with it.
|
|
405
463
|
*/
|
|
406
464
|
private typeahead;
|
|
465
|
+
/**
|
|
466
|
+
* The rows of the notes `id` links to or is linked from, in the list's
|
|
467
|
+
* order, which is the order of their names. Only the notes the list holds:
|
|
468
|
+
* a search, the legend, or `depth` leaves the others out.
|
|
469
|
+
*/
|
|
470
|
+
private linkedRows;
|
|
471
|
+
/** How the note `id` stands to the note `from`, in the words the list uses for the selection. */
|
|
472
|
+
private relationFrom;
|
|
473
|
+
/**
|
|
474
|
+
* A walk along the links from the note the keys are on, with Alt and the
|
|
475
|
+
* arrow keys, so a reader follows the lines of the drawing and not only
|
|
476
|
+
* the order of the list. **The same four keys a tree takes**, with Alt,
|
|
477
|
+
* since the list's own arrows already move through it:
|
|
478
|
+
*
|
|
479
|
+
* - Alt+Right follows a link, to the first note the one the keys are on
|
|
480
|
+
* links to or from.
|
|
481
|
+
* - Alt+Down and Alt+Up move to the next and the previous note linked to
|
|
482
|
+
* the one the step came from, wrapping.
|
|
483
|
+
* - Alt+Left goes back along the links walked.
|
|
484
|
+
*
|
|
485
|
+
* Right and Left swap on a right-to-left page. Each step says the note, how
|
|
486
|
+
* it stands to the note it was reached from, and its place among that
|
|
487
|
+
* note's links. Returns whether the key was one of the walk's.
|
|
488
|
+
*/
|
|
489
|
+
private walk;
|
|
490
|
+
/** Says the note at row `at` and how many notes in the list it links to or from. */
|
|
491
|
+
private sayLinks;
|
|
407
492
|
/**
|
|
408
493
|
* How many rows Page Down and Page Up move: the rows that fit in the list's
|
|
409
494
|
* box, or in the window where the list does not scroll, less one, so the
|
|
@@ -412,10 +497,22 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
412
497
|
private pageRows;
|
|
413
498
|
/**
|
|
414
499
|
* The search field's keys, which belong to the field and never pan the
|
|
415
|
-
* graph.
|
|
416
|
-
*
|
|
500
|
+
* graph.
|
|
501
|
+
*
|
|
502
|
+
* **The field is a combobox over the list**, as React Aria's Autocomplete
|
|
503
|
+
* and the APG combobox are: the focus stays in the field, and Down and Up
|
|
504
|
+
* move the keys through the notes, which the field names as its
|
|
505
|
+
* `aria-activedescendant`. Down starts on the note the keys were last on,
|
|
506
|
+
* the first after a search, and Up on the last. Page Down and Page Up move
|
|
507
|
+
* a page, Enter selects, Alt and the arrows walk the links as in the list,
|
|
508
|
+
* and Left, Right, Home and End go back to the text.
|
|
509
|
+
*
|
|
510
|
+
* Escape lets the note go, then empties a search that has words in it, and
|
|
511
|
+
* passes through an empty one to whatever the page does with it.
|
|
417
512
|
*/
|
|
418
513
|
private readonly handleSearchKeydown;
|
|
514
|
+
/** Leaving the field lets go of the note its keys were on. */
|
|
515
|
+
private readonly handleSearchBlur;
|
|
419
516
|
/**
|
|
420
517
|
* Opens the search and the list over the drawing, or closes them, with
|
|
421
518
|
* `no-panel`. **Opened, the search field takes the focus**, since finding
|
|
@@ -430,6 +527,7 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
|
|
|
430
527
|
* the controls after zoom, where the frame leaves them out with `no-panel`.
|
|
431
528
|
*/
|
|
432
529
|
protected renderMoreControls(): TemplateResult | typeof nothing;
|
|
530
|
+
/** Typing is a new search, read from its top, with no note under the keys until Down. */
|
|
433
531
|
private readonly handleSearch;
|
|
434
532
|
/**
|
|
435
533
|
* The mark in the search field, behind a seam: a slot for the page, and a
|
|
@@ -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 { auditName, customElement, focusItemOf, formatWords, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, KeyboardNavigationController, MixinControllerActive, MixinViewport, scrollItemIntoView, scrollRoom, Typeahead, typeaheadBy, VirtualizeController, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { auditName, customElement, focusItemOf, formatWords, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, isRtl, KeyboardNavigationController, MixinControllerActive, MixinViewport, scrollItemIntoView, scrollRoom, Typeahead, typeaheadBy, VirtualizeController, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { list, magnifyingGlass } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -23,6 +23,7 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
23
23
|
this.label = '';
|
|
24
24
|
this.legend = false;
|
|
25
25
|
this.legendLabel = 'Groups';
|
|
26
|
+
this.linkCountLabel = '{note}. {count, plural, =0 {No links} one {# link} other {# links}}';
|
|
26
27
|
this.linkedBothLabel = 'Linked both ways with {note}';
|
|
27
28
|
this.linkedFromLabel = 'Linked from {note}';
|
|
28
29
|
this.links = [];
|
|
@@ -42,12 +43,16 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
42
43
|
this.selectionLabel = '{note}, selected. {count}';
|
|
43
44
|
this.twoFingerHintLabel = 'Use two fingers to move the graph';
|
|
44
45
|
this.usingLabel = 'Using the graph. Press Escape to scroll the page again.';
|
|
46
|
+
this.walkLabel = '{note}. {relation}. {index} of {count}';
|
|
45
47
|
this.wheelHintLabel = 'Use {key} and scroll to zoom the graph';
|
|
46
48
|
this.zoomedInLabel = 'Zoomed in';
|
|
47
49
|
this.zoomedOutLabel = 'Zoomed out';
|
|
48
50
|
this.query = '';
|
|
49
51
|
this.cursor = 0;
|
|
50
52
|
this.panelOpen = false;
|
|
53
|
+
this.fieldKeys = false;
|
|
54
|
+
this.listKeys = false;
|
|
55
|
+
this.trail = [];
|
|
51
56
|
this.adjacency = new Map();
|
|
52
57
|
this.sorted = [];
|
|
53
58
|
this.entries = [];
|
|
@@ -74,6 +79,9 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
74
79
|
})),
|
|
75
80
|
});
|
|
76
81
|
this.handleListKeydown = (event) => {
|
|
82
|
+
if (this.walk(event)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
77
85
|
const moves = {
|
|
78
86
|
ArrowDown: this.cursor + 1,
|
|
79
87
|
ArrowUp: this.cursor - 1,
|
|
@@ -102,19 +110,51 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
102
110
|
};
|
|
103
111
|
this.typed = new Typeahead();
|
|
104
112
|
this.handleSearchKeydown = (event) => {
|
|
105
|
-
if (event.
|
|
113
|
+
if (event.isComposing) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (this.fieldKeys && this.walk(event)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const last = this.entries.length - 1;
|
|
120
|
+
const moves = this.fieldKeys
|
|
121
|
+
? {
|
|
122
|
+
ArrowDown: this.cursor + 1,
|
|
123
|
+
ArrowUp: this.cursor - 1,
|
|
124
|
+
PageDown: this.cursor + this.pageRows(),
|
|
125
|
+
PageUp: this.cursor - this.pageRows(),
|
|
126
|
+
}
|
|
127
|
+
: { ArrowDown: this.cursor, ArrowUp: last };
|
|
128
|
+
if (event.key in moves && !event.altKey && last >= 0) {
|
|
106
129
|
event.preventDefault();
|
|
107
|
-
this.
|
|
130
|
+
this.fieldKeys = true;
|
|
131
|
+
this.moveTo(moves[event.key]);
|
|
132
|
+
}
|
|
133
|
+
else if (event.key === 'Enter' && this.fieldKeys) {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
this.choose(this.cursor);
|
|
108
136
|
}
|
|
109
|
-
if (event.key === 'Escape') {
|
|
110
|
-
if (this.
|
|
137
|
+
else if (event.key === 'Escape') {
|
|
138
|
+
if (this.fieldKeys) {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
this.fieldKeys = false;
|
|
141
|
+
}
|
|
142
|
+
else if (this.query === '') {
|
|
111
143
|
return;
|
|
112
144
|
}
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
else {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
this.query = '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (['ArrowLeft', 'ArrowRight', 'End', 'Home'].includes(event.key)) {
|
|
151
|
+
this.fieldKeys = false;
|
|
115
152
|
}
|
|
116
153
|
event.stopPropagation();
|
|
117
154
|
};
|
|
155
|
+
this.handleSearchBlur = () => {
|
|
156
|
+
this.fieldKeys = false;
|
|
157
|
+
};
|
|
118
158
|
this.handlePanelKeydown = (event) => {
|
|
119
159
|
if (event.key === 'Escape' && this.noPanel && this.panelOpen) {
|
|
120
160
|
event.preventDefault();
|
|
@@ -124,12 +164,20 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
124
164
|
};
|
|
125
165
|
this.handleSearch = (event) => {
|
|
126
166
|
this.query = event.target.value;
|
|
167
|
+
this.fieldKeys = false;
|
|
127
168
|
};
|
|
128
169
|
}
|
|
129
170
|
static { this.words = 'helix-graph'; }
|
|
130
171
|
get shown() {
|
|
131
172
|
return this.kept;
|
|
132
173
|
}
|
|
174
|
+
get keyed() {
|
|
175
|
+
const id = this.keyedId();
|
|
176
|
+
return id === undefined ? undefined : { id, walked: this.trail.length > 0 };
|
|
177
|
+
}
|
|
178
|
+
keyedId() {
|
|
179
|
+
return this.listKeys || this.fieldKeys ? this.entries[this.cursor]?.node.id : undefined;
|
|
180
|
+
}
|
|
133
181
|
get ready() {
|
|
134
182
|
return this.whole;
|
|
135
183
|
}
|
|
@@ -204,6 +252,7 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
204
252
|
if (['active', 'depth', 'hiddenGroups', 'query'].some((name) => changed.has(name)) ||
|
|
205
253
|
!this.hasUpdated) {
|
|
206
254
|
this.cursor = 0;
|
|
255
|
+
this.trail = [];
|
|
207
256
|
if (list) {
|
|
208
257
|
list.scrollTop = 0;
|
|
209
258
|
}
|
|
@@ -230,10 +279,16 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
230
279
|
updated(changed) {
|
|
231
280
|
super.updated(changed);
|
|
232
281
|
this.virtualize.measure();
|
|
282
|
+
const keyed = this.keyedId();
|
|
283
|
+
const rekeyed = keyed !== this.keyedSent;
|
|
284
|
+
this.keyedSent = keyed;
|
|
233
285
|
if (changed.has('nodes') || changed.has('links')) {
|
|
234
286
|
this.dispatchEvent(new HelixGraphPartChangeEvent({ kind: 'data' }));
|
|
235
287
|
}
|
|
236
|
-
else if (changed.has('active') ||
|
|
288
|
+
else if (changed.has('active') ||
|
|
289
|
+
changed.has('depth') ||
|
|
290
|
+
changed.has('hiddenGroups') ||
|
|
291
|
+
rekeyed) {
|
|
237
292
|
this.dispatchEvent(new HelixGraphPartChangeEvent({ kind: 'focus' }));
|
|
238
293
|
}
|
|
239
294
|
}
|
|
@@ -316,7 +371,10 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
316
371
|
this.requestActive(entry.node.id);
|
|
317
372
|
}
|
|
318
373
|
}
|
|
319
|
-
moveTo(index) {
|
|
374
|
+
moveTo(index, walking = false) {
|
|
375
|
+
if (!walking) {
|
|
376
|
+
this.trail = [];
|
|
377
|
+
}
|
|
320
378
|
const last = this.entries.length - 1;
|
|
321
379
|
this.cursor = Math.min(Math.max(index, 0), Math.max(last, 0));
|
|
322
380
|
void this.updateComplete.then(() => this.revealCursor());
|
|
@@ -352,6 +410,99 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
352
410
|
this.moveTo(at);
|
|
353
411
|
}
|
|
354
412
|
}
|
|
413
|
+
linkedRows(id) {
|
|
414
|
+
const near = this.adjacency.get(id);
|
|
415
|
+
if (!near) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const rows = [];
|
|
419
|
+
for (const [index, entry] of this.entries.entries()) {
|
|
420
|
+
const other = entry.node.id;
|
|
421
|
+
if (other !== id && (near.to.has(other) || near.from.has(other))) {
|
|
422
|
+
rows.push(index);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return rows;
|
|
426
|
+
}
|
|
427
|
+
relationFrom(from, id) {
|
|
428
|
+
const near = this.adjacency.get(from);
|
|
429
|
+
const origin = this.nodes.find((node) => node.id === from);
|
|
430
|
+
const note = origin ? nameOf(origin) : from;
|
|
431
|
+
const to = near?.to.has(id) === true;
|
|
432
|
+
const back = near?.from.has(id) === true;
|
|
433
|
+
return formatWords(this, to && back ? 'linkedBothLabel' : to ? 'linkedFromLabel' : 'linksToLabel', { note });
|
|
434
|
+
}
|
|
435
|
+
walk(event) {
|
|
436
|
+
if (!event.altKey || event.ctrlKey || event.metaKey || this.entries.length === 0) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const flip = isRtl(this);
|
|
440
|
+
const key = event.key === 'ArrowRight'
|
|
441
|
+
? flip
|
|
442
|
+
? 'back'
|
|
443
|
+
: 'follow'
|
|
444
|
+
: event.key === 'ArrowLeft'
|
|
445
|
+
? flip
|
|
446
|
+
? 'follow'
|
|
447
|
+
: 'back'
|
|
448
|
+
: event.key === 'ArrowDown'
|
|
449
|
+
? 'next'
|
|
450
|
+
: event.key === 'ArrowUp'
|
|
451
|
+
? 'previous'
|
|
452
|
+
: undefined;
|
|
453
|
+
if (key === undefined) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
event.stopPropagation();
|
|
458
|
+
const here = this.entries[this.cursor]?.node.id;
|
|
459
|
+
if (here === undefined) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (key === 'back') {
|
|
463
|
+
const back = this.trail.pop();
|
|
464
|
+
const row = back === undefined ? -1 : this.entries.findIndex((entry) => entry.node.id === back);
|
|
465
|
+
const at = row >= 0 ? row : this.cursor;
|
|
466
|
+
this.moveTo(at, true);
|
|
467
|
+
this.sayLinks(at);
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
const from = key === 'follow' || this.trail.length === 0 ? here : this.trail.at(-1);
|
|
471
|
+
const rows = this.linkedRows(from);
|
|
472
|
+
if (rows.length === 0) {
|
|
473
|
+
this.sayLinks(this.cursor);
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
let place;
|
|
477
|
+
if (from === here) {
|
|
478
|
+
place = key === 'previous' ? rows.length - 1 : 0;
|
|
479
|
+
this.trail.push(here);
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
const now = rows.indexOf(this.cursor);
|
|
483
|
+
const by = key === 'previous' ? -1 : 1;
|
|
484
|
+
place = now < 0 ? 0 : (now + by + rows.length) % rows.length;
|
|
485
|
+
}
|
|
486
|
+
const at = rows[place];
|
|
487
|
+
this.moveTo(at, true);
|
|
488
|
+
const entry = this.entries[at];
|
|
489
|
+
this.say(formatWords(this, 'walkLabel', {
|
|
490
|
+
count: rows.length,
|
|
491
|
+
index: place + 1,
|
|
492
|
+
note: nameOf(entry.node),
|
|
493
|
+
relation: this.relationFrom(from, entry.node.id),
|
|
494
|
+
}));
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
sayLinks(at) {
|
|
498
|
+
const entry = this.entries[at];
|
|
499
|
+
if (entry) {
|
|
500
|
+
this.say(formatWords(this, 'linkCountLabel', {
|
|
501
|
+
count: this.linkedRows(entry.node.id).length,
|
|
502
|
+
note: nameOf(entry.node),
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
355
506
|
pageRows() {
|
|
356
507
|
const room = scrollRoom(this.partNamed('list'));
|
|
357
508
|
const tall = this.virtualize.sizeOf(this.cursor) || 32;
|
|
@@ -473,11 +624,17 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
473
624
|
<input
|
|
474
625
|
part="search-field"
|
|
475
626
|
type="text"
|
|
627
|
+
role="combobox"
|
|
476
628
|
autocomplete="off"
|
|
477
629
|
spellcheck="false"
|
|
478
630
|
aria-label=${this.searchLabel}
|
|
631
|
+
aria-autocomplete="list"
|
|
632
|
+
aria-controls="note-list"
|
|
633
|
+
aria-expanded=${this.entries.length > 0 ? 'true' : 'false'}
|
|
634
|
+
aria-activedescendant=${this.fieldKeys ? cursor : nothing}
|
|
479
635
|
placeholder=${this.searchLabel}
|
|
480
636
|
.value=${this.query}
|
|
637
|
+
@blur=${this.handleSearchBlur}
|
|
481
638
|
@input=${this.handleSearch}
|
|
482
639
|
@keydown=${this.handleSearchKeydown}
|
|
483
640
|
/>
|
|
@@ -485,12 +642,20 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
|
|
|
485
642
|
</div>
|
|
486
643
|
<div
|
|
487
644
|
part="list"
|
|
645
|
+
id="note-list"
|
|
488
646
|
role="listbox"
|
|
489
647
|
tabindex="0"
|
|
490
648
|
data-scroller
|
|
491
649
|
aria-label=${this.notesLabel}
|
|
492
650
|
aria-activedescendant=${cursor}
|
|
493
651
|
aria-busy=${this.loading ? 'true' : nothing}
|
|
652
|
+
aria-keyshortcuts="Alt+ArrowRight Alt+ArrowLeft Alt+ArrowDown Alt+ArrowUp"
|
|
653
|
+
@blur=${() => {
|
|
654
|
+
this.listKeys = false;
|
|
655
|
+
}}
|
|
656
|
+
@focus=${() => {
|
|
657
|
+
this.listKeys = true;
|
|
658
|
+
}}
|
|
494
659
|
@keydown=${this.handleListKeydown}
|
|
495
660
|
@scroll=${() => this.virtualize.measure()}
|
|
496
661
|
>
|
|
@@ -548,6 +713,10 @@ __decorate([
|
|
|
548
713
|
words,
|
|
549
714
|
property({ attribute: 'legend-label' })
|
|
550
715
|
], HelixHeadlessGraph.prototype, "legendLabel", void 0);
|
|
716
|
+
__decorate([
|
|
717
|
+
words,
|
|
718
|
+
property({ attribute: 'link-count-label' })
|
|
719
|
+
], HelixHeadlessGraph.prototype, "linkCountLabel", void 0);
|
|
551
720
|
__decorate([
|
|
552
721
|
words,
|
|
553
722
|
property({ attribute: 'linked-both-label' })
|
|
@@ -623,6 +792,10 @@ __decorate([
|
|
|
623
792
|
words,
|
|
624
793
|
property({ attribute: 'using-label' })
|
|
625
794
|
], HelixHeadlessGraph.prototype, "usingLabel", void 0);
|
|
795
|
+
__decorate([
|
|
796
|
+
words,
|
|
797
|
+
property({ attribute: 'walk-label' })
|
|
798
|
+
], HelixHeadlessGraph.prototype, "walkLabel", void 0);
|
|
626
799
|
__decorate([
|
|
627
800
|
words,
|
|
628
801
|
property({ attribute: 'wheel-hint-label' })
|
|
@@ -644,6 +817,12 @@ __decorate([
|
|
|
644
817
|
__decorate([
|
|
645
818
|
state()
|
|
646
819
|
], HelixHeadlessGraph.prototype, "panelOpen", void 0);
|
|
820
|
+
__decorate([
|
|
821
|
+
state()
|
|
822
|
+
], HelixHeadlessGraph.prototype, "fieldKeys", void 0);
|
|
823
|
+
__decorate([
|
|
824
|
+
state()
|
|
825
|
+
], HelixHeadlessGraph.prototype, "listKeys", void 0);
|
|
647
826
|
__decorate([
|
|
648
827
|
state()
|
|
649
828
|
], HelixHeadlessGraph.prototype, "said", void 0);
|
|
@@ -131,9 +131,14 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
|
|
|
131
131
|
* **Every sentence a control raises in the browser's bubble is a property**,
|
|
132
132
|
* named for the validity flag it reports, so a form in another language
|
|
133
133
|
* speaks it. Unset is the sentence registered for the page's language, then
|
|
134
|
-
* the English. `{
|
|
135
|
-
* `
|
|
136
|
-
*
|
|
134
|
+
* the English. `{example}` is a time written as the picker writes one,
|
|
135
|
+
* `2:30 PM` in `en-US` and `14:30` in `de-DE`, `{shape}` is `HH:mm` or
|
|
136
|
+
* `HH:mm:ss`, and `{mode}` is `single`, `range` or `multiple`, so each can
|
|
137
|
+
* have its own sentence.
|
|
138
|
+
*
|
|
139
|
+
* **The default names a time as the picker shows one.** It said "Enter a
|
|
140
|
+
* time as HH:mm." to a reader whose list showed `2:30 PM`, and a pattern
|
|
141
|
+
* token is words for a developer. Time Input says the same.
|
|
137
142
|
*/
|
|
138
143
|
badInputMessage: string;
|
|
139
144
|
/**
|
|
@@ -14,7 +14,7 @@ import '../../option/root/index.js';
|
|
|
14
14
|
let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerFeedback(MixinControllerValue(HelixFormElement)) {
|
|
15
15
|
constructor() {
|
|
16
16
|
super(...arguments);
|
|
17
|
-
this.badInputMessage = '{mode, select, single {Enter a time as {
|
|
17
|
+
this.badInputMessage = '{mode, select, single {Enter a time such as {example}.} other {Enter times such as {example}, separated by commas.}}';
|
|
18
18
|
this.disableTimes = '';
|
|
19
19
|
this.emptyText = 'No times to choose from';
|
|
20
20
|
this.hint = '';
|
|
@@ -300,6 +300,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
|
|
|
300
300
|
return [
|
|
301
301
|
{ badInput: true },
|
|
302
302
|
formatWords(this, 'badInputMessage', {
|
|
303
|
+
example: writeClock(this.seconds ? '14:30:15' : '14:30', this.clock, this.seconds),
|
|
303
304
|
mode: this.mode ?? 'single',
|
|
304
305
|
shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
|
|
305
306
|
}),
|