@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/ful.d.mts CHANGED
@@ -740,7 +740,8 @@ declare class FormLoader {
740
740
  *
741
741
  * Attributes on the host starting with `form-` are forwarded, without the
742
742
  * prefix, to the native form. `autocomplete` is copied onto the native form as
743
- * 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.
744
745
  * `clear-invalid-on-change` clears the custom validity of a field of this form
745
746
  * when a `change` event bubbles from it; the field's form is read from its
746
747
  * `form` property, or from `internals.form` for a form-associated element
@@ -3160,11 +3161,15 @@ declare class Drawer extends ParsedElement {
3160
3161
  close(result?: string): void;
3161
3162
  }
3162
3163
  /**
3163
- * A menu of commands on the native popover platform, opened by the element
3164
- * whose id `for` names. The menu is the popover itself: light dismiss and
3165
- * Escape come from the browser, and it is anchored to its invoker through
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
3166
3169
  * `Anchors.wire`, placed in script where the platform has no css anchor
3167
- * 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)`.
3168
3173
  *
3169
3174
  * The items are the buttons and `a[href]` links written as its direct
3170
3175
  * children, kept as they are. Activation is the item's own click, which the
@@ -3174,7 +3179,8 @@ declare class Drawer extends ParsedElement {
3174
3179
  * The keyboard is the ARIA menu protocol: the focus moves to the
3175
3180
  * first item as the menu opens, the arrows, Home and End walk the items,
3176
3181
  * typing searches their text, Enter and Space activate the focused one, and
3177
- * 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.
3178
3184
  */
3179
3185
  declare class Menu extends ParsedElement {
3180
3186
  #private;
@@ -3182,13 +3188,27 @@ declare class Menu extends ParsedElement {
3182
3188
  static attributes: string[];
3183
3189
  /**
3184
3190
  * Gives the menu the `menu` role and an empty `popover` attribute unless
3185
- * it already has one, gives the invoker `aria-haspopup="menu"`, an
3186
- * `aria-expanded` kept in step, and the toggle of the menu, and marks the
3187
- * items. `for` is read here, once.
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.
3188
3194
  * @returns {void}
3189
- * @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
3190
3196
  */
3191
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;
3192
3212
  /**
3193
3213
  * The commands, in the order the arrows walk them: the menu's direct
3194
3214
  * `button` and `a[href]` children, minus those carrying a `disabled` or
package/dist/ful.iife.js CHANGED
@@ -1358,7 +1358,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1393
1394
  const form = document.createElement('form');
1394
1395
  this.form = form;
1395
1396
  form.setAttribute('novalidate', '');
1396
- index_mjs.Attributes.forward('form-', this, form);
1397
1397
  index_mjs.Attributes.set(form, 'autocomplete', this.declared('autocomplete'));
1398
+ index_mjs.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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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, and where the focus goes back
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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.focus();
5598
+ invoker()?.focus();
5596
5599
  }
5597
5600
  });
5598
5601
  menu.addEventListener('toggle', (/** @type any */ evt) => {
@@ -5636,11 +5639,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5636
5639
  case 'Space': {
5637
5640
  evt.preventDefault();
5638
5641
  item.click();
5639
- invoker.focus();
5642
+ invoker()?.focus();
5640
5643
  return;
5641
5644
  }
5642
5645
  case 'Escape': {
5643
- invoker.focus();
5646
+ invoker()?.focus();
5644
5647
  return;
5645
5648
  }
5646
5649
  }
@@ -5865,7 +5868,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7577
7580
  }
7578
7581
 
7579
7582
  /**
7580
- * A menu of commands on the native popover platform, opened by the element
7581
- * whose id `for` names. The menu is the popover itself: light dismiss and
7582
- * Escape come from the browser, and it is anchored to its invoker through
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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 index_mjs.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, gives the invoker `aria-haspopup="menu"`, an
7602
- * `aria-expanded` kept in step, and the toggle of the menu, and marks the
7603
- * items. `for` is read here, once.
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 ?? ''}" must name an element already in the page when the menu upgrades`,
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.setAttribute('aria-haspopup', 'menu');
7620
- Anchors.wire(invoker, this, { prefix: 'ful-menu', invoke: true, expanded: true });
7621
- wireMenuKeys(invoker, this, { items: () => this.items });
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
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]')]);