@optionfactory/fml 9.0.0-rc14 → 9.0.0-rc15

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
@@ -664,8 +664,9 @@ declare class Anchors {
664
664
  * start-aligned, clamped into the viewport; `flip` moves it above the
665
665
  * anchor where the viewport leaves no room below, as the css
666
666
  * `position-try-fallbacks` the paired popovers declare. A popover not
667
- * already open is shown before it is placed, so the first position anybody
668
- * sees is the right one.
667
+ * already open is shown and placed before the call returns, so the first
668
+ * position anybody sees is the right one and the caller can move the focus
669
+ * into it on the next line.
669
670
  *
670
671
  * @param {HTMLElement} popover the `[popover]` element to show, or an
671
672
  * already-shown element to place
@@ -1228,13 +1229,19 @@ declare class Dialog extends ParsedElement {
1228
1229
  *
1229
1230
  * The body is text or a node: a string is never read as markup, an
1230
1231
  * `innerHTML` sink being a thing the library does not carry. Each button is
1231
- * a `[result, label, className?]` tuple, rendered as its answer.
1232
+ * a `[result, label, className?]` tuple rendered as its answer, or a node
1233
+ * the caller has built itself, which is how a button carries anything a
1234
+ * tuple cannot say and how a footer gets a spacer.
1232
1235
  * @param {string} header
1233
1236
  * @param {string|Node} body
1234
- * @param {[string, string, string?][]} [buttons]
1237
+ * @param {([string, string, string?]|Node)[]} [buttons]
1238
+ * @param {{ className?: string }} [options] `className` dresses the dialog,
1239
+ * the way a declared `ful-dialog` is given one to size it
1235
1240
  * @returns {Promise<DialogOutcome>}
1236
1241
  */
1237
- static ask(header: string, body: string | Node, buttons?: [string, string, string?][]): Promise<DialogOutcome>;
1242
+ static ask(header: string, body: string | Node, buttons?: ([string, string, string?] | Node)[], { className }?: {
1243
+ className?: string;
1244
+ }): Promise<DialogOutcome>;
1238
1245
  /**
1239
1246
  * Asks a yes/no question, resolving with the confirm button's answer:
1240
1247
  * the cancel button, the close button and Escape all answer false, a
@@ -1242,11 +1249,14 @@ declare class Dialog extends ParsedElement {
1242
1249
  * @param {string} header
1243
1250
  * @param {string|Node} body
1244
1251
  * @param {{ confirm?: string, cancel?: string }} [labels] overriding the localized defaults
1252
+ * @param {{ className?: string }} [options] as `ask` takes them
1245
1253
  * @returns {Promise<boolean>}
1246
1254
  */
1247
1255
  static confirm(header: string, body: string | Node, labels?: {
1248
1256
  confirm?: string;
1249
1257
  cancel?: string;
1258
+ }, options?: {
1259
+ className?: string;
1250
1260
  }): Promise<boolean>;
1251
1261
  }
1252
1262
  /**
@@ -1284,6 +1294,26 @@ declare class Drawer extends ParsedElement {
1284
1294
  open(): void;
1285
1295
  close(): void;
1286
1296
  }
1297
+ /**
1298
+ * A menu of commands on the native popover platform: the invoker named by
1299
+ * `for` toggles it and light dismiss and Escape come from the browser, while
1300
+ * the element adds the menu semantics and the keyboard protocol.
1301
+ *
1302
+ * The items are the buttons and links written inside it, kept as they are and
1303
+ * given their menu role and their place in the roving focus. Activation is the
1304
+ * item's own click, which the element only follows by closing.
1305
+ */
1306
+ declare class Menu extends ParsedElement {
1307
+ #private;
1308
+ static attributes: string[];
1309
+ render(): void;
1310
+ /**
1311
+ * The commands, in the order the arrows walk them: the menu's own buttons
1312
+ * and links, minus the disabled and hidden ones, which keep their role and
1313
+ * leave the walk.
1314
+ */
1315
+ get items(): HTMLElement[];
1316
+ }
1287
1317
  /** A transient feedback region: each show() stacks a toast that retires on its own timer. */
1288
1318
  declare class Toasts extends ParsedElement {
1289
1319
  #private;
@@ -1412,6 +1442,6 @@ declare class Plugin {
1412
1442
  });
1413
1443
  configure(registry: any): void;
1414
1444
  }
