@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
|
@@ -192,11 +192,11 @@
|
|
|
192
192
|
"name": "Menu",
|
|
193
193
|
"tagName": "ful-menu",
|
|
194
194
|
"customElement": true,
|
|
195
|
-
"description": "A menu of commands on the native popover platform: the invoker named by for toggles it,
|
|
195
|
+
"description": "A menu of commands on the native popover platform: the invoker named by for toggles it, or, without for, one menu serves many invokers and showFor(invoker) opens it beside the one it is for. Light dismiss and Escape come from the browser, and the element adds the menu semantics and the keyboard protocol (roving focus with the arrows, Home and End, type-ahead, Enter and Space to activate, Escape and every close handing the focus back to the invoker). The items are the buttons and links written inside it, kept as they are and given their menuitem role; activation is the item's own click, which the menu only follows by closing.",
|
|
196
196
|
"attributes": [
|
|
197
197
|
{
|
|
198
198
|
"name": "for",
|
|
199
|
-
"description": "The id of the invoker the menu belongs to: it toggles the menu, carries aria-haspopup and aria-expanded, and takes the focus back when the menu closes. Read once at the upgrade, so the invoker must already be in the page by then.",
|
|
199
|
+
"description": "The id of the invoker the menu belongs to: it toggles the menu, carries aria-haspopup and aria-expanded, and takes the focus back when the menu closes. Read once at the upgrade, so the invoker must already be in the page by then. Without it the menu is shared, opened by showFor(invoker).",
|
|
200
200
|
"type": {
|
|
201
201
|
"text": "string"
|
|
202
202
|
}
|
|
@@ -419,7 +419,7 @@
|
|
|
419
419
|
},
|
|
420
420
|
{
|
|
421
421
|
"name": "submit:requested",
|
|
422
|
-
"description": "Fired after the submit was accepted, to let a listener resolve the response itself.",
|
|
422
|
+
"description": "Fired after the submit was accepted, to let a listener resolve the response itself. Register it through AsyncEvents.asyncOn: the form awaits only such a listener, and one added with addEventListener is not waited for.",
|
|
423
423
|
"type": {
|
|
424
424
|
"text": "CustomEvent"
|
|
425
425
|
}
|
package/dist/fml.iife.js
CHANGED
|
@@ -6963,6 +6963,11 @@ var fml = (function (exports) {
|
|
|
6963
6963
|
* `rest` being the remainder of the path (`''` on an exact match) that a
|
|
6964
6964
|
* composite can use to route the problem to an inner control.
|
|
6965
6965
|
*
|
|
6966
|
+
* A control counts only when it can show a problem, through a
|
|
6967
|
+
* `setCustomValidity` of its own: a form-associated element that has none is
|
|
6968
|
+
* skipped, so its problems fall to a shorter prefix or to the banner instead
|
|
6969
|
+
* of being dropped.
|
|
6970
|
+
*
|
|
6966
6971
|
* Every other problem, and a field problem naming no control, is shown in
|
|
6967
6972
|
* each `ful-errors` banner of the form: the banner gets `role="alert"`, is
|
|
6968
6973
|
* revealed and holds the reasons one per line. With no such problem the
|
|
@@ -7001,7 +7006,7 @@ var fml = (function (exports) {
|
|
|
7001
7006
|
const parts = name.split('.');
|
|
7002
7007
|
for (let i = parts.length; i !== 0; --i) {
|
|
7003
7008
|
const prefix = parts.slice(0, i).join('.');
|
|
7004
|
-
const targets = named.get(prefix) ?? [];
|
|
7009
|
+
const targets = (named.get(prefix) ?? []).filter((el) => typeof el.setCustomValidity === 'function');
|
|
7005
7010
|
if (targets.length === 0) {
|
|
7006
7011
|
continue;
|
|
7007
7012
|
}
|
|
@@ -7459,6 +7464,14 @@ var fml = (function (exports) {
|
|
|
7459
7464
|
* accessibility tree.
|
|
7460
7465
|
* - `also` are further controls that mirror `disabled` and `readOnly`
|
|
7461
7466
|
* beside the claim target.
|
|
7467
|
+
*
|
|
7468
|
+
* The pieces are elements the field rendered itself, native controls or
|
|
7469
|
+
* wrappers of its own. A ful field is not one: it has no `readOnly`, its
|
|
7470
|
+
* host's role takes no aria, and a label's `for` names its host rather than
|
|
7471
|
+
* its control. A composite built from ful fields answers the inner field as
|
|
7472
|
+
* `control`, which forwards `focus()`, lets it render its own label with
|
|
7473
|
+
* `label` and `announces` null, and forwards the claims in its setters,
|
|
7474
|
+
* calling super first.
|
|
7462
7475
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
7463
7476
|
* @returns {any} the pieces, or a promise of them
|
|
7464
7477
|
* @throws {Error} when the subclass does not implement it
|
|
@@ -7615,7 +7628,8 @@ var fml = (function (exports) {
|
|
|
7615
7628
|
*
|
|
7616
7629
|
* Attributes on the host starting with `form-` are forwarded, without the
|
|
7617
7630
|
* prefix, to the native form. `autocomplete` is copied onto the native form as
|
|
7618
|
-
* well and stays on the host, the fields reading it from either
|
|
7631
|
+
* well and stays on the host, the fields reading it from either; a
|
|
7632
|
+
* `form-autocomplete` has the last word on the native form.
|
|
7619
7633
|
* `clear-invalid-on-change` clears the custom validity of a field of this form
|
|
7620
7634
|
* when a `change` event bubbles from it; the field's form is read from its
|
|
7621
7635
|
* `form` property, or from `internals.form` for a form-associated element
|
|
@@ -7650,8 +7664,8 @@ var fml = (function (exports) {
|
|
|
7650
7664
|
const form = document.createElement('form');
|
|
7651
7665
|
this.form = form;
|
|
7652
7666
|
form.setAttribute('novalidate', '');
|
|
7653
|
-
Attributes.forward('form-', this, form);
|
|
7654
7667
|
Attributes.set(form, 'autocomplete', this.declared('autocomplete'));
|
|
7668
|
+
Attributes.forward('form-', this, form);
|
|
7655
7669
|
form.replaceChildren(...this.childNodes);
|
|
7656
7670
|
form.addEventListener('submit', async (e) => {
|
|
7657
7671
|
e.preventDefault();
|
|
@@ -10846,7 +10860,7 @@ var fml = (function (exports) {
|
|
|
10846
10860
|
return;
|
|
10847
10861
|
}
|
|
10848
10862
|
/** @type {HTMLInputElement|null} */
|
|
10849
|
-
const el = this.querySelector(`input[type=radio][value
|
|
10863
|
+
const el = this.querySelector(`input[type=radio][value="${CSS.escape(String(value))}"]`);
|
|
10850
10864
|
if (el === null) {
|
|
10851
10865
|
clear();
|
|
10852
10866
|
return;
|
|
@@ -11839,7 +11853,9 @@ var fml = (function (exports) {
|
|
|
11839
11853
|
* `items` answers the items in the order they are walked, and is called on
|
|
11840
11854
|
* every gesture, so a menu that fills itself later needs no rewiring.
|
|
11841
11855
|
*
|
|
11842
|
-
* @param {HTMLElement} invoker the element the menu belongs to
|
|
11856
|
+
* @param {() => HTMLElement|null} invoker answers the element the menu belongs to
|
|
11857
|
+
* now, where the focus goes back; called on every gesture, so a menu shared
|
|
11858
|
+
* by several invokers hands the focus back to the one that opened it
|
|
11843
11859
|
* @param {HTMLElement} menu the `[popover]` holding the items
|
|
11844
11860
|
* @param {{items: () => HTMLElement[], current?: () => HTMLElement|null|undefined}} conf
|
|
11845
11861
|
* `items` answers the items; `current` answers the item an opening focuses
|
|
@@ -11849,7 +11865,7 @@ var fml = (function (exports) {
|
|
|
11849
11865
|
let typedAt = 0;
|
|
11850
11866
|
menu.addEventListener('beforetoggle', (/** @type any */ evt) => {
|
|
11851
11867
|
if (evt.newState === 'closed' && menu.contains(document.activeElement)) {
|
|
11852
|
-
invoker
|
|
11868
|
+
invoker()?.focus();
|
|
11853
11869
|
}
|
|
11854
11870
|
});
|
|
11855
11871
|
menu.addEventListener('toggle', (/** @type any */ evt) => {
|
|
@@ -11893,11 +11909,11 @@ var fml = (function (exports) {
|
|
|
11893
11909
|
case 'Space': {
|
|
11894
11910
|
evt.preventDefault();
|
|
11895
11911
|
item.click();
|
|
11896
|
-
invoker
|
|
11912
|
+
invoker()?.focus();
|
|
11897
11913
|
return;
|
|
11898
11914
|
}
|
|
11899
11915
|
case 'Escape': {
|
|
11900
|
-
invoker
|
|
11916
|
+
invoker()?.focus();
|
|
11901
11917
|
return;
|
|
11902
11918
|
}
|
|
11903
11919
|
}
|
|
@@ -12122,7 +12138,7 @@ var fml = (function (exports) {
|
|
|
12122
12138
|
}
|
|
12123
12139
|
#wire() {
|
|
12124
12140
|
Anchors.wire(this.#button, this.#menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
|
|
12125
|
-
wireMenuKeys(this.#button, this.#menu, {
|
|
12141
|
+
wireMenuKeys(() => this.#button, this.#menu, {
|
|
12126
12142
|
items: () => this.#items(),
|
|
12127
12143
|
current: () => this.#items().find((a) => a.getAttribute('value') === this.value),
|
|
12128
12144
|
});
|
|
@@ -13120,11 +13136,12 @@ var fml = (function (exports) {
|
|
|
13120
13136
|
* `show`, and fills the content with what the callback resolves to. A
|
|
13121
13137
|
* rejection is shown, as the problems' reasons, in the error section, and
|
|
13122
13138
|
* the content is hidden. When a newer `update()` starts before the callback
|
|
13123
|
-
* settles, this call paints nothing
|
|
13124
|
-
*
|
|
13139
|
+
* settles, this call paints nothing and resolves `null`, the section being
|
|
13140
|
+
* the newer call's; it still rejects with the callback's error.
|
|
13125
13141
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
13126
13142
|
* @param {() => void} show opens the dialog when it is closed
|
|
13127
|
-
* @returns {Promise<HTMLElement>} the content section
|
|
13143
|
+
* @returns {Promise<HTMLElement|null>} the content section, or `null` for a
|
|
13144
|
+
* call a newer one overtook
|
|
13128
13145
|
* @throws {any} what the callback threw or rejected with
|
|
13129
13146
|
*/
|
|
13130
13147
|
async update(cb, show) {
|
|
@@ -13137,7 +13154,7 @@ var fml = (function (exports) {
|
|
|
13137
13154
|
try {
|
|
13138
13155
|
const delivered = await cb();
|
|
13139
13156
|
if (claim.stale) {
|
|
13140
|
-
return
|
|
13157
|
+
return null;
|
|
13141
13158
|
}
|
|
13142
13159
|
this.#section.replaceChildren(delivered);
|
|
13143
13160
|
this.#loading?.setAttribute('hidden', '');
|
|
@@ -13383,11 +13400,12 @@ var fml = (function (exports) {
|
|
|
13383
13400
|
*
|
|
13384
13401
|
* No `section:requested` is dispatched, and no `ask()` waiter is added:
|
|
13385
13402
|
* listen for the `close` event to learn how the dialog ended. When a newer
|
|
13386
|
-
* `update()` starts before the callback settles, this call paints nothing
|
|
13387
|
-
*
|
|
13388
|
-
* callback's error.
|
|
13403
|
+
* `update()` starts before the callback settles, this call paints nothing
|
|
13404
|
+
* and resolves `null`, the body being the newer call's; it still rejects
|
|
13405
|
+
* with the callback's error.
|
|
13389
13406
|
* @param {() => Node|string|Promise<Node|string>} cb producing the body content
|
|
13390
|
-
* @returns {Promise<Element>} the body section, in the document
|
|
13407
|
+
* @returns {Promise<Element|null>} the body section, in the document, or
|
|
13408
|
+
* `null` for a call a newer one overtook
|
|
13391
13409
|
* @throws {any} what the callback threw or rejected with
|
|
13392
13410
|
*/
|
|
13393
13411
|
async update(cb) {
|
|
@@ -13677,12 +13695,13 @@ var fml = (function (exports) {
|
|
|
13677
13695
|
* which has `role="alert"`, and the content is hidden.
|
|
13678
13696
|
*
|
|
13679
13697
|
* No `section:requested` is dispatched. When a newer `update()` starts
|
|
13680
|
-
* before the callback settles, this call paints nothing
|
|
13681
|
-
* the content section
|
|
13682
|
-
* error.
|
|
13698
|
+
* before the callback settles, this call paints nothing and resolves
|
|
13699
|
+
* `null`, the content section being the newer call's; it still rejects with
|
|
13700
|
+
* the callback's error.
|
|
13683
13701
|
* @param {string} header the title, set as text
|
|
13684
13702
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
13685
|
-
* @returns {Promise<Element>} the content section, in the document
|
|
13703
|
+
* @returns {Promise<Element|null>} the content section, in the document, or
|
|
13704
|
+
* `null` for a call a newer one overtook
|
|
13686
13705
|
* @throws {any} what the callback threw or rejected with
|
|
13687
13706
|
*/
|
|
13688
13707
|
async update(header, cb) {
|
|
@@ -13831,11 +13850,15 @@ var fml = (function (exports) {
|
|
|
13831
13850
|
}
|
|
13832
13851
|
|
|
13833
13852
|
/**
|
|
13834
|
-
* A menu of commands on the native popover platform
|
|
13835
|
-
*
|
|
13836
|
-
*
|
|
13853
|
+
* A menu of commands on the native popover platform. The menu is the popover
|
|
13854
|
+
* itself: light dismiss and Escape come from the browser.
|
|
13855
|
+
*
|
|
13856
|
+
* A menu naming an invoker by id through `for` belongs to that element: the
|
|
13857
|
+
* invoker toggles it, and it is anchored to the invoker through
|
|
13837
13858
|
* `Anchors.wire`, placed in script where the platform has no css anchor
|
|
13838
|
-
* positioning.
|
|
13859
|
+
* positioning. A menu without `for` is shared: one menu serves many
|
|
13860
|
+
* invokers, such as the "more" button of every row of a list, and a page
|
|
13861
|
+
* opens it beside the invoker it is for with `showFor(invoker)`.
|
|
13839
13862
|
*
|
|
13840
13863
|
* The items are the buttons and `a[href]` links written as its direct
|
|
13841
13864
|
* children, kept as they are. Activation is the item's own click, which the
|
|
@@ -13845,34 +13868,44 @@ var fml = (function (exports) {
|
|
|
13845
13868
|
* The keyboard is the ARIA menu protocol: the focus moves to the
|
|
13846
13869
|
* first item as the menu opens, the arrows, Home and End walk the items,
|
|
13847
13870
|
* typing searches their text, Enter and Space activate the focused one, and
|
|
13848
|
-
* Escape and a close from inside the menu hand the focus back to the invoker
|
|
13871
|
+
* Escape and a close from inside the menu hand the focus back to the invoker
|
|
13872
|
+
* that opened it.
|
|
13849
13873
|
*/
|
|
13850
13874
|
class Menu extends ParsedElement {
|
|
13851
13875
|
/** @type {string[]} */
|
|
13852
13876
|
static attributes = ['for'];
|
|
13877
|
+
/** @type {HTMLElement|null} */
|
|
13878
|
+
#invoker = null;
|
|
13879
|
+
/** @type {HTMLElement|null} */
|
|
13880
|
+
#pressed = null;
|
|
13853
13881
|
/**
|
|
13854
13882
|
* Gives the menu the `menu` role and an empty `popover` attribute unless
|
|
13855
|
-
* it already has one,
|
|
13856
|
-
*
|
|
13857
|
-
*
|
|
13883
|
+
* it already has one, and marks the items. With `for`, read here once, it
|
|
13884
|
+
* also gives the invoker `aria-haspopup="menu"`, an `aria-expanded` kept
|
|
13885
|
+
* in step, and the toggle of the menu.
|
|
13858
13886
|
* @returns {void}
|
|
13859
|
-
* @throws {Error} when `for` names no element in the document at the render
|
|
13887
|
+
* @throws {Error} when `for` is given and names no element in the document at the render
|
|
13860
13888
|
*/
|
|
13861
13889
|
render() {
|
|
13862
13890
|
const named = this.declared('for');
|
|
13863
13891
|
const invoker = named ? document.getElementById(named) : null;
|
|
13864
|
-
if (!invoker) {
|
|
13892
|
+
if (named && !invoker) {
|
|
13865
13893
|
throw new Error(
|
|
13866
|
-
`ful-menu names no invoker: for="${named
|
|
13894
|
+
`ful-menu names no invoker: for="${named}" must name an element already in the page when the menu upgrades`,
|
|
13867
13895
|
);
|
|
13868
13896
|
}
|
|
13869
13897
|
this.setAttribute('role', 'menu');
|
|
13870
13898
|
if (!this.hasAttribute('popover')) {
|
|
13871
13899
|
this.setAttribute('popover', '');
|
|
13872
13900
|
}
|
|
13873
|
-
invoker
|
|
13874
|
-
|
|
13875
|
-
|
|
13901
|
+
if (invoker) {
|
|
13902
|
+
this.#invoker = invoker;
|
|
13903
|
+
invoker.setAttribute('aria-haspopup', 'menu');
|
|
13904
|
+
Anchors.wire(invoker, this, { prefix: 'ful-menu', invoke: true, expanded: true });
|
|
13905
|
+
} else {
|
|
13906
|
+
this.#wireShared();
|
|
13907
|
+
}
|
|
13908
|
+
wireMenuKeys(() => this.#invoker, this, { items: () => this.items });
|
|
13876
13909
|
this.#mark();
|
|
13877
13910
|
this.addEventListener('click', (evt) => {
|
|
13878
13911
|
const target = /** @type HTMLElement */ (evt.target);
|
|
@@ -13881,6 +13914,43 @@ var fml = (function (exports) {
|
|
|
13881
13914
|
}
|
|
13882
13915
|
});
|
|
13883
13916
|
}
|
|
13917
|
+
/**
|
|
13918
|
+
* Opens a shared menu, one without `for`, beside `invoker`, or moves it
|
|
13919
|
+
* there when it is open for another one. The menu goes below the invoker,
|
|
13920
|
+
* or above it where there is no room below, and is placed once, as
|
|
13921
|
+
* `Anchors.show` places it. The invoker gets `aria-haspopup="menu"` and an
|
|
13922
|
+
* `aria-expanded` that reads true until the menu closes, and the focus
|
|
13923
|
+
* goes back to it on Escape. A call for the invoker whose press has just
|
|
13924
|
+
* closed the menu does nothing, so a second click on the same button
|
|
13925
|
+
* closes the menu rather than opening it again.
|
|
13926
|
+
* @param {HTMLElement} invoker the element the menu opens for
|
|
13927
|
+
* @returns {void}
|
|
13928
|
+
* @throws {Error} when the menu names its invoker through `for`
|
|
13929
|
+
*/
|
|
13930
|
+
showFor(invoker) {
|
|
13931
|
+
if (this.declared('for')) {
|
|
13932
|
+
throw new Error('ful-menu: showFor serves a menu without for; a menu with for is opened by its invoker');
|
|
13933
|
+
}
|
|
13934
|
+
const pressed = this.#pressed;
|
|
13935
|
+
this.#pressed = null;
|
|
13936
|
+
if (pressed === invoker) {
|
|
13937
|
+
return;
|
|
13938
|
+
}
|
|
13939
|
+
if (this.#invoker && this.#invoker !== invoker) {
|
|
13940
|
+
this.#invoker.setAttribute('aria-expanded', 'false');
|
|
13941
|
+
}
|
|
13942
|
+
this.#invoker = invoker;
|
|
13943
|
+
invoker.setAttribute('aria-haspopup', 'menu');
|
|
13944
|
+
invoker.setAttribute('aria-expanded', 'true');
|
|
13945
|
+
//the previous placement wrote margin: 0 inline, hiding the stylesheet's gap
|
|
13946
|
+
this.style.margin = '';
|
|
13947
|
+
const gap = Number.parseFloat(getComputedStyle(this).marginBlockStart) || 0;
|
|
13948
|
+
const moving = this.matches(':popover-open');
|
|
13949
|
+
Anchors.show(this, invoker, { flip: true, gap });
|
|
13950
|
+
if (moving) {
|
|
13951
|
+
this.items[0]?.focus();
|
|
13952
|
+
}
|
|
13953
|
+
}
|
|
13884
13954
|
/**
|
|
13885
13955
|
* The commands, in the order the arrows walk them: the menu's direct
|
|
13886
13956
|
* `button` and `a[href]` children, minus those carrying a `disabled` or
|
|
@@ -13892,6 +13962,21 @@ var fml = (function (exports) {
|
|
|
13892
13962
|
get items() {
|
|
13893
13963
|
return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
|
|
13894
13964
|
}
|
|
13965
|
+
#wireShared() {
|
|
13966
|
+
document.addEventListener(
|
|
13967
|
+
'pointerdown',
|
|
13968
|
+
(evt) => {
|
|
13969
|
+
const target = /** @type Node */ (evt.target);
|
|
13970
|
+
this.#pressed = this.matches(':popover-open') && this.#invoker?.contains(target) ? this.#invoker : null;
|
|
13971
|
+
},
|
|
13972
|
+
true,
|
|
13973
|
+
);
|
|
13974
|
+
this.addEventListener('toggle', (/** @type any */ evt) => {
|
|
13975
|
+
if (evt.newState === 'closed') {
|
|
13976
|
+
this.#invoker?.setAttribute('aria-expanded', 'false');
|
|
13977
|
+
}
|
|
13978
|
+
});
|
|
13979
|
+
}
|
|
13895
13980
|
/** @returns {HTMLElement[]} every item, disabled and hidden ones included, once marked */
|
|
13896
13981
|
#mark() {
|
|
13897
13982
|
const all = /** @type HTMLElement[] */ ([...this.querySelectorAll(':scope > button, :scope > a[href]')]);
|