@optionfactory/fml 9.0.0-rc2 → 9.0.0-rc3

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
@@ -539,6 +539,52 @@ declare class InputFile extends Input {
539
539
  get dropzone(): any;
540
540
  set dropzone(v: any);
541
541
  }
542
+ /**
543
+ * CSS anchor positioning for a popover and the invoker it belongs to, with the
544
+ * hand-placed fallback for the platforms that do not have it.
545
+ */
546
+ declare class Anchors {
547
+ /**
548
+ * Anchors a popover to its invoker.
549
+ *
550
+ * The invoker is given an `anchor-name` and the popover a `position-anchor`
551
+ * pointing at it, which is what a stylesheet needs to place the popover
552
+ * itself: the library's own menus say `top: anchor(bottom); left:
553
+ * anchor(left)`. **Writing that css is the caller's half of this.** Without
554
+ * it the popover lands wherever the user agent puts a popover, which is not
555
+ * beside the invoker.
556
+ *
557
+ * Where the platform has no anchor positioning the popover is placed here
558
+ * instead, beside the invoker whenever it opens, clamped into the viewport,
559
+ * following it on scroll and resize, and cleaned up on close. That placement
560
+ * draws the geometry the css above describes, so the two agree.
561
+ *
562
+ * @param {HTMLElement} invoker the element the popover belongs to
563
+ * @param {HTMLElement} popover the `[popover]` element to place
564
+ * @param {object} [options]
565
+ * @param {string} [options.prefix] prefixes the generated anchor name and id,
566
+ * so the dom says which component a name belongs to
567
+ * @param {boolean} [options.invoke] points the invoker's `popovertarget` at
568
+ * the popover, giving toggle and light dismiss with no script of your own
569
+ * @param {boolean} [options.expanded] keeps the invoker's `aria-expanded` in
570
+ * step with the popover
571
+ * @param {boolean} [options.stretch] widens the popover to its invoker, which
572
+ * is what a combobox dropdown wants
573
+ * @param {boolean} [options.handPlace] places here on every platform rather
574
+ * than only as a fallback, which a popover asks for when it needs to know
575
+ * where its invoker ended up: the tooltip's note points a callout at it, and
576
+ * a pseudo-element cannot read an anchor outside its own containing block.
577
+ * Such a popover declares no anchor placement in css, there being none to
578
+ * agree with
579
+ */
580
+ static wire(invoker: HTMLElement, popover: HTMLElement, { prefix, invoke, expanded, stretch, handPlace }?: {
581
+ prefix?: string;
582
+ invoke?: boolean;
583
+ expanded?: boolean;
584
+ stretch?: boolean;
585
+ handPlace?: boolean;
586
+ }): void;
587
+ }
542
588
  /**
543
589
  * Fetches a select's whole vocabulary from a url and serves every later read
544
590
  * from it. Concurrent callers share one request, the options may be cached in
@@ -991,7 +1037,15 @@ declare class Dialog extends ParsedElement {
991
1037
  refresh(): Promise<any[] | undefined>;
992
1038
  close(result: any): void;
993
1039
  }
994
- /** A side panel drawer on the native dialog platform, update() owning its open-deliver cycle. */
1040
+ /**
1041
+ * A side panel drawer on the native dialog platform, update() owning its
1042
+ * open-deliver cycle.
1043
+ *
1044
+ * The `header` slot is content beside the title, before it: an icon, a badge, a
1045
+ * status. It sits outside the heading rather than in it because `update()` sets
1046
+ * the title through `textContent`, which would take anything nested there with
1047
+ * it.
1048
+ */
995
1049
  declare class Drawer extends ParsedElement {
996
1050
  #private;
997
1051
  static attributes: string[];
@@ -1141,6 +1195,6 @@ declare class Plugin {
1141
1195
  });
1142
1196
  configure(registry: any): void;
1143
1197
  }
1144
- export { Accordion, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, 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 };
1198
+ export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, 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 };
1145
1199
 
1146
1200
  export as namespace ful;
