@threadlabs/looma 0.14.5 → 0.14.6
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.
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Warnings about authoring mistakes the controllers work around but cannot fix. They run in
|
|
2
|
+
// development only: bundlers replace process.env.NODE_ENV, the convention Vue and React follow, and a
|
|
3
|
+
// production build ("production") skips them. Without a bundler there is no process, and they run.
|
|
4
|
+
export const DEV = (() => {
|
|
5
|
+
try {
|
|
6
|
+
return process.env.NODE_ENV !== "production";
|
|
7
|
+
} catch {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
})();
|
|
11
|
+
|
|
12
|
+
// True when another element in the same document or shadow root has this element's id.
|
|
13
|
+
export function duplicateId(element) {
|
|
14
|
+
return Boolean(element.id) && element.getRootNode().querySelectorAll(`[id="${CSS.escape(element.id)}"]`).length > 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A Form Field's authoring problems, read before the field links its label and input.
|
|
18
|
+
export function fieldProblems(label, input) {
|
|
19
|
+
const problems = [];
|
|
20
|
+
const target = label?.getAttribute("for");
|
|
21
|
+
if (label && !target && !label.contains(input)) {
|
|
22
|
+
problems.push(
|
|
23
|
+
"ui-form-field: the label has no for, so the field linked it to its input, which works only once JavaScript runs. Give the input an id and the label a matching for, or put the input inside the label, so the label works without JavaScript.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (duplicateId(input)) {
|
|
27
|
+
problems.push(
|
|
28
|
+
`ui-form-field: the input's id "${input.id}" is used by another element in the same document or shadow root, so its label and description can reach the wrong element. Give each input its own id.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (target && input.getRootNode().querySelector(`#${CSS.escape(target)}`) !== input) {
|
|
32
|
+
problems.push(
|
|
33
|
+
`ui-form-field: the label's for="${target}" does not point at this field's input, so the label names another element or none. Set for to the input's id.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return problems;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Each problem is said once per field, with the field, however often the controller re-links it.
|
|
40
|
+
export function warnOnce(warned, problems, element) {
|
|
41
|
+
for (const problem of problems) {
|
|
42
|
+
if (warned.has(problem)) continue;
|
|
43
|
+
warned.add(problem);
|
|
44
|
+
console.warn(problem, element);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEV, fieldProblems, warnOnce } from "../shared/authoring.js";
|
|
2
|
+
|
|
1
3
|
let nextFieldId = 0;
|
|
2
4
|
|
|
3
5
|
function id(prefix) {
|
|
@@ -14,6 +16,7 @@ export default function controller(host) {
|
|
|
14
16
|
let activeInput = null;
|
|
15
17
|
let owned = new Set();
|
|
16
18
|
let queued = false;
|
|
19
|
+
const warned = new Set();
|
|
17
20
|
const removeOwned = (input) => {
|
|
18
21
|
if (!input || owned.size === 0) return;
|
|
19
22
|
const remaining = descriptionIds(input).filter((value) => !owned.has(value));
|
|
@@ -32,6 +35,7 @@ export default function controller(host) {
|
|
|
32
35
|
owned = new Set();
|
|
33
36
|
}
|
|
34
37
|
if (!input) return;
|
|
38
|
+
if (DEV) warnOnce(warned, fieldProblems(label, input), element);
|
|
35
39
|
if (!input.id) input.id = id("form-field-input");
|
|
36
40
|
if (label && !label.getAttribute("for")) label.setAttribute("for", input.id);
|
|
37
41
|
const external = descriptionIds(input).filter((value) => !owned.has(value));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEV, duplicateId, warnOnce } from "../shared/authoring.js";
|
|
2
|
+
|
|
1
3
|
let nextAffixId = 0;
|
|
2
4
|
// An affix holding a control keeps it: its clicks are its own, and it is not hidden or described.
|
|
3
5
|
const INTERACTIVE = "a[href], button, input, select, textarea, [tabindex], [contenteditable]";
|
|
@@ -6,6 +8,9 @@ function descriptionIds(input) {
|
|
|
6
8
|
return (input.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean);
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
const affixProblem = (affix) =>
|
|
12
|
+
`ui-input-group: the affix id "${affix.id}" is used by another element in the same document or shadow root, so the input's description can read the wrong element. Give each affix its own id, or leave it off and the group adds one.`;
|
|
13
|
+
|
|
9
14
|
// A text affix is hidden from assistive technology and linked to the input as its description
|
|
10
15
|
// instead, so the input's name stays its label and the affix is read once, with the input. The
|
|
11
16
|
// consumer's own aria-describedby (or a Form Field's) is kept. A press on an affix, or anywhere in
|
|
@@ -14,6 +19,7 @@ export default function controller(host) {
|
|
|
14
19
|
const element = host.element;
|
|
15
20
|
let owned = [];
|
|
16
21
|
let described = null;
|
|
22
|
+
const warned = new Set();
|
|
17
23
|
const wire = () => {
|
|
18
24
|
const input = element.querySelector(".field input");
|
|
19
25
|
const affixes = Array.from(element.querySelectorAll(":scope > .affix")).filter((affix) => {
|
|
@@ -23,6 +29,7 @@ export default function controller(host) {
|
|
|
23
29
|
return text;
|
|
24
30
|
});
|
|
25
31
|
for (const affix of affixes) affix.id ||= `ui-input-group-affix-${(nextAffixId += 1)}`;
|
|
32
|
+
if (DEV) warnOnce(warned, affixes.filter(duplicateId).map(affixProblem), element);
|
|
26
33
|
if (described && described !== input) {
|
|
27
34
|
const rest = descriptionIds(described).filter((id) => !owned.includes(id));
|
|
28
35
|
if (rest.length) described.setAttribute("aria-describedby", rest.join(" "));
|