@lo-tek/interfaces 0.37.0 → 0.39.0
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/CHANGELOG.md +160 -2
- package/README.md +4 -3
- package/agent/astro.md +1 -1
- package/agent/bundles.md +4 -2
- package/agent/datastar.md +48 -48
- package/agent/html.md +51 -51
- package/agent/react.md +6 -5
- package/agent/templates.md +49 -49
- package/core/lotek-core.wasm +0 -0
- package/core/lotek.wasm +0 -0
- package/dist/components/default-texts.d.ts +6 -0
- package/dist/components/default-texts.d.ts.map +1 -1
- package/dist/components/default-texts.js +6 -0
- package/dist/components/default-texts.js.map +1 -1
- package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -1
- package/dist/components/frame/dialog/lt-dialog.js +5 -2
- package/dist/components/frame/dialog/lt-dialog.js.map +1 -1
- package/dist/components/frame/error-summary/error-summary.d.ts +15 -2
- package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -1
- package/dist/components/frame/error-summary/error-summary.js +33 -11
- package/dist/components/frame/error-summary/error-summary.js.map +1 -1
- package/dist/components/frame/error-summary/lt-error-summary.d.ts +11 -0
- package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -1
- package/dist/components/frame/error-summary/lt-error-summary.js +114 -8
- package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -1
- package/dist/components/frame/field/lt-field.d.ts.map +1 -1
- package/dist/components/frame/field/lt-field.js +34 -14
- package/dist/components/frame/field/lt-field.js.map +1 -1
- package/dist/components/frame/popover/lt-popover.d.ts +6 -2
- package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
- package/dist/components/frame/popover/lt-popover.js +44 -14
- package/dist/components/frame/popover/lt-popover.js.map +1 -1
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -1
- package/dist/components/frame/session-timeout/lt-session-timeout.js +43 -6
- package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -1
- package/dist/components/frame/suggestion/lt-suggestion.d.ts +8 -2
- package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -1
- package/dist/components/frame/suggestion/lt-suggestion.js +56 -37
- package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -1
- package/dist/components/frame/tabs/lt-tabs.d.ts +32 -10
- package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -1
- package/dist/components/frame/tabs/lt-tabs.js +85 -21
- package/dist/components/frame/tabs/lt-tabs.js.map +1 -1
- package/dist/components/host-element.d.ts +52 -0
- package/dist/components/host-element.d.ts.map +1 -1
- package/dist/components/host-element.js +164 -1
- package/dist/components/host-element.js.map +1 -1
- package/dist/components/standalone/connection-status/connection-status.d.ts +5 -1
- package/dist/components/standalone/connection-status/connection-status.d.ts.map +1 -1
- package/dist/components/standalone/connection-status/connection-status.js +5 -1
- package/dist/components/standalone/connection-status/connection-status.js.map +1 -1
- package/dist/components/standalone/connection-status/lt-connection-status.d.ts +11 -6
- package/dist/components/standalone/connection-status/lt-connection-status.d.ts.map +1 -1
- package/dist/components/standalone/connection-status/lt-connection-status.js +34 -10
- package/dist/components/standalone/connection-status/lt-connection-status.js.map +1 -1
- package/dist/diagnostics/core.d.ts +34 -0
- package/dist/diagnostics/core.d.ts.map +1 -1
- package/dist/diagnostics/core.js +14 -2
- package/dist/diagnostics/core.js.map +1 -1
- package/dist/lotek.css +1 -1
- package/dist/lt.d.ts +1 -1
- package/dist/react.d.ts +2 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/vocabulary/classes.d.ts.map +1 -1
- package/dist/vocabulary/classes.js +34 -0
- package/dist/vocabulary/classes.js.map +1 -1
- package/dist/vocabulary/elements.d.ts.map +1 -1
- package/dist/vocabulary/elements.js +87 -7
- package/dist/vocabulary/elements.js.map +1 -1
- package/dist/vocabulary/types.d.ts +60 -0
- package/dist/vocabulary/types.d.ts.map +1 -1
- package/live-templates.xml +3 -2
- package/manifest/manifest.json +172 -8
- package/manifest/manifest.schema.json +80 -0
- package/package.json +2 -1
- package/src/components/default-texts.ts +7 -0
- package/src/components/frame/dialog/lt-dialog.ts +5 -2
- package/src/components/frame/error-summary/error-summary.ts +45 -11
- package/src/components/frame/error-summary/lt-error-summary.ts +121 -7
- package/src/components/frame/field/lt-field.ts +65 -28
- package/src/components/frame/popover/lt-popover.ts +43 -12
- package/src/components/frame/session-timeout/lt-session-timeout.ts +40 -6
- package/src/components/frame/suggestion/lt-suggestion.ts +57 -36
- package/src/components/frame/tabs/lt-tabs.ts +114 -38
- package/src/components/host-element.ts +190 -1
- package/src/components/standalone/connection-status/connection-status.ts +7 -1
- package/src/components/standalone/connection-status/lt-connection-status.ts +36 -10
- package/src/diagnostics/core.ts +19 -2
- package/src/vocabulary/classes.ts +34 -0
- package/src/vocabulary/elements.ts +87 -7
- package/src/vocabulary/types.ts +56 -0
- package/web-types.json +1 -1
|
@@ -12,12 +12,108 @@
|
|
|
12
12
|
* without `customElements` nothing is registered, and without registration
|
|
13
13
|
* the browser never creates an instance. It only exists so the module can
|
|
14
14
|
* load.
|
|
15
|
+
*
|
|
16
|
+
* In the browser the base does one thing: it adopts the properties a script
|
|
17
|
+
* set before the registration, see `adoptPropertiesSetBeforeUpgrade()`.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
export const HostElement =
|
|
18
21
|
typeof HTMLElement === "undefined"
|
|
19
22
|
? (class {} as unknown as typeof HTMLElement)
|
|
20
|
-
: HTMLElement
|
|
23
|
+
: (class extends HTMLElement {
|
|
24
|
+
constructor() {
|
|
25
|
+
super()
|
|
26
|
+
// Here the own properties are exactly what a script set before the
|
|
27
|
+
// upgrade: the component's own fields are assigned after `super()`
|
|
28
|
+
// returns. An element created after the registration has none, and
|
|
29
|
+
// costs nothing.
|
|
30
|
+
const names = Object.getOwnPropertyNames(this)
|
|
31
|
+
if (names.length === 0) return
|
|
32
|
+
const pending = holdPropertiesSetBeforeUpgrade(this, names)
|
|
33
|
+
if (pending.size === 0) return
|
|
34
|
+
// Written in a microtask, not here: the field initialisers run
|
|
35
|
+
// after `super()`, and a setter that notes something on the
|
|
36
|
+
// instance, as `<lt-popover>` does with `open`, would have been
|
|
37
|
+
// overwritten by them. By the microtask the whole constructor and
|
|
38
|
+
// the upgrade's `connectedCallback` are done.
|
|
39
|
+
queueMicrotask(() => {
|
|
40
|
+
for (const [name, value] of pending) {
|
|
41
|
+
Reflect.deleteProperty(this, name)
|
|
42
|
+
;(this as unknown as Record<string, unknown>)[name] = value
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
} as unknown as typeof HTMLElement)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Takes the properties a script set on the element before it was upgraded out
|
|
50
|
+
* of the way, so they can be written through the component's setters.
|
|
51
|
+
*
|
|
52
|
+
* `dialog.open = true` from a script that runs before `defineLt()` lands on a
|
|
53
|
+
* plain `HTMLElement`, and becomes an own data property. When the element is
|
|
54
|
+
* upgraded, that property shadows the accessor on the prototype: the
|
|
55
|
+
* attribute is never set, and every later write, `dialog.open = false`
|
|
56
|
+
* included, goes to the shadow and never reaches the setter. Tested against
|
|
57
|
+
* `dist` on 2026-11-01: the setters were dead for the rest of the element's
|
|
58
|
+
* life.
|
|
59
|
+
*
|
|
60
|
+
* Each such property is replaced by a stand-in for the time until the
|
|
61
|
+
* microtask that writes the value. A read goes to the getter, so the
|
|
62
|
+
* component's first `sync()` sees the attribute and not the script's value:
|
|
63
|
+
* when it saw the value, it opened a dialog the script closed again right
|
|
64
|
+
* after the registration, and the write of `false` then removed an attribute
|
|
65
|
+
* that was never there, so nothing closed the dialog (found by GitHub's
|
|
66
|
+
* review). A write goes to the setter and cancels the adoption, so the last
|
|
67
|
+
* write wins. The values returned are what the microtask writes.
|
|
68
|
+
*
|
|
69
|
+
* Only a property with a setter somewhere on the prototype chain is taken;
|
|
70
|
+
* anything else a script put on the element stays as it is. A property set
|
|
71
|
+
* after the upgrade is never an own property, so there is nothing to do for
|
|
72
|
+
* an element created once the registration is there. A property someone made
|
|
73
|
+
* non-configurable cannot be moved out of the way, and is left shadowing the
|
|
74
|
+
* setter, as before. (Documented: the "lazy properties" pattern in Google's
|
|
75
|
+
* custom elements best practices.)
|
|
76
|
+
*/
|
|
77
|
+
function holdPropertiesSetBeforeUpgrade(
|
|
78
|
+
element: HTMLElement,
|
|
79
|
+
names: readonly string[],
|
|
80
|
+
): Map<string, unknown> {
|
|
81
|
+
const pending = new Map<string, unknown>()
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const own = Object.getOwnPropertyDescriptor(element, name)
|
|
84
|
+
if (!own || !("value" in own) || !own.configurable) continue
|
|
85
|
+
const accessor = accessorOnPrototype(element, name)
|
|
86
|
+
if (!accessor?.set) continue
|
|
87
|
+
const { get, set } = accessor
|
|
88
|
+
pending.set(name, own.value)
|
|
89
|
+
Object.defineProperty(element, name, {
|
|
90
|
+
configurable: true,
|
|
91
|
+
enumerable: true,
|
|
92
|
+
get: () => get?.call(element),
|
|
93
|
+
set: (value: unknown) => {
|
|
94
|
+
pending.delete(name)
|
|
95
|
+
Reflect.deleteProperty(element, name)
|
|
96
|
+
set.call(element, value)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return pending
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function accessorOnPrototype(
|
|
104
|
+
element: object,
|
|
105
|
+
name: string,
|
|
106
|
+
): PropertyDescriptor | undefined {
|
|
107
|
+
for (
|
|
108
|
+
let proto = Object.getPrototypeOf(element);
|
|
109
|
+
proto !== null;
|
|
110
|
+
proto = Object.getPrototypeOf(proto)
|
|
111
|
+
) {
|
|
112
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, name)
|
|
113
|
+
if (descriptor) return descriptor
|
|
114
|
+
}
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
21
117
|
|
|
22
118
|
/*
|
|
23
119
|
* The types of the events, such as `dialog-toggle` with `detail.open`. Every
|
|
@@ -96,6 +192,79 @@ export function setAttr(
|
|
|
96
192
|
}
|
|
97
193
|
}
|
|
98
194
|
|
|
195
|
+
/**
|
|
196
|
+
* The attribute a component marks what it wrote with: the names of the
|
|
197
|
+
* attributes on the element whose value is the component's, separated by
|
|
198
|
+
* spaces.
|
|
199
|
+
*
|
|
200
|
+
* Some attributes are both the server's and the component's: `aria-invalid`
|
|
201
|
+
* on a field, `aria-selected` on a tab. To tell the server's word from its
|
|
202
|
+
* own echo, a component remembered the value it wrote, and read anything
|
|
203
|
+
* else as the server's. A server that sent the same value was never seen: a
|
|
204
|
+
* morph does not touch an attribute whose value is already right, and the
|
|
205
|
+
* component took the server's `aria-invalid="true"` for its own and removed
|
|
206
|
+
* it. The mark is what a morph does touch. The server never sends it, so a
|
|
207
|
+
* morph from the server removes it, and an attribute without the mark is the
|
|
208
|
+
* server's, whatever its value. A script that changes the element without a
|
|
209
|
+
* morph leaves the mark, and is seen as before, by the value.
|
|
210
|
+
*
|
|
211
|
+
* Only what differs from the server's word is marked. A value that is the
|
|
212
|
+
* server's has nothing to tell apart, and markup that is already right, as
|
|
213
|
+
* React renders `lt.tabs()` with the state, must not be touched: hydration
|
|
214
|
+
* reports every attribute it did not render.
|
|
215
|
+
*/
|
|
216
|
+
export const WRITTEN = "data-lt-written"
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Marks `name` on `element` as the component's when `mine`, that is, when
|
|
220
|
+
* what the component wrote differs from the server's word, and removes the
|
|
221
|
+
* mark otherwise.
|
|
222
|
+
*/
|
|
223
|
+
export function markWritten(
|
|
224
|
+
element: Element,
|
|
225
|
+
name: string,
|
|
226
|
+
mine: boolean,
|
|
227
|
+
): void {
|
|
228
|
+
const marked = new Set(
|
|
229
|
+
(element.getAttribute(WRITTEN) ?? "").split(/\s+/).filter(Boolean),
|
|
230
|
+
)
|
|
231
|
+
if (mine) marked.add(name)
|
|
232
|
+
else marked.delete(name)
|
|
233
|
+
setAttr(
|
|
234
|
+
element,
|
|
235
|
+
WRITTEN,
|
|
236
|
+
marked.size > 0 ? [...marked].sort().join(" ") : null,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Warns when `element` carries the mark before the component has written on
|
|
242
|
+
* it: the mark is in the markup it was given, copied into a template from
|
|
243
|
+
* the browser's developer tools. A morph then never removes it, and the
|
|
244
|
+
* component takes the server's word for its own echo again. Called where the
|
|
245
|
+
* component meets the element for the first time; the check cannot wait, so
|
|
246
|
+
* the warning is not checked again when the page has settled.
|
|
247
|
+
*/
|
|
248
|
+
export function warnAboutCopiedMark(host: Element, element: Element): void {
|
|
249
|
+
if (!element.hasAttribute(WRITTEN)) return
|
|
250
|
+
warnAboutMarkup(
|
|
251
|
+
host,
|
|
252
|
+
`found ${WRITTEN} in the markup it was given. The browser writes the ` +
|
|
253
|
+
"attribute to tell the component's own values from the server's; " +
|
|
254
|
+
"copied into a template, it makes the component take what the " +
|
|
255
|
+
"server sends for its own. Remove it from the template.",
|
|
256
|
+
() => true,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Whether the component's mark for `name` is still on `element`: no morph
|
|
262
|
+
* from the server has been there since the component wrote it.
|
|
263
|
+
*/
|
|
264
|
+
export function stillMarked(element: Element, name: string): boolean {
|
|
265
|
+
return (element.getAttribute(WRITTEN) ?? "").split(/\s+/).includes(name)
|
|
266
|
+
}
|
|
267
|
+
|
|
99
268
|
/**
|
|
100
269
|
* Turns a boolean attribute on or off, without writing when it is already so.
|
|
101
270
|
*
|
|
@@ -175,6 +344,26 @@ export function uniqueId(prefix: string): string {
|
|
|
175
344
|
return `${prefix}-${Math.random().toString(36).slice(2, 9)}`
|
|
176
345
|
}
|
|
177
346
|
|
|
347
|
+
/**
|
|
348
|
+
* The id an element should have: the one it has, otherwise the one the
|
|
349
|
+
* component created for that node before. A morph keeps the node and tears
|
|
350
|
+
* the id off, and it gets the same one back, so a screen reader does not
|
|
351
|
+
* follow a reference that changes under it.
|
|
352
|
+
*/
|
|
353
|
+
export function rememberedId(
|
|
354
|
+
memory: WeakMap<Element, string>,
|
|
355
|
+
element: Element,
|
|
356
|
+
prefix: string,
|
|
357
|
+
): string {
|
|
358
|
+
if (element.id) return element.id
|
|
359
|
+
let id = memory.get(element)
|
|
360
|
+
if (!id) {
|
|
361
|
+
id = uniqueId(prefix)
|
|
362
|
+
memory.set(element, id)
|
|
363
|
+
}
|
|
364
|
+
return id
|
|
365
|
+
}
|
|
366
|
+
|
|
178
367
|
/**
|
|
179
368
|
* The parts with a role, and those without only when the component set the
|
|
180
369
|
* roles itself.
|
|
@@ -12,6 +12,8 @@ export type ConnectionStatusOptions = {
|
|
|
12
12
|
|
|
13
13
|
export type ConnectionStatusAttributes = {
|
|
14
14
|
class: typeof CONNECTION_STATUS_CLASS
|
|
15
|
+
role: "status"
|
|
16
|
+
"aria-live": "polite"
|
|
15
17
|
"offline-text"?: string
|
|
16
18
|
"online-text"?: string
|
|
17
19
|
"data-ignore-morph": ""
|
|
@@ -23,7 +25,9 @@ export type ConnectionStatusAttributes = {
|
|
|
23
25
|
* By definition the server cannot tell you it is unreachable. When it is
|
|
24
26
|
* down, nothing comes from it to render. So the component creates all the
|
|
25
27
|
* content itself, and `data-ignore-morph` stops a later patch from tearing
|
|
26
|
-
* it away the moment the connection is back.
|
|
28
|
+
* it away the moment the connection is back. The host is the live region,
|
|
29
|
+
* `role="status"` with `aria-live="polite"`, so the region exists before the
|
|
30
|
+
* bar and its text arrive.
|
|
27
31
|
*
|
|
28
32
|
* ```ts
|
|
29
33
|
* <lt-connection-status {...lt.connectionStatus()} />
|
|
@@ -35,6 +39,8 @@ export const connectionStatus = ({
|
|
|
35
39
|
}: ConnectionStatusOptions = {}): ConnectionStatusAttributes =>
|
|
36
40
|
attributes({
|
|
37
41
|
class: CONNECTION_STATUS_CLASS,
|
|
42
|
+
role: "status" as const,
|
|
43
|
+
"aria-live": "polite" as const,
|
|
38
44
|
"offline-text": offlineText,
|
|
39
45
|
"online-text": onlineText,
|
|
40
46
|
"data-ignore-morph": "" as const,
|
|
@@ -77,6 +77,23 @@ export class LtConnectionStatus extends HostElement {
|
|
|
77
77
|
|
|
78
78
|
connectedCallback(): void {
|
|
79
79
|
this.classList.add(CONNECTION_STATUS_CLASS)
|
|
80
|
+
/*
|
|
81
|
+
* The host is the live region, from the moment it is connected, so the
|
|
82
|
+
* region exists long before the bar and its text arrive. Only what is
|
|
83
|
+
* missing: a role or politeness the server wrote stays. When the bar was
|
|
84
|
+
* the region, it was inserted together with its text, and Firefox sent
|
|
85
|
+
* no live-region text through AT-SPI at all, so "No connection" was
|
|
86
|
+
* never announced. `check-accessibility-tree.ts` checks it.
|
|
87
|
+
*/
|
|
88
|
+
this.markRegion()
|
|
89
|
+
// A morph without `data-ignore-morph` takes both off, and then the next
|
|
90
|
+
// outage is never announced. Writing only what is missing keeps the
|
|
91
|
+
// observer from triggering itself.
|
|
92
|
+
this.regionObserver = new MutationObserver(() => this.markRegion())
|
|
93
|
+
this.regionObserver.observe(this, {
|
|
94
|
+
attributes: true,
|
|
95
|
+
attributeFilter: ["role", "aria-live"],
|
|
96
|
+
})
|
|
80
97
|
window.addEventListener("offline", this.handleOffline)
|
|
81
98
|
window.addEventListener("online", this.handleOnline)
|
|
82
99
|
if (!navigator.onLine) {
|
|
@@ -96,7 +113,22 @@ export class LtConnectionStatus extends HostElement {
|
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
115
|
|
|
116
|
+
private regionObserver?: MutationObserver
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The region, as a pair and only when both are missing. A role the server
|
|
120
|
+
* wrote carries its own politeness (`alert` is assertive), and an
|
|
121
|
+
* `aria-live` added next to it would override that.
|
|
122
|
+
*/
|
|
123
|
+
private markRegion(): void {
|
|
124
|
+
if (this.hasAttribute("role") || this.hasAttribute("aria-live")) return
|
|
125
|
+
this.setAttribute("role", "status")
|
|
126
|
+
this.setAttribute("aria-live", "polite")
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
disconnectedCallback(): void {
|
|
130
|
+
this.regionObserver?.disconnect()
|
|
131
|
+
this.regionObserver = undefined
|
|
100
132
|
window.removeEventListener("offline", this.handleOffline)
|
|
101
133
|
window.removeEventListener("online", this.handleOnline)
|
|
102
134
|
if (this.timer) window.clearTimeout(this.timer)
|
|
@@ -107,12 +139,10 @@ export class LtConnectionStatus extends HostElement {
|
|
|
107
139
|
* The bar, created empty first and filled afterwards.
|
|
108
140
|
*
|
|
109
141
|
* A live region must exist before its content arrives, or assistive
|
|
110
|
-
* technology does not announce the first change.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* checked without a screen reader; the rule is known from NVDA, JAWS and
|
|
115
|
-
* VoiceOver.
|
|
142
|
+
* technology does not announce the first change. The region is the host
|
|
143
|
+
* (see `connectedCallback`), so the bar is content inside a region that is
|
|
144
|
+
* already there; the text is still set in the next frame when the bar is
|
|
145
|
+
* new, so the bar and its text arrive as two changes.
|
|
116
146
|
*
|
|
117
147
|
* The bar is created only when it is needed, not on connection: when it
|
|
118
148
|
* was in the DOM before React hydrated, React found an element it had not
|
|
@@ -123,10 +153,6 @@ export class LtConnectionStatus extends HostElement {
|
|
|
123
153
|
|
|
124
154
|
const bar = document.createElement("div")
|
|
125
155
|
bar.className = CONNECTION_STATUS_BAR_CLASS
|
|
126
|
-
// polite and not assertive: the message matters, but should not
|
|
127
|
-
// interrupt mid-word. It stays until the connection is back.
|
|
128
|
-
bar.setAttribute("role", "status")
|
|
129
|
-
bar.setAttribute("aria-live", "polite")
|
|
130
156
|
this.append(bar)
|
|
131
157
|
this.bar = bar
|
|
132
158
|
return { bar, fresh: true }
|
package/src/diagnostics/core.ts
CHANGED
|
@@ -148,12 +148,23 @@ type Exports = {
|
|
|
148
148
|
inspect_theme_raw(pointer: number, length: number): number
|
|
149
149
|
inspect_styles_raw(pointer: number, length: number): void
|
|
150
150
|
diagnose_styled_raw(pointer: number, length: number): number
|
|
151
|
+
accessibility_tree_raw(pointer: number, length: number): number
|
|
152
|
+
accessibility_facts_raw(): void
|
|
153
|
+
accessibility_roles_raw(): void
|
|
154
|
+
accessibility_states_raw(): void
|
|
151
155
|
result_ptr(): number
|
|
152
156
|
result_len(): number
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
159
|
+
/**
|
|
160
|
+
* @internal Not part of the package's API.
|
|
161
|
+
*
|
|
162
|
+
* An instance of the core and the helpers every call goes through: the text
|
|
163
|
+
* written into the module's memory, and the answer read back. Not part of
|
|
164
|
+
* the package: `index.ts` exports `loadCore`. The tests reach the core's
|
|
165
|
+
* other functions, such as the accessibility tree, through this.
|
|
166
|
+
*/
|
|
167
|
+
export function instantiate(source: BufferSource | WebAssembly.Module) {
|
|
157
168
|
const module =
|
|
158
169
|
source instanceof WebAssembly.Module
|
|
159
170
|
? source
|
|
@@ -181,6 +192,12 @@ export function loadCore(source: BufferSource | WebAssembly.Module): Core {
|
|
|
181
192
|
if (status !== 0) throw new Error(read<{ error: string }>().error)
|
|
182
193
|
return read<T>()
|
|
183
194
|
}
|
|
195
|
+
return { e, write, read, answer }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Loads the core from the module's bytes or an already compiled module. */
|
|
199
|
+
export function loadCore(source: BufferSource | WebAssembly.Module): Core {
|
|
200
|
+
const { e, write, read, answer } = instantiate(source)
|
|
184
201
|
|
|
185
202
|
return {
|
|
186
203
|
diagnoseMarkup: (html) => answer(e.diagnose_markup_raw(...write(html))),
|
|
@@ -311,6 +311,7 @@ export const classes: Classes = {
|
|
|
311
311
|
"optional": true
|
|
312
312
|
}
|
|
313
313
|
},
|
|
314
|
+
"name": {},
|
|
314
315
|
"attributes": {
|
|
315
316
|
"data-state": {
|
|
316
317
|
"values": [
|
|
@@ -328,6 +329,22 @@ export const classes: Classes = {
|
|
|
328
329
|
"link": "https://lo-tek.sobernetics.no/components/fieldset/",
|
|
329
330
|
"content": "text",
|
|
330
331
|
"partOf": "lt-fieldset",
|
|
332
|
+
"texts": [
|
|
333
|
+
{
|
|
334
|
+
"variable": "--lt-label-required-text",
|
|
335
|
+
"text": " (required)",
|
|
336
|
+
"attribute": "data-required",
|
|
337
|
+
"value": "text"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"variable": "--lt-label-optional-text",
|
|
341
|
+
"text": " (optional)",
|
|
342
|
+
"attribute": "data-optional",
|
|
343
|
+
"unless": [
|
|
344
|
+
"data-required"
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
],
|
|
331
348
|
"attributes": {
|
|
332
349
|
"data-required": {
|
|
333
350
|
"values": [
|
|
@@ -453,6 +470,22 @@ export const classes: Classes = {
|
|
|
453
470
|
"description": "A label for form fields, with marking of required and optional. A CSS component.",
|
|
454
471
|
"link": "https://lo-tek.sobernetics.no/components/label/",
|
|
455
472
|
"content": "text",
|
|
473
|
+
"texts": [
|
|
474
|
+
{
|
|
475
|
+
"variable": "--lt-label-required-text",
|
|
476
|
+
"text": " (required)",
|
|
477
|
+
"attribute": "data-required",
|
|
478
|
+
"value": "text"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"variable": "--lt-label-optional-text",
|
|
482
|
+
"text": " (optional)",
|
|
483
|
+
"attribute": "data-optional",
|
|
484
|
+
"unless": [
|
|
485
|
+
"data-required"
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
],
|
|
456
489
|
"attributes": {
|
|
457
490
|
"data-required": {
|
|
458
491
|
"values": [
|
|
@@ -885,6 +918,7 @@ export const classes: Classes = {
|
|
|
885
918
|
"multiple": true
|
|
886
919
|
}
|
|
887
920
|
},
|
|
921
|
+
"name": {},
|
|
888
922
|
"attributes": {}
|
|
889
923
|
},
|
|
890
924
|
"lt-toggle-group__option": {
|
|
@@ -30,7 +30,10 @@ export const elements: Elements = {
|
|
|
30
30
|
"description": "The control the field wires: an `<input>`, `<textarea>` or `<select>` with its `<option>`s. `type=\"hidden\"` does not count, and only the first one is wired. The template writes `for` on the label and `id` on the control, or wraps the control in the label.",
|
|
31
31
|
"content": "none",
|
|
32
32
|
"unique": true,
|
|
33
|
-
"stableId": true
|
|
33
|
+
"stableId": true,
|
|
34
|
+
"name": {
|
|
35
|
+
"after": "label"
|
|
36
|
+
}
|
|
34
37
|
},
|
|
35
38
|
"help": {
|
|
36
39
|
"selector": ".lt-help-text",
|
|
@@ -75,7 +78,8 @@ export const elements: Elements = {
|
|
|
75
78
|
"selector": "[role='tablist'], .lt-tabs__list",
|
|
76
79
|
"description": "The tab list, a `<ul>` where each `<li>` gets `role=\"presentation\"`. It needs a name, such as `aria-label`. When the server has written `role=\"tab\"` on the links, the list can also hold other links.",
|
|
77
80
|
"content": "free",
|
|
78
|
-
"unique": true
|
|
81
|
+
"unique": true,
|
|
82
|
+
"name": {}
|
|
79
83
|
},
|
|
80
84
|
"tab": {
|
|
81
85
|
"selector": "a[role='tab'][href^='#'], .lt-tabs__list a[href^='#']:not([role])",
|
|
@@ -87,7 +91,10 @@ export const elements: Elements = {
|
|
|
87
91
|
"selector": "[role='tabpanel'], .lt-tabs__panel:not([role])",
|
|
88
92
|
"description": "The content of one tab, with the id the link points to. The panel has the same name as the tab: the heading at the top of the panel is the name when it says the same as the tab.",
|
|
89
93
|
"content": "free",
|
|
90
|
-
"multiple": true
|
|
94
|
+
"multiple": true,
|
|
95
|
+
"name": {
|
|
96
|
+
"component": true
|
|
97
|
+
}
|
|
91
98
|
}
|
|
92
99
|
},
|
|
93
100
|
"events": {
|
|
@@ -127,6 +134,10 @@ export const elements: Elements = {
|
|
|
127
134
|
"content": "text",
|
|
128
135
|
"multiple": true
|
|
129
136
|
}
|
|
137
|
+
},
|
|
138
|
+
"name": {
|
|
139
|
+
"after": "title",
|
|
140
|
+
"id": true
|
|
130
141
|
}
|
|
131
142
|
},
|
|
132
143
|
"lt-popover": {
|
|
@@ -220,13 +231,19 @@ export const elements: Elements = {
|
|
|
220
231
|
"description": "The field the user types in: `role=\"combobox\"`, otherwise the first `<input>`. The template writes `for` on the label and `id` on the field, or wraps the field in the label.",
|
|
221
232
|
"content": "none",
|
|
222
233
|
"unique": true,
|
|
223
|
-
"stableId": true
|
|
234
|
+
"stableId": true,
|
|
235
|
+
"name": {
|
|
236
|
+
"after": "label"
|
|
237
|
+
}
|
|
224
238
|
},
|
|
225
239
|
"list": {
|
|
226
240
|
"selector": "[role='listbox'], .lt-suggestion__list",
|
|
227
241
|
"description": "The list of suggestions. It sits in the top layer when it is open, `popover=\"manual\"`, and has the label as its name.",
|
|
228
242
|
"content": "parts",
|
|
229
|
-
"unique": true
|
|
243
|
+
"unique": true,
|
|
244
|
+
"name": {
|
|
245
|
+
"component": true
|
|
246
|
+
}
|
|
230
247
|
},
|
|
231
248
|
"option": {
|
|
232
249
|
"selector": "[role='option'], .lt-suggestion__list > li:not([role])",
|
|
@@ -252,6 +269,34 @@ export const elements: Elements = {
|
|
|
252
269
|
"content": "none",
|
|
253
270
|
"unique": true,
|
|
254
271
|
"optional": true
|
|
272
|
+
},
|
|
273
|
+
"label": {
|
|
274
|
+
"selector": "label",
|
|
275
|
+
"description": "The label. The component writes `for` on it from the field's id when it does not wrap the field, and names the list after it.",
|
|
276
|
+
"content": "text",
|
|
277
|
+
"optional": true
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"texts": {
|
|
281
|
+
"suggestionNone": {
|
|
282
|
+
"text": "No matches",
|
|
283
|
+
"replacedBy": [
|
|
284
|
+
"count-none",
|
|
285
|
+
"count-other"
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
"suggestionOne": {
|
|
289
|
+
"text": "One match",
|
|
290
|
+
"replacedBy": [
|
|
291
|
+
"count-one",
|
|
292
|
+
"count-other"
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
"suggestionOther": {
|
|
296
|
+
"text": "{n} matches",
|
|
297
|
+
"replacedBy": [
|
|
298
|
+
"count-other"
|
|
299
|
+
]
|
|
255
300
|
}
|
|
256
301
|
},
|
|
257
302
|
"events": {
|
|
@@ -281,7 +326,10 @@ export const elements: Elements = {
|
|
|
281
326
|
"selector": ":scope > dialog",
|
|
282
327
|
"description": "The `<dialog>` element, as a direct child of the host.",
|
|
283
328
|
"content": "free",
|
|
284
|
-
"unique": true
|
|
329
|
+
"unique": true,
|
|
330
|
+
"name": {
|
|
331
|
+
"after": "title"
|
|
332
|
+
}
|
|
285
333
|
},
|
|
286
334
|
"title": {
|
|
287
335
|
"selector": "h1, h2, h3, h4, h5, h6",
|
|
@@ -324,6 +372,21 @@ export const elements: Elements = {
|
|
|
324
372
|
}
|
|
325
373
|
},
|
|
326
374
|
"parts": {},
|
|
375
|
+
"texts": {
|
|
376
|
+
"toastRegion": {
|
|
377
|
+
"text": "Notifications",
|
|
378
|
+
"replacedBy": [
|
|
379
|
+
"label",
|
|
380
|
+
"aria-label"
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
"toastClose": {
|
|
384
|
+
"text": "Close message",
|
|
385
|
+
"replacedBy": [
|
|
386
|
+
"close-label"
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
},
|
|
327
390
|
"events": {
|
|
328
391
|
"toast-dismiss": {}
|
|
329
392
|
}
|
|
@@ -350,7 +413,10 @@ export const elements: Elements = {
|
|
|
350
413
|
"selector": "dialog",
|
|
351
414
|
"description": "The `<dialog>` element with the warning.",
|
|
352
415
|
"content": "free",
|
|
353
|
-
"unique": true
|
|
416
|
+
"unique": true,
|
|
417
|
+
"name": {
|
|
418
|
+
"after": "title"
|
|
419
|
+
}
|
|
354
420
|
},
|
|
355
421
|
"title": {
|
|
356
422
|
"selector": "h1, h2, h3, h4, h5, h6",
|
|
@@ -404,6 +470,20 @@ export const elements: Elements = {
|
|
|
404
470
|
}
|
|
405
471
|
},
|
|
406
472
|
"parts": {},
|
|
473
|
+
"texts": {
|
|
474
|
+
"connectionOffline": {
|
|
475
|
+
"text": "No connection. What you type is not being saved.",
|
|
476
|
+
"replacedBy": [
|
|
477
|
+
"offline-text"
|
|
478
|
+
]
|
|
479
|
+
},
|
|
480
|
+
"connectionOnline": {
|
|
481
|
+
"text": "Connection restored.",
|
|
482
|
+
"replacedBy": [
|
|
483
|
+
"online-text"
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
},
|
|
407
487
|
"events": {
|
|
408
488
|
"connection-lost": {},
|
|
409
489
|
"connection-restored": {}
|
package/src/vocabulary/types.ts
CHANGED
|
@@ -34,8 +34,29 @@ export type Part = {
|
|
|
34
34
|
optional?: true
|
|
35
35
|
/** The part must have an id from the server; the component never writes one. */
|
|
36
36
|
stableId?: true
|
|
37
|
+
/** LO-TEK demands a name for the part. */
|
|
38
|
+
name?: Name
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A name LO-TEK demands where WAI-ARIA does not, or one the component gives
|
|
43
|
+
* itself. WAI-ARIA's own demands, by role, are in the core, and `lotek check`
|
|
44
|
+
* reports an element the markup gives no name (`without-name`).
|
|
45
|
+
*
|
|
46
|
+
* `after` is the part the component points `aria-labelledby` at when the
|
|
47
|
+
* markup has neither `aria-label` nor `aria-labelledby`, such as the heading
|
|
48
|
+
* of a dialog; then that part's name is enough. With `id` the part must have
|
|
49
|
+
* an id from the template, since the component does not write one. With
|
|
50
|
+
* `component` the component names it itself, after what the other rules
|
|
51
|
+
* check, such as the list in a suggestion field after the label, and no
|
|
52
|
+
* rule judges its name in the markup.
|
|
53
|
+
*
|
|
54
|
+
* Each of them is held to the component by a contract in
|
|
55
|
+
* `interfaces/scripts/accessibility/contracts.ts`: a mutation that takes the name away
|
|
56
|
+
* must fail in the browsers, and `lotek check` must report it.
|
|
57
|
+
*/
|
|
58
|
+
export type Name = { after?: string; id?: true; component?: true }
|
|
59
|
+
|
|
39
60
|
/**
|
|
40
61
|
* An event an element dispatches, such as `dialog-toggle`. `detail` is the
|
|
41
62
|
* fields in `event.detail` with their types, and is absent when the event
|
|
@@ -52,10 +73,35 @@ export type Element = {
|
|
|
52
73
|
/** Empty for the elements that create all of their content themselves. */
|
|
53
74
|
parts?: Record<string, Part>
|
|
54
75
|
events?: Record<string, ElementEvent>
|
|
76
|
+
/** LO-TEK demands a name for the element itself. */
|
|
77
|
+
name?: Name
|
|
78
|
+
/** The texts the element writes itself, by their name in `DEFAULT_TEXTS`. */
|
|
79
|
+
texts?: Record<string, DefaultText>
|
|
55
80
|
/** Attributes that have been removed, with what applies instead. */
|
|
56
81
|
removed?: Record<string, string>
|
|
57
82
|
}
|
|
58
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A text the element writes itself when nobody has given it another, from
|
|
86
|
+
* `DEFAULT_TEXTS`. `replacedBy` is the attributes that replace it; any one
|
|
87
|
+
* of them is enough, and `lotek check` warns when none is there on a page in
|
|
88
|
+
* another language than the text's.
|
|
89
|
+
*/
|
|
90
|
+
export type DefaultText = { text: string; replacedBy: readonly string[] }
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A text the stylesheet shows in `::after` on an element with the class and
|
|
94
|
+
* `attribute`, with `value` when the selector requires one, and none of the
|
|
95
|
+
* attributes in `unless`. `variable` is the CSS variable that replaces it.
|
|
96
|
+
*/
|
|
97
|
+
export type CssText = {
|
|
98
|
+
variable: string
|
|
99
|
+
text: string
|
|
100
|
+
attribute: string
|
|
101
|
+
value?: string
|
|
102
|
+
unless?: readonly string[]
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
/** The content of `elements.ts`: tag to element. */
|
|
60
106
|
export type Elements = Record<string, Element>
|
|
61
107
|
|
|
@@ -88,6 +134,10 @@ export type ClassInfo = {
|
|
|
88
134
|
partOf?: string
|
|
89
135
|
/** The parts the markup consists of, for the classes with a fixed structure. */
|
|
90
136
|
parts?: Record<string, Part>
|
|
137
|
+
/** LO-TEK demands a name for the element with the class. */
|
|
138
|
+
name?: Name
|
|
139
|
+
/** The texts the stylesheet shows on it. */
|
|
140
|
+
texts?: readonly CssText[]
|
|
91
141
|
attributes: Record<string, ClassAttribute>
|
|
92
142
|
}
|
|
93
143
|
|
|
@@ -147,6 +197,12 @@ export type Builder = {
|
|
|
147
197
|
description?: string
|
|
148
198
|
}[]
|
|
149
199
|
returns: BuilderReturns
|
|
200
|
+
/**
|
|
201
|
+
* The classes for the parts the template writes itself, as `lt.card.title`:
|
|
202
|
+
* the name on the function and the class. Only on the builder functions
|
|
203
|
+
* that have one.
|
|
204
|
+
*/
|
|
205
|
+
classes?: Record<string, string>
|
|
150
206
|
}
|
|
151
207
|
|
|
152
208
|
/**
|
package/web-types.json
CHANGED