@optionfactory/fml 9.0.0-rc2 → 9.0.0-rc4

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
@@ -6462,6 +6462,48 @@ var fml = (function (exports) {
6462
6462
  }
6463
6463
  }
6464
6464
 
6465
+ /**
6466
+ * The protocol by which content standing inside a field becomes part of the
6467
+ * accessible description of that field's control.
6468
+ *
6469
+ * A field owns its control's `aria-describedby`: it is the only thing that
6470
+ * knows which element the description belongs on, and it already writes the
6471
+ * entry for its own error region. Content the author slotted into the field
6472
+ * cannot write that attribute itself without becoming a second owner of it, and
6473
+ * it cannot be wired by the field either, because a slotted custom element
6474
+ * renders after the field has mounted and has nothing to point at when the
6475
+ * field looks.
6476
+ *
6477
+ * So the content asks, once it has something to offer. `describable(el)`
6478
+ * answers the nearest ancestor that accepts a description, and the caller hands
6479
+ * its element to that ancestor's `describedBy`, which answers whether it was
6480
+ * taken. Nothing here names a field or a tooltip: the relation is expressed as
6481
+ * a capability, so the two ends need not import each other, which matters
6482
+ * because the library's own arrow runs from the forms to the disclosures.
6483
+ *
6484
+ * The lookup lives here rather than at its one call site so the protocol has a
6485
+ * name, a place to be documented and a single definition to change.
6486
+ */
6487
+
6488
+ /**
6489
+ * @typedef {{ describedBy(el: HTMLElement): boolean }} Describable
6490
+ */
6491
+
6492
+ /**
6493
+ * The nearest ancestor of `el` that accepts elements into the description of
6494
+ * whatever it considers its control, or null when nothing in the ancestry does.
6495
+ * @param {Element} el
6496
+ * @returns {(Element & Describable) | null}
6497
+ */
6498
+ const describable = (el) => {
6499
+ for (let at = el.parentElement; at; at = at.parentElement) {
6500
+ if (typeof (/** @type {any} */ (at).describedBy) === 'function') {
6501
+ return /** @type {any} */ (at);
6502
+ }
6503
+ }
6504
+ return null;
6505
+ };
6506
+
6465
6507
  /**
6466
6508
  * Sleeping, debouncing and throttling. Debounce and throttle both return the
6467
6509
  * wrapped function together with a cancel function.
@@ -6856,6 +6898,9 @@ var fml = (function (exports) {
6856
6898
  /** the role the element internals carry, 'presentation' unless the control is its own */
6857
6899
  static ROLE = 'presentation';
6858
6900
  #control;
6901
+ #described;
6902
+ #descriptions = [];
6903
+ #errorId = null;
6859
6904
  #fieldError;
6860
6905
  #claims;
6861
6906
  #announces;
@@ -6907,15 +6952,18 @@ var fml = (function (exports) {
6907
6952
  true,
6908
6953
  );
6909
6954
  }
6910
- //the error region describes the control, or the host where there is no
6955
+ //the description lands on the control, or on the host where there is no
6911
6956
  //single control to describe (a radio group's legend names its fieldset)
6957
+ this.#described = described ?? control;
6912
6958
  if (error) {
6913
- (described ?? control).ariaDescribedByElements = [error];
6959
+ //named for what it is, the generic id being for whoever brings no name
6960
+ error.id = error.id || Attributes.uid('ful-field-error');
6961
+ this.#errorId = error.id;
6914
6962
  }
6963
+ //anything handed over before the field had a target lands here
6964
+ this.#describe();
6915
6965
  if (label) {
6916
- control.ariaLabelledByElements = [label];
6917
- //a label that does not natively target the control still focuses it
6918
- label.addEventListener('click', () => this.focus());
6966
+ Field.#name(this, label, control);
6919
6967
  }
6920
6968
  //the platform's implicit submission, stood in for where the field's own
