@optionfactory/fml 9.0.0-rc26 → 9.0.0-rc28

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/fml.iife.js CHANGED
@@ -14188,15 +14188,25 @@ var fml = (function (exports) {
14188
14188
  * on the host, bubbling, with detail `{ name: null, section, index, first }`,
14189
14189
  * and awaits the answers registered through `AsyncEvents.asyncOn`, so a panel
14190
14190
  * can deliver itself asynchronously: see `SectionRequests`.
14191
+ *
14192
+ * The tablist stays on one line. When the tabs do not fit it scrolls sideways,
14193
+ * and a previous and a next button appear at the side that has more tabs,
14194
+ * each bringing the next hidden tab into view. They stay out of the tab order:
14195
+ * the keyboard moves through the tabs with the arrow keys. The active tab is
14196
+ * kept in view when it changes and when the tablist changes width.
14191
14197
  */
14192
14198
  class Tabs extends ParsedElement {
14193
14199
  static slots = true;
14194
14200
  static observed = ['active:number'];
14195
14201
  static template = `
14202
+ <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>
14196
14203
  <ful-tablist role="tablist">{{{{ slots.tabs }}}}</ful-tablist>
14204
+ <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>
14197
14205
  {{{{ slots.default }}}}
14198
14206
  `;
14199
14207
  #tablist;
14208
+ #scrollStart;
14209
+ #scrollEnd;
14200
14210
  #tabs = [];
14201
14211
  #panels = [];
14202
14212
  #requests = new SectionRequests();
@@ -14211,8 +14221,11 @@ var fml = (function (exports) {
14211
14221
  render({ slots }) {
14212
14222
  const fragment = this.template().withOverlay({ slots }).render();
14213
14223
  this.#tablist = fragment.querySelector('ful-tablist');
14224
+ this.#scrollStart = fragment.querySelector('[data-ref="scroll-start"]');
14225
+ this.#scrollEnd = fragment.querySelector('[data-ref="scroll-end"]');
14226
+ const chrome = [this.#tablist, this.#scrollStart, this.#scrollEnd];
14214
14227
  const declared = [...this.#tablist.children];
14215
- this.#panels = [...fragment.children].filter((el) => el !== this.#tablist);
14228
+ this.#panels = [...fragment.children].filter((el) => !chrome.includes(el));
14216
14229
  if (declared.length !== this.#panels.length) {
14217
14230
  console.warn(
14218
14231
  `ful-tabs: ${declared.length} tabs declared for ${this.#panels.length} panels, the surplus is left alone`,
@@ -14260,8 +14273,75 @@ var fml = (function (exports) {
14260
14273
  this.active = target;
14261
14274
  this.#tabs[target].focus();
14262
14275
  });
14276
+ this.#scrollStart.addEventListener('click', () => this.#scrollByOneTab(-1));
14277
+ this.#scrollEnd.addEventListener('click', () => this.#scrollByOneTab(1));
14278
+ this.#tablist.addEventListener('scroll', () => this.#showScrollButtons(), { passive: true });
14279
+ let width = -1;
14280
+ const resizes = new ResizeObserver(() => {
14281
+ this.#showScrollButtons();
14282
+ if (this.#tablist.clientWidth !== width) {
14283
+ width = this.#tablist.clientWidth;
14284
+ this.#bringIntoView(this.#tabs[this.#active], 'auto');
14285
+ }
14286
+ });
14287
+ resizes.observe(this.#tablist);
14288
+ for (const tab of this.#tabs) {
14289
+ resizes.observe(tab);
14290
+ }
14263
14291
  this.replaceChildren(fragment);
14264
14292
  }
14293
+ /**
14294
+ * Shows each scroll button only when there are tabs beyond its side, and
14295
+ * sizes both to the height of the tablist above its border.
14296
+ * @returns {void}
14297
+ */
14298
+ #showScrollButtons() {
14299
+ const list = this.#tablist;
14300
+ const overflow = list.scrollWidth - list.clientWidth;
14301
+ this.#scrollStart.hidden = list.scrollLeft <= 1;
14302
+ this.#scrollEnd.hidden = list.scrollLeft >= overflow - 1;
14303
+ this.#scrollStart.style.blockSize = this.#scrollEnd.style.blockSize = `${list.clientHeight}px`;
14304
+ }
14305
+ /**
14306
+ * The part of the tablist that no scroll button covers.
14307
+ * @returns {{ start: number, end: number }}
14308
+ */
14309
+ #visibleRange() {
14310
+ const view = this.#tablist.getBoundingClientRect();
14311
+ return { start: view.left + this.#scrollStart.offsetWidth, end: view.right - this.#scrollEnd.offsetWidth };
14312
+ }
14313
+ /**
14314
+ * Scrolls the tablist so that the first tab cut by the side the direction
14315
+ * points to is wholly visible.
14316
+ * @param {1 | -1} direction 1 towards the end, -1 towards the start
14317
+ * @returns {void}
14318
+ */
14319
+ #scrollByOneTab(direction) {
14320
+ const { start, end } = this.#visibleRange();
14321
+ const target =
14322
+ direction > 0
14323
+ ? this.#tabs.find((tab) => tab.getBoundingClientRect().right > end + 1)
14324
+ : this.#tabs.findLast((tab) => tab.getBoundingClientRect().left < start - 1);
14325
+ this.#bringIntoView(target, matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth');
14326
+ }
14327
+ /**
14328
+ * Scrolls the tablist, and only the tablist, the least needed to show a tab
14329
+ * wholly.
14330
+ * @param {HTMLElement | undefined} tab
14331
+ * @param {ScrollBehavior} behavior
14332
+ * @returns {void}
14333
+ */
14334
+ #bringIntoView(tab, behavior) {
14335
+ if (!tab) {
14336
+ return;
14337
+ }
14338
+ const { start, end } = this.#visibleRange();
14339
+ const box = tab.getBoundingClientRect();
14340
+ const delta = box.right > end ? Math.ceil(box.right - end) : box.left < start ? Math.floor(box.left - start) : 0;
14341
+ if (delta !== 0) {
14342
+ this.#tablist.scrollBy({ left: delta, behavior });
14343
+ }
14344
+ }
14265
14345
  /**
14266
14346
  * The index of the visible panel, reflected to the `active` attribute.
14267
14347
  * Writing it clamps the value to the paired tabs, a value that is not a
@@ -14304,6 +14384,7 @@ var fml = (function (exports) {
14304
14384
  }
14305
14385
  this.#active = index;
14306
14386
  this.reflectTo('active', index);
14387
+ this.#bringIntoView(this.#tabs[index], 'auto');
14307
14388
  if (this.rendered && index !== previous) {
14308
14389
  this.dispatchEvent(new CustomEvent('tabs:change', { detail: { active: index, previous } }));
14309
14390
  }
@@ -14599,6 +14680,8 @@ var fml = (function (exports) {
14599
14680
  'dialog.discard.cancel': 'Keep editing',
14600
14681
  'drawer.close': 'Close',
14601
14682
  'spinner.loading': 'Loading…',
14683
+ 'tabs.previous': 'Previous tabs',
14684
+ 'tabs.next': 'Next tabs',
14602
14685
  'toast.region': 'Notifications',
14603
14686
  'toast.dismiss': 'Dismiss',
14604
14687
  'wizard.progress': 'Progress',
@@ -14648,6 +14731,8 @@ var fml = (function (exports) {
14648
14731
  'dialog.discard.cancel': 'Continua a modificare',
14649
14732
  'drawer.close': 'Chiudi',
14650
14733
  'spinner.loading': 'Caricamento…',
14734
+ 'tabs.previous': 'Schede precedenti',
14735
+ 'tabs.next': 'Schede successive',
14651
14736
  'toast.region': 'Notifiche',
14652
14737
  'toast.dismiss': 'Chiudi',
14653
14738
  'wizard.progress': 'Avanzamento',
@@ -14700,6 +14785,8 @@ var fml = (function (exports) {
14700
14785
  'dialog.discard.cancel': 'Seguir editando',
14701
14786
  'drawer.close': 'Cerrar',
14702
14787
  'spinner.loading': 'Cargando…',
14788
+ 'tabs.previous': 'Pestañas anteriores',
14789
+ 'tabs.next': 'Pestañas siguientes',
14703
14790
  'toast.region': 'Notificaciones',
14704
14791
  'toast.dismiss': 'Cerrar',
14705
14792
  'wizard.progress': 'Progreso',
@@ -14752,6 +14839,8 @@ var fml = (function (exports) {
14752
14839
  'dialog.discard.cancel': 'Continuer à modifier',
14753
14840
  'drawer.close': 'Fermer',
14754
14841
  'spinner.loading': 'Chargement…',
14842
+ 'tabs.previous': 'Onglets précédents',
14843
+ 'tabs.next': 'Onglets suivants',
14755
14844
  'toast.region': 'Notifications',
14756
14845
  'toast.dismiss': 'Fermer',
14757
14846
  'wizard.progress': 'Progression',