@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/ful.iife.js CHANGED
@@ -233,6 +233,48 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
233
233
  }
234
234
  }
235
235
 
236
+ /**
237
+ * The protocol by which content standing inside a field becomes part of the
238
+ * accessible description of that field's control.
239
+ *
240
+ * A field owns its control's `aria-describedby`: it is the only thing that
241
+ * knows which element the description belongs on, and it already writes the
242
+ * entry for its own error region. Content the author slotted into the field
243
+ * cannot write that attribute itself without becoming a second owner of it, and
244
+ * it cannot be wired by the field either, because a slotted custom element
245
+ * renders after the field has mounted and has nothing to point at when the
246
+ * field looks.
247
+ *
248
+ * So the content asks, once it has something to offer. `describable(el)`
249
+ * answers the nearest ancestor that accepts a description, and the caller hands
250
+ * its element to that ancestor's `describedBy`, which answers whether it was
251
+ * taken. Nothing here names a field or a tooltip: the relation is expressed as
252
+ * a capability, so the two ends need not import each other, which matters
253
+ * because the library's own arrow runs from the forms to the disclosures.
254
+ *
255
+ * The lookup lives here rather than at its one call site so the protocol has a
256
+ * name, a place to be documented and a single definition to change.
257
+ */
258
+
259
+ /**
260
+ * @typedef {{ describedBy(el: HTMLElement): boolean }} Describable
261
+ */
262
+
263
+ /**
264
+ * The nearest ancestor of `el` that accepts elements into the description of
265
+ * whatever it considers its control, or null when nothing in the ancestry does.
266
+ * @param {Element} el
267
+ * @returns {(Element & Describable) | null}
268
+ */
269
+ const describable = (el) => {
270
+ for (let at = el.parentElement; at; at = at.parentElement) {
271
+ if (typeof (/** @type {any} */ (at).describedBy) === 'function') {
272
+ return /** @type {any} */ (at);
273
+ }
274
+ }
275
+ return null;
276
+ };
277
+
236
278
  /**
237
279
  * Sleeping, debouncing and throttling. Debounce and throttle both return the
238
280
  * wrapped function together with a cancel function.
@@ -627,6 +669,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
627
669
  /** the role the element internals carry, 'presentation' unless the control is its own */
628
670
  static ROLE = 'presentation';
629
671
  #control;
672
+ #described;
673
+ #descriptions = [];
674
+ #errorId = null;
630
675
  #fieldError;
631
676
  #claims;
632
677
  #announces;
@@ -678,15 +723,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
678
723
  true,
679
724
  );
680
725
  }
681
- //the error region describes the control, or the host where there is no
726
+ //the description lands on the control, or on the host where there is no
682
727
  //single control to describe (a radio group's legend names its fieldset)
728
+ this.#described = described ?? control;
683
729
  if (error) {
684
- (described ?? control).ariaDescribedByElements = [error];
730
+ //named for what it is, the generic id being for whoever brings no name
731
+ error.id = error.id || index_mjs.Attributes.uid('ful-field-error');
732
+ this.#errorId = error.id;
685
733
  }
734
+ //anything handed over before the field had a target lands here
735
+ this.#describe();
686
736
  if (label) {
687
- control.ariaLabelledByElements = [label];
688
- //a label that does not natively target the control still focuses it
689
- label.addEventListener('click', () => this.focus());
737
+ Field.#name(this, label, control);
690
738
  }
691
739
  //the platform's implicit submission, stood in for where the field's own
692
740
  //protocol took it away: the inner controls carry form="", so Enter in one
@@ -719,6 +767,61 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
719
767
  static #submitsOnEnter(el) {
720
768
  return el instanceof HTMLInputElement && !['file', 'button', 'submit', 'reset', 'image'].includes(el.type);
721
769
  }
