@sveltia/ui 0.62.0 → 0.63.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/dist/components/button/button.svelte +6 -1
- package/dist/components/resizable-pane/resizable-handle.svelte +12 -3
- package/dist/components/select/select-tags.svelte +8 -1
- package/dist/components/text-editor/constants.d.ts +0 -1
- package/dist/components/text-editor/constants.js +1 -36
- package/dist/components/text-editor/core.js +24 -31
- package/dist/components/text-editor/lexical-root.svelte +0 -68
- package/dist/components/text-editor/shiki/cache.d.ts +3 -0
- package/dist/components/text-editor/shiki/cache.js +121 -0
- package/dist/components/text-editor/shiki/engine-entry.d.ts +2 -0
- package/dist/components/text-editor/shiki/engine-entry.js +20 -0
- package/dist/components/text-editor/shiki/facade.d.ts +16 -0
- package/dist/components/text-editor/shiki/facade.js +452 -0
- package/dist/components/text-editor/shiki/generated.d.ts +28 -0
- package/dist/components/text-editor/shiki/generated.js +25 -0
- package/dist/components/text-editor/shiki/highlighter.d.ts +11 -0
- package/dist/components/text-editor/shiki/highlighter.js +477 -0
- package/dist/components/text-editor/shiki/loader.d.ts +6 -0
- package/dist/components/text-editor/shiki/loader.js +102 -0
- package/dist/components/text-editor/shiki/theme.d.ts +12 -0
- package/dist/components/text-editor/shiki/theme.js +86 -0
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -34
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +1 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/services/group.svelte.d.ts +2 -1
- package/dist/services/group.svelte.js +183 -50
- package/dist/services/tree.svelte.d.ts +8 -6
- package/dist/services/tree.svelte.js +104 -45
- package/dist/shiki-engine.js +152 -0
- package/dist/typedefs.d.ts +52 -0
- package/dist/typedefs.js +29 -0
- package/package.json +13 -11
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
// @see https://github.com/remix-run/remix/discussions/8182
|
|
4
|
-
import 'prismjs';
|
|
5
|
-
|
|
6
|
-
import { $isCodeNode as isCodeNode } from '@lexical/code';
|
|
2
|
+
import { $isCodeNode as isCodeNode } from '@lexical/code-core';
|
|
7
3
|
import { _ } from '@sveltia/i18n';
|
|
8
4
|
import { $getNodeByKey as getNodeByKey, $getRoot as getRoot } from 'lexical';
|
|
9
|
-
import prismComponents from 'prismjs/components';
|
|
10
5
|
import { getContext } from 'svelte';
|
|
11
6
|
import Option from '../../listbox/option.svelte';
|
|
12
7
|
import Select from '../../select/select.svelte';
|
|
13
8
|
import { focusEditor, loadCodeHighlighter } from '../core.js';
|
|
9
|
+
import { LANGUAGES } from '../shiki/generated.js';
|
|
14
10
|
|
|
15
11
|
/**
|
|
16
12
|
* @import { TextEditorStore } from '../../../typedefs';
|
|
@@ -29,23 +25,11 @@
|
|
|
29
25
|
} = $props();
|
|
30
26
|
|
|
31
27
|
/** @type {{ key: string, label: string, aliases: string[] }[]} */
|
|
32
|
-
const codeLanguages =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (alias && !aliasTitles) {
|
|
39
|
-
aliases = Array.isArray(alias) ? alias : [alias];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return [
|
|
43
|
-
{ key, label, aliases },
|
|
44
|
-
...Object.entries(aliasTitles ?? {}).map(([k, v]) => ({ key: k, label: v, aliases: [] })),
|
|
45
|
-
];
|
|
46
|
-
})
|
|
47
|
-
.flat(1)
|
|
48
|
-
.sort((a, b) => a.label.localeCompare(b.label));
|
|
28
|
+
const codeLanguages = LANGUAGES.map(({ id, name, aliases = [] }) => ({
|
|
29
|
+
key: id,
|
|
30
|
+
label: name,
|
|
31
|
+
aliases,
|
|
32
|
+
}));
|
|
49
33
|
|
|
50
34
|
/** @type {TextEditorStore} */
|
|
51
35
|
const editorStore = getContext('editorStore');
|
|
@@ -77,20 +61,26 @@
|
|
|
77
61
|
}
|
|
78
62
|
|
|
79
63
|
await focusEditor(editorStore.editor);
|
|
64
|
+
await loadCodeHighlighter(lang);
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
editorStore.editor.update(() => {
|
|
67
|
+
// Resolve the target the same way the effect above does. A code editor has exactly one code
|
|
68
|
+
// block, and its `blockNodeKey` is still unset the first time the switcher is used, before
|
|
69
|
+
// the editor has ever been focused.
|
|
70
|
+
// https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx#L713
|
|
71
|
+
const { blockNodeKey } = editorStore.selection;
|
|
83
72
|
|
|
84
|
-
editorStore.
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
const node = editorStore.config.isCodeEditor
|
|
74
|
+
? getRoot().getChildren()[0]
|
|
75
|
+
: blockNodeKey
|
|
76
|
+
? getNodeByKey(blockNodeKey)
|
|
77
|
+
: null;
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
79
|
+
if (isCodeNode(node)) {
|
|
80
|
+
node.setLanguage(lang);
|
|
81
|
+
selectedLanguage = lang;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
94
84
|
}}
|
|
95
85
|
>
|
|
96
86
|
<Option label={_('_sui.text_editor.plain_text')} value="plain" dir="ltr" />
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
// @see https://github.com/remix-run/remix/discussions/8182
|
|
4
|
-
import 'prismjs';
|
|
5
|
-
|
|
6
|
-
import { $createCodeNode as createCodeNode } from '@lexical/code';
|
|
2
|
+
import { $createCodeNode as createCodeNode } from '@lexical/code-core';
|
|
7
3
|
import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list';
|
|
8
4
|
import {
|
|
9
5
|
$createHeadingNode as createHeadingNode,
|
package/dist/index.d.ts
CHANGED
|
@@ -81,5 +81,9 @@ export { default as Group } from "./components/util/group.svelte";
|
|
|
81
81
|
export { default as Modal } from "./components/util/modal.svelte";
|
|
82
82
|
export { default as Placeholder } from "./components/util/placeholder.svelte";
|
|
83
83
|
export { default as VisibilityObserver } from "./components/util/visibility-observer.svelte";
|
|
84
|
+
export { loadCodeHighlighter } from "./components/text-editor/core.js";
|
|
85
|
+
export { setCodeHighlighterCacheEnabled } from "./components/text-editor/shiki/cache.js";
|
|
86
|
+
export { highlightCodeToHTML } from "./components/text-editor/shiki/facade.js";
|
|
87
|
+
export { setCodeHighlighterLoaders } from "./components/text-editor/shiki/loader.js";
|
|
84
88
|
export * from "./typedefs.js";
|
|
85
89
|
export { initLocales, strings } from "./services/i18n.js";
|
package/dist/index.js
CHANGED
|
@@ -81,6 +81,10 @@ export { default as Group } from './components/util/group.svelte';
|
|
|
81
81
|
export { default as Modal } from './components/util/modal.svelte';
|
|
82
82
|
export { default as Placeholder } from './components/util/placeholder.svelte';
|
|
83
83
|
export { default as VisibilityObserver } from './components/util/visibility-observer.svelte';
|
|
84
|
+
export { loadCodeHighlighter } from './components/text-editor/core.js';
|
|
85
|
+
export { setCodeHighlighterCacheEnabled } from './components/text-editor/shiki/cache.js';
|
|
86
|
+
export { highlightCodeToHTML } from './components/text-editor/shiki/facade.js';
|
|
87
|
+
export { setCodeHighlighterLoaders } from './components/text-editor/shiki/loader.js';
|
|
84
88
|
export { initLocales, strings } from './services/i18n.js';
|
|
85
89
|
|
|
86
90
|
// eslint-disable-next-line import/export
|
|
@@ -8,7 +8,6 @@ export class Group {
|
|
|
8
8
|
* @param {HTMLElement} parent Parent element.
|
|
9
9
|
* @param {object} [options] Options.
|
|
10
10
|
* @param {boolean} [options.clickToSelect] Whether to select an item by clicking on it.
|
|
11
|
-
* @todo Check for added elements probably with `MutationObserver`.
|
|
12
11
|
*/
|
|
13
12
|
constructor(parent: HTMLElement, { clickToSelect }?: {
|
|
14
13
|
clickToSelect?: boolean | undefined;
|
|
@@ -46,6 +45,7 @@ export class Group {
|
|
|
46
45
|
* @type {'selected' | 'first'}
|
|
47
46
|
*/
|
|
48
47
|
rovingTabStop: "selected" | "first";
|
|
48
|
+
observer: MutationObserver;
|
|
49
49
|
/**
|
|
50
50
|
* Activate the members.
|
|
51
51
|
*/
|
|
@@ -152,6 +152,7 @@ export class Group {
|
|
|
152
152
|
onUpdate({ searchTerms }: {
|
|
153
153
|
searchTerms: string;
|
|
154
154
|
}): void;
|
|
155
|
+
#private;
|
|
155
156
|
}
|
|
156
157
|
export function activateGroup(paramsOrGetter?: object | (() => object) | undefined): Attachment;
|
|
157
158
|
import type { Attachment } from 'svelte/attachments';
|
|
@@ -105,19 +105,45 @@ const config = {
|
|
|
105
105
|
const FOCUSABLE_SELECTOR = 'a[href], button, input, select, textarea, summary, [tabindex]';
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* sequence, are not accounted for; they’re discouraged and absent from
|
|
108
|
+
* Move focus to the tab stop next to the given element in document order. Positive `tabindex`
|
|
109
|
+
* values, which reorder the sequence, are not accounted for; they’re discouraged and absent from
|
|
110
|
+
* this library.
|
|
111
|
+
*
|
|
112
|
+
* The candidates are narrowed by attribute alone before any of them is measured, and the measuring
|
|
113
|
+
* — `getClientRects()`, which forces the browser to lay the page out — then walks outward from the
|
|
114
|
+
* element and stops at the first candidate that is actually rendered. Measuring every focusable
|
|
115
|
+
* element in the document up front is what a menu sitting on a large page cannot afford.
|
|
110
116
|
* @internal
|
|
111
|
-
* @
|
|
117
|
+
* @param {HTMLElement} element Element to start from.
|
|
118
|
+
* @param {boolean} backwards Whether to move to the previous tab stop rather than the next.
|
|
112
119
|
*/
|
|
113
|
-
const
|
|
114
|
-
/** @type {HTMLElement[]} */ ([
|
|
115
|
-
(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const focusAdjacentTabStop = (element, backwards) => {
|
|
121
|
+
const candidates = /** @type {HTMLElement[]} */ ([
|
|
122
|
+
...document.querySelectorAll(FOCUSABLE_SELECTOR),
|
|
123
|
+
]).filter(
|
|
124
|
+
(candidate) =>
|
|
125
|
+
candidate === element ||
|
|
126
|
+
(candidate.tabIndex >= 0 &&
|
|
127
|
+
!candidate.matches(':disabled, [aria-disabled="true"], [hidden], [inert], [inert] *')),
|
|
119
128
|
);
|
|
120
129
|
|
|
130
|
+
const index = candidates.indexOf(element);
|
|
131
|
+
|
|
132
|
+
if (index === -1) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const step = backwards ? -1 : 1;
|
|
137
|
+
|
|
138
|
+
for (let i = index + step; i >= 0 && i < candidates.length; i += step) {
|
|
139
|
+
if (candidates[i].getClientRects().length) {
|
|
140
|
+
candidates[i].focus();
|
|
141
|
+
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
121
147
|
/**
|
|
122
148
|
* Find the element that opens the menu the given element belongs to. A menu lives outside its
|
|
123
149
|
* opener in the DOM tree, so the link runs the other way: the opener points at the menu’s container
|
|
@@ -151,12 +177,60 @@ const getMenuOpener = (element) => {
|
|
|
151
177
|
* Implement keyboard and mouse interactions for a grouping composite widget.
|
|
152
178
|
*/
|
|
153
179
|
export class Group {
|
|
180
|
+
/**
|
|
181
|
+
* Memoized member lists, discarded whenever the widget’s subtree changes. See {@link #members}.
|
|
182
|
+
* @type {{ all: HTMLElement[], active: HTMLElement[] } | undefined}
|
|
183
|
+
*/
|
|
184
|
+
#memberCache = undefined;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Normalized search value of each member, kept alongside the raw value it was derived from so it
|
|
188
|
+
* can be reused until the member’s label actually changes. Filtering runs over every member on
|
|
189
|
+
* every keystroke, and `normalize()` is not free — it decomposes the string and strips the
|
|
190
|
+
* diacritics.
|
|
191
|
+
* @type {WeakMap<HTMLElement, { raw: string, normalized: string }>}
|
|
192
|
+
*/
|
|
193
|
+
#searchValueCache = new WeakMap();
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Hidden state each member was last told about, so the filter can skip the members whose state
|
|
197
|
+
* hasn’t moved. The DOM is deliberately not consulted for this: the listener updates a component,
|
|
198
|
+
* which renders asynchronously, so two keystrokes within a frame would read a stale attribute.
|
|
199
|
+
* @type {WeakMap<HTMLElement, boolean>}
|
|
200
|
+
*/
|
|
201
|
+
#hiddenState = new WeakMap();
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the normalized value a member is searched by, computing it only when the underlying raw
|
|
205
|
+
* value has changed since the last call.
|
|
206
|
+
* @param {HTMLElement} member Member element.
|
|
207
|
+
* @returns {string} Normalized search value.
|
|
208
|
+
*/
|
|
209
|
+
#getNormalizedSearchValue(member) {
|
|
210
|
+
const raw =
|
|
211
|
+
member.dataset.searchValue ??
|
|
212
|
+
member.dataset.label ??
|
|
213
|
+
member.querySelector('.label')?.textContent ??
|
|
214
|
+
/** @type {string} */ (member.textContent);
|
|
215
|
+
|
|
216
|
+
const cached = this.#searchValueCache.get(member);
|
|
217
|
+
|
|
218
|
+
if (cached?.raw === raw) {
|
|
219
|
+
return cached.normalized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const normalized = normalize(raw);
|
|
223
|
+
|
|
224
|
+
this.#searchValueCache.set(member, { raw, normalized });
|
|
225
|
+
|
|
226
|
+
return normalized;
|
|
227
|
+
}
|
|
228
|
+
|
|
154
229
|
/**
|
|
155
230
|
* Initialize a new `Group` instance.
|
|
156
231
|
* @param {HTMLElement} parent Parent element.
|
|
157
232
|
* @param {object} [options] Options.
|
|
158
233
|
* @param {boolean} [options.clickToSelect] Whether to select an item by clicking on it.
|
|
159
|
-
* @todo Check for added elements probably with `MutationObserver`.
|
|
160
234
|
*/
|
|
161
235
|
constructor(parent, { clickToSelect = true } = {}) {
|
|
162
236
|
parent.dispatchEvent(new CustomEvent('Initializing'));
|
|
@@ -218,6 +292,20 @@ export class Group {
|
|
|
218
292
|
|
|
219
293
|
this.parent.tabIndex = focusChild ? -1 : 0;
|
|
220
294
|
|
|
295
|
+
// The members can be added, removed, disabled or hidden at any time, which is what invalidates
|
|
296
|
+
// the cached lists. Only the attributes that decide membership are watched, so the group’s own
|
|
297
|
+
// writes — the selected state and the roving `tabindex` — don’t needlessly discard the cache.
|
|
298
|
+
this.observer = new globalThis.MutationObserver(() => {
|
|
299
|
+
this.#memberCache = undefined;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
this.observer.observe(parent, {
|
|
303
|
+
childList: true,
|
|
304
|
+
subtree: true,
|
|
305
|
+
attributes: true,
|
|
306
|
+
attributeFilter: ['aria-disabled', 'aria-hidden'],
|
|
307
|
+
});
|
|
308
|
+
|
|
221
309
|
// Wait a bit before the relevant components, including the `aria-controls` target are mounted
|
|
222
310
|
(async () => {
|
|
223
311
|
await sleep(100);
|
|
@@ -292,8 +380,9 @@ export class Group {
|
|
|
292
380
|
|
|
293
381
|
const tabStop =
|
|
294
382
|
this.rovingTabStop === 'selected'
|
|
295
|
-
? (activeMembers.find(
|
|
296
|
-
|
|
383
|
+
? (activeMembers.find(
|
|
384
|
+
(element) => element.getAttribute(this.childSelectedAttr) === 'true',
|
|
385
|
+
) ?? activeMembers[0])
|
|
297
386
|
: activeMembers[0];
|
|
298
387
|
|
|
299
388
|
allMembers.forEach((element) => {
|
|
@@ -309,12 +398,42 @@ export class Group {
|
|
|
309
398
|
return this.childRoles.map((role) => `[role="${role}"]`).join(',');
|
|
310
399
|
}
|
|
311
400
|
|
|
401
|
+
/**
|
|
402
|
+
* The member lists, recomputed only once the widget’s subtree has actually changed. A single
|
|
403
|
+
* arrow key reaches these several times over, and a composite widget can hold thousands of
|
|
404
|
+
* members, so the `querySelectorAll` and the `matches()` per member that back them are far too
|
|
405
|
+
* expensive to repeat on every read.
|
|
406
|
+
*
|
|
407
|
+
* The observer’s pending records are drained here rather than left to its callback, which runs a
|
|
408
|
+
* microtask later — too late for a read that happens synchronously after a mutation, as when a
|
|
409
|
+
* key is dispatched right after the members are swapped out.
|
|
410
|
+
* @type {{ all: HTMLElement[], active: HTMLElement[] }}
|
|
411
|
+
*/
|
|
412
|
+
get #members() {
|
|
413
|
+
if (this.observer.takeRecords().length) {
|
|
414
|
+
this.#memberCache = undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!this.#memberCache) {
|
|
418
|
+
const all = /** @type {HTMLElement[]} */ ([...this.parent.querySelectorAll(this.selector)]);
|
|
419
|
+
|
|
420
|
+
this.#memberCache = {
|
|
421
|
+
all,
|
|
422
|
+
active: all.filter(
|
|
423
|
+
(element) => !element.matches('[aria-disabled="true"], [aria-hidden="true"]'),
|
|
424
|
+
),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return this.#memberCache;
|
|
429
|
+
}
|
|
430
|
+
|
|
312
431
|
/**
|
|
313
432
|
* List of all the members.
|
|
314
433
|
* @type {HTMLElement[]}
|
|
315
434
|
*/
|
|
316
435
|
get allMembers() {
|
|
317
|
-
return
|
|
436
|
+
return this.#members.all;
|
|
318
437
|
}
|
|
319
438
|
|
|
320
439
|
/**
|
|
@@ -322,9 +441,7 @@ export class Group {
|
|
|
322
441
|
* @type {HTMLElement[]}
|
|
323
442
|
*/
|
|
324
443
|
get activeMembers() {
|
|
325
|
-
return this.
|
|
326
|
-
(element) => !element.matches('[aria-disabled="true"], [aria-hidden="true"]'),
|
|
327
|
-
);
|
|
444
|
+
return this.#members.active;
|
|
328
445
|
}
|
|
329
446
|
|
|
330
447
|
/**
|
|
@@ -392,13 +509,7 @@ export class Group {
|
|
|
392
509
|
// Somewhere to stand while the menu is torn down, and the fallback if there’s nothing beyond
|
|
393
510
|
opener.focus();
|
|
394
511
|
await sleep(50);
|
|
395
|
-
|
|
396
|
-
const tabStops = getTabStops();
|
|
397
|
-
const index = tabStops.indexOf(opener);
|
|
398
|
-
|
|
399
|
-
if (index > -1) {
|
|
400
|
-
tabStops[index + (backwards ? -1 : 1)]?.focus();
|
|
401
|
-
}
|
|
512
|
+
focusAdjacentTabStop(opener, backwards);
|
|
402
513
|
}
|
|
403
514
|
|
|
404
515
|
/**
|
|
@@ -455,8 +566,8 @@ export class Group {
|
|
|
455
566
|
* @type {HTMLElement | undefined}
|
|
456
567
|
*/
|
|
457
568
|
get selected() {
|
|
458
|
-
return this.activeMembers.find(
|
|
459
|
-
element.
|
|
569
|
+
return this.activeMembers.find(
|
|
570
|
+
(element) => element.getAttribute(this.childSelectedAttr) === 'true',
|
|
460
571
|
);
|
|
461
572
|
}
|
|
462
573
|
|
|
@@ -503,14 +614,28 @@ export class Group {
|
|
|
503
614
|
const selectByKeydown =
|
|
504
615
|
event.type === 'keydown' && /** @type {KeyboardEvent} */ (event).key === ' ';
|
|
505
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Members that took part in this selection, and whose roving `tabindex` therefore has to be
|
|
619
|
+
* updated. The ones skipped below belong to another menu group and are left alone.
|
|
620
|
+
* @type {HTMLElement[]}
|
|
621
|
+
*/
|
|
622
|
+
const affected = [];
|
|
623
|
+
/**
|
|
624
|
+
* Whether the target itself took part, meaning it’s the one to receive focus.
|
|
625
|
+
* @type {boolean}
|
|
626
|
+
*/
|
|
627
|
+
let targetAffected = false;
|
|
628
|
+
|
|
506
629
|
this.activeMembers.forEach((element) => {
|
|
507
|
-
|
|
508
|
-
|
|
630
|
+
// Reading the role once and comparing it is markedly cheaper than putting every member
|
|
631
|
+
// through the selector engine three times over
|
|
632
|
+
const role = element.getAttribute('role');
|
|
633
|
+
const isMenuItemCheckbox = role === 'menuitemcheckbox';
|
|
634
|
+
const isMenuItemRadio = role === 'menuitemradio';
|
|
509
635
|
|
|
510
636
|
if (
|
|
511
637
|
(isMenuItemCheckbox || isMenuItemRadio) &&
|
|
512
|
-
(element.
|
|
513
|
-
element.closest(this.parentGroupSelector) !== targetParent)
|
|
638
|
+
(role !== targetRole || element.closest(this.parentGroupSelector) !== targetParent)
|
|
514
639
|
) {
|
|
515
640
|
return;
|
|
516
641
|
}
|
|
@@ -518,10 +643,13 @@ export class Group {
|
|
|
518
643
|
const multiSelect = isMenuItemCheckbox || this.multi;
|
|
519
644
|
const singleSelect = isMenuItemRadio || !multiSelect;
|
|
520
645
|
const isTarget = element === newTarget;
|
|
521
|
-
const isSelected = element.
|
|
646
|
+
const isSelected = element.getAttribute(this.childSelectedAttr) === 'true';
|
|
522
647
|
const controlTargetId = this.controlsPanel ? element.getAttribute('aria-controls') : null;
|
|
523
648
|
const controlTarget = controlTargetId ? document.getElementById(controlTargetId) : null;
|
|
524
649
|
|
|
650
|
+
affected.push(element);
|
|
651
|
+
targetAffected ||= isTarget;
|
|
652
|
+
|
|
525
653
|
if (multiSelect && isTarget && (selectByClick || selectByKeydown)) {
|
|
526
654
|
element.setAttribute(this.childSelectedAttr, String(!isSelected));
|
|
527
655
|
element.dispatchEvent(
|
|
@@ -544,7 +672,7 @@ export class Group {
|
|
|
544
672
|
);
|
|
545
673
|
|
|
546
674
|
if (isTarget) {
|
|
547
|
-
if (event.type === 'keydown' &&
|
|
675
|
+
if (event.type === 'keydown' && role === 'radio') {
|
|
548
676
|
element.click();
|
|
549
677
|
}
|
|
550
678
|
|
|
@@ -552,17 +680,7 @@ export class Group {
|
|
|
552
680
|
}
|
|
553
681
|
}
|
|
554
682
|
|
|
555
|
-
if (this.focusChild) {
|
|
556
|
-
// Wait a bit before the element is rerendered
|
|
557
|
-
globalThis.requestAnimationFrame(() => {
|
|
558
|
-
element.tabIndex = isTarget ? 0 : -1;
|
|
559
|
-
|
|
560
|
-
if (isTarget) {
|
|
561
|
-
element.focus();
|
|
562
|
-
element.dispatchEvent(new CustomEvent('Focus'));
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
} else {
|
|
683
|
+
if (!this.focusChild) {
|
|
566
684
|
element.classList.toggle('focused', isTarget);
|
|
567
685
|
|
|
568
686
|
if (isTarget) {
|
|
@@ -602,6 +720,21 @@ export class Group {
|
|
|
602
720
|
}
|
|
603
721
|
});
|
|
604
722
|
|
|
723
|
+
if (this.focusChild) {
|
|
724
|
+
// Wait a bit before the elements are rerendered. A single frame serves the whole group;
|
|
725
|
+
// scheduling a callback per member would queue thousands of them on a large widget.
|
|
726
|
+
globalThis.requestAnimationFrame(() => {
|
|
727
|
+
affected.forEach((element) => {
|
|
728
|
+
element.tabIndex = element === newTarget ? 0 : -1;
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
if (targetAffected) {
|
|
732
|
+
newTarget.focus();
|
|
733
|
+
newTarget.dispatchEvent(new CustomEvent('Focus'));
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
605
738
|
this.parent.dispatchEvent(
|
|
606
739
|
new CustomEvent('Change', { detail: getSelectedItemDetail(newTarget) }),
|
|
607
740
|
);
|
|
@@ -796,6 +929,7 @@ export class Group {
|
|
|
796
929
|
* Clean up event listeners.
|
|
797
930
|
*/
|
|
798
931
|
destroy() {
|
|
932
|
+
this.observer.disconnect();
|
|
799
933
|
this.parent.removeEventListener('click', this._onClick);
|
|
800
934
|
this.parent.removeEventListener('keydown', this._onKeyDown);
|
|
801
935
|
}
|
|
@@ -811,16 +945,15 @@ export class Group {
|
|
|
811
945
|
|
|
812
946
|
const matched = allMembers
|
|
813
947
|
.map((member) => {
|
|
814
|
-
const searchValue =
|
|
815
|
-
member.dataset.searchValue ??
|
|
816
|
-
member.dataset.label ??
|
|
817
|
-
member.querySelector('.label')?.textContent ??
|
|
818
|
-
/** @type {string} */ (member.textContent),
|
|
819
|
-
);
|
|
820
|
-
|
|
948
|
+
const searchValue = this.#getNormalizedSearchValue(member);
|
|
821
949
|
const hidden = !_terms.every((term) => searchValue.includes(term));
|
|
822
950
|
|
|
823
|
-
|
|
951
|
+
// Only report an actual change. Every keystroke runs this over every member, and the
|
|
952
|
+
// listener on the other end drives a component update
|
|
953
|
+
if (this.#hiddenState.get(member) !== hidden) {
|
|
954
|
+
this.#hiddenState.set(member, hidden);
|
|
955
|
+
member.dispatchEvent(new CustomEvent('Toggle', { detail: { hidden } }));
|
|
956
|
+
}
|
|
824
957
|
|
|
825
958
|
return hidden;
|
|
826
959
|
})
|
|
@@ -137,12 +137,6 @@ export class Tree {
|
|
|
137
137
|
* @returns {boolean} Result.
|
|
138
138
|
*/
|
|
139
139
|
isExpanded(item: HTMLElement): boolean;
|
|
140
|
-
/**
|
|
141
|
-
* Whether any of the ancestors of the given item is collapsed, meaning the item is not displayed.
|
|
142
|
-
* @param {HTMLElement} item Item.
|
|
143
|
-
* @returns {boolean} Result.
|
|
144
|
-
*/
|
|
145
|
-
hasCollapsedAncestor(item: HTMLElement): boolean;
|
|
146
140
|
/**
|
|
147
141
|
* Get the text label of the given item, which is used for the type-ahead search.
|
|
148
142
|
* @param {HTMLElement} item Item.
|
|
@@ -159,6 +153,14 @@ export class Tree {
|
|
|
159
153
|
* @param {HTMLElement} element Element to be scrolled into view.
|
|
160
154
|
*/
|
|
161
155
|
scrollIntoView(element: HTMLElement): void;
|
|
156
|
+
/**
|
|
157
|
+
* Put exactly one item in the tab order. Only the items actually in it are touched: writing a
|
|
158
|
+
* `tabindex` to every item costs a pass over the whole widget on each arrow key, and all but one
|
|
159
|
+
* of those writes set the value the item already had.
|
|
160
|
+
* @param {HTMLElement} [item] Item to become the tab stop. When omitted, no item is left in the
|
|
161
|
+
* tab order, which is the case for a tree with nothing to focus.
|
|
162
|
+
*/
|
|
163
|
+
setTabStop(item?: HTMLElement | undefined): void;
|
|
162
164
|
/**
|
|
163
165
|
* Move focus to the given item.
|
|
164
166
|
* @param {HTMLElement} item Item to be focused.
|