6921
6969
  //protocol took it away: the inner controls carry form="", so Enter in one
@@ -6948,6 +6996,61 @@ var fml = (function (exports) {
6948
6996
  static #submitsOnEnter(el) {
6949
6997
  return el instanceof HTMLInputElement && !['file', 'button', 'submit', 'reset', 'image'].includes(el.type);
6950
6998
  }
6999
+ /**
7000
+ * Adds an element to the accessible description of the field's control and
7001
+ * answers whether the field took it.
7002
+ *
7003
+ * A field takes one whenever it is offered, before its own render as
7004
+ * readily as after: content slotted into a field is a custom element of its
7005
+ * own and may upgrade on either side of the field it stands in, which
7006
+ * happens in both directions in practice, a tooltip beating an async select
7007
+ * to its render while losing to a plain input. A description handed over
7008
+ * early waits here and is written the moment the field has somewhere to
7009
+ * write it, so the caller never has to know the order.
7010
+ *
7011
+ * The reference lands on the element handed over rather than on a wrapper
7012
+ * around it: a hidden element is included in a description only where it is
7013
+ * named directly, and content that reaches the description through a
7014
+ * wrapper is skipped while it is hidden. A popover closed until someone
7015
+ * opens it is exactly that, so the caller passes the popover itself.
7016
+ *
7017
+ * An attribute rather than `ariaDescribedByElements`: the property reflects
7018
+ * to nothing, so the description would live in the accessibility tree alone
7019
+ * and vanish entirely on a browser without aria element reflection.
7020
+ *
7021
+ * This is the field's half of the description protocol; `describable` in
7022
+ * `ful/descriptions.mjs` is the half the content uses to find the field.
7023
+ * @param {HTMLElement} el
7024
+ * @returns {boolean}
7025
+ */
7026
+ describedBy(el) {
7027
+ if (!el) {
7028
+ return false;
7029
+ }
7030
+ if (!el.id) {
7031
+ el.id = Attributes.uid('ful-described');
7032
+ }
7033
+ if (!this.#descriptions.includes(el.id)) {
7034
+ this.#descriptions.push(el.id);
7035
+ }
7036
+ this.#describe();
7037
+ return true;
7038
+ }
7039
+ /**
7040
+ * Writes the description the field has collected, the error region last:
7041
+ * the standing explanations are what the field always says, the problem is
7042
+ * the news. The field owns the attribute outright rather than appending to
7043
+ * whatever is there, so the order does not depend on who arrived when.
7044
+ */
7045
+ #describe() {
7046
+ if (!this.#described) {
7047
+ return;
7048
+ }
7049
+ const ids = [...this.#descriptions, this.#errorId].filter((id) => id);
7050
+ if (ids.length) {
7051
+ this.#described.setAttribute('aria-describedby', ids.join(' '));
7052
+ }
7053
+ }
6951
7054
  focus(options) {
6952
7055
  this.#control?.focus(options);
6953
7056
  }
@@ -7004,6 +7107,49 @@ var fml = (function (exports) {
7004
7107
  }),
7005
7108
  );
7006
7109
  }
