@optionfactory/fml 9.0.0-rc25 → 9.0.0-rc27

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.mjs CHANGED
@@ -7918,15 +7918,25 @@ class Toasts extends ParsedElement {
7918
7918
  * on the host, bubbling, with detail `{ name: null, section, index, first }`,
7919
7919
  * and awaits the answers registered through `AsyncEvents.asyncOn`, so a panel
7920
7920
  * can deliver itself asynchronously: see `SectionRequests`.
7921
+ *
7922
+ * The tablist stays on one line. When the tabs do not fit it scrolls sideways,
7923
+ * and a previous and a next button appear at the side that has more tabs,
7924
+ * each bringing the next hidden tab into view. They stay out of the tab order:
7925
+ * the keyboard moves through the tabs with the arrow keys. The active tab is
7926
+ * kept in view when it changes and when the tablist changes width.
7921
7927
  */
7922
7928
  class Tabs extends ParsedElement {
7923
7929
  static slots = true;
7924
7930
  static observed = ['active:number'];
7925
7931
  static template = `
7932
+ <button type="button" class="ful-tabs-scroll" data-ref="scroll-start" tabindex="-1" hidden data-tpl-aria-label="#l10n:t('tabs.previous')"><ful-icon name="chevron-left" aria-hidden="true"></ful-icon></button>
7926
7933
  <ful-tablist role="tablist">{{{{ slots.tabs }}}}</ful-tablist>
7934
+ <button type="button" class="ful-tabs-scroll" data-ref="scroll-end" tabindex="-1" hidden data-tpl-aria-label="#l10n:t('tabs.next')"><ful-icon name="chevron-right" aria-hidden="true"></ful-icon></button>
7927
7935
  {{{{ slots.default }}}}
7928
7936
  `;
7929
7937
  #tablist;
7938
+ #scrollStart;
7939
+ #scrollEnd;
7930
7940
  #tabs = [];
7931
7941
  #panels = [];
7932
7942
  #requests = new SectionRequests();
@@ -7941,8 +7951,11 @@ class Tabs extends ParsedElement {
7941
7951
  render({ slots }) {
7942
7952
  const fragment = this.template().withOverlay({ slots }).render();
7943
7953
  this.#tablist = fragment.querySelector('ful-tablist');
7954
+ this.#scrollStart = fragment.querySelector('[data-ref="scroll-start"]');
7955
+ this.#scrollEnd = fragment.querySelector('[data-ref="scroll-end"]');
7956
+ const chrome = [this.#tablist, this.#scrollStart, this.#scrollEnd];
7944
7957
  const declared = [...this.#tablist.children];
7945
- this.#panels = [...fragment.children].filter((el) => el !== this.#tablist);
7958
+ this.#panels = [...fragment.children].filter((el) => !chrome.includes(el));
7946
7959
  if (declared.length !== this.#panels.length) {
7947
7960
  console.warn(
7948
7961
  `ful-tabs: ${declared.length} tabs declared for ${this.#panels.length} panels, the surplus is left alone`,
@@ -7990,8 +8003,75 @@ class Tabs extends ParsedElement {
7990
8003
  this.active = target;
7991
8004
  this.#tabs[target].focus();
7992
8005
  });
8006
+ this.#scrollStart.addEventListener('click', () => this.#scrollByOneTab(-1));
8007
+ this.#scrollEnd.addEventListener('click', () => this.#scrollByOneTab(1));
8008
+ this.#tablist.addEventListener('scroll', () => this.#showScrollButtons(), { passive: true });
8009
+ let width = -1;
8010
+ const resizes = new ResizeObserver(() => {
8011
+ this.#showScrollButtons();
8012
+ if (this.#tablist.clientWidth !== width) {
8013
+ width = this.#tablist.clientWidth;
8014
+ this.#bringIntoView(this.#tabs[this.#active], 'auto');
8015
+ }
8016
+ });
8017
+ resizes.observe(this.#tablist);
8018
+ for (const tab of this.#tabs) {
8019
+ resizes.observe(tab);
8020
+ }
7993
8021
  this.replaceChildren(fragment);
7994
8022
  }
8023
+ /**
8024
+ * Shows each scroll button only when there are tabs beyond its side, and
8025
+ * sizes both to the height of the tablist above its border.
8026
+ * @returns {void}
8027
+ */
8028
+ #showScrollButtons() {
8029
+ const list = this.#tablist;
8030
+ const overflow = list.scrollWidth - list.clientWidth;
8031
+ this.#scrollStart.hidden = list.scrollLeft <= 1;
8032
+ this.#scrollEnd.hidden = list.scrollLeft >= overflow - 1;
8033
+ this.#scrollStart.style.blockSize = this.#scrollEnd.style.blockSize = `${list.clientHeight}px`;
8034
+ }
8035
+ /**
8036
+ * The part of the tablist that no scroll button covers.
8037
+ * @returns {{ start: number, end: number }}
8038
+ */
8039
+ #visibleRange() {
8040
+ const view = this.#tablist.getBoundingClientRect();
8041
+ return { start: view.left + this.#scrollStart.offsetWidth, end: view.right - this.#scrollEnd.offsetWidth };
8042
+ }
8043
+ /**
8044
+ * Scrolls the tablist so that the first tab cut by the side the direction
8045
+ * points to is wholly visible.
8046
+ * @param {1 | -1} direction 1 towards the end, -1 towards the start
8047
+ * @returns {void}
8048
+ */
8049
+ #scrollByOneTab(direction) {
8050
+ const { start, end } = this.#visibleRange();
8051
+ const target =
8052
+ direction > 0
8053
+ ? this.#tabs.find((tab) => tab.getBoundingClientRect().right > end + 1)
8054
+ : this.#tabs.findLast((tab) => tab.getBoundingClientRect().left < start - 1);
8055
+ this.#bringIntoView(target, matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth');
8056
+ }
8057
+ /**
8058
+ * Scrolls the tablist, and only the tablist, the least needed to show a tab
8059
+ * wholly.
8060
+ * @param {HTMLElement | undefined} tab
8061
+ * @param {ScrollBehavior} behavior
8062
+ * @returns {void}
8063
+ */
8064
+ #bringIntoView(tab, behavior) {
8065
+ if (!tab) {
8066
+ return;
8067
+ }
8068
+ const { start, end } = this.#visibleRange();
8069
+ const box = tab.getBoundingClientRect();
8070
+ const delta = box.right > end ? Math.ceil(box.right - end) : box.left < start ? Math.floor(box.left - start) : 0;
8071
+ if (delta !== 0) {
8072
+ this.#tablist.scrollBy({ left: delta, behavior });
8073
+ }
8074
+ }
7995
8075
  /**
7996
8076
  * The index of the visible panel, reflected to the `active` attribute.
7997
8077
  * Writing it clamps the value to the paired tabs, a value that is not a
@@ -8034,6 +8114,7 @@ class Tabs extends ParsedElement {
8034
8114
  }
8035
8115
  this.#active = index;
8036
8116
  this.reflectTo('active', index);
8117
+ this.#bringIntoView(this.#tabs[index], 'auto');
8037
8118
  if (this.rendered && index !== previous) {
8038
8119
  this.dispatchEvent(new CustomEvent('tabs:change', { detail: { active: index, previous } }));
8039
8120
  }
@@ -8329,6 +8410,8 @@ var en = {
8329
8410
  'dialog.discard.cancel': 'Keep editing',
8330
8411
  'drawer.close': 'Close',
8331
8412
  'spinner.loading': 'Loading…',
8413
+ 'tabs.previous': 'Previous tabs',
8414
+ 'tabs.next': 'Next tabs',
8332
8415
  'toast.region': 'Notifications',
8333
8416
  'toast.dismiss': 'Dismiss',
8334
8417
  'wizard.progress': 'Progress',
@@ -8378,6 +8461,8 @@ var it = {
8378
8461
  'dialog.discard.cancel': 'Continua a modificare',
8379
8462
  'drawer.close': 'Chiudi',
8380
8463
  'spinner.loading': 'Caricamento…',
8464
+ 'tabs.previous': 'Schede precedenti',
8465
+ 'tabs.next': 'Schede successive',
8381
8466
  'toast.region': 'Notifiche',
8382
8467
  'toast.dismiss': 'Chiudi',
8383
8468
  'wizard.progress': 'Avanzamento',
@@ -8430,6 +8515,8 @@ var es = {
8430
8515
  'dialog.discard.cancel': 'Seguir editando',
8431
8516
  'drawer.close': 'Cerrar',
8432
8517
  'spinner.loading': 'Cargando…',
8518
+ 'tabs.previous': 'Pestañas anteriores',
8519
+ 'tabs.next': 'Pestañas siguientes',
8433
8520
  'toast.region': 'Notificaciones',
8434
8521
  'toast.dismiss': 'Cerrar',
8435
8522
  'wizard.progress': 'Progreso',
@@ -8482,6 +8569,8 @@ var fr = {
8482
8569
  'dialog.discard.cancel': 'Continuer à modifier',
8483
8570
  'drawer.close': 'Fermer',
8484
8571
  'spinner.loading': 'Chargement…',
8572
+ 'tabs.previous': 'Onglets précédents',
8573
+ 'tabs.next': 'Onglets suivants',
8485
8574
  'toast.region': 'Notifications',
8486
8575
  'toast.dismiss': 'Fermer',
8487
8576
  'wizard.progress': 'Progression',