1415
- export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, NumberFilter, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, TableSchemaParser, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
1445
+ export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, Menu, NumberFilter, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, TableSchemaParser, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
1416
1446
 
1417
1447
  export as namespace ful;
package/dist/ful.iife.js CHANGED
@@ -2304,7 +2304,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2304
2304
 
2305
2305
  const showOncePlaced = (popover, anchored) => {
2306
2306
  popover.style.visibility = 'hidden';
2307
- requestAnimationFrame(() => {
2307
+ queueMicrotask(() => {
2308
2308
  if (popover.matches(':popover-open')) {
2309
2309
  place(popover, anchored);
2310
2310
  }
@@ -2443,8 +2443,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2443
2443
  * start-aligned, clamped into the viewport; `flip` moves it above the
2444
2444
  * anchor where the viewport leaves no room below, as the css
2445
2445
  * `position-try-fallbacks` the paired popovers declare. A popover not
2446
- * already open is shown before it is placed, so the first position anybody
2447
- * sees is the right one.
2446
+ * already open is shown and placed before the call returns, so the first
2447
+ * position anybody sees is the right one and the caller can move the focus
2448
+ * into it on the next line.
2448
2449
  *
2449
2450
  * @param {HTMLElement} popover the `[popover]` element to show, or an
2450
2451
  * already-shown element to place
@@ -2470,16 +2471,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2470
2471
  popover.style.left = `${clamp(box.left, PAD, Math.max(PAD, viewport.clientWidth - here.width - PAD))}px`;
2471
2472
  popover.style.top = `${clamp(top, PAD, Math.max(PAD, viewport.clientHeight - here.height - PAD))}px`;
2472
2473
  };
2473
- if (popover.matches(':popover-open') || typeof popover.showPopover !== 'function') {
2474
- placeShown();
2475
- return popover;
2476
- }
2477
- popover.style.visibility = 'hidden';
2478
- popover.showPopover();
2479
- requestAnimationFrame(() => {
2480
- placeShown();
2481
- popover.style.removeProperty('visibility');
2482
- });
2474
+ if (typeof popover.showPopover === 'function' && !popover.matches(':popover-open')) {
2475
+ popover.showPopover();
2476
+ }
2477
+ placeShown();
2483
2478
  return popover;
2484
2479
  }
2485
2480
  }
@@ -2771,6 +2766,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2771
2766
  //from the host when it gave one, since it has to set aria-controls before
2772
2767
  //this element upgrades
2773
2768
  this.#menu.id = this.declared('listbox') || index_mjs.Attributes.uid('ful-listbox');
2769
+ this.#menu.addEventListener('mousedown', (evt) => {
2770
+ if (evt.target.closest('li')) {
2771
+ evt.preventDefault();
2772
+ }
2773
+ });
2774
2774
  this.#menu.addEventListener('click', (evt) => {
2775
2775
  evt.stopPropagation();
2776
2776
  const li = evt.target.closest('li');
@@ -2778,7 +2778,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2778
2778
  this.hide();
2779
2779
  return;
2780
2780
  }
2781
- if (li.hasAttribute('aria-disabled')) {
2781
+ if (li.matches('[aria-disabled="true"]')) {
2782
2782
  return;
2783
2783
  }
2784
2784
  this.#change(li);
@@ -2787,8 +2787,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2787
2787
  }
2788
2788
  #selected() {
2789
2789
  return (
2790
- this.#menu?.querySelector('[selected]:not([aria-disabled])') ??
2791
- this.#menu?.querySelector('li:not([aria-disabled])') ??
2790
+ this.#menu?.querySelector('[selected]:not([aria-disabled="true"])') ??
2791
+ this.#menu?.querySelector('li:not([aria-disabled="true"])') ??
2792
2792
  null
2793
2793
  );
2794
2794
  }
