@optionfactory/fml 9.0.0-rc3 → 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/custom-elements.json +16 -2
- package/dist/fml.iife.js +154 -19
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ful.d.mts +84 -4
- package/dist/ful.iife.js +152 -18
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +152 -19
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +10 -2
- package/dist/web-types.json +19 -3
- package/package.json +1 -1
package/dist/ful.mjs
CHANGED
|
@@ -233,6 +233,48 @@ class Claims {
|
|
|
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 @@ class Field extends ParsedElement {
|
|
|
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,17 +723,16 @@ class Field extends ParsedElement {
|
|
|
678
723
|
true,
|
|
679
724
|
);
|
|
680
725
|
}
|
|
681
|
-
//the
|
|
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
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (!error.id) {
|
|
688
|
-
error.id = Attributes.uid('ful-field-error');
|
|
689
|
-
}
|
|
690
|
-
(described ?? control).setAttribute('aria-describedby', error.id);
|
|
730
|
+
//named for what it is, the generic id being for whoever brings no name
|
|
731
|
+
error.id = error.id || Attributes.uid('ful-field-error');
|
|
732
|
+
this.#errorId = error.id;
|
|
691
733
|
}
|
|
734
|
+
//anything handed over before the field had a target lands here
|
|
735
|
+
this.#describe();
|
|
692
736
|
if (label) {
|
|
693
737
|
Field.#name(this, label, control);
|
|
694
738
|
}
|
|
@@ -723,6 +767,61 @@ class Field extends ParsedElement {
|
|
|
723
767
|
static #submitsOnEnter(el) {
|
|
724
768
|
return el instanceof HTMLInputElement && !['file', 'button', 'submit', 'reset', 'image'].includes(el.type);
|
|
725
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 = 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
|
+
}
|
|
726
825
|
focus(options) {
|
|
727
826
|
this.#control?.focus(options);
|
|
728
827
|
}
|
|
@@ -959,8 +1058,9 @@ class Field extends ParsedElement {
|
|
|
959
1058
|
* three claims reach it
|
|
960
1059
|
* - `error` is the field's live region
|
|
961
1060
|
* - `label`, when given, names the control and focuses it on click
|
|
962
|
-
* - `described` moves the
|
|
963
|
-
*
|
|
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
|
|
964
1064
|
* - `claims` moves the three claims onto a wrapper the field disables as a
|
|
965
1065
|
* whole, leaving focus and aria on the control
|
|
966
1066
|
* - `announces` is the element whose role carries `aria-readonly` and
|
|
@@ -4690,30 +4790,63 @@ const wireTargets = () => {
|
|
|
4690
4790
|
});
|
|
4691
4791
|
};
|
|
4692
4792
|
|
|
4693
|
-
/**
|
|
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
|
+
*/
|
|
4694
4809
|
class Tooltip extends ParsedElement {
|
|
4695
4810
|
static slots = true;
|
|
4696
|
-
static attributes = ['placement'];
|
|
4811
|
+
static attributes = ['placement', 'icon', 'describes:presence'];
|
|
4697
4812
|
static config = {
|
|
4698
4813
|
icon: 'info-circle-fill',
|
|
4699
4814
|
};
|
|
4700
4815
|
static template = `
|
|
4701
|
-
<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>
|
|
4702
4817
|
<ful-note popover data-ref="content">{{{{ slots.default }}}}</ful-note>
|
|
4703
4818
|
`;
|
|
4704
4819
|
render({ slots }) {
|
|
4705
|
-
const fragment = this.template().withOverlay({ slots }).render();
|
|
4820
|
+
const fragment = this.template().withOverlay({ slots, icon: this.declared('icon') }).render();
|
|
4706
4821
|
const trigger = fragment.querySelector('[data-ref=trigger]');
|
|
4707
4822
|
const content = fragment.querySelector('[data-ref=content]');
|
|
4708
4823
|
//placed here rather than by the anchor css: the note draws a callout that
|
|
4709
4824
|
//has to point at the trigger wherever the viewport left room for the note,
|
|
4710
4825
|
//which is a measurement the stylesheet cannot make for a pseudo-element
|
|
4711
4826
|
Anchors.wire(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
}
|
|
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');
|
|
4716
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;
|
|
4717
4850
|
}
|
|
4718
4851
|
}
|
|
4719
4852
|
|
|
@@ -5555,5 +5688,5 @@ class Plugin {
|
|
|
5555
5688
|
}
|
|
5556
5689
|
}
|
|
5557
5690
|
|
|
5558
|
-
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 };
|
|
5691
|
+
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, describable };
|
|
5559
5692
|
//# sourceMappingURL=ful.mjs.map
|