770
+ /**
771
+ * Adds an element to the accessible description of the field's control and
772
+ * answers whether the field took it.
773
+ *
774
+ * A field takes one whenever it is offered, before its own render as
775
+ * readily as after: content slotted into a field is a custom element of its
776
+ * own and may upgrade on either side of the field it stands in, which
777
+ * happens in both directions in practice, a tooltip beating an async select
778
+ * to its render while losing to a plain input. A description handed over
779
+ * early waits here and is written the moment the field has somewhere to
780
+ * write it, so the caller never has to know the order.
781
+ *
782
+ * The reference lands on the element handed over rather than on a wrapper
783
+ * around it: a hidden element is included in a description only where it is
784
+ * named directly, and content that reaches the description through a
785
+ * wrapper is skipped while it is hidden. A popover closed until someone
786
+ * opens it is exactly that, so the caller passes the popover itself.
787
+ *
788
+ * An attribute rather than `ariaDescribedByElements`: the property reflects
789
+ * to nothing, so the description would live in the accessibility tree alone
790
+ * and vanish entirely on a browser without aria element reflection.
791
+ *
792
+ * This is the field's half of the description protocol; `describable` in
793
+ * `ful/descriptions.mjs` is the half the content uses to find the field.
794
+ * @param {HTMLElement} el
795
+ * @returns {boolean}
796
+ */
797
+ describedBy(el) {
798
+ if (!el) {
799
+ return false;
800
+ }
801
+ if (!el.id) {
802
+ el.id = index_mjs.Attributes.uid('ful-described');
803
+ }
804
+ if (!this.#descriptions.includes(el.id)) {
805
+ this.#descriptions.push(el.id);
806
+ }
807
+ this.#describe();
808
+ return true;
809
+ }
810
+ /**
811
+ * Writes the description the field has collected, the error region last:
812
+ * the standing explanations are what the field always says, the problem is
813
+ * the news. The field owns the attribute outright rather than appending to
814
+ * whatever is there, so the order does not depend on who arrived when.
815
+ */
816
+ #describe() {
817
+ if (!this.#described) {
818
+ return;
819
+ }
820
+ const ids = [...this.#descriptions, this.#errorId].filter((id) => id);
821
+ if (ids.length) {
822
+ this.#described.setAttribute('aria-describedby', ids.join(' '));
823
+ }
824
+ }
722
825
  focus(options) {
723
826
  this.#control?.focus(options);
724
827
  }
@@ -775,6 +878,49 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
775
878
  }),
776
879
  );
777
880
  }