package/dist/ful.iife.js CHANGED
@@ -681,12 +681,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
681
681
  //the error region describes the control, or the host where there is no
682
682
  //single control to describe (a radio group's legend names its fieldset)
683
683
  if (error) {
684
- (described ?? control).ariaDescribedByElements = [error];
684
+ //an attribute, not the aria element property: the property reflects to
685
+ //nothing, so the description lived in the accessibility tree alone and
686
+ //vanished entirely on a browser without aria element reflection
687
+ if (!error.id) {
688
+ error.id = index_mjs.Attributes.uid('ful-field-error');
689
+ }
690
+ (described ?? control).setAttribute('aria-describedby', error.id);
685
691
  }
686
692
  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());
693
+ Field.#name(this, label, control);
690
694
  }
691
695
  //the platform's implicit submission, stood in for where the field's own
692
696
  //protocol took it away: the inner controls carry form="", so Enter in one
@@ -775,6 +779,49 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
775
779
  }),
776
780
  );
777
781
  }
782
+ /** The html elements a label's `for` may point at, `input[type=hidden]` excepted. */
783
+ static #LABELABLE = new Set(['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA']);
784
+ /**
785
+ * Names the control from the field's label, natively wherever the platform
786
+ * allows it.
787
+ *
788
+ * `for` and `id` are the form the dom itself carries, so the association is
789
+ * there for anything reading the markup rather than the accessibility tree:
790
+ * an audit tool, the browser's autofill, a translation pass. It also makes
791
+ * the label's click reach the control the way it does in a plain form, which
792
+ * is focus for a text control and activation for a checkbox, so the field
793
+ * needs no handler of its own.
794
+ *
795
+ * A control the platform will not let a label target, a composite carrying
796
+ * `role="radiogroup"` among them, takes `aria-labelledby` instead. That is an
797
+ * attribute too, so the association is equally visible; what it does not carry
798
+ * is the label's click, which is why the handler stays on that path only.
799
+ *
800
+ * Neither branch uses `ariaLabelledByElements`. The property reflects to no
801
+ * attribute, so the name lived in the accessibility tree alone: nothing reading
802
+ * the dom saw it, and on a browser without aria element reflection the
803
+ * assignment is a silent expando and the field has no name at all.
804
+ * @param {any} field
805
+ * @param {HTMLElement} label
806
+ * @param {any} control
807
+ */
808
+ static #name(field, label, control) {
809
+ const labelable =
810
+ Field.#LABELABLE.has(control.tagName) && control.getAttribute('type') !== 'hidden';
811
+ if (!labelable) {
812
+ if (!label.id) {
813
+ label.id = index_mjs.Attributes.uid('ful-label');
814
+ }
815
+ control.setAttribute('aria-labelledby', label.id);
816
+ //aria-labelledby carries the name but not the label's click
817
+ label.addEventListener('click', () => field.focus());
818
+ return;
819
+ }
820
+ if (!control.id) {
821
+ control.id = index_mjs.Attributes.uid('ful-control');
822
+ }
823
+ label.setAttribute('for', control.id);
824
+ }
778
825
  /**
779
826
  * Whether the field's chrome should answer a gesture. Badges, dropzones,
780
827
  * menus and labels are not form controls, so their handlers must ask the
@@ -1964,8 +2011,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1964
2011
  const box = invoker.getBoundingClientRect();
1965
2012
  const here = popover.getBoundingClientRect();
1966
2013
  //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`);
2014
+ popover.style.setProperty(
2015
+ '--ful-note-callout-inline',
2016
+ `${box.left + box.width / 2 - here.left - popover.clientLeft}px`,
2017
+ );
2018
+ popover.style.setProperty(
2019
+ '--ful-note-callout-block',
2020
+ `${box.top + box.height / 2 - here.top - popover.clientTop}px`,
2021
+ );
1969
2022
  };
1970
2023
 
1971
2024
  const place = (popover, anchored) => {
@@ -2069,67 +2122,93 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2069
2122
  };
2070
2123
 
2071
2124
  /**
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.
2125
+ * CSS anchor positioning for a popover and the invoker it belongs to, with the
2126
+ * hand-placed fallback for the platforms that do not have it.
2083
2127
  */
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');
2128
+ class Anchors {
2129
+ /**
2130
+ * Anchors a popover to its invoker.
2131
+ *
2132
+ * The invoker is given an `anchor-name` and the popover a `position-anchor`
2133
+ * pointing at it, which is what a stylesheet needs to place the popover
2134
+ * itself: the library's own menus say `top: anchor(bottom); left:
2135
+ * anchor(left)`. **Writing that css is the caller's half of this.** Without
2136
+ * it the popover lands wherever the user agent puts a popover, which is not
2137
+ * beside the invoker.
2138
+ *
2139
+ * Where the platform has no anchor positioning the popover is placed here
2140
+ * instead, beside the invoker whenever it opens, clamped into the viewport,
2141
+ * following it on scroll and resize, and cleaned up on close. That placement
2142
+ * draws the geometry the css above describes, so the two agree.
2143
+ *
2144
+ * @param {HTMLElement} invoker the element the popover belongs to
2145
+ * @param {HTMLElement} popover the `[popover]` element to place
2146
+ * @param {object} [options]
2147
+ * @param {string} [options.prefix] prefixes the generated anchor name and id,
2148
+ * so the dom says which component a name belongs to
2149
+ * @param {boolean} [options.invoke] points the invoker's `popovertarget` at
2150
+ * the popover, giving toggle and light dismiss with no script of your own
2151
+ * @param {boolean} [options.expanded] keeps the invoker's `aria-expanded` in
2152
+ * step with the popover
2153
+ * @param {boolean} [options.stretch] widens the popover to its invoker, which
2154
+ * is what a combobox dropdown wants
2155
+ * @param {boolean} [options.handPlace] places here on every platform rather
2156
+ * than only as a fallback, which a popover asks for when it needs to know
2157
+ * where its invoker ended up: the tooltip's note points a callout at it, and
2158
+ * a pseudo-element cannot read an anchor outside its own containing block.
2159
+ * Such a popover declares no anchor placement in css, there being none to
2160
+ * agree with
2161
+ */
2162
+ static wire(
2163
+ invoker,
2164
+ popover,
2165
+ { prefix = 'ful-anchor', invoke = false, expanded = false, stretch = false, handPlace = false } = {},
2166
+ ) {
2167
+ const uid = index_mjs.Attributes.uid(prefix);
2168
+ if (invoke) {
2169
+ //popovertarget needs a target that can be named
2170
+ popover.id = popover.id || uid;
2171
+ invoker.setAttribute('popovertarget', popover.id);
2172
+ }
2173
+ const anchor = `--${uid}`;
2174
+ invoker.style.anchorName = anchor;
2175
+ popover.style.positionAnchor = anchor;
2176
+ if (expanded) {
2177
+ invoker.setAttribute('aria-expanded', 'false');
2178
+ popover.addEventListener('toggle', (/** @type any */ evt) => {
2179
+ invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
2180
+ });
2181
+ }
2182
+ //the naming above is what the stylesheet reads, so it happens either way:
2183
+ //only the hand placement below is the fallback, and only for a popover that
2184
+ //did not ask to be placed here whatever the platform offers
2185
+ if (!handPlace && platformAnchors()) {
2186
+ return;
2187
+ }
2188
+ const anchored = { invoker, stretch };
2189
+ popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
2190
+ //placed before the showing, refined once laid out: the platform's
2191
+ //centered or corner spot never paints
2192
+ if (evt.newState === 'open') {
2193
+ place(popover, anchored);
2194
+ }
2195
+ });
2100
2196
  popover.addEventListener('toggle', (/** @type any */ evt) => {
2101
- invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
2197
+ if (evt.newState === 'open') {
2198
+ open.set(popover, anchored);
2199
+ place(popover, anchored);
2200
+ } else {
2201
+ open.delete(popover);
2202
+ unplace(popover);
2203
+ }
2102
2204
  });
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);
2205
+ if (!reflowWired) {
2206
+ reflowWired = true;
2207
+ document.addEventListener('scroll', schedule, true);
2208
+ window.addEventListener('resize', schedule);
2125
2209
  }
