@optionfactory/fml 9.0.0-rc24 → 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 +2 -2
- package/dist/fml.iife.js +90 -21
- 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 +30 -10
- package/dist/ful.iife.js +90 -21
- 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 +90 -21
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +2 -2
- package/dist/web-types.json +3 -3
- package/package.json +1 -1
package/dist/ful.mjs
CHANGED
|
@@ -1358,7 +1358,8 @@ const ownerOf = (el) =>
|
|
|
1358
1358
|
*
|
|
1359
1359
|
* Attributes on the host starting with `form-` are forwarded, without the
|
|
1360
1360
|
* prefix, to the native form. `autocomplete` is copied onto the native form as
|
|
1361
|
-
* 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.
|
|
1362
1363
|
* `clear-invalid-on-change` clears the custom validity of a field of this form
|
|
1363
1364
|
* when a `change` event bubbles from it; the field's form is read from its
|
|
1364
1365
|
* `form` property, or from `internals.form` for a form-associated element
|
|
@@ -1393,8 +1394,8 @@ class Form extends ParsedElement {
|
|
|
1393
1394
|
const form = document.createElement('form');
|
|
1394
1395
|
this.form = form;
|
|
1395
1396
|
form.setAttribute('novalidate', '');
|
|
1396
|
-
Attributes.forward('form-', this, form);
|
|
1397
1397
|
Attributes.set(form, 'autocomplete', this.declared('autocomplete'));
|
|
1398
|
+
Attributes.forward('form-', this, form);
|
|
1398
1399
|
form.replaceChildren(...this.childNodes);
|
|
1399
1400
|
form.addEventListener('submit', async (e) => {
|
|
1400
1401
|
e.preventDefault();
|
|
@@ -5582,7 +5583,9 @@ const TYPEAHEAD_WINDOW = 500;
|
|
|
5582
5583
|
* `items` answers the items in the order they are walked, and is called on
|
|
5583
5584
|
* every gesture, so a menu that fills itself later needs no rewiring.
|
|
5584
5585
|
*
|
|
5585
|
-
* @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
|
|
5586
5589
|
* @param {HTMLElement} menu the `[popover]` holding the items
|
|
5587
5590
|
* @param {{items: () => HTMLElement[], current?: () => HTMLElement|null|undefined}} conf
|
|
5588
5591
|
* `items` answers the items; `current` answers the item an opening focuses
|
|
@@ -5592,7 +5595,7 @@ const wireMenuKeys = (invoker, menu, { items, current = () => null }) => {
|
|
|
5592
5595
|
let typedAt = 0;
|
|
5593
5596
|
menu.addEventListener('beforetoggle', (/** @type any */ evt) => {
|
|
5594
5597
|
if (evt.newState === 'closed' && menu.contains(document.activeElement)) {
|
|
5595
|
-
invoker
|
|
5598
|
+
invoker()?.focus();
|
|
5596
5599
|
}
|
|
5597
5600
|
});
|
|
5598
5601
|
menu.addEventListener('toggle', (/** @type any */ evt) => {
|
|
@@ -5636,11 +5639,11 @@ const wireMenuKeys = (invoker, menu, { items, current = () => null }) => {
|
|
|
5636
5639
|
case 'Space': {
|
|
5637
5640
|
evt.preventDefault();
|
|
5638
5641
|
item.click();
|
|
5639
|
-
invoker
|
|
5642
|
+
invoker()?.focus();
|
|
5640
5643
|
return;
|
|
5641
5644
|
}
|
|
5642
5645
|
case 'Escape': {
|
|
5643
|
-
invoker
|
|
5646
|
+
invoker()?.focus();
|
|
5644
5647
|
return;
|
|
5645
5648
|
}
|
|
5646
5649
|
}
|
|
@@ -5865,7 +5868,7 @@ class ChoiceButton {
|
|
|
5865
5868
|
}
|
|
5866
5869
|
#wire() {
|
|
5867
5870
|
Anchors.wire(this.#button, this.#menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
|
|
5868
|
-
wireMenuKeys(this.#button, this.#menu, {
|
|
5871
|
+
wireMenuKeys(() => this.#button, this.#menu, {
|
|
5869
5872
|
items: () => this.#items(),
|
|
5870
5873
|
current: () => this.#items().find((a) => a.getAttribute('value') === this.value),
|
|
5871
5874
|
});
|
|
@@ -7577,11 +7580,15 @@ class Drawer extends ParsedElement {
|
|
|
7577
7580
|
}
|
|
7578
7581
|
|
|
7579
7582
|
/**
|
|
7580
|
-
* A menu of commands on the native popover platform
|
|
7581
|
-
*
|
|
7582
|
-
*
|
|
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
|
|
7583
7588
|
* `Anchors.wire`, placed in script where the platform has no css anchor
|
|
7584
|
-
* 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)`.
|
|
7585
7592
|
*
|
|
7586
7593
|
* The items are the buttons and `a[href]` links written as its direct
|
|
7587
7594
|
* children, kept as they are. Activation is the item's own click, which the
|
|
@@ -7591,34 +7598,44 @@ class Drawer extends ParsedElement {
|
|
|
7591
7598
|
* The keyboard is the ARIA menu protocol: the focus moves to the
|
|
7592
7599
|
* first item as the menu opens, the arrows, Home and End walk the items,
|
|
7593
7600
|
* typing searches their text, Enter and Space activate the focused one, and
|
|
7594
|
-
* 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.
|
|
7595
7603
|
*/
|
|
7596
7604
|
class Menu extends ParsedElement {
|
|
7597
7605
|
/** @type {string[]} */
|
|
7598
7606
|
static attributes = ['for'];
|
|
7607
|
+
/** @type {HTMLElement|null} */
|
|
7608
|
+
#invoker = null;
|
|
7609
|
+
/** @type {HTMLElement|null} */
|
|
7610
|
+
#pressed = null;
|
|
7599
7611
|
/**
|
|
7600
7612
|
* Gives the menu the `menu` role and an empty `popover` attribute unless
|
|
7601
|
-
* it already has one,
|
|
7602
|
-
*
|
|
7603
|
-
*
|
|
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.
|
|
7604
7616
|
* @returns {void}
|
|
7605
|
-
* @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
|
|
7606
7618
|
*/
|
|
7607
7619
|
render() {
|
|
7608
7620
|
const named = this.declared('for');
|
|
7609
7621
|
const invoker = named ? document.getElementById(named) : null;
|
|
7610
|
-
if (!invoker) {
|
|
7622
|
+
if (named && !invoker) {
|
|
7611
7623
|
throw new Error(
|
|
7612
|
-
`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`,
|
|
7613
7625
|
);
|
|
7614
7626
|
}
|
|
7615
7627
|
this.setAttribute('role', 'menu');
|
|
7616
7628
|
if (!this.hasAttribute('popover')) {
|
|
7617
7629
|
this.setAttribute('popover', '');
|
|
7618
7630
|
}
|
|
7619
|
-
invoker
|
|
7620
|
-
|
|
7621
|
-
|
|
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 });
|
|
7622
7639
|
this.#mark();
|
|
7623
7640
|
this.addEventListener('click', (evt) => {
|
|
7624
7641
|
const target = /** @type HTMLElement */ (evt.target);
|
|
@@ -7627,6 +7644,43 @@ class Menu extends ParsedElement {
|
|
|
7627
7644
|
}
|
|
7628
7645
|
});
|
|
7629
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
|
+
}
|
|
7630
7684
|
/**
|
|
7631
7685
|
* The commands, in the order the arrows walk them: the menu's direct
|
|
7632
7686
|
* `button` and `a[href]` children, minus those carrying a `disabled` or
|
|
@@ -7638,6 +7692,21 @@ class Menu extends ParsedElement {
|
|
|
7638
7692
|
get items() {
|
|
7639
7693
|
return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
|
|
7640
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
|
+
}
|
|
7641
7710
|
/** @returns {HTMLElement[]} every item, disabled and hidden ones included, once marked */
|
|
7642
7711
|
#mark() {
|
|
7643
7712
|
const all = /** @type HTMLElement[] */ ([...this.querySelectorAll(':scope > button, :scope > a[href]')]);
|