@optionfactory/fml 9.0.0-rc3 → 9.0.0-rc5
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 +23 -2
- package/dist/fml.css +1 -1
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +198 -25
- 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.css +1 -1
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +104 -5
- package/dist/ful.iife.js +196 -24
- 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 +196 -25
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +14 -2
- package/dist/web-types.json +27 -3
- package/package.json +1 -1
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,17 +6952,16 @@ var fml = (function (exports) {
|
|
|
6907
6952
|
true,
|
|
6908
6953
|
);
|
|
6909
6954
|
}
|
|
6910
|
-
//the
|
|
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
|
-
//
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
if (!error.id) {
|
|
6917
|
-
error.id = Attributes.uid('ful-field-error');
|
|
6918
|
-
}
|
|
6919
|
-
(described ?? control).setAttribute('aria-describedby', error.id);
|
|
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;
|
|
6920
6962
|
}
|
|
6963
|
+
//anything handed over before the field had a target lands here
|
|
6964
|
+
this.#describe();
|
|
6921
6965
|
if (label) {
|
|
6922
6966
|
Field.#name(this, label, control);
|
|
6923
6967
|
}
|
|
@@ -6952,6 +6996,61 @@ var fml = (function (exports) {
|
|
|
6952
6996
|
static #submitsOnEnter(el) {
|
|
6953
6997
|
return el instanceof HTMLInputElement && !['file', 'button', 'submit', 'reset', 'image'].includes(el.type);
|
|
6954
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
|
+
}
|
|
6955
7054
|
focus(options) {
|
|
6956
7055
|
this.#control?.focus(options);
|
|
6957
7056
|
}
|
|
@@ -7188,8 +7287,9 @@ var fml = (function (exports) {
|
|
|
7188
7287
|
* three claims reach it
|
|
7189
7288
|
* - `error` is the field's live region
|
|
7190
7289
|
* - `label`, when given, names the control and focuses it on click
|
|
7191
|
-
* - `described` moves the
|
|
7192
|
-
*
|
|
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
|
|
7193
7293
|
* - `claims` moves the three claims onto a wrapper the field disables as a
|
|
7194
7294
|
* whole, leaving focus and aria on the control
|
|
7195
7295
|
* - `announces` is the element whose role carries `aria-readonly` and
|
|
@@ -10919,42 +11019,97 @@ var fml = (function (exports) {
|
|
|
10919
11019
|
});
|
|
10920
11020
|
};
|
|
10921
11021
|
|
|
10922
|
-
/**
|
|
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
|
+
*/
|
|
10923
11038
|
class Tooltip extends ParsedElement {
|
|
10924
11039
|
static slots = true;
|
|
10925
|
-
static attributes = ['placement'];
|
|
11040
|
+
static attributes = ['placement', 'icon', 'describes:presence'];
|
|
10926
11041
|
static config = {
|
|
10927
11042
|
icon: 'info-circle-fill',
|
|
10928
11043
|
};
|
|
10929
11044
|
static template = `
|
|
10930
|
-
<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>
|
|
10931
11046
|
<ful-note popover data-ref="content">{{{{ slots.default }}}}</ful-note>
|
|
10932
11047
|
`;
|
|
10933
11048
|
render({ slots }) {
|
|
10934
|
-
const fragment = this.template().withOverlay({ slots }).render();
|
|
11049
|
+
const fragment = this.template().withOverlay({ slots, icon: this.declared('icon') }).render();
|
|
10935
11050
|
const trigger = fragment.querySelector('[data-ref=trigger]');
|
|
10936
11051
|
const content = fragment.querySelector('[data-ref=content]');
|
|
10937
11052
|
//placed here rather than by the anchor css: the note draws a callout that
|
|
10938
11053
|
//has to point at the trigger wherever the viewport left room for the note,
|
|
10939
11054
|
//which is a measurement the stylesheet cannot make for a pseudo-element
|
|
10940
11055
|
Anchors.wire(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
}
|
|
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');
|
|
10945
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;
|
|
10946
11079
|
}
|
|
10947
11080
|
}
|
|
10948
11081
|
|
|
10949
|
-
/**
|
|
11082
|
+
/**
|
|
11083
|
+
* A modal dialog on the native platform, open()/ask() resolving with the
|
|
11084
|
+
* closer's data-result.
|
|
11085
|
+
*
|
|
11086
|
+
* The header carries a close button, as the drawer's does: Escape dismisses a
|
|
11087
|
+
* modal on its own, but nothing says so, and a dialog whose only exit is a key
|
|
11088
|
+
* you have to know about leaves a pointer with nowhere to go. It answers the way
|
|
11089
|
+
* Escape does, with null.
|
|
11090
|
+
*
|
|
11091
|
+
* `requires-answer` is for the dialog that must be answered: the close button is not
|
|
11092
|
+
* rendered and Escape is refused, so the only way out is a button that carries a
|
|
11093
|
+
* result. It has to be both, a close button withheld while Escape still worked
|
|
11094
|
+
* being decoration rather than a rule.
|
|
11095
|
+
*
|
|
11096
|
+
* The chrome is reachable by class as well as by tag, so a plain `<dialog
|
|
11097
|
+
* class="ful-dialog">` written by a page gets the same look whatever its
|
|
11098
|
+
* structure: the tag form matches a direct child, and `ful-dialog-header`,
|
|
11099
|
+
* `ful-dialog-body` and `ful-dialog-footer` match at any depth, which is what a
|
|
11100
|
+
* dialog whose content is wrapped in a form needs.
|
|
11101
|
+
*/
|
|
10950
11102
|
class Dialog extends ParsedElement {
|
|
10951
|
-
static attributes = ['header'];
|
|
11103
|
+
static attributes = ['header', 'requires-answer:presence'];
|
|
10952
11104
|
static slots = true;
|
|
10953
11105
|
static template = `
|
|
10954
11106
|
<dialog data-ref="dialog" class="ful-dialog">
|
|
10955
|
-
<header data-tpl-if="header"
|
|
10956
|
-
|
|
10957
|
-
|
|
11107
|
+
<header data-tpl-if="header || !requiresAnswer" class="ful-dialog-header">
|
|
11108
|
+
<h2 data-tpl-if="header">{{ header }}</h2>
|
|
11109
|
+
<button data-tpl-if="!requiresAnswer" type="button" data-ref="close" data-tpl-aria-label="#l10n:t('dialog.close')"><ful-icon name="x-lg" aria-hidden="true"></ful-icon></button>
|
|
11110
|
+
</header>
|
|
11111
|
+
<div data-ref="body" class="ful-dialog-body">{{{{ slots.default }}}}</div>
|
|
11112
|
+
<footer class="ful-dialog-footer">
|
|
10958
11113
|
<button type="button" data-ref="acknowledge" data-result="acknowledged" data-tpl-if="!slots.buttons" data-tpl-aria-label="#l10n:t('dialog.acknowledge')">{{ #l10n:t('dialog.acknowledge') }}</button>
|
|
10959
11114
|
{{{{ slots.buttons }}}}
|
|
10960
11115
|
</footer>
|
|
@@ -10965,8 +11120,9 @@ var fml = (function (exports) {
|
|
|
10965
11120
|
#requests = new SectionRequests();
|
|
10966
11121
|
#resolvers = [];
|
|
10967
11122
|
render({ slots }) {
|
|
11123
|
+
const requiresAnswer = this.declared('requires-answer');
|
|
10968
11124
|
const fragment = this.template()
|
|
10969
|
-
.withOverlay({ slots, header: this.declared('header') ?? '' })
|
|
11125
|
+
.withOverlay({ slots, header: this.declared('header') ?? '', requiresAnswer })
|
|
10970
11126
|
.render();
|
|
10971
11127
|
this.#dialog = fragment.querySelector('[data-ref=dialog]');
|
|
10972
11128
|
this.#body = fragment.querySelector('[data-ref=body]');
|
|
@@ -10984,6 +11140,17 @@ var fml = (function (exports) {
|
|
|
10984
11140
|
this.#dialog.close(result);
|
|
10985
11141
|
}
|
|
10986
11142
|
});
|
|
11143
|
+
//dismissal, not an answer: the waiters are settled with null, as Escape does.
|
|
11144
|
+
//Optional because a subclass overriding the template owns what it renders
|
|
11145
|
+
fragment
|
|
11146
|
+
.querySelector('[data-ref=close]')
|
|
11147
|
+
?.addEventListener('click', () => this.#dialog.close(''));
|
|
11148
|
+
if (requiresAnswer) {
|
|
11149
|
+
//the platform's own dismissal, refused where the dialog must be
|
|
11150
|
+
//answered: cancel fires for Escape and for a close request the
|
|
11151
|
+
//browser makes on its own, and preventing it leaves the dialog open
|
|
11152
|
+
this.#dialog.addEventListener('cancel', (/** @type any */ e) => e.preventDefault());
|
|
11153
|
+
}
|
|
10987
11154
|
this.replaceChildren(fragment);
|
|
10988
11155
|
wireTargets();
|
|
10989
11156
|
}
|
|
@@ -11572,6 +11739,7 @@ var fml = (function (exports) {
|
|
|
11572
11739
|
'filters.boolean.false': 'No',
|
|
11573
11740
|
'info.tooltip': 'More information',
|
|
11574
11741
|
'dialog.acknowledge': 'Got it',
|
|
11742
|
+
'dialog.close': 'Close',
|
|
11575
11743
|
'drawer.close': 'Close',
|
|
11576
11744
|
'spinner.loading': 'Loading…',
|
|
11577
11745
|
'toast.region': 'Notifications',
|
|
@@ -11613,6 +11781,7 @@ var fml = (function (exports) {
|
|
|
11613
11781
|
'filters.boolean.false': 'No',
|
|
11614
11782
|
'info.tooltip': 'Maggiori informazioni',
|
|
11615
11783
|
'dialog.acknowledge': 'Ho capito',
|
|
11784
|
+
'dialog.close': 'Chiudi',
|
|
11616
11785
|
'drawer.close': 'Chiudi',
|
|
11617
11786
|
'spinner.loading': 'Caricamento…',
|
|
11618
11787
|
'toast.region': 'Notifiche',
|
|
@@ -11654,6 +11823,7 @@ var fml = (function (exports) {
|
|
|
11654
11823
|
'filters.boolean.false': 'No',
|
|
11655
11824
|
'info.tooltip': 'Más información',
|
|
11656
11825
|
'dialog.acknowledge': 'Entendido',
|
|
11826
|
+
'dialog.close': 'Cerrar',
|
|
11657
11827
|
'drawer.close': 'Cerrar',
|
|
11658
11828
|
'spinner.loading': 'Cargando…',
|
|
11659
11829
|
'toast.region': 'Notificaciones',
|
|
@@ -11698,6 +11868,7 @@ var fml = (function (exports) {
|
|
|
11698
11868
|
'filters.boolean.false': 'Non',
|
|
11699
11869
|
'info.tooltip': 'Plus d’informations',
|
|
11700
11870
|
'dialog.acknowledge': 'J’ai compris',
|
|
11871
|
+
'dialog.close': 'Fermer',
|
|
11701
11872
|
'drawer.close': 'Fermer',
|
|
11702
11873
|
'spinner.loading': 'Chargement…',
|
|
11703
11874
|
'toast.region': 'Notifications',
|
|
@@ -11828,7 +11999,8 @@ var fml = (function (exports) {
|
|
|
11828
11999
|
Tooltip: Tooltip,
|
|
11829
12000
|
VersionedLocalStorage: VersionedLocalStorage,
|
|
11830
12001
|
VersionedSessionStorage: VersionedSessionStorage,
|
|
11831
|
-
Wizard: Wizard
|
|
12002
|
+
Wizard: Wizard,
|
|
12003
|
+
describable: describable
|
|
11832
12004
|
});
|
|
11833
12005
|
|
|
11834
12006
|
if (typeof window !== 'undefined') {
|
|
@@ -11906,6 +12078,7 @@ var fml = (function (exports) {
|
|
|
11906
12078
|
exports.VersionedLocalStorage = VersionedLocalStorage;
|
|
11907
12079
|
exports.VersionedSessionStorage = VersionedSessionStorage;
|
|
11908
12080
|
exports.Wizard = Wizard;
|
|
12081
|
+
exports.describable = describable;
|
|
11909
12082
|
exports.registry = registry;
|
|
11910
12083
|
|
|
11911
12084
|
return exports;
|