881
+ /** The html elements a label's `for` may point at, `input[type=hidden]` excepted. */
882
+ static #LABELABLE = new Set(['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA']);
883
+ /**
884
+ * Names the control from the field's label, natively wherever the platform
885
+ * allows it.
886
+ *
887
+ * `for` and `id` are the form the dom itself carries, so the association is
888
+ * there for anything reading the markup rather than the accessibility tree:
889
+ * an audit tool, the browser's autofill, a translation pass. It also makes
890
+ * the label's click reach the control the way it does in a plain form, which
891
+ * is focus for a text control and activation for a checkbox, so the field
892
+ * needs no handler of its own.
893
+ *
894
+ * A control the platform will not let a label target, a composite carrying
895
+ * `role="radiogroup"` among them, takes `aria-labelledby` instead. That is an
896
+ * attribute too, so the association is equally visible; what it does not carry
897
+ * is the label's click, which is why the handler stays on that path only.
898
+ *
899
+ * Neither branch uses `ariaLabelledByElements`. The property reflects to no
900
+ * attribute, so the name lived in the accessibility tree alone: nothing reading
901
+ * the dom saw it, and on a browser without aria element reflection the
902
+ * assignment is a silent expando and the field has no name at all.
903
+ * @param {any} field
904
+ * @param {HTMLElement} label
905
+ * @param {any} control
906
+ */
907
+ static #name(field, label, control) {
908
+ const labelable =
909
+ Field.#LABELABLE.has(control.tagName) && control.getAttribute('type') !== 'hidden';
910
+ if (!labelable) {
911
+ if (!label.id) {
912
+ label.id = index_mjs.Attributes.uid('ful-label');
913
+ }
914
+ control.setAttribute('aria-labelledby', label.id);
915
+ //aria-labelledby carries the name but not the label's click
916
+ label.addEventListener('click', () => field.focus());
917
+ return;
918
+ }
919
+ if (!control.id) {
920
+ control.id = index_mjs.Attributes.uid('ful-control');
921
+ }
922
+ label.setAttribute('for', control.id);
923
+ }
778
924
  /**
779
925
  * Whether the field's chrome should answer a gesture. Badges, dropzones,
780
926
  * menus and labels are not form controls, so their handlers must ask the
@@ -912,8 +1058,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
912
1058
  * three claims reach it
913
1059
  * - `error` is the field's live region
914
1060
  * - `label`, when given, names the control and focuses it on click
915
- * - `described` moves the error's description off the control and onto
916
- * another element, the host where no single control can carry it
1061
+ * - `described` moves the description off the control and onto another
1062
+ * element, the host where no single control can carry it: the error
1063
+ * region and anything `describedBy` is later handed both land there
917
1064
  * - `claims` moves the three claims onto a wrapper the field disables as a
918
1065
  * whole, leaving focus and aria on the control
919
1066
  * - `announces` is the element whose role carries `aria-readonly` and
@@ -1964,8 +2111,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1964
2111
  const box = invoker.getBoundingClientRect();
1965
2112
  const here = popover.getBoundingClientRect();
1966
2113
  //against the padding box, which is what a percentage inset resolves against
1967
- popover.style.setProperty('--ful-note-callout-inline', `${box.left + box.width / 2 - here.left - popover.clientLeft}px`);
1968
- popover.style.setProperty('--ful-note-callout-block', `${box.top + box.height / 2 - here.top - popover.clientTop}px`);
2114
+ popover.style.setProperty(
2115
+ '--ful-note-callout-inline',
2116
+ `${box.left + box.width / 2 - here.left - popover.clientLeft}px`,
2117
+ );
2118
+ popover.style.setProperty(
2119
+ '--ful-note-callout-block',
2120
+ `${box.top + box.height / 2 - here.top - popover.clientTop}px`,
2121
+ );
1969
2122
  };
1970
2123
 
1971
2124
  const place = (popover, anchored) => {
@@ -2069,67 +2222,93 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2069
2222
  };
2070
2223
 
2071
2224
  /**
2072
- * Wires an invoker/popover pair ful anchors through css: where the
2073
- * platform lacks anchor positioning the popover is placed beside its
2074
- * invoker whenever it opens, stretched to the invoker's width when
2075
- * asked, and cleaned up when it closes. Where the css works the call
2076
- * is a no-op.
2077
- *
2078
- * `handPlace` takes the placement here on every platform, which a popover
2079
- * asks for when it needs to know where its invoker ended up: the note's
2080
- * callout points at the invoker, and a pseudo-element cannot read an anchor
2081
- * that is not inside its own containing block, so the note is measured rather
2082
- * than placed by the css. Its stylesheet declares no position-area to match.
2225
+ * CSS anchor positioning for a popover and the invoker it belongs to, with the
2226
+ * hand-placed fallback for the platforms that do not have it.
2083
2227
  */