2126
- });
2127
- if (!reflowWired) {
2128
- reflowWired = true;
2129
- document.addEventListener('scroll', schedule, true);
2130
- window.addEventListener('resize', schedule);
2131
2210
  }
2132
- };
2211
+ }
2133
2212
 
2134
2213
  /**
2135
2214
  * Fetches a select's whole vocabulary from a url and serves every later read
@@ -2658,7 +2737,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2658
2737
  });
2659
2738
  //each pair carries its own anchor: two selects on a page must not share one
2660
2739
  const group = fragment.querySelector('ful-control-group');
2661
- wireAnchoredPopover(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
2740
+ Anchors.wire(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
2662
2741
  [this.#dload, this.#abortdload] = Timing.throttle(400, () => this.#open());
2663
2742
  this.#wireChrome();
2664
2743
  this.#wireChips();
@@ -3248,16 +3327,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
3248
3327
  evt.stopPropagation();
3249
3328
  this._notifyChange();
3250
3329
  });
3330
+ //the base points the label at the input with for/id, so the click toggles
3331
+ //the way it does in a plain form: the input's own change listener above
3332
+ //carries the notification, and readonly is refused by the freeze below
3251
3333
  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
3334
  //a checkbox has no editable text to preserve, so readonly freezes the
3262
3335
  //whole choice, label click included: the container is the frozen piece
3263
3336
  return {
@@ -4036,7 +4109,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4036
4109
  #wire() {
4037
4110
  const button = this.#button;
4038
4111
  const menu = this.#menu;
4039
- wireAnchoredPopover(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
4112
+ Anchors.wire(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
4040
4113
  menu.addEventListener('toggle', (/** @type any */ evt) => {
4041
4114
  if (evt.newState !== 'open') {
4042
4115
  //give the invoker back the focus the menu had borrowed, without
@@ -4635,7 +4708,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4635
4708
  //placed here rather than by the anchor css: the note draws a callout that
4636
4709
  //has to point at the trigger wherever the viewport left room for the note,
4637
4710
  //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 });
4711
+ Anchors.wire(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
4639
4712
  const placement = this.declared('placement');
4640
4713
  if (placement) {
4641
4714
  content.setAttribute('placement', placement);
@@ -4727,13 +4800,22 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
4727
4800
  }
4728
4801
  }
4729
4802
 
4730
- /** A side panel drawer on the native dialog platform, update() owning its open-deliver cycle. */
4803
+ /**
4804
+ * A side panel drawer on the native dialog platform, update() owning its
4805
+ * open-deliver cycle.
4806
+ *
4807
+ * The `header` slot is content beside the title, before it: an icon, a badge, a
4808
+ * status. It sits outside the heading rather than in it because `update()` sets
4809
+ * the title through `textContent`, which would take anything nested there with
4810
+ * it.
4811
+ */
4731
4812
  class Drawer extends index_mjs.ParsedElement {
4732
4813
  static attributes = ['title', 'placement'];
4733
4814
  static slots = true;
4734
4815
  static template = `
4735
4816
  <dialog data-ref="dialog" class="ful-drawer">
4736
4817
  <header>
4818
+ {{{{ slots.header }}}}
4737
4819
  <h2 data-ref="title">{{ title }}</h2>
4738
4820
  <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
4821
  </header>
@@ -5474,6 +5556,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
5474
5556
  }
5475
5557
 
5476
5558
  exports.Accordion = Accordion;
5559
+ exports.Anchors = Anchors;
5477
5560
  exports.AsyncEvents = AsyncEvents;
5478
5561
  exports.Bindings = Bindings;
5479
5562
  exports.BooleanFilter = BooleanFilter;