7110
+ /** The html elements a label's `for` may point at, `input[type=hidden]` excepted. */
7111
+ static #LABELABLE = new Set(['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA']);
7112
+ /**
7113
+ * Names the control from the field's label, natively wherever the platform
7114
+ * allows it.
7115
+ *
7116
+ * `for` and `id` are the form the dom itself carries, so the association is
7117
+ * there for anything reading the markup rather than the accessibility tree:
7118
+ * an audit tool, the browser's autofill, a translation pass. It also makes
7119
+ * the label's click reach the control the way it does in a plain form, which
7120
+ * is focus for a text control and activation for a checkbox, so the field
7121
+ * needs no handler of its own.
7122
+ *
7123
+ * A control the platform will not let a label target, a composite carrying
7124
+ * `role="radiogroup"` among them, takes `aria-labelledby` instead. That is an
7125
+ * attribute too, so the association is equally visible; what it does not carry
7126
+ * is the label's click, which is why the handler stays on that path only.
7127
+ *
7128
+ * Neither branch uses `ariaLabelledByElements`. The property reflects to no
7129
+ * attribute, so the name lived in the accessibility tree alone: nothing reading
7130
+ * the dom saw it, and on a browser without aria element reflection the
7131
+ * assignment is a silent expando and the field has no name at all.
7132
+ * @param {any} field
7133
+ * @param {HTMLElement} label
7134
+ * @param {any} control
7135
+ */
7136
+ static #name(field, label, control) {
7137
+ const labelable =
7138
+ Field.#LABELABLE.has(control.tagName) && control.getAttribute('type') !== 'hidden';
7139
+ if (!labelable) {
7140
+ if (!label.id) {
7141
+ label.id = Attributes.uid('ful-label');
7142
+ }
7143
+ control.setAttribute('aria-labelledby', label.id);
7144
+ //aria-labelledby carries the name but not the label's click
7145
+ label.addEventListener('click', () => field.focus());
7146
+ return;
7147
+ }
7148
+ if (!control.id) {
7149
+ control.id = Attributes.uid('ful-control');
7150
+ }
7151
+ label.setAttribute('for', control.id);
7152
+ }
7007
7153
  /**
7008
7154
  * Whether the field's chrome should answer a gesture. Badges, dropzones,
7009
7155
  * menus and labels are not form controls, so their handlers must ask the
@@ -7141,8 +7287,9 @@ var fml = (function (exports) {
7141
7287
  * three claims reach it
7142
7288
  * - `error` is the field's live region
7143
7289
  * - `label`, when given, names the control and focuses it on click
7144
- * - `described` moves the error's description off the control and onto
7145
- * another element, the host where no single control can carry it
7290
+ * - `described` moves the description off the control and onto another
7291
+ * element, the host where no single control can carry it: the error
7292
+ * region and anything `describedBy` is later handed both land there
7146
7293
  * - `claims` moves the three claims onto a wrapper the field disables as a
7147
7294
  * whole, leaving focus and aria on the control
7148
7295
  * - `announces` is the element whose role carries `aria-readonly` and
@@ -8193,8 +8340,14 @@ var fml = (function (exports) {
8193
8340
  const box = invoker.getBoundingClientRect();
8194
8341
  const here = popover.getBoundingClientRect();
8195
8342
  //against the padding box, which is what a percentage inset resolves against
8196
- popover.style.setProperty('--ful-note-callout-inline', `${box.left + box.width / 2 - here.left - popover.clientLeft}px`);
8197
- popover.style.setProperty('--ful-note-callout-block', `${box.top + box.height / 2 - here.top - popover.clientTop}px`);
8343
+ popover.style.setProperty(
8344
+ '--ful-note-callout-inline',
8345
+ `${box.left + box.width / 2 - here.left - popover.clientLeft}px`,
8346
+ );
8347
+ popover.style.setProperty(
8348
+ '--ful-note-callout-block',
8349
+ `${box.top + box.height / 2 - here.top - popover.clientTop}px`,
8350
+ );
8198
8351
  };
8199
8352
 
8200
8353
  const place = (popover, anchored) => {
@@ -8298,67 +8451,93 @@ var fml = (function (exports) {
8298
8451
  };
8299
8452
 
8300
8453
  /**
8301
- * Wires an invoker/popover pair ful anchors through css: where the
8302
- * platform lacks anchor positioning the popover is placed beside its
8303
- * invoker whenever it opens, stretched to the invoker's width when
8304
- * asked, and cleaned up when it closes. Where the css works the call
8305
- * is a no-op.
8306
- *
8307
- * `handPlace` takes the placement here on every platform, which a popover
8308
- * asks for when it needs to know where its invoker ended up: the note's
8309
- * callout points at the invoker, and a pseudo-element cannot read an anchor
8310
- * that is not inside its own containing block, so the note is measured rather
8311
- * than placed by the css. Its stylesheet declares no position-area to match.
8454
+ * CSS anchor positioning for a popover and the invoker it belongs to, with the
8455
+ * hand-placed fallback for the platforms that do not have it.
8312
8456
  */