2084
- const wireAnchoredPopover = (
2085
- invoker,
2086
- popover,
2087
- { prefix = 'ful-anchor', invoke = false, expanded = false, stretch = false, handPlace = false } = {},
2088
- ) => {
2089
- const uid = index_mjs.Attributes.uid(prefix);
2090
- if (invoke) {
2091
- //popovertarget needs a target that can be named
2092
- popover.id = popover.id || uid;
2093
- invoker.setAttribute('popovertarget', popover.id);
2094
- }
2095
- const anchor = `--${uid}`;
2096
- invoker.style.anchorName = anchor;
2097
- popover.style.positionAnchor = anchor;
2098
- if (expanded) {
2099
- invoker.setAttribute('aria-expanded', 'false');
2228
+ class Anchors {
2229
+ /**
2230
+ * Anchors a popover to its invoker.
2231
+ *
2232
+ * The invoker is given an `anchor-name` and the popover a `position-anchor`
2233
+ * pointing at it, which is what a stylesheet needs to place the popover
2234
+ * itself: the library's own menus say `top: anchor(bottom); left:
2235
+ * anchor(left)`. **Writing that css is the caller's half of this.** Without
2236
+ * it the popover lands wherever the user agent puts a popover, which is not
2237
+ * beside the invoker.
2238
+ *
2239
+ * Where the platform has no anchor positioning the popover is placed here
2240
+ * instead, beside the invoker whenever it opens, clamped into the viewport,
2241
+ * following it on scroll and resize, and cleaned up on close. That placement
2242
+ * draws the geometry the css above describes, so the two agree.
2243
+ *
2244
+ * @param {HTMLElement} invoker the element the popover belongs to
2245
+ * @param {HTMLElement} popover the `[popover]` element to place
2246
+ * @param {object} [options]
2247
+ * @param {string} [options.prefix] prefixes the generated anchor name and id,
2248
+ * so the dom says which component a name belongs to
2249
+ * @param {boolean} [options.invoke] points the invoker's `popovertarget` at
2250
+ * the popover, giving toggle and light dismiss with no script of your own
2251
+ * @param {boolean} [options.expanded] keeps the invoker's `aria-expanded` in
2252
+ * step with the popover
2253
+ * @param {boolean} [options.stretch] widens the popover to its invoker, which
2254
+ * is what a combobox dropdown wants
2255
+ * @param {boolean} [options.handPlace] places here on every platform rather
2256
+ * than only as a fallback, which a popover asks for when it needs to know
2257
+ * where its invoker ended up: the tooltip's note points a callout at it, and
2258
+ * a pseudo-element cannot read an anchor outside its own containing block.
2259
+ * Such a popover declares no anchor placement in css, there being none to
2260
+ * agree with
2261
+ */
2262
+ static wire(
2263
+ invoker,
2264
+ popover,
2265
+ { prefix = 'ful-anchor', invoke = false, expanded = false, stretch = false, handPlace = false } = {},
2266
+ ) {
2267
+ const uid = index_mjs.Attributes.uid(prefix);
2268
+ if (invoke) {
2269
+ //popovertarget needs a target that can be named
2270
+ popover.id = popover.id || uid;
2271
+ invoker.setAttribute('popovertarget', popover.id);
2272
+ }
2273
+ const anchor = `--${uid}`;
2274
+ invoker.style.anchorName = anchor;
2275
+ popover.style.positionAnchor = anchor;
2276
+ if (expanded) {
2277
+ invoker.setAttribute('aria-expanded', 'false');
2278
+ popover.addEventListener('toggle', (/** @type any */ evt) => {
2279
+ invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
2280
+ });
2281
+ }
2282
+ //the naming above is what the stylesheet reads, so it happens either way:
2283
+ //only the hand placement below is the fallback, and only for a popover that
2284
+ //did not ask to be placed here whatever the platform offers
2285
+ if (!handPlace && platformAnchors()) {
2286
+ return;
2287
+ }
2288
+ const anchored = { invoker, stretch };
2289
+ popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
2290
+ //placed before the showing, refined once laid out: the platform's
2291
+ //centered or corner spot never paints
2292
+ if (evt.newState === 'open') {
2293
+ place(popover, anchored);
2294
+ }
2295
+ });
2100
2296
  popover.addEventListener('toggle', (/** @type any */ evt) => {
2101
- invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
2297
+ if (evt.newState === 'open') {
2298
+ open.set(popover, anchored);
2299
+ place(popover, anchored);
2300
+ } else {
2301
+ open.delete(popover);
2302
+ unplace(popover);
2303
+ }
2102
2304
  });
2103
- }
2104
- //the naming above is what the stylesheet reads, so it happens either way:
2105
- //only the hand placement below is the fallback, and only for a popover that
2106
- //did not ask to be placed here whatever the platform offers
2107
- if (!handPlace && platformAnchors()) {
2108
- return;
2109
- }
2110
- const anchored = { invoker, stretch };
2111
- popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
2112
- //placed before the showing, refined once laid out: the platform's
2113
- //centered or corner spot never paints
2114
- if (evt.newState === 'open') {
2115
- place(popover, anchored);
2116
- }
2117
- });
2118
- popover.addEventListener('toggle', (/** @type any */ evt) => {
2119
- if (evt.newState === 'open') {
2120
- open.set(popover, anchored);
2121
- place(popover, anchored);
2122
- } else {
2123
- open.delete(popover);
2124
- unplace(popover);
2305
+ if (!reflowWired) {
2306
+ reflowWired = true;
2307
+ document.addEventListener('scroll', schedule, true);
2308
+ window.addEventListener('resize', schedule);
2125
2309
  }
2126
- });
2127
- if (!reflowWired) {
2128
- reflowWired = true;
2129
- document.addEventListener('scroll', schedule, true);
2130
- window.addEventListener('resize', schedule);
2131
2310
  }
2132
- };
2311
+ }
2133
2312
 
