@optionfactory/fml 9.0.0-rc23 → 9.0.0-rc25
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/custom-elements.json +3 -3
- package/dist/fml.iife.js +120 -35
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ful.d.mts +55 -20
- package/dist/ful.iife.js +120 -35
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +120 -35
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +2 -2
- package/dist/web-types.json +4 -4
- package/package.json +1 -1
package/dist/ful.d.mts
CHANGED
|
@@ -363,6 +363,11 @@ declare class Bindings {
|
|
|
363
363
|
* `rest` being the remainder of the path (`''` on an exact match) that a
|
|
364
364
|
* composite can use to route the problem to an inner control.
|
|
365
365
|
*
|
|
366
|
+
* A control counts only when it can show a problem, through a
|
|
367
|
+
* `setCustomValidity` of its own: a form-associated element that has none is
|
|
368
|
+
* skipped, so its problems fall to a shorter prefix or to the banner instead
|
|
369
|
+
* of being dropped.
|
|
370
|
+
*
|
|
366
371
|
* Every other problem, and a field problem naming no control, is shown in
|
|
367
372
|
* each `ful-errors` banner of the form: the banner gets `role="alert"`, is
|
|
368
373
|
* revealed and holds the reasons one per line. With no such problem the
|
|
@@ -616,6 +621,14 @@ declare class Field extends ParsedElement {
|
|
|
616
621
|
* accessibility tree.
|
|
617
622
|
* - `also` are further controls that mirror `disabled` and `readOnly`
|
|
618
623
|
* beside the claim target.
|
|
624
|
+
*
|
|
625
|
+
* The pieces are elements the field rendered itself, native controls or
|
|
626
|
+
* wrappers of its own. A ful field is not one: it has no `readOnly`, its
|
|
627
|
+
* host's role takes no aria, and a label's `for` names its host rather than
|
|
628
|
+
* its control. A composite built from ful fields answers the inner field as
|
|
629
|
+
* `control`, which forwards `focus()`, lets it render its own label with
|
|
630
|
+
* `label` and `announces` null, and forwards the claims in its setters,
|
|
631
|
+
* calling super first.
|
|
619
632
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
620
633
|
* @returns {any} the pieces, or a promise of them
|
|
621
634
|
* @throws {Error} when the subclass does not implement it
|
|
@@ -727,7 +740,8 @@ declare class FormLoader {
|
|
|
727
740
|
*
|
|
728
741
|
* Attributes on the host starting with `form-` are forwarded, without the
|
|
729
742
|
* prefix, to the native form. `autocomplete` is copied onto the native form as
|
|
730
|
-
* well and stays on the host, the fields reading it from either
|
|
743
|
+
* well and stays on the host, the fields reading it from either; a
|
|
744
|
+
* `form-autocomplete` has the last word on the native form.
|
|
731
745
|
* `clear-invalid-on-change` clears the custom validity of a field of this form
|
|
732
746
|
* when a `change` event bubbles from it; the field's form is read from its
|
|
733
747
|
* `form` property, or from `internals.form` for a form-associated element
|
|
@@ -2931,14 +2945,15 @@ declare class Dialog extends ParsedElement {
|
|
|
2931
2945
|
*
|
|
2932
2946
|
* No `section:requested` is dispatched, and no `ask()` waiter is added:
|
|
2933
2947
|
* listen for the `close` event to learn how the dialog ended. When a newer
|
|
2934
|
-
* `update()` starts before the callback settles, this call paints nothing
|
|
2935
|
-
*
|
|
2936
|
-
* callback's error.
|
|
2948
|
+
* `update()` starts before the callback settles, this call paints nothing
|
|
2949
|
+
* and resolves `null`, the body being the newer call's; it still rejects
|
|
2950
|
+
* with the callback's error.
|
|
2937
2951
|
* @param {() => Node|string|Promise<Node|string>} cb producing the body content
|
|
2938
|
-
* @returns {Promise<Element>} the body section, in the document
|
|
2952
|
+
* @returns {Promise<Element|null>} the body section, in the document, or
|
|
2953
|
+
* `null` for a call a newer one overtook
|
|
2939
2954
|
* @throws {any} what the callback threw or rejected with
|
|
2940
2955
|
*/
|
|
2941
|
-
update(cb: () => Node | string | Promise<Node | string>): Promise<Element>;
|
|
2956
|
+
update(cb: () => Node | string | Promise<Node | string>): Promise<Element | null>;
|
|
2942
2957
|
/**
|
|
2943
2958
|
* Dispatches `section:requested` for the body again, as `ask()` does,
|
|
2944
2959
|
* whether the dialog is open or closed. A failed request is painted into
|
|
@@ -3067,15 +3082,16 @@ declare class Drawer extends ParsedElement {
|
|
|
3067
3082
|
* which has `role="alert"`, and the content is hidden.
|
|
3068
3083
|
*
|
|
3069
3084
|
* No `section:requested` is dispatched. When a newer `update()` starts
|
|
3070
|
-
* before the callback settles, this call paints nothing
|
|
3071
|
-
* the content section
|
|
3072
|
-
* error.
|
|
3085
|
+
* before the callback settles, this call paints nothing and resolves
|
|
3086
|
+
* `null`, the content section being the newer call's; it still rejects with
|
|
3087
|
+
* the callback's error.
|
|
3073
3088
|
* @param {string} header the title, set as text
|
|
3074
3089
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
3075
|
-
* @returns {Promise<Element>} the content section, in the document
|
|
3090
|
+
* @returns {Promise<Element|null>} the content section, in the document, or
|
|
3091
|
+
* `null` for a call a newer one overtook
|
|
3076
3092
|
* @throws {any} what the callback threw or rejected with
|
|
3077
3093
|
*/
|
|
3078
|
-
update(header: string, cb: () => Node | string | Promise<Node | string>): Promise<Element>;
|
|
3094
|
+
update(header: string, cb: () => Node | string | Promise<Node | string>): Promise<Element | null>;
|
|
3079
3095
|
/**
|
|
3080
3096
|
* Dispatches `section:requested` for the content again, as `open()` does,
|
|
3081
3097
|
* whether the drawer is open or closed. A failed request is painted into
|
|
@@ -3145,11 +3161,15 @@ declare class Drawer extends ParsedElement {
|
|
|
3145
3161
|
close(result?: string): void;
|
|
3146
3162
|
}
|
|
3147
3163
|
/**
|
|
3148
|
-
* A menu of commands on the native popover platform
|
|
3149
|
-
*
|
|
3150
|
-
*
|
|
3164
|
+
* A menu of commands on the native popover platform. The menu is the popover
|
|
3165
|
+
* itself: light dismiss and Escape come from the browser.
|
|
3166
|
+
*
|
|
3167
|
+
* A menu naming an invoker by id through `for` belongs to that element: the
|
|
3168
|
+
* invoker toggles it, and it is anchored to the invoker through
|
|
3151
3169
|
* `Anchors.wire`, placed in script where the platform has no css anchor
|
|
3152
|
-
* positioning.
|
|
3170
|
+
* positioning. A menu without `for` is shared: one menu serves many
|
|
3171
|
+
* invokers, such as the "more" button of every row of a list, and a page
|
|
3172
|
+
* opens it beside the invoker it is for with `showFor(invoker)`.
|
|
3153
3173
|
*
|
|
3154
3174
|
* The items are the buttons and `a[href]` links written as its direct
|
|
3155
3175
|
* children, kept as they are. Activation is the item's own click, which the
|
|
@@ -3159,7 +3179,8 @@ declare class Drawer extends ParsedElement {
|
|
|
3159
3179
|
* The keyboard is the ARIA menu protocol: the focus moves to the
|
|
3160
3180
|
* first item as the menu opens, the arrows, Home and End walk the items,
|
|
3161
3181
|
* typing searches their text, Enter and Space activate the focused one, and
|
|
3162
|
-
* Escape and a close from inside the menu hand the focus back to the invoker
|
|
3182
|
+
* Escape and a close from inside the menu hand the focus back to the invoker
|
|
3183
|
+
* that opened it.
|
|
3163
3184
|
*/
|
|
3164
3185
|
declare class Menu extends ParsedElement {
|
|
3165
3186
|
#private;
|
|
@@ -3167,13 +3188,27 @@ declare class Menu extends ParsedElement {
|
|
|
3167
3188
|
static attributes: string[];
|
|
3168
3189
|
/**
|
|
3169
3190
|
* Gives the menu the `menu` role and an empty `popover` attribute unless
|
|
3170
|
-
* it already has one,
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3191
|
+
* it already has one, and marks the items. With `for`, read here once, it
|
|
3192
|
+
* also gives the invoker `aria-haspopup="menu"`, an `aria-expanded` kept
|
|
3193
|
+
* in step, and the toggle of the menu.
|
|
3173
3194
|
* @returns {void}
|
|
3174
|
-
* @throws {Error} when `for` names no element in the document at the render
|
|
3195
|
+
* @throws {Error} when `for` is given and names no element in the document at the render
|
|
3175
3196
|
*/
|
|
3176
3197
|
render(): void;
|
|
3198
|
+
/**
|
|
3199
|
+
* Opens a shared menu, one without `for`, beside `invoker`, or moves it
|
|
3200
|
+
* there when it is open for another one. The menu goes below the invoker,
|
|
3201
|
+
* or above it where there is no room below, and is placed once, as
|
|
3202
|
+
* `Anchors.show` places it. The invoker gets `aria-haspopup="menu"` and an
|
|
3203
|
+
* `aria-expanded` that reads true until the menu closes, and the focus
|
|
3204
|
+
* goes back to it on Escape. A call for the invoker whose press has just
|
|
3205
|
+
* closed the menu does nothing, so a second click on the same button
|
|
3206
|
+
* closes the menu rather than opening it again.
|
|
3207
|
+
* @param {HTMLElement} invoker the element the menu opens for
|
|
3208
|
+
* @returns {void}
|
|
3209
|
+
* @throws {Error} when the menu names its invoker through `for`
|
|
3210
|
+
*/
|
|
3211
|
+
showFor(invoker: HTMLElement): void;
|
|
3177
3212
|
/**
|
|
3178
3213
|
* The commands, in the order the arrows walk them: the menu's direct
|
|
3179
3214
|
* `button` and `a[href]` children, minus those carrying a `disabled` or
|
package/dist/ful.iife.js
CHANGED
|
@@ -693,6 +693,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
693
693
|
* `rest` being the remainder of the path (`''` on an exact match) that a
|
|
694
694
|
* composite can use to route the problem to an inner control.
|
|
695
695
|
*
|
|
696
|
+
* A control counts only when it can show a problem, through a
|
|
697
|
+
* `setCustomValidity` of its own: a form-associated element that has none is
|
|
698
|
+
* skipped, so its problems fall to a shorter prefix or to the banner instead
|
|
699
|
+
* of being dropped.
|
|
700
|
+
*
|
|
696
701
|
* Every other problem, and a field problem naming no control, is shown in
|
|
697
702
|
* each `ful-errors` banner of the form: the banner gets `role="alert"`, is
|
|
698
703
|
* revealed and holds the reasons one per line. With no such problem the
|
|
@@ -731,7 +736,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
731
736
|
const parts = name.split('.');
|
|
732
737
|
for (let i = parts.length; i !== 0; --i) {
|
|
733
738
|
const prefix = parts.slice(0, i).join('.');
|
|
734
|
-
const targets = named.get(prefix) ?? [];
|
|
739
|
+
const targets = (named.get(prefix) ?? []).filter((el) => typeof el.setCustomValidity === 'function');
|
|
735
740
|
if (targets.length === 0) {
|
|
736
741
|
continue;
|
|
737
742
|
}
|
|
@@ -1189,6 +1194,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1189
1194
|
* accessibility tree.
|
|
1190
1195
|
* - `also` are further controls that mirror `disabled` and `readOnly`
|
|
1191
1196
|
* beside the claim target.
|
|
1197
|
+
*
|
|
1198
|
+
* The pieces are elements the field rendered itself, native controls or
|
|
1199
|
+
* wrappers of its own. A ful field is not one: it has no `readOnly`, its
|
|
1200
|
+
* host's role takes no aria, and a label's `for` names its host rather than
|
|
1201
|
+
* its control. A composite built from ful fields answers the inner field as
|
|
1202
|
+
* `control`, which forwards `focus()`, lets it render its own label with
|
|
1203
|
+
* `label` and `announces` null, and forwards the claims in its setters,
|
|
1204
|
+
* calling super first.
|
|
1192
1205
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
1193
1206
|
* @returns {any} the pieces, or a promise of them
|
|
1194
1207
|
* @throws {Error} when the subclass does not implement it
|
|
@@ -1345,7 +1358,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1345
1358
|
*
|
|
1346
1359
|
* Attributes on the host starting with `form-` are forwarded, without the
|
|
1347
1360
|
* prefix, to the native form. `autocomplete` is copied onto the native form as
|
|
1348
|
-
* well and stays on the host, the fields reading it from either
|
|
1361
|
+
* well and stays on the host, the fields reading it from either; a
|
|
1362
|
+
* `form-autocomplete` has the last word on the native form.
|
|
1349
1363
|
* `clear-invalid-on-change` clears the custom validity of a field of this form
|
|
1350
1364
|
* when a `change` event bubbles from it; the field's form is read from its
|
|
1351
1365
|
* `form` property, or from `internals.form` for a form-associated element
|
|
@@ -1380,8 +1394,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1380
1394
|
const form = document.createElement('form');
|
|
1381
1395
|
this.form = form;
|
|
1382
1396
|
form.setAttribute('novalidate', '');
|
|
1383
|
-
index_mjs.Attributes.forward('form-', this, form);
|
|
1384
1397
|
index_mjs.Attributes.set(form, 'autocomplete', this.declared('autocomplete'));
|
|
1398
|
+
index_mjs.Attributes.forward('form-', this, form);
|
|
1385
1399
|
form.replaceChildren(...this.childNodes);
|
|
1386
1400
|
form.addEventListener('submit', async (e) => {
|
|
1387
1401
|
e.preventDefault();
|
|
@@ -4576,7 +4590,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4576
4590
|
return;
|
|
4577
4591
|
}
|
|
4578
4592
|
/** @type {HTMLInputElement|null} */
|
|
4579
|
-
const el = this.querySelector(`input[type=radio][value
|
|
4593
|
+
const el = this.querySelector(`input[type=radio][value="${CSS.escape(String(value))}"]`);
|
|
4580
4594
|
if (el === null) {
|
|
4581
4595
|
clear();
|
|
4582
4596
|
return;
|
|
@@ -5569,7 +5583,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5569
5583
|
* `items` answers the items in the order they are walked, and is called on
|
|
5570
5584
|
* every gesture, so a menu that fills itself later needs no rewiring.
|
|
5571
5585
|
*
|
|
5572
|
-
* @param {HTMLElement} invoker the element the menu belongs to
|
|
5586
|
+
* @param {() => HTMLElement|null} invoker answers the element the menu belongs to
|
|
5587
|
+
* now, where the focus goes back; called on every gesture, so a menu shared
|
|
5588
|
+
* by several invokers hands the focus back to the one that opened it
|
|
5573
5589
|
* @param {HTMLElement} menu the `[popover]` holding the items
|
|
5574
5590
|
* @param {{items: () => HTMLElement[], current?: () => HTMLElement|null|undefined}} conf
|
|
5575
5591
|
* `items` answers the items; `current` answers the item an opening focuses
|
|
@@ -5579,7 +5595,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5579
5595
|
let typedAt = 0;
|
|
5580
5596
|
menu.addEventListener('beforetoggle', (/** @type any */ evt) => {
|
|
5581
5597
|
if (evt.newState === 'closed' && menu.contains(document.activeElement)) {
|
|
5582
|
-
invoker
|
|
5598
|
+
invoker()?.focus();
|
|
5583
5599
|
}
|
|
5584
5600
|
});
|
|
5585
5601
|
menu.addEventListener('toggle', (/** @type any */ evt) => {
|
|
@@ -5623,11 +5639,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5623
5639
|
case 'Space': {
|
|
5624
5640
|
evt.preventDefault();
|
|
5625
5641
|
item.click();
|
|
5626
|
-
invoker
|
|
5642
|
+
invoker()?.focus();
|
|
5627
5643
|
return;
|
|
5628
5644
|
}
|
|
5629
5645
|
case 'Escape': {
|
|
5630
|
-
invoker
|
|
5646
|
+
invoker()?.focus();
|
|
5631
5647
|
return;
|
|
5632
5648
|
}
|
|
5633
5649
|
}
|
|
@@ -5852,7 +5868,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5852
5868
|
}
|
|
5853
5869
|
#wire() {
|
|
5854
5870
|
Anchors.wire(this.#button, this.#menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
|
|
5855
|
-
wireMenuKeys(this.#button, this.#menu, {
|
|
5871
|
+
wireMenuKeys(() => this.#button, this.#menu, {
|
|
5856
5872
|
items: () => this.#items(),
|
|
5857
5873
|
current: () => this.#items().find((a) => a.getAttribute('value') === this.value),
|
|
5858
5874
|
});
|
|
@@ -6850,11 +6866,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6850
6866
|
* `show`, and fills the content with what the callback resolves to. A
|
|
6851
6867
|
* rejection is shown, as the problems' reasons, in the error section, and
|
|
6852
6868
|
* the content is hidden. When a newer `update()` starts before the callback
|
|
6853
|
-
* settles, this call paints nothing
|
|
6854
|
-
*
|
|
6869
|
+
* settles, this call paints nothing and resolves `null`, the section being
|
|
6870
|
+
* the newer call's; it still rejects with the callback's error.
|
|
6855
6871
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
6856
6872
|
* @param {() => void} show opens the dialog when it is closed
|
|
6857
|
-
* @returns {Promise<HTMLElement>} the content section
|
|
6873
|
+
* @returns {Promise<HTMLElement|null>} the content section, or `null` for a
|
|
6874
|
+
* call a newer one overtook
|
|
6858
6875
|
* @throws {any} what the callback threw or rejected with
|
|
6859
6876
|
*/
|
|
6860
6877
|
async update(cb, show) {
|
|
@@ -6867,7 +6884,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6867
6884
|
try {
|
|
6868
6885
|
const delivered = await cb();
|
|
6869
6886
|
if (claim.stale) {
|
|
6870
|
-
return
|
|
6887
|
+
return null;
|
|
6871
6888
|
}
|
|
6872
6889
|
this.#section.replaceChildren(delivered);
|
|
6873
6890
|
this.#loading?.setAttribute('hidden', '');
|
|
@@ -7113,11 +7130,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7113
7130
|
*
|
|
7114
7131
|
* No `section:requested` is dispatched, and no `ask()` waiter is added:
|
|
7115
7132
|
* listen for the `close` event to learn how the dialog ended. When a newer
|
|
7116
|
-
* `update()` starts before the callback settles, this call paints nothing
|
|
7117
|
-
*
|
|
7118
|
-
* callback's error.
|
|
7133
|
+
* `update()` starts before the callback settles, this call paints nothing
|
|
7134
|
+
* and resolves `null`, the body being the newer call's; it still rejects
|
|
7135
|
+
* with the callback's error.
|
|
7119
7136
|
* @param {() => Node|string|Promise<Node|string>} cb producing the body content
|
|
7120
|
-
* @returns {Promise<Element>} the body section, in the document
|
|
7137
|
+
* @returns {Promise<Element|null>} the body section, in the document, or
|
|
7138
|
+
* `null` for a call a newer one overtook
|
|
7121
7139
|
* @throws {any} what the callback threw or rejected with
|
|
7122
7140
|
*/
|
|
7123
7141
|
async update(cb) {
|
|
@@ -7407,12 +7425,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7407
7425
|
* which has `role="alert"`, and the content is hidden.
|
|
7408
7426
|
*
|
|
7409
7427
|
* No `section:requested` is dispatched. When a newer `update()` starts
|
|
7410
|
-
* before the callback settles, this call paints nothing
|
|
7411
|
-
* the content section
|
|
7412
|
-
* error.
|
|
7428
|
+
* before the callback settles, this call paints nothing and resolves
|
|
7429
|
+
* `null`, the content section being the newer call's; it still rejects with
|
|
7430
|
+
* the callback's error.
|
|
7413
7431
|
* @param {string} header the title, set as text
|
|
7414
7432
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
7415
|
-
* @returns {Promise<Element>} the content section, in the document
|
|
7433
|
+
* @returns {Promise<Element|null>} the content section, in the document, or
|
|
7434
|
+
* `null` for a call a newer one overtook
|
|
7416
7435
|
* @throws {any} what the callback threw or rejected with
|
|
7417
7436
|
*/
|
|
7418
7437
|
async update(header, cb) {
|
|
@@ -7561,11 +7580,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7561
7580
|
}
|
|
7562
7581
|
|
|
7563
7582
|
/**
|
|
7564
|
-
* A menu of commands on the native popover platform
|
|
7565
|
-
*
|
|
7566
|
-
*
|
|
7583
|
+
* A menu of commands on the native popover platform. The menu is the popover
|
|
7584
|
+
* itself: light dismiss and Escape come from the browser.
|
|
7585
|
+
*
|
|
7586
|
+
* A menu naming an invoker by id through `for` belongs to that element: the
|
|
7587
|
+
* invoker toggles it, and it is anchored to the invoker through
|
|
7567
7588
|
* `Anchors.wire`, placed in script where the platform has no css anchor
|
|
7568
|
-
* positioning.
|
|
7589
|
+
* positioning. A menu without `for` is shared: one menu serves many
|
|
7590
|
+
* invokers, such as the "more" button of every row of a list, and a page
|
|
7591
|
+
* opens it beside the invoker it is for with `showFor(invoker)`.
|
|
7569
7592
|
*
|
|
7570
7593
|
* The items are the buttons and `a[href]` links written as its direct
|
|
7571
7594
|
* children, kept as they are. Activation is the item's own click, which the
|
|
@@ -7575,34 +7598,44 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7575
7598
|
* The keyboard is the ARIA menu protocol: the focus moves to the
|
|
7576
7599
|
* first item as the menu opens, the arrows, Home and End walk the items,
|
|
7577
7600
|
* typing searches their text, Enter and Space activate the focused one, and
|
|
7578
|
-
* Escape and a close from inside the menu hand the focus back to the invoker
|
|
7601
|
+
* Escape and a close from inside the menu hand the focus back to the invoker
|
|
7602
|
+
* that opened it.
|
|
7579
7603
|
*/
|
|
7580
7604
|
class Menu extends index_mjs.ParsedElement {
|
|
7581
7605
|
/** @type {string[]} */
|
|
7582
7606
|
static attributes = ['for'];
|
|
7607
|
+
/** @type {HTMLElement|null} */
|
|
7608
|
+
#invoker = null;
|
|
7609
|
+
/** @type {HTMLElement|null} */
|
|
7610
|
+
#pressed = null;
|
|
7583
7611
|
/**
|
|
7584
7612
|
* Gives the menu the `menu` role and an empty `popover` attribute unless
|
|
7585
|
-
* it already has one,
|
|
7586
|
-
*
|
|
7587
|
-
*
|
|
7613
|
+
* it already has one, and marks the items. With `for`, read here once, it
|
|
7614
|
+
* also gives the invoker `aria-haspopup="menu"`, an `aria-expanded` kept
|
|
7615
|
+
* in step, and the toggle of the menu.
|
|
7588
7616
|
* @returns {void}
|
|
7589
|
-
* @throws {Error} when `for` names no element in the document at the render
|
|
7617
|
+
* @throws {Error} when `for` is given and names no element in the document at the render
|
|
7590
7618
|
*/
|
|
7591
7619
|
render() {
|
|
7592
7620
|
const named = this.declared('for');
|
|
7593
7621
|
const invoker = named ? document.getElementById(named) : null;
|
|
7594
|
-
if (!invoker) {
|
|
7622
|
+
if (named && !invoker) {
|
|
7595
7623
|
throw new Error(
|
|
7596
|
-
`ful-menu names no invoker: for="${named
|
|
7624
|
+
`ful-menu names no invoker: for="${named}" must name an element already in the page when the menu upgrades`,
|
|
7597
7625
|
);
|
|
7598
7626
|
}
|
|
7599
7627
|
this.setAttribute('role', 'menu');
|
|
7600
7628
|
if (!this.hasAttribute('popover')) {
|
|
7601
7629
|
this.setAttribute('popover', '');
|
|
7602
7630
|
}
|
|
7603
|
-
invoker
|
|
7604
|
-
|
|
7605
|
-
|
|
7631
|
+
if (invoker) {
|
|
7632
|
+
this.#invoker = invoker;
|
|
7633
|
+
invoker.setAttribute('aria-haspopup', 'menu');
|
|
7634
|
+
Anchors.wire(invoker, this, { prefix: 'ful-menu', invoke: true, expanded: true });
|
|
7635
|
+
} else {
|
|
7636
|
+
this.#wireShared();
|
|
7637
|
+
}
|
|
7638
|
+
wireMenuKeys(() => this.#invoker, this, { items: () => this.items });
|
|
7606
7639
|
this.#mark();
|
|
7607
7640
|
this.addEventListener('click', (evt) => {
|
|
7608
7641
|
const target = /** @type HTMLElement */ (evt.target);
|
|
@@ -7611,6 +7644,43 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7611
7644
|
}
|
|
7612
7645
|
});
|
|
7613
7646
|
}
|
|
7647
|
+
/**
|
|
7648
|
+
* Opens a shared menu, one without `for`, beside `invoker`, or moves it
|
|
7649
|
+
* there when it is open for another one. The menu goes below the invoker,
|
|
7650
|
+
* or above it where there is no room below, and is placed once, as
|
|
7651
|
+
* `Anchors.show` places it. The invoker gets `aria-haspopup="menu"` and an
|
|
7652
|
+
* `aria-expanded` that reads true until the menu closes, and the focus
|
|
7653
|
+
* goes back to it on Escape. A call for the invoker whose press has just
|
|
7654
|
+
* closed the menu does nothing, so a second click on the same button
|
|
7655
|
+
* closes the menu rather than opening it again.
|
|
7656
|
+
* @param {HTMLElement} invoker the element the menu opens for
|
|
7657
|
+
* @returns {void}
|
|
7658
|
+
* @throws {Error} when the menu names its invoker through `for`
|
|
7659
|
+
*/
|
|
7660
|
+
showFor(invoker) {
|
|
7661
|
+
if (this.declared('for')) {
|
|
7662
|
+
throw new Error('ful-menu: showFor serves a menu without for; a menu with for is opened by its invoker');
|
|
7663
|
+
}
|
|
7664
|
+
const pressed = this.#pressed;
|
|
7665
|
+
this.#pressed = null;
|
|
7666
|
+
if (pressed === invoker) {
|
|
7667
|
+
return;
|
|
7668
|
+
}
|
|
7669
|
+
if (this.#invoker && this.#invoker !== invoker) {
|
|
7670
|
+
this.#invoker.setAttribute('aria-expanded', 'false');
|
|
7671
|
+
}
|
|
7672
|
+
this.#invoker = invoker;
|
|
7673
|
+
invoker.setAttribute('aria-haspopup', 'menu');
|
|
7674
|
+
invoker.setAttribute('aria-expanded', 'true');
|
|
7675
|
+
//the previous placement wrote margin: 0 inline, hiding the stylesheet's gap
|
|
7676
|
+
this.style.margin = '';
|
|
7677
|
+
const gap = Number.parseFloat(getComputedStyle(this).marginBlockStart) || 0;
|
|
7678
|
+
const moving = this.matches(':popover-open');
|
|
7679
|
+
Anchors.show(this, invoker, { flip: true, gap });
|
|
7680
|
+
if (moving) {
|
|
7681
|
+
this.items[0]?.focus();
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7614
7684
|
/**
|
|
7615
7685
|
* The commands, in the order the arrows walk them: the menu's direct
|
|
7616
7686
|
* `button` and `a[href]` children, minus those carrying a `disabled` or
|
|
@@ -7622,6 +7692,21 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7622
7692
|
get items() {
|
|
7623
7693
|
return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
|
|
7624
7694
|
}
|
|
7695
|
+
#wireShared() {
|
|
7696
|
+
document.addEventListener(
|
|
7697
|
+
'pointerdown',
|
|
7698
|
+
(evt) => {
|
|
7699
|
+
const target = /** @type Node */ (evt.target);
|
|
7700
|
+
this.#pressed = this.matches(':popover-open') && this.#invoker?.contains(target) ? this.#invoker : null;
|
|
7701
|
+
},
|
|
7702
|
+
true,
|
|
7703
|
+
);
|
|
7704
|
+
this.addEventListener('toggle', (/** @type any */ evt) => {
|
|
7705
|
+
if (evt.newState === 'closed') {
|
|
7706
|
+
this.#invoker?.setAttribute('aria-expanded', 'false');
|
|
7707
|
+
}
|
|
7708
|
+
});
|
|
7709
|
+
}
|
|
7625
7710
|
/** @returns {HTMLElement[]} every item, disabled and hidden ones included, once marked */
|
|
7626
7711
|
#mark() {
|
|
7627
7712
|
const all = /** @type HTMLElement[] */ ([...this.querySelectorAll(':scope > button, :scope > a[href]')]);
|