8313
- const wireAnchoredPopover = (
8314
- invoker,
8315
- popover,
8316
- { prefix = 'ful-anchor', invoke = false, expanded = false, stretch = false, handPlace = false } = {},
8317
- ) => {
8318
- const uid = Attributes.uid(prefix);
8319
- if (invoke) {
8320
- //popovertarget needs a target that can be named
8321
- popover.id = popover.id || uid;
8322
- invoker.setAttribute('popovertarget', popover.id);
8323
- }
8324
- const anchor = `--${uid}`;
8325
- invoker.style.anchorName = anchor;
8326
- popover.style.positionAnchor = anchor;
8327
- if (expanded) {
8328
- invoker.setAttribute('aria-expanded', 'false');
8457
+ class Anchors {
8458
+ /**
8459
+ * Anchors a popover to its invoker.
8460
+ *
8461
+ * The invoker is given an `anchor-name` and the popover a `position-anchor`
8462
+ * pointing at it, which is what a stylesheet needs to place the popover
8463
+ * itself: the library's own menus say `top: anchor(bottom); left:
8464
+ * anchor(left)`. **Writing that css is the caller's half of this.** Without
8465
+ * it the popover lands wherever the user agent puts a popover, which is not
8466
+ * beside the invoker.
8467
+ *
8468
+ * Where the platform has no anchor positioning the popover is placed here
8469
+ * instead, beside the invoker whenever it opens, clamped into the viewport,
8470
+ * following it on scroll and resize, and cleaned up on close. That placement
8471
+ * draws the geometry the css above describes, so the two agree.
8472
+ *
8473
+ * @param {HTMLElement} invoker the element the popover belongs to
8474
+ * @param {HTMLElement} popover the `[popover]` element to place
8475
+ * @param {object} [options]
8476
+ * @param {string} [options.prefix] prefixes the generated anchor name and id,
8477
+ * so the dom says which component a name belongs to
8478
+ * @param {boolean} [options.invoke] points the invoker's `popovertarget` at
8479
+ * the popover, giving toggle and light dismiss with no script of your own
8480
+ * @param {boolean} [options.expanded] keeps the invoker's `aria-expanded` in
8481
+ * step with the popover
8482
+ * @param {boolean} [options.stretch] widens the popover to its invoker, which
8483
+ * is what a combobox dropdown wants
8484
+ * @param {boolean} [options.handPlace] places here on every platform rather
8485
+ * than only as a fallback, which a popover asks for when it needs to know
8486
+ * where its invoker ended up: the tooltip's note points a callout at it, and
8487
+ * a pseudo-element cannot read an anchor outside its own containing block.
8488
+ * Such a popover declares no anchor placement in css, there being none to
8489
+ * agree with
8490
+ */
8491
+ static wire(
8492
+ invoker,
8493
+ popover,
8494
+ { prefix = 'ful-anchor', invoke = false, expanded = false, stretch = false, handPlace = false } = {},
8495
+ ) {
8496
+ const uid = Attributes.uid(prefix);
8497
+ if (invoke) {
8498
+ //popovertarget needs a target that can be named
8499
+ popover.id = popover.id || uid;
8500
+ invoker.setAttribute('popovertarget', popover.id);
8501
+ }
8502
+ const anchor = `--${uid}`;
8503
+ invoker.style.anchorName = anchor;
8504
+ popover.style.positionAnchor = anchor;
8505
+ if (expanded) {
8506
+ invoker.setAttribute('aria-expanded', 'false');
8507
+ popover.addEventListener('toggle', (/** @type any */ evt) => {
8508
+ invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
8509
+ });
8510
+ }
8511
+ //the naming above is what the stylesheet reads, so it happens either way:
8512
+ //only the hand placement below is the fallback, and only for a popover that
8513
+ //did not ask to be placed here whatever the platform offers
8514
+ if (!handPlace && platformAnchors()) {
8515
+ return;
8516
+ }
8517
+ const anchored = { invoker, stretch };
8518
+ popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
8519
+ //placed before the showing, refined once laid out: the platform's
8520
+ //centered or corner spot never paints
8521
+ if (evt.newState === 'open') {
8522
+ place(popover, anchored);
8523
+ }
8524
+ });
8329
8525
  popover.addEventListener('toggle', (/** @type any */ evt) => {
8330
- invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
8526
+ if (evt.newState === 'open') {
8527
+ open.set(popover, anchored);
8528
+ place(popover, anchored);
8529
+ } else {
8530
+ open.delete(popover);
8531
+ unplace(popover);
8532
+ }
8331
8533
  });
8332
- }
8333
- //the naming above is what the stylesheet reads, so it happens either way:
8334
- //only the hand placement below is the fallback, and only for a popover that
8335
- //did not ask to be placed here whatever the platform offers
8336
- if (!handPlace && platformAnchors()) {
8337
- return;
8338
- }
8339
- const anchored = { invoker, stretch };
8340
- popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
8341
- //placed before the showing, refined once laid out: the platform's
8342
- //centered or corner spot never paints
8343
- if (evt.newState === 'open') {
8344
- place(popover, anchored);
8345
- }
8346
- });
8347
- popover.addEventListener('toggle', (/** @type any */ evt) => {
8348
- if (evt.newState === 'open') {
8349
- open.set(popover, anchored);
8350
- place(popover, anchored);
8351
- } else {
8352
- open.delete(popover);
8353
- unplace(popover);
8534
+ if (!reflowWired) {
8535
+ reflowWired = true;
8536
+ document.addEventListener('scroll', schedule, true);
8537
+ window.addEventListener('resize', schedule);
8354
8538
  }
8355
- });
8356
- if (!reflowWired) {
8357
- reflowWired = true;
8358
- document.addEventListener('scroll', schedule, true);
8359
- window.addEventListener('resize', schedule);
8360
8539
  }
8361
- };
8540
+ }
8362
8541
 