2134
2313
  /**
2135
2314
  * Fetches a select's whole vocabulary from a url and serves every later read
@@ -2658,7 +2837,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2658
2837
  });
2659
2838
  //each pair carries its own anchor: two selects on a page must not share one
2660
2839
  const group = fragment.querySelector('ful-control-group');
2661
- wireAnchoredPopover(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
2840
+ Anchors.wire(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
2662
2841
  [this.#dload, this.#abortdload] = Timing.throttle(400, () => this.#open());
2663
2842
  this.#wireChrome();
2664
2843
  this.#wireChips();
@@ -3248,16 +3427,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
3248
3427
  evt.stopPropagation();
3249
3428
  this._notifyChange();
3250
3429
  });
3430
+ //the base points the label at the input with for/id, so the click toggles
3431
+ //the way it does in a plain form: the input's own change listener above
3432
+ //carries the notification, and readonly is refused by the freeze below
3251
3433
  const label = fragment.querySelector('label');
3252
- //the label neither wraps the input nor targets it, so the toggle is the
3253
- //field's; the base adds the focus
3254
- label.addEventListener('click', () => {
3255
- if (!this._interactive()) {
3256
- return;
3257
- }
3258
- this.value = !this.value;
3259
- this._notifyChange();
3260
- });
3261
3434
  //a checkbox has no editable text to preserve, so readonly freezes the
3262
3435
  //whole choice, label click included: the container is the frozen piece
3263
3436
  return {
@@ -4036,7 +4209,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4036
4209
  #wire() {
4037
4210
  const button = this.#button;
4038
4211
  const menu = this.#menu;
4039
- wireAnchoredPopover(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
4212
+ Anchors.wire(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
4040
4213
  menu.addEventListener('toggle', (/** @type any */ evt) => {
4041
4214
  if (evt.newState !== 'open') {
4042
4215
  //give the invoker back the focus the menu had borrowed, without
@@ -4617,30 +4790,63 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4617
4790
  });
4618
4791
  };
4619
4792
 
