@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/README.md +2 -3
- package/dist/custom-elements.json +4 -0
- package/dist/fml.css +1 -1
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +159 -75
- 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 +56 -2
- package/dist/ful.iife.js +158 -75
- 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 +158 -76
- package/dist/ful.mjs.map +1 -1
- package/dist/web-types.json +5 -1
- package/package.json +1 -1
package/dist/fml.iife.js
CHANGED
|
@@ -6910,12 +6910,16 @@ var fml = (function (exports) {
|
|
|
6910
6910
|
//the error region describes the control, or the host where there is no
|
|
6911
6911
|
//single control to describe (a radio group's legend names its fieldset)
|
|
6912
6912
|
if (error) {
|
|
6913
|
-
|
|
6913
|
+
//an attribute, not the aria element property: the property reflects to
|
|
6914
|
+
//nothing, so the description lived in the accessibility tree alone and
|
|
6915
|
+
//vanished entirely on a browser without aria element reflection
|
|
6916
|
+
if (!error.id) {
|
|
6917
|
+
error.id = Attributes.uid('ful-field-error');
|
|
6918
|
+
}
|
|
6919
|
+
(described ?? control).setAttribute('aria-describedby', error.id);
|
|
6914
6920
|
}
|
|
6915
6921
|
if (label) {
|
|
6916
|
-
|
|
6917
|
-
//a label that does not natively target the control still focuses it
|
|
6918
|
-
label.addEventListener('click', () => this.focus());
|
|
6922
|
+
Field.#name(this, label, control);
|
|
6919
6923
|
}
|
|
6920
6924
|
//the platform's implicit submission, stood in for where the field's own
|
|
6921
6925
|
//protocol took it away: the inner controls carry form="", so Enter in one
|
|
@@ -7004,6 +7008,49 @@ var fml = (function (exports) {
|
|
|
7004
7008
|
}),
|
|
7005
7009
|
);
|
|
7006
7010
|
}
|
|
7011
|
+
/** The html elements a label's `for` may point at, `input[type=hidden]` excepted. */
|
|
7012
|
+
static #LABELABLE = new Set(['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA']);
|
|
7013
|
+
/**
|
|
7014
|
+
* Names the control from the field's label, natively wherever the platform
|
|
7015
|
+
* allows it.
|
|
7016
|
+
*
|
|
7017
|
+
* `for` and `id` are the form the dom itself carries, so the association is
|
|
7018
|
+
* there for anything reading the markup rather than the accessibility tree:
|
|
7019
|
+
* an audit tool, the browser's autofill, a translation pass. It also makes
|
|
7020
|
+
* the label's click reach the control the way it does in a plain form, which
|
|
7021
|
+
* is focus for a text control and activation for a checkbox, so the field
|
|
7022
|
+
* needs no handler of its own.
|
|
7023
|
+
*
|
|
7024
|
+
* A control the platform will not let a label target, a composite carrying
|
|
7025
|
+
* `role="radiogroup"` among them, takes `aria-labelledby` instead. That is an
|
|
7026
|
+
* attribute too, so the association is equally visible; what it does not carry
|
|
7027
|
+
* is the label's click, which is why the handler stays on that path only.
|
|
7028
|
+
*
|
|
7029
|
+
* Neither branch uses `ariaLabelledByElements`. The property reflects to no
|
|
7030
|
+
* attribute, so the name lived in the accessibility tree alone: nothing reading
|
|
7031
|
+
* the dom saw it, and on a browser without aria element reflection the
|
|
7032
|
+
* assignment is a silent expando and the field has no name at all.
|
|
7033
|
+
* @param {any} field
|
|
7034
|
+
* @param {HTMLElement} label
|
|
7035
|
+
* @param {any} control
|
|
7036
|
+
*/
|
|
7037
|
+
static #name(field, label, control) {
|
|
7038
|
+
const labelable =
|
|
7039
|
+
Field.#LABELABLE.has(control.tagName) && control.getAttribute('type') !== 'hidden';
|
|
7040
|
+
if (!labelable) {
|
|
7041
|
+
if (!label.id) {
|
|
7042
|
+
label.id = Attributes.uid('ful-label');
|
|
7043
|
+
}
|
|
7044
|
+
control.setAttribute('aria-labelledby', label.id);
|
|
7045
|
+
//aria-labelledby carries the name but not the label's click
|
|
7046
|
+
label.addEventListener('click', () => field.focus());
|
|
7047
|
+
return;
|
|
7048
|
+
}
|
|
7049
|
+
if (!control.id) {
|
|
7050
|
+
control.id = Attributes.uid('ful-control');
|
|
7051
|
+
}
|
|
7052
|
+
label.setAttribute('for', control.id);
|
|
7053
|
+
}
|
|
7007
7054
|
/**
|
|
7008
7055
|
* Whether the field's chrome should answer a gesture. Badges, dropzones,
|
|
7009
7056
|
* menus and labels are not form controls, so their handlers must ask the
|
|
@@ -8193,8 +8240,14 @@ var fml = (function (exports) {
|
|
|
8193
8240
|
const box = invoker.getBoundingClientRect();
|
|
8194
8241
|
const here = popover.getBoundingClientRect();
|
|
8195
8242
|
//against the padding box, which is what a percentage inset resolves against
|
|
8196
|
-
popover.style.setProperty(
|
|
8197
|
-
|
|
8243
|
+
popover.style.setProperty(
|
|
8244
|
+
'--ful-note-callout-inline',
|
|
8245
|
+
`${box.left + box.width / 2 - here.left - popover.clientLeft}px`,
|
|
8246
|
+
);
|
|
8247
|
+
popover.style.setProperty(
|
|
8248
|
+
'--ful-note-callout-block',
|
|
8249
|
+
`${box.top + box.height / 2 - here.top - popover.clientTop}px`,
|
|
8250
|
+
);
|
|
8198
8251
|
};
|
|
8199
8252
|
|
|
8200
8253
|
const place = (popover, anchored) => {
|
|
@@ -8298,67 +8351,93 @@ var fml = (function (exports) {
|
|
|
8298
8351
|
};
|
|
8299
8352
|
|
|
8300
8353
|
/**
|
|
8301
|
-
*
|
|
8302
|
-
*
|
|
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.
|
|
8354
|
+
* CSS anchor positioning for a popover and the invoker it belongs to, with the
|
|
8355
|
+
* hand-placed fallback for the platforms that do not have it.
|
|
8312
8356
|
*/
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8357
|
+
class Anchors {
|
|
8358
|
+
/**
|
|
8359
|
+
* Anchors a popover to its invoker.
|
|
8360
|
+
*
|
|
8361
|
+
* The invoker is given an `anchor-name` and the popover a `position-anchor`
|
|
8362
|
+
* pointing at it, which is what a stylesheet needs to place the popover
|
|
8363
|
+
* itself: the library's own menus say `top: anchor(bottom); left:
|
|
8364
|
+
* anchor(left)`. **Writing that css is the caller's half of this.** Without
|
|
8365
|
+
* it the popover lands wherever the user agent puts a popover, which is not
|
|
8366
|
+
* beside the invoker.
|
|
8367
|
+
*
|
|
8368
|
+
* Where the platform has no anchor positioning the popover is placed here
|
|
8369
|
+
* instead, beside the invoker whenever it opens, clamped into the viewport,
|
|
8370
|
+
* following it on scroll and resize, and cleaned up on close. That placement
|
|
8371
|
+
* draws the geometry the css above describes, so the two agree.
|
|
8372
|
+
*
|
|
8373
|
+
* @param {HTMLElement} invoker the element the popover belongs to
|
|
8374
|
+
* @param {HTMLElement} popover the `[popover]` element to place
|
|
8375
|
+
* @param {object} [options]
|
|
8376
|
+
* @param {string} [options.prefix] prefixes the generated anchor name and id,
|
|
8377
|
+
* so the dom says which component a name belongs to
|
|
8378
|
+
* @param {boolean} [options.invoke] points the invoker's `popovertarget` at
|
|
8379
|
+
* the popover, giving toggle and light dismiss with no script of your own
|
|
8380
|
+
* @param {boolean} [options.expanded] keeps the invoker's `aria-expanded` in
|
|
8381
|
+
* step with the popover
|
|
8382
|
+
* @param {boolean} [options.stretch] widens the popover to its invoker, which
|
|
8383
|
+
* is what a combobox dropdown wants
|
|
8384
|
+
* @param {boolean} [options.handPlace] places here on every platform rather
|
|
8385
|
+
* than only as a fallback, which a popover asks for when it needs to know
|
|
8386
|
+
* where its invoker ended up: the tooltip's note points a callout at it, and
|
|
8387
|
+
* a pseudo-element cannot read an anchor outside its own containing block.
|
|
8388
|
+
* Such a popover declares no anchor placement in css, there being none to
|
|
8389
|
+
* agree with
|
|
8390
|
+
*/
|
|
8391
|
+
static wire(
|
|
8392
|
+
invoker,
|
|
8393
|
+
popover,
|
|
8394
|
+
{ prefix = 'ful-anchor', invoke = false, expanded = false, stretch = false, handPlace = false } = {},
|
|
8395
|
+
) {
|
|
8396
|
+
const uid = Attributes.uid(prefix);
|
|
8397
|
+
if (invoke) {
|
|
8398
|
+
//popovertarget needs a target that can be named
|
|
8399
|
+
popover.id = popover.id || uid;
|
|
8400
|
+
invoker.setAttribute('popovertarget', popover.id);
|
|
8401
|
+
}
|
|
8402
|
+
const anchor = `--${uid}`;
|
|
8403
|
+
invoker.style.anchorName = anchor;
|
|
8404
|
+
popover.style.positionAnchor = anchor;
|
|
8405
|
+
if (expanded) {
|
|
8406
|
+
invoker.setAttribute('aria-expanded', 'false');
|
|
8407
|
+
popover.addEventListener('toggle', (/** @type any */ evt) => {
|
|
8408
|
+
invoker.setAttribute('aria-expanded', evt.newState === 'open' ? 'true' : 'false');
|
|
8409
|
+
});
|
|
8410
|
+
}
|
|
8411
|
+
//the naming above is what the stylesheet reads, so it happens either way:
|
|
8412
|
+
//only the hand placement below is the fallback, and only for a popover that
|
|
8413
|
+
//did not ask to be placed here whatever the platform offers
|
|
8414
|
+
if (!handPlace && platformAnchors()) {
|
|
8415
|
+
return;
|
|
8416
|
+
}
|
|
8417
|
+
const anchored = { invoker, stretch };
|
|
8418
|
+
popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
|
|
8419
|
+
//placed before the showing, refined once laid out: the platform's
|
|
8420
|
+
//centered or corner spot never paints
|
|
8421
|
+
if (evt.newState === 'open') {
|
|
8422
|
+
place(popover, anchored);
|
|
8423
|
+
}
|
|
8424
|
+
});
|
|
8329
8425
|
popover.addEventListener('toggle', (/** @type any */ evt) => {
|
|
8330
|
-
|
|
8426
|
+
if (evt.newState === 'open') {
|
|
8427
|
+
open.set(popover, anchored);
|
|
8428
|
+
place(popover, anchored);
|
|
8429
|
+
} else {
|
|
8430
|
+
open.delete(popover);
|
|
8431
|
+
unplace(popover);
|
|
8432
|
+
}
|
|
8331
8433
|
});
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
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);
|
|
8434
|
+
if (!reflowWired) {
|
|
8435
|
+
reflowWired = true;
|
|
8436
|
+
document.addEventListener('scroll', schedule, true);
|
|
8437
|
+
window.addEventListener('resize', schedule);
|
|
8354
8438
|
}
|
|
8355
|
-
});
|
|
8356
|
-
if (!reflowWired) {
|
|
8357
|
-
reflowWired = true;
|
|
8358
|
-
document.addEventListener('scroll', schedule, true);
|
|
8359
|
-
window.addEventListener('resize', schedule);
|
|
8360
8439
|
}
|
|
8361
|
-
}
|
|
8440
|
+
}
|
|
8362
8441
|
|
|
8363
8442
|
/**
|
|
8364
8443
|
* Fetches a select's whole vocabulary from a url and serves every later read
|
|
@@ -8887,7 +8966,7 @@ var fml = (function (exports) {
|
|
|
8887
8966
|
});
|
|
8888
8967
|
//each pair carries its own anchor: two selects on a page must not share one
|
|
8889
8968
|
const group = fragment.querySelector('ful-control-group');
|
|
8890
|
-
|
|
8969
|
+
Anchors.wire(group, this.#ddmenu, { prefix: 'ful-select', stretch: true });
|
|
8891
8970
|
[this.#dload, this.#abortdload] = Timing.throttle(400, () => this.#open());
|
|
8892
8971
|
this.#wireChrome();
|
|
8893
8972
|
this.#wireChips();
|
|
@@ -9477,16 +9556,10 @@ var fml = (function (exports) {
|
|
|
9477
9556
|
evt.stopPropagation();
|
|
9478
9557
|
this._notifyChange();
|
|
9479
9558
|
});
|
|
9559
|
+
//the base points the label at the input with for/id, so the click toggles
|
|
9560
|
+
//the way it does in a plain form: the input's own change listener above
|
|
9561
|
+
//carries the notification, and readonly is refused by the freeze below
|
|
9480
9562
|
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
9563
|
//a checkbox has no editable text to preserve, so readonly freezes the
|
|
9491
9564
|
//whole choice, label click included: the container is the frozen piece
|
|
9492
9565
|
return {
|
|
@@ -10265,7 +10338,7 @@ var fml = (function (exports) {
|
|
|
10265
10338
|
#wire() {
|
|
10266
10339
|
const button = this.#button;
|
|
10267
10340
|
const menu = this.#menu;
|
|
10268
|
-
|
|
10341
|
+
Anchors.wire(button, menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
|
|
10269
10342
|
menu.addEventListener('toggle', (/** @type any */ evt) => {
|
|
10270
10343
|
if (evt.newState !== 'open') {
|
|
10271
10344
|
//give the invoker back the focus the menu had borrowed, without
|
|
@@ -10864,7 +10937,7 @@ var fml = (function (exports) {
|
|
|
10864
10937
|
//placed here rather than by the anchor css: the note draws a callout that
|
|
10865
10938
|
//has to point at the trigger wherever the viewport left room for the note,
|
|
10866
10939
|
//which is a measurement the stylesheet cannot make for a pseudo-element
|
|
10867
|
-
|
|
10940
|
+
Anchors.wire(trigger, content, { prefix: 'ful-tooltip', invoke: true, expanded: true, handPlace: true });
|
|
10868
10941
|
const placement = this.declared('placement');
|
|
10869
10942
|
if (placement) {
|
|
10870
10943
|
content.setAttribute('placement', placement);
|
|
@@ -10956,13 +11029,22 @@ var fml = (function (exports) {
|
|
|
10956
11029
|
}
|
|
10957
11030
|
}
|
|
10958
11031
|
|
|
10959
|
-
/**
|
|
11032
|
+
/**
|
|
11033
|
+
* A side panel drawer on the native dialog platform, update() owning its
|
|
11034
|
+
* open-deliver cycle.
|
|
11035
|
+
*
|
|
11036
|
+
* The `header` slot is content beside the title, before it: an icon, a badge, a
|
|
11037
|
+
* status. It sits outside the heading rather than in it because `update()` sets
|
|
11038
|
+
* the title through `textContent`, which would take anything nested there with
|
|
11039
|
+
* it.
|
|
11040
|
+
*/
|
|
10960
11041
|
class Drawer extends ParsedElement {
|
|
10961
11042
|
static attributes = ['title', 'placement'];
|
|
10962
11043
|
static slots = true;
|
|
10963
11044
|
static template = `
|
|
10964
11045
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
10965
11046
|
<header>
|
|
11047
|
+
{{{{ slots.header }}}}
|
|
10966
11048
|
<h2 data-ref="title">{{ title }}</h2>
|
|
10967
11049
|
<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
11050
|
</header>
|
|
@@ -11705,6 +11787,7 @@ var fml = (function (exports) {
|
|
|
11705
11787
|
var ful = /*#__PURE__*/Object.freeze({
|
|
11706
11788
|
__proto__: null,
|
|
11707
11789
|
Accordion: Accordion,
|
|
11790
|
+
Anchors: Anchors,
|
|
11708
11791
|
AsyncEvents: AsyncEvents,
|
|
11709
11792
|
Bindings: Bindings,
|
|
11710
11793
|
BooleanFilter: BooleanFilter,
|
|
@@ -11757,6 +11840,7 @@ var fml = (function (exports) {
|
|
|
11757
11840
|
}
|
|
11758
11841
|
|
|
11759
11842
|
exports.Accordion = Accordion;
|
|
11843
|
+
exports.Anchors = Anchors;
|
|
11760
11844
|
exports.AsyncEvents = AsyncEvents;
|
|
11761
11845
|
exports.Attributes = Attributes;
|
|
11762
11846
|
exports.Base64 = Base64;
|