8363
8542
  /**
8364
8543
  * Fetches a select's whole vocabulary from a url and serves every later read
@@ -8887,7 +9066,7 @@ var fml = (function (exports) {
8887
9066
  });
8888
9067
  //each pair carries its own anchor: two selects on a page must not share one
8889
9068
  const group = fragment.querySelector('ful-control-group');
8890
- wireAnchoredPopover(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
9069
+ Anchors.wire(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
8891
9070
  [this.#dload, this.#abortdload] = Timing.throttle(400, () => this.#open());
8892
9071
  this.#wireChrome();
8893
9072
  this.#wireChips();
@@ -9477,16 +9656,10 @@ var fml = (function (exports) {
9477
9656
  evt.stopPropagation();
9478
9657
  this._notifyChange();
9479
9658
  });
9659
+ //the base points the label at the input with for/id, so the click toggles
9660
+ //the way it does in a plain form: the input's own change listener above
9661
+ //carries the notification, and readonly is refused by the freeze below
9480
9662
  const label = fragment.querySelector('label');
9481
- //the label neither wraps the input nor targets it, so the toggle is the
9482
- //field's; the base adds the focus
9483
- label.addEventListener('click', () => {
9484
- if (!this._interactive()) {
9485
- return;
9486
- }
9487
- this.value = !this.value;
9488
- this._notifyChange();
9489
- });
9490
9663
  //a checkbox has no editable text to preserve, so readonly freezes the
9491
9664
  //whole choice, label click included: the container is the frozen piece
9492
9665
  return {
@@ -10265,7 +10438,7 @@ var fml = (function (exports) {
10265
10438
  #wire() {
10266
10439
  const button = this.#button;
10267
10440
  const menu = this.#menu;
10268
- wireAnchoredPopover(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
10441
+ Anchors.wire(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
10269
10442
  menu.addEventListener('toggle', (/** @type any */ evt) => {
10270
10443
  if (evt.newState !== 'open') {
10271
10444
  //give the invoker back the focus the menu had borrowed, without
@@ -10846,30 +11019,63 @@ var fml = (function (exports) {
10846
11019
  });
10847
11020
  };
10848
11021
 
10849
- /** An info icon button toggling a popover with a short explanation. */
11022
+ /**
11023
+ * An info icon button toggling a popover with a short explanation.
11024
+ *
11025
+ * The marker is the page's `config.icon`, and the `icon` attribute names a
11026
+ * `ful-icon` for the tooltip that means something other than plain information:
11027
+ * a caveat, a warning, a setting. A name the library does not paint is the
11028
+ * page's own, declared as `ful-icon[name='...'] { mask-image: ... }`.
11029
+ *
11030
+ * `describes` is for the tooltip standing in a field: the note becomes part of
11031
+ * the accessible description of that field's control, so it is announced on
11032
+ * reaching the field rather than only on opening the marker, and the marker
11033
+ * leaves the tab order, so a form of hinted fields costs no extra keystrokes to
11034
+ * walk. The marker stays clickable, and stays a tab stop wherever the note was
11035
+ * not taken, a tooltip claiming `describes` outside a field among them: the
11036
+ * stop only goes where something else delivers the content.
11037
+ */
10850
11038
  class Tooltip extends ParsedElement {
10851
11039
  static slots = true;
10852
- static attributes = ['placement'];
11040
+ static attributes = ['placement', 'icon', 'describes:presence'];
10853
11041
  static config = {
10854
11042
  icon: 'info-circle-fill',
10855
11043
  };
10856
11044
  static template = `
10857
- <button type="button" class="ful-tip" data-ref="trigger" data-tpl-aria-label="#l10n:t('info.tooltip')"><ful-icon data-tpl-name="config.icon" aria-hidden="true"></ful-icon></button>
11045
+ <button type="button" class="ful-tip" data-ref="trigger" data-tpl-aria-label="#l10n:t('info.tooltip')"><ful-icon data-tpl-name="icon ?? config.icon" aria-hidden="true"></ful-icon></button>
10858
11046
  <ful-note popover data-ref="content">{{{{ slots.default }}}}</ful-note>
10859
11047
  `;
10860
11048
  render({ slots }) {
10861
- const fragment = this.template().withOverlay({ slots }).render();
11049
+ const fragment = this.template().withOverlay({ slots, icon: this.declared('icon') }).render();
10862
11050
  const trigger = fragment.querySelector('[data-ref=trigger]');
10863
11051
  const content = fragment.querySelector('[data-ref=content]');
10864
11052
  //placed here rather than by the anchor css: the note draws a callout that
10865
11053
  //has to point at the trigger wherever the viewport left room for the note,
10866
11054
  //which is a measurement the stylesheet cannot make for a pseudo-element
10867
- wireAnchoredPopover(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
10868
- const placement = this.declared('placement');
10869
- if (placement) {
10870
- content.setAttribute('placement', placement);
10871
- }
11055
+ Anchors.wire(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
11056
+ //above the marker by default: a note opening downwards covers the control
11057
+ //the marker explains, the marker riding the field's label
11058
+ content.setAttribute('placement', this.declared('placement') ?? 'top');
10872
11059
  this.replaceChildren(fragment);
11060
+ if (this.declared('describes')) {
11061
+ Tooltip.#describe(this, trigger, content);
11062
+ }
11063
+ }
11064
+ /**
11065
+ * Offers the note to the field the tooltip stands in, and takes the trigger
11066
+ * out of the tab order only where the offer was accepted: a note nothing
11067
+ * carries is reachable by the keyboard through the marker alone, so
11068
+ * dropping the stop there would leave it reachable by nothing at all.
11069
+ *
11070
+ * The offer goes through the description protocol rather than naming a
11071
+ * field, the library's own arrow running from the forms to the disclosures.
11072
+ */
11073
+ static #describe(tooltip, trigger, content) {
11074
+ if (!describable(tooltip)?.describedBy(content)) {
11075
+ console.warn('a ful-tooltip declares describes but stands in nothing that takes a description', tooltip);
11076
+ return;
11077
+ }
11078
+ trigger.tabIndex = -1;
10873
11079
  }
10874
11080
  }
10875
11081
 
@@ -10956,13 +11162,22 @@ var fml = (function (exports) {
10956
11162
  }
10957
11163
  }
10958
11164
 
10959
- /** A side panel drawer on the native dialog platform, update() owning its open-deliver cycle. */
11165
+ /**
11166
+ * A side panel drawer on the native dialog platform, update() owning its
11167
+ * open-deliver cycle.
11168
+ *
11169
+ * The `header` slot is content beside the title, before it: an icon, a badge, a
11170
+ * status. It sits outside the heading rather than in it because `update()` sets
11171
+ * the title through `textContent`, which would take anything nested there with
11172
+ * it.
11173
+ */
10960
11174
  class Drawer extends ParsedElement {
10961
11175
  static attributes = ['title', 'placement'];
10962
11176
  static slots = true;
10963
11177
  static template = `
10964
11178
  <dialog data-ref="dialog" class="ful-drawer">
10965
11179
  <header>
11180
+ {{{{ slots.header }}}}
10966
11181
  <h2 data-ref="title">{{ title }}</h2>
10967
11182
  <button type="button" data-ref="close" data-tpl-aria-label="#l10n:t('drawer.close')"><ful-icon name="x-lg" aria-hidden="true"></ful-icon></button>
10968
11183
  </header>
@@ -11705,6 +11920,7 @@ var fml = (function (exports) {
11705
11920
  var ful = /*#__PURE__*/Object.freeze({
11706
11921
  __proto__: null,
11707
11922
  Accordion: Accordion,
11923
+ Anchors: Anchors,
11708
11924
  AsyncEvents: AsyncEvents,
11709
11925
  Bindings: Bindings,
11710
11926
  BooleanFilter: BooleanFilter,
@@ -11745,7 +11961,8 @@ var fml = (function (exports) {
11745
11961
  Tooltip: Tooltip,
11746
11962
  VersionedLocalStorage: VersionedLocalStorage,
11747
11963
  VersionedSessionStorage: VersionedSessionStorage,
11748
- Wizard: Wizard
11964
+ Wizard: Wizard,
11965
+ describable: describable
11749
11966
  });
11750
11967
 
11751
11968
  if (typeof window !== 'undefined') {
@@ -11757,6 +11974,7 @@ var fml = (function (exports) {
11757
11974
  }
11758
11975
 
11759
11976
  exports.Accordion = Accordion;
11977
+ exports.Anchors = Anchors;
11760
11978
  exports.AsyncEvents = AsyncEvents;
11761
11979
  exports.Attributes = Attributes;
11762
11980
  exports.Base64 = Base64;
@@ -11822,6 +12040,7 @@ var fml = (function (exports) {
11822
12040
  exports.VersionedLocalStorage = VersionedLocalStorage;
11823
12041
  exports.VersionedSessionStorage = VersionedSessionStorage;
11824
12042
  exports.Wizard = Wizard;
12043
+ exports.describable = describable;
11825
12044
  exports.registry = registry;
11826
12045
 
11827
12046
  return exports;