4620
- /** An info icon button toggling a popover with a short explanation. */
4793
+ /**
4794
+ * An info icon button toggling a popover with a short explanation.
4795
+ *
4796
+ * The marker is the page's `config.icon`, and the `icon` attribute names a
4797
+ * `ful-icon` for the tooltip that means something other than plain information:
4798
+ * a caveat, a warning, a setting. A name the library does not paint is the
4799
+ * page's own, declared as `ful-icon[name='...'] { mask-image: ... }`.
4800
+ *
4801
+ * `describes` is for the tooltip standing in a field: the note becomes part of
4802
+ * the accessible description of that field's control, so it is announced on
4803
+ * reaching the field rather than only on opening the marker, and the marker
4804
+ * leaves the tab order, so a form of hinted fields costs no extra keystrokes to
4805
+ * walk. The marker stays clickable, and stays a tab stop wherever the note was
4806
+ * not taken, a tooltip claiming `describes` outside a field among them: the
4807
+ * stop only goes where something else delivers the content.
4808
+ */
4621
4809
  class Tooltip extends index_mjs.ParsedElement {
4622
4810
  static slots = true;
4623
- static attributes = ['placement'];
4811
+ static attributes = ['placement', 'icon', 'describes:presence'];
4624
4812
  static config = {
4625
4813
  icon: 'info-circle-fill',
4626
4814
  };
4627
4815
  static template = `
4628
- <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>
4816
+ <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>
4629
4817
  <ful-note popover data-ref="content">{{{{ slots.default }}}}</ful-note>
4630
4818
  `;
4631
4819
  render({ slots }) {
4632
- const fragment = this.template().withOverlay({ slots }).render();
4820
+ const fragment = this.template().withOverlay({ slots, icon: this.declared('icon') }).render();
4633
4821
  const trigger = fragment.querySelector('[data-ref=trigger]');
4634
4822
  const content = fragment.querySelector('[data-ref=content]');
4635
4823
  //placed here rather than by the anchor css: the note draws a callout that
4636
4824
  //has to point at the trigger wherever the viewport left room for the note,
4637
4825
  //which is a measurement the stylesheet cannot make for a pseudo-element
4638
- wireAnchoredPopover(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
4639
- const placement = this.declared('placement');
4640
- if (placement) {
4641
- content.setAttribute('placement', placement);
4642
- }
4826
+ Anchors.wire(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
4827
+ //above the marker by default: a note opening downwards covers the control
4828
+ //the marker explains, the marker riding the field's label
4829
+ content.setAttribute('placement', this.declared('placement') ?? 'top');
4643
4830
  this.replaceChildren(fragment);
4831
+ if (this.declared('describes')) {
4832
+ Tooltip.#describe(this, trigger, content);
4833
+ }
4834
+ }
4835
+ /**
4836
+ * Offers the note to the field the tooltip stands in, and takes the trigger
4837
+ * out of the tab order only where the offer was accepted: a note nothing
4838
+ * carries is reachable by the keyboard through the marker alone, so
4839
+ * dropping the stop there would leave it reachable by nothing at all.
4840
+ *
4841
+ * The offer goes through the description protocol rather than naming a
4842
+ * field, the library's own arrow running from the forms to the disclosures.
4843
+ */
4844
+ static #describe(tooltip, trigger, content) {
4845
+ if (!describable(tooltip)?.describedBy(content)) {
4846
+ console.warn('a ful-tooltip declares describes but stands in nothing that takes a description', tooltip);
4847
+ return;
4848
+ }
4849
+ trigger.tabIndex = -1;
4644
4850
  }
4645
4851
  }
4646
4852
 
@@ -4727,13 +4933,22 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4727
4933
  }
4728
4934
  }
4729
4935
 
4730
- /** A side panel drawer on the native dialog platform, update() owning its open-deliver cycle. */
4936
+ /**
4937
+ * A side panel drawer on the native dialog platform, update() owning its
4938
+ * open-deliver cycle.
4939
+ *
4940
+ * The `header` slot is content beside the title, before it: an icon, a badge, a
4941
+ * status. It sits outside the heading rather than in it because `update()` sets
4942
+ * the title through `textContent`, which would take anything nested there with
4943
+ * it.
4944
+ */
4731
4945
  class Drawer extends index_mjs.ParsedElement {
4732
4946
  static attributes = ['title', 'placement'];
4733
4947
  static slots = true;
4734
4948
  static template = `
4735
4949
  <dialog data-ref="dialog" class="ful-drawer">
4736
4950
  <header>
4951
+ {{{{ slots.header }}}}
4737
4952
  <h2 data-ref="title">{{ title }}</h2>
4738
4953
  <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>
4739
4954
  </header>
@@ -5474,6 +5689,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5474
5689
  }
5475
5690
 
5476
5691
  exports.Accordion = Accordion;
5692
+ exports.Anchors = Anchors;
5477
5693
  exports.AsyncEvents = AsyncEvents;
5478
5694
  exports.Bindings = Bindings;
5479
5695
  exports.BooleanFilter = BooleanFilter;
@@ -5515,6 +5731,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5515
5731
  exports.VersionedLocalStorage = VersionedLocalStorage;
5516
5732
  exports.VersionedSessionStorage = VersionedSessionStorage;
5517
5733
  exports.Wizard = Wizard;
5734
+ exports.describable = describable;
5518
5735
 
5519
5736
  return exports;
5520
5737