@@ -2830,7 +2830,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2830
2830
  li.setAttribute('aria-selected', picked ? 'true' : 'false');
2831
2831
  //metadata.disabled refuses the entry, metadata.reason saying why
2832
2832
  const disabled = !!entry?.metadata?.disabled;
2833
- li.toggleAttribute('aria-disabled', disabled);
2833
+ if (disabled) {
2834
+ li.setAttribute('aria-disabled', 'true');
2835
+ } else {
2836
+ li.removeAttribute('aria-disabled');
2837
+ }
2834
2838
  if (disabled && entry.metadata.reason) {
2835
2839
  li.setAttribute('title', String(entry.metadata.reason));
2836
2840
  } else {
@@ -2919,7 +2923,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2919
2923
  }
2920
2924
  #walk(from, forward) {
2921
2925
  for (let li = from; li; li = li[`${forward ? 'next' : 'previous'}ElementSibling`]) {
2922
- if (!li.hasAttribute('aria-disabled')) {
2926
+ if (!li.matches('[aria-disabled="true"]')) {
2923
2927
  return li;
2924
2928
  }
2925
2929
  }
@@ -4363,15 +4367,110 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4363
4367
  }
4364
4368
  }
4365
4369
 
4370
+ /** how long a type-ahead buffer stands before the next character starts a new search */
4371
+ const TYPEAHEAD_WINDOW = 500;
4372
+
4373
+ /**
4374
+ * The keyboard protocol of a menu popover: the focus moving into the menu as it
4375
+ * opens and back to the invoker as it closes, roving focus over the items with
4376
+ * the arrows, Home and End, type-ahead over their text, and Enter and Space
4377
+ * activating the focused one.
4378
+ *
4379
+ * The menu owns what an item is: `items` answers them in the order they are
4380
+ * walked, read on every gesture so a menu that fills itself later needs no
4381
+ * rewiring, and `current` names the one an opening focuses, the first otherwise.
4382
+ *
4383
+ * @param {HTMLElement} invoker the element the menu belongs to, and where the focus goes back
4384
+ * @param {HTMLElement} menu the `[popover]` holding the items
4385
+ * @param {{items: () => HTMLElement[], current?: () => HTMLElement|null|undefined}} conf
4386
+ */
4387
+ const wireMenuKeys = (invoker, menu, { items, current = () => null }) => {
4388
+ let typed = '';
4389
+ let typedAt = 0;
4390
+ menu.addEventListener('toggle', (/** @type any */ evt) => {
4391
+ if (evt.newState !== 'open') {
4392
+ //give the invoker back the focus the menu had borrowed, without
4393
+ //stealing it from wherever else the close came from
4394
+ if (menu.contains(document.activeElement)) {
4395
+ invoker.focus();
4396
+ }
4397
+ return;
4398
+ }
4399
+ const all = items();
4400
+ (current() ?? all[0])?.focus();
4401
+ });
4402
+ menu.addEventListener('keydown', (evt) => {
4403
+ const target = /** @type HTMLElement */ (evt.target);
4404
+ const all = items();
4405
+ const item = all.find((candidate) => candidate === target || candidate.contains(target));
4406
+ if (!item) {
4407
+ return;
4408
+ }
4409
+ const at = all.indexOf(item);
4410
+ switch (evt.code) {
4411
+ case 'ArrowDown': {
4412
+ evt.preventDefault();
4413
+ all[(at + 1) % all.length]?.focus();
4414
+ return;
4415
+ }
4416
+ case 'ArrowUp': {
4417
+ evt.preventDefault();
4418
+ all[(at - 1 + all.length) % all.length]?.focus();
4419
+ return;
4420
+ }
4421
+ case 'Home': {
4422
+ evt.preventDefault();
4423
+ all[0]?.focus();
4424
+ return;
4425
+ }
4426
+ case 'End': {
4427
+ evt.preventDefault();
4428
+ all[all.length - 1]?.focus();
4429
+ return;
4430
+ }
4431
+ case 'Enter':
4432
+ case 'Space': {
4433
+ evt.preventDefault();
4434
+ item.click();
4435
+ invoker.focus();
4436
+ return;
4437
+ }
4438
+ case 'Escape': {
4439
+ //the platform's close request hides the menu, the focus is placed
4440
+ //on the invoker before the focused item is detached from it
4441
+ invoker.focus();
4442
+ return;
4443
+ }
4444
+ }
4445
+ if (evt.key.length !== 1 || evt.ctrlKey || evt.altKey || evt.metaKey) {
4446
+ return;
4447
+ }
4448
+ const now = Date.now();
4449
+ typed = now - typedAt > TYPEAHEAD_WINDOW ? evt.key : typed + evt.key;
4450
+ typedAt = now;
4451
+ const wanted = typed.toLowerCase();
4452
+ const repeated = [...wanted].every((character) => character === wanted[0]);
4453
+ const search = repeated ? wanted[0] : wanted;
4454
+ const from = repeated ? at + 1 : at;
4455
+ const found = [...all.slice(from), ...all.slice(0, from)].find((candidate) =>
4456
+ (candidate.textContent ?? '').trim().toLowerCase().startsWith(search),
4457
+ );
4458
+ if (found) {
4459
+ evt.preventDefault();
4460
+ found.focus();
4461
+ }
4462
+ });
4463
+ };
4464
+
4366
4465
  /**
4367
4466
  * An invoker button paired with the `ul[popover][role=menu]` that follows it:
4368
4467
  * the chrome behind every filter's operator, sensitivity and boolean value.
4369
4468
  *
4370
4469
  * It fills the menu from a vocabulary, wires it the first time more than one
4371
4470
  * choice survives the whitelist, pins the button to a static glyph when a
4372
- * single one does, owns the roving focus and the Escape/Enter handling, and
4373
- * keeps the button's value, glyph and aria-label in step. A pick that changes
4374
- * the value calls back; the host decides what that means.
4471
+ * single one does, takes the shared menu keyboard protocol, and keeps the
4472
+ * button's value, glyph and aria-label in step. A pick that changes the value
4473
+ * calls back; the host decides what that means.
4375
4474
  *
4376
4475
  * The button's `value` attribute is the store, as it is for a native control:
4377
4476
  * the menu protocol finds the current item by it, and nothing mirrors it.
@@ -4529,64 +4628,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4529
4628
  return Array.from(this.#menu.querySelectorAll('li > a'), (a) => /** @type HTMLAnchorElement */ (a));
4530
4629
  }
4531
4630
  #wire() {
4532
- const button = this.#button;
4533
- const menu = this.#menu;
4534
- Anchors.wire(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
4535
- menu.addEventListener('toggle', (/** @type any */ evt) => {
4536
- if (evt.newState !== 'open') {
4537
- //give the invoker back the focus the menu had borrowed, without
4538
- //stealing it from wherever else the close came from
4539
- if (menu.contains(document.activeElement)) {
4540
- button.focus();
4541
- }
4542
- return;
4543
- }
4544
- const items = this.#items();
4545
- (items.find((a) => a.getAttribute('value') === this.value) ?? items[0])?.focus();
4546
- });
4547
- menu.addEventListener('keydown', (evt) => {
4548
- const target = /** @type HTMLElement */ (evt.target);
4549
- const item = /** @type HTMLAnchorElement | null */ (target.closest('li > a'));
4550
- if (!item) {
4551
- return;
4552
- }
4553
- const items = this.#items();
4554
- const at = items.indexOf(item);
4555
- switch (evt.code) {
4556
- case 'ArrowDown': {
4557
- evt.preventDefault();
4558
- items[(at + 1) % items.length]?.focus();
4559
- break;
4560
- }
4561
- case 'ArrowUp': {
4562
- evt.preventDefault();
4563
- items[(at - 1 + items.length) % items.length]?.focus();
4564
- break;
4565
- }
4566
- case 'Home': {
4567
- evt.preventDefault();
4568
- items[0]?.focus();
4569
- break;
4570
- }
4571
- case 'End': {
4572
- evt.preventDefault();
4573
- items[items.length - 1]?.focus();
4574
- break;
4575
- }
4576
- case 'Enter':
4577
- case 'Space': {
4578
- evt.preventDefault();
4579
- item.click();
4580
- button.focus();
4581
- break;
4582
- }
4583
- case 'Escape': {
4584
- //the platform's close request hides the menu, the focus is placed
4585
- //on the invoker before the focused item is detached from it
4586
- button.focus();
4587
- break;
4588
- }
4589
- }
4631
+ Anchors.wire(this.#button, this.#menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
4632
+ wireMenuKeys(this.#button, this.#menu, {
4633
+ items: () => this.#items(),
4634
+ current: () => this.#items().find((a) => a.getAttribute('value') === this.value),
4590
4635
  });
4591
4636
  }
4592
4637
  }
@@ -5561,15 +5606,22 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5561
5606
  *
5562
5607
  * The body is text or a node: a string is never read as markup, an
5563
5608
  * `innerHTML` sink being a thing the library does not carry. Each button is
5564
- * a `[result, label, className?]` tuple, rendered as its answer.
5609
+ * a `[result, label, className?]` tuple rendered as its answer, or a node
5610
+ * the caller has built itself, which is how a button carries anything a
5611
+ * tuple cannot say and how a footer gets a spacer.
5565
5612
  * @param {string} header
5566
5613
  * @param {string|Node} body
5567
- * @param {[string, string, string?][]} [buttons]
5614
+ * @param {([string, string, string?]|Node)[]} [buttons]
5615
+ * @param {{ className?: string }} [options] `className` dresses the dialog,
5616
+ * the way a declared `ful-dialog` is given one to size it
5568
5617
  * @returns {Promise<DialogOutcome>}
5569
5618
  */
5570
- static async ask(header, body, buttons = []) {
5619
+ static async ask(header, body, buttons = [], { className } = {}) {
5571
5620
  const dialog = document.createElement('ful-dialog');
5572
5621
  dialog.setAttribute('header', header);
5622
+ if (className) {
5623
+ dialog.className = className;
5624
+ }
5573
5625
  if (typeof body === 'string') {
5574
5626
  dialog.textContent = body;
5575
5627
  } else if (body) {
@@ -5580,12 +5632,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5580
5632
  if (buttons.length > 0) {
5581
5633
  const choices = document.createElement('template');
5582
5634
  choices.setAttribute('slot', 'buttons');
5583
- for (const [result, label, className] of buttons) {
5635
+ for (const choice of buttons) {
5636
+ if (choice instanceof Node) {
5637
+ choices.content.append(choice);
5638
+ continue;
5639
+ }
5640
+ const [result, label, buttonClass] = choice;
5584
5641
  const button = document.createElement('button');
5585
5642
  button.type = 'button';
5586
5643
  button.dataset.result = result;
5587
- if (className) {
5588
- button.className = className;
5644
+ if (buttonClass) {
5645
+ button.className = buttonClass;
5589
5646
  }
5590
5647
  button.textContent = label;
5591
5648
  choices.content.append(button);
@@ -5607,13 +5664,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5607
5664
  * @param {string} header
5608
5665
  * @param {string|Node} body
5609
5666
  * @param {{ confirm?: string, cancel?: string }} [labels] overriding the localized defaults
5667
+ * @param {{ className?: string }} [options] as `ask` takes them
5610
5668
  * @returns {Promise<boolean>}
5611
5669
  */
5612
- static async confirm(header, body, labels = {}) {
5613
- const outcome = await Dialog.ask(header, body, [
5614
- ['cancel', labels.cancel ?? index_mjs.Localization.of().t('dialog.cancel')],
5615
- ['confirm', labels.confirm ?? index_mjs.Localization.of().t('dialog.confirm'), 'ful-button'],
5616
- ]);
5670
+ static async confirm(header, body, labels = {}, options = {}) {
5671
+ const outcome = await Dialog.ask(
5672
+ header,
5673
+ body,
5674
+ [
5675
+ ['cancel', labels.cancel ?? index_mjs.Localization.of().t('dialog.cancel')],
5676
+ ['confirm', labels.confirm ?? index_mjs.Localization.of().t('dialog.confirm'), 'ful-button'],
5677
+ ],
5678
+ options,
5679
+ );
5617
5680
  return !outcome.dismissed && outcome.result === 'confirm';
5618
5681
  }
5619
5682
  }
@@ -5820,6 +5883,65 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5820
5883
  }
5821
5884
  }
5822
5885
 
5886
+ /**
5887
+ * A menu of commands on the native popover platform: the invoker named by
5888
+ * `for` toggles it and light dismiss and Escape come from the browser, while
5889
+ * the element adds the menu semantics and the keyboard protocol.
5890
+ *
5891
+ * The items are the buttons and links written inside it, kept as they are and
5892
+ * given their menu role and their place in the roving focus. Activation is the
5893
+ * item's own click, which the element only follows by closing.
5894
+ */
5895
+ class Menu extends index_mjs.ParsedElement {
5896
+ static attributes = ['for'];
5897
+ render() {
5898
+ const named = this.declared('for');
5899
+ const invoker = named ? document.getElementById(named) : null;
5900
+ if (!invoker) {
5901
+ throw new Error(
5902
+ `ful-menu names no invoker: for="${named ?? ''}" must name an element already in the page when the menu upgrades`,
5903
+ );
5904
+ }
5905
+ this.setAttribute('role', 'menu');
5906
+ if (!this.hasAttribute('popover')) {
5907
+ this.setAttribute('popover', '');
5908
+ }
5909
+ invoker.setAttribute('aria-haspopup', 'menu');
5910
+ Anchors.wire(invoker, this, { prefix: 'ful-menu', invoke: true, expanded: true });
5911
+ wireMenuKeys(invoker, this, { items: () => this.items });
5912
+ this.#mark();
5913
+ this.addEventListener('click', (evt) => {
5914
+ const target = /** @type HTMLElement */ (evt.target);
5915
+ if (this.items.some((item) => item === target || item.contains(target))) {
5916
+ this.hidePopover();
5917
+ }
5918
+ });
5919
+ }
5920
+ /**
5921
+ * The commands, in the order the arrows walk them: the menu's own buttons
5922
+ * and links, minus the disabled and hidden ones, which keep their role and
5923
+ * leave the walk.
5924
+ */
5925
+ get items() {
5926
+ return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
5927
+ }
5928
+ /**
5929
+ * Gives every button and link of the menu its item role and takes it out of
5930
+ * the tab order, and answers them. Read on every gesture as well as at the
5931
+ * render, so a menu a page fills or empties later needs no rewiring.
5932
+ */
5933
+ #mark() {
5934
+ const all = /** @type HTMLElement[] */ ([...this.querySelectorAll(':scope > button, :scope > a[href]')]);
5935
+ for (const item of all) {
5936
+ if (!item.hasAttribute('role')) {
5937
+ item.setAttribute('role', 'menuitem');
5938
+ }
5939
+ item.tabIndex = -1;
5940
+ }
5941
+ return all;
5942
+ }
5943
+ }
5944
+
5823
5945
  const SEVERITIES = ['info', 'success', 'warning', 'error'];
5824
5946
 
5825
5947
  //the regions alive in the document: the show-toast listener is wired once and
@@ -6465,6 +6587,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6465
6587
  .defineElement('ful-tooltip', Tooltip)
6466
6588
  .defineElement('ful-dialog', Dialog)
6467
6589
  .defineElement('ful-drawer', Drawer)
6590
+ .defineElement('ful-menu', Menu)
6468
6591
  .defineElement('ful-toasts', Toasts)
6469
6592
  .defineElement('ful-tabs', Tabs)
6470
6593
  .defineElement('ful-accordion', Accordion)
@@ -6529,6 +6652,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6529
6652
  exports.LocalDate = LocalDate;
6530
6653
  exports.LocalDateFilter = LocalDateFilter;
6531
6654
  exports.LocalStorage = LocalStorage;
6655
+ exports.Menu = Menu;
6532
6656
  exports.NumberFilter = NumberFilter;
6533
6657
  exports.Pagination = Pagination;
6534
6658
  exports.Plugin = Plugin;