@lo-tek/interfaces 0.41.0 → 0.43.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 +107 -0
- package/agent/astro.md +1 -1
- package/agent/bundles.md +1 -1
- package/agent/datastar.md +57 -50
- package/agent/html.md +53 -53
- package/agent/kotlin.md +4 -4
- package/agent/react.md +1 -1
- package/agent/templates.md +50 -50
- package/dist/components/frame/dialog/lt-dialog.d.ts +11 -3
- package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -1
- package/dist/components/frame/dialog/lt-dialog.js +59 -46
- package/dist/components/frame/dialog/lt-dialog.js.map +1 -1
- package/dist/components/frame/error-summary/lt-error-summary.d.ts +9 -6
- package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -1
- package/dist/components/frame/error-summary/lt-error-summary.js +38 -57
- package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -1
- package/dist/components/frame/field/lt-field.d.ts +17 -26
- package/dist/components/frame/field/lt-field.d.ts.map +1 -1
- package/dist/components/frame/field/lt-field.js +81 -143
- package/dist/components/frame/field/lt-field.js.map +1 -1
- package/dist/components/frame/popover/lt-popover.d.ts +1 -1
- package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
- package/dist/components/frame/popover/lt-popover.js +31 -33
- package/dist/components/frame/popover/lt-popover.js.map +1 -1
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +14 -2
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -1
- package/dist/components/frame/session-timeout/lt-session-timeout.js +73 -46
- package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -1
- package/dist/components/frame/suggestion/lt-suggestion.d.ts +5 -7
- package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -1
- package/dist/components/frame/suggestion/lt-suggestion.js +57 -72
- package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -1
- package/dist/components/frame/tabs/lt-tabs.d.ts +14 -5
- package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -1
- package/dist/components/frame/tabs/lt-tabs.js +86 -106
- package/dist/components/frame/tabs/lt-tabs.js.map +1 -1
- package/dist/components/frame/term/lt-term.d.ts +5 -1
- package/dist/components/frame/term/lt-term.d.ts.map +1 -1
- package/dist/components/frame/term/lt-term.js +23 -21
- package/dist/components/frame/term/lt-term.js.map +1 -1
- package/dist/components/host-element.d.ts +137 -2
- package/dist/components/host-element.d.ts.map +1 -1
- package/dist/components/host-element.js +187 -4
- package/dist/components/host-element.js.map +1 -1
- package/dist/lotek.css +1 -1
- package/dist/react.d.ts +51 -437
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +48 -208
- package/dist/react.js.map +1 -1
- package/live-templates-kotlin.xml +4 -6
- package/manifest/manifest.json +1 -1
- package/package.json +7 -3
- package/src/components/frame/dialog/lt-dialog.ts +56 -50
- package/src/components/frame/error-summary/lt-error-summary.ts +33 -46
- package/src/components/frame/field/lt-field.ts +99 -207
- package/src/components/frame/popover/lt-popover.ts +37 -35
- package/src/components/frame/session-timeout/lt-session-timeout.ts +88 -46
- package/src/components/frame/suggestion/lt-suggestion.ts +59 -73
- package/src/components/frame/tabs/lt-tabs.ts +96 -122
- package/src/components/frame/term/lt-term.ts +27 -21
- package/src/components/host-element.ts +306 -3
- package/src/react.ts +125 -240
- package/web-types.json +1 -1
|
@@ -2,12 +2,17 @@ import {
|
|
|
2
2
|
addClass,
|
|
3
3
|
defineElement,
|
|
4
4
|
HostElement,
|
|
5
|
+
hasName,
|
|
5
6
|
isServerControlled,
|
|
7
|
+
nameFromHeading,
|
|
8
|
+
ownership,
|
|
9
|
+
restoreModalOpen,
|
|
6
10
|
SERVER_CONTROLLED,
|
|
7
11
|
setAttr,
|
|
8
12
|
setFlag,
|
|
9
13
|
uniqueId,
|
|
10
14
|
warnAboutMarkup,
|
|
15
|
+
watcher,
|
|
11
16
|
} from "../../host-element.js"
|
|
12
17
|
import {
|
|
13
18
|
DIALOG_BODY_CLASS,
|
|
@@ -89,7 +94,23 @@ const upgradeChecked = new WeakSet<HTMLDialogElement>()
|
|
|
89
94
|
export class LtDialog extends HostElement {
|
|
90
95
|
static observedAttributes = ["open", SERVER_CONTROLLED] as const
|
|
91
96
|
|
|
92
|
-
private
|
|
97
|
+
private readonly watch = watcher(
|
|
98
|
+
(records) => {
|
|
99
|
+
const swapped = records.some(
|
|
100
|
+
(record) => record.type === "childList" && record.target === this,
|
|
101
|
+
)
|
|
102
|
+
if (swapped) this.sync()
|
|
103
|
+
else this.rewire()
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
childList: true,
|
|
107
|
+
subtree: true,
|
|
108
|
+
attributes: true,
|
|
109
|
+
// `aria-label` too: removed, the dialog is named after its title
|
|
110
|
+
// again, and without it here the dialog was left without a name.
|
|
111
|
+
attributeFilter: ["class", "id", "aria-labelledby", "aria-label"],
|
|
112
|
+
},
|
|
113
|
+
)
|
|
93
114
|
/**
|
|
94
115
|
* A separate observer for `open` on the `<dialog>` itself.
|
|
95
116
|
*
|
|
@@ -98,9 +119,12 @@ export class LtDialog extends HostElement {
|
|
|
98
119
|
* an observer running all of `sync()` there would have seen a host still
|
|
99
120
|
* saying "open" and a dialog that was not modal, and opened it again before
|
|
100
121
|
* `close` was handled. This one does only one thing: it puts the attribute
|
|
101
|
-
* back on a dialog that is still in the top layer.
|
|
122
|
+
* back on a dialog that is still in the top layer. It is started again on
|
|
123
|
+
* a dialog the server replaced.
|
|
102
124
|
*/
|
|
103
|
-
private
|
|
125
|
+
private readonly openWatch = watcher(() => this.repairOpen(), {
|
|
126
|
+
attributeFilter: ["open"],
|
|
127
|
+
})
|
|
104
128
|
private dialogElement?: HTMLDialogElement
|
|
105
129
|
/**
|
|
106
130
|
* What the component last reported, so it reports only when the state
|
|
@@ -148,29 +172,13 @@ export class LtDialog extends HostElement {
|
|
|
148
172
|
* and opened it again before the close was handled. Every write compares
|
|
149
173
|
* first, or the observer would trigger itself.
|
|
150
174
|
*/
|
|
151
|
-
this.
|
|
152
|
-
const swapped = records.some(
|
|
153
|
-
(record) => record.type === "childList" && record.target === this,
|
|
154
|
-
)
|
|
155
|
-
if (swapped) this.sync()
|
|
156
|
-
else this.rewire()
|
|
157
|
-
})
|
|
158
|
-
this.observer.observe(this, {
|
|
159
|
-
childList: true,
|
|
160
|
-
subtree: true,
|
|
161
|
-
attributes: true,
|
|
162
|
-
// `aria-label` too: removed, the dialog is named after its title
|
|
163
|
-
// again, and without it here the dialog was left without a name.
|
|
164
|
-
attributeFilter: ["class", "id", "aria-labelledby", "aria-label"],
|
|
165
|
-
})
|
|
175
|
+
this.watch.start(this)
|
|
166
176
|
this.sync()
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
disconnectedCallback(): void {
|
|
170
|
-
this.
|
|
171
|
-
this.
|
|
172
|
-
this.openObserver?.disconnect()
|
|
173
|
-
this.openObserver = undefined
|
|
180
|
+
this.watch.stop()
|
|
181
|
+
this.openWatch.stop()
|
|
174
182
|
this.dialogElement?.removeEventListener("close", this.handleClose)
|
|
175
183
|
this.dialogElement = undefined
|
|
176
184
|
}
|
|
@@ -191,6 +199,13 @@ export class LtDialog extends HostElement {
|
|
|
191
199
|
|
|
192
200
|
/** The id the component gave the heading, so a patch gets the same one back. */
|
|
193
201
|
private titleId?: string
|
|
202
|
+
/**
|
|
203
|
+
* The `aria-labelledby` the component wrote on the `<dialog>`. Another
|
|
204
|
+
* value there is the server's, and stays; the component's own follows the
|
|
205
|
+
* title, so a patch that replaces the title does not leave the name
|
|
206
|
+
* pointing at an id that is gone.
|
|
207
|
+
*/
|
|
208
|
+
private readonly own = ownership()
|
|
194
209
|
|
|
195
210
|
/** The wiring alone, for an attribute record. See the observer. */
|
|
196
211
|
private rewire(): void {
|
|
@@ -230,8 +245,7 @@ export class LtDialog extends HostElement {
|
|
|
230
245
|
private nameless(dialog: HTMLDialogElement): boolean {
|
|
231
246
|
return (
|
|
232
247
|
dialog.childElementCount > 0 &&
|
|
233
|
-
!dialog
|
|
234
|
-
!dialog.hasAttribute("aria-label") &&
|
|
248
|
+
!hasName(dialog) &&
|
|
235
249
|
this.headingOf(dialog) === null
|
|
236
250
|
)
|
|
237
251
|
}
|
|
@@ -248,22 +262,27 @@ export class LtDialog extends HostElement {
|
|
|
248
262
|
private wire(dialog: HTMLDialogElement): void {
|
|
249
263
|
addClass(dialog, DIALOG_CLASS)
|
|
250
264
|
|
|
251
|
-
const
|
|
252
|
-
|
|
265
|
+
const name = nameFromHeading(this.own, this, dialog, () => {
|
|
266
|
+
const title = this.headingOf(dialog)
|
|
267
|
+
if (!title) return null
|
|
268
|
+
addClass(title, DIALOG_TITLE_CLASS)
|
|
269
|
+
if (!title.id) {
|
|
270
|
+
this.titleId ??= uniqueId("lt-dialog-title")
|
|
271
|
+
setAttr(title, "id", this.titleId)
|
|
272
|
+
}
|
|
273
|
+
return title.id
|
|
274
|
+
})
|
|
275
|
+
if (name === "server") {
|
|
253
276
|
// The attribute can list several ids. The title is the first one that
|
|
254
277
|
// is inside the dialog.
|
|
278
|
+
const named = (dialog.getAttribute("aria-labelledby") ?? "").trim()
|
|
255
279
|
const root = this.getRootNode() as Document | ShadowRoot
|
|
256
280
|
const title = named
|
|
257
281
|
.split(/\s+/)
|
|
258
282
|
.map((id) => root.getElementById?.(id) ?? null)
|
|
259
283
|
.find((element) => element !== null && dialog.contains(element))
|
|
260
284
|
if (title) addClass(title, DIALOG_TITLE_CLASS)
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
if (dialog.hasAttribute("aria-label")) return
|
|
264
|
-
|
|
265
|
-
const title = this.headingOf(dialog)
|
|
266
|
-
if (!title) {
|
|
285
|
+
} else if (name === "none") {
|
|
267
286
|
warnAboutMarkup(
|
|
268
287
|
this,
|
|
269
288
|
"the dialog has no name: it has no aria-labelledby, no " +
|
|
@@ -275,14 +294,7 @@ export class LtDialog extends HostElement {
|
|
|
275
294
|
return now !== null && this.nameless(now)
|
|
276
295
|
},
|
|
277
296
|
)
|
|
278
|
-
return
|
|
279
297
|
}
|
|
280
|
-
addClass(title, DIALOG_TITLE_CLASS)
|
|
281
|
-
if (!title.id) {
|
|
282
|
-
this.titleId ??= uniqueId("lt-dialog-title")
|
|
283
|
-
setAttr(title, "id", this.titleId)
|
|
284
|
-
}
|
|
285
|
-
setAttr(dialog, "aria-labelledby", title.id)
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
private notify(open: boolean, returnValue = ""): void {
|
|
@@ -316,14 +328,10 @@ export class LtDialog extends HostElement {
|
|
|
316
328
|
*/
|
|
317
329
|
private repairOpen(): void {
|
|
318
330
|
const dialog = this.dialogElement
|
|
319
|
-
if (!dialog?.isConnected || !dialog.matches(":modal") || dialog.open) {
|
|
320
|
-
return
|
|
321
|
-
}
|
|
322
|
-
|
|
323
331
|
// The attribute must come back either way: without it `close()` does
|
|
324
332
|
// nothing, and the dialog stayed in the top layer, invisible, with the
|
|
325
333
|
// rest of the page inert.
|
|
326
|
-
|
|
334
|
+
if (!dialog || !restoreModalOpen(dialog)) return
|
|
327
335
|
if (!isServerControlled(this)) return
|
|
328
336
|
|
|
329
337
|
/*
|
|
@@ -370,8 +378,7 @@ export class LtDialog extends HostElement {
|
|
|
370
378
|
// listener stayed attached to a detached node for as long as the host
|
|
371
379
|
// lived.
|
|
372
380
|
if (!dialog) {
|
|
373
|
-
this.
|
|
374
|
-
this.openObserver = undefined
|
|
381
|
+
this.openWatch.stop()
|
|
375
382
|
this.dialogElement?.removeEventListener("close", this.handleClose)
|
|
376
383
|
this.dialogElement = undefined
|
|
377
384
|
}
|
|
@@ -384,7 +391,8 @@ export class LtDialog extends HostElement {
|
|
|
384
391
|
// If the component is detached, a morph is building the server's
|
|
385
392
|
// version in a separate tree, and there is nothing to report. An
|
|
386
393
|
// empty element is a region the server has not filled yet.
|
|
387
|
-
() => this.
|
|
394
|
+
() => this.dialog === null,
|
|
395
|
+
{ silentWhenEmpty: true },
|
|
388
396
|
)
|
|
389
397
|
return
|
|
390
398
|
}
|
|
@@ -397,9 +405,7 @@ export class LtDialog extends HostElement {
|
|
|
397
405
|
this.dialogElement = dialog
|
|
398
406
|
dialog.addEventListener("close", this.handleClose)
|
|
399
407
|
|
|
400
|
-
this.
|
|
401
|
-
this.openObserver = new MutationObserver(() => this.repairOpen())
|
|
402
|
-
this.openObserver.observe(dialog, { attributeFilter: ["open"] })
|
|
408
|
+
this.openWatch.start(dialog)
|
|
403
409
|
}
|
|
404
410
|
|
|
405
411
|
this.repairOpen()
|
|
@@ -2,8 +2,12 @@ import {
|
|
|
2
2
|
addClass,
|
|
3
3
|
defineElement,
|
|
4
4
|
HostElement,
|
|
5
|
+
hasName,
|
|
6
|
+
nameFromHeading,
|
|
7
|
+
ownership,
|
|
5
8
|
setAttr,
|
|
6
9
|
warnAboutMarkup,
|
|
10
|
+
watcher,
|
|
7
11
|
} from "../../host-element.js"
|
|
8
12
|
import {
|
|
9
13
|
ERROR_SUMMARY_CLASS,
|
|
@@ -82,28 +86,18 @@ export class LtErrorSummary extends HostElement {
|
|
|
82
86
|
private focusAttempted = false
|
|
83
87
|
/** Where focus was when `hidden` last changed, until the deferred round has run. */
|
|
84
88
|
private focusBefore: Element | null | undefined
|
|
85
|
-
private observer?: MutationObserver
|
|
86
89
|
/**
|
|
87
|
-
* The `aria-labelledby` the component wrote on the box,
|
|
88
|
-
*
|
|
89
|
-
* heading, so a patch that replaces the heading does not leave the name
|
|
90
|
-
* pointing at an id that is gone.
|
|
90
|
+
* The `aria-labelledby` the component wrote on the box, through
|
|
91
|
+
* `nameFromHeading()`.
|
|
91
92
|
*/
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// does not fire in plain DOM. The attributes are the ones the component
|
|
101
|
-
// fills in itself, so a patch that removes them gets them back. Every
|
|
102
|
-
// write compares first, or the observer would trigger itself.
|
|
103
|
-
// An id only matters on the heading, which the name points at, and a
|
|
104
|
-
// name only on the box. The same attributes on a link or a field inside
|
|
105
|
-
// the box are not ours to follow.
|
|
106
|
-
this.observer = new MutationObserver((records) => {
|
|
93
|
+
private readonly own = ownership()
|
|
94
|
+
/**
|
|
95
|
+
* An id only matters on the heading, which the name points at, and a name
|
|
96
|
+
* only on the box. The same attributes on a link or a field inside the box
|
|
97
|
+
* are not ours to follow.
|
|
98
|
+
*/
|
|
99
|
+
private readonly watch = watcher(
|
|
100
|
+
(records) => {
|
|
107
101
|
const relevant = records.some((r) => {
|
|
108
102
|
if (r.attributeName === "id")
|
|
109
103
|
return (r.target as Element).matches(LT_ERROR_SUMMARY_PARTS.title)
|
|
@@ -115,8 +109,8 @@ export class LtErrorSummary extends HostElement {
|
|
|
115
109
|
return true
|
|
116
110
|
})
|
|
117
111
|
if (relevant) this.sync()
|
|
118
|
-
}
|
|
119
|
-
|
|
112
|
+
},
|
|
113
|
+
{
|
|
120
114
|
childList: true,
|
|
121
115
|
subtree: true,
|
|
122
116
|
attributes: true,
|
|
@@ -128,7 +122,19 @@ export class LtErrorSummary extends HostElement {
|
|
|
128
122
|
"aria-label",
|
|
129
123
|
"id",
|
|
130
124
|
],
|
|
131
|
-
}
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
connectedCallback(): void {
|
|
129
|
+
// One listener on the host, not one per link. The set of links was only
|
|
130
|
+
// emptied on disconnect, so every link a patch replaced stayed in
|
|
131
|
+
// memory.
|
|
132
|
+
this.addEventListener("click", this.handleLinkClick)
|
|
133
|
+
// The errors come and go while the user corrects them, and `slotchange`
|
|
134
|
+
// does not fire in plain DOM. The attributes are the ones the component
|
|
135
|
+
// fills in itself, so a patch that removes them gets them back. Every
|
|
136
|
+
// write compares first, or the observer would trigger itself.
|
|
137
|
+
this.watch.start(this)
|
|
132
138
|
this.sync()
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -164,8 +170,7 @@ export class LtErrorSummary extends HostElement {
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
disconnectedCallback(): void {
|
|
167
|
-
this.
|
|
168
|
-
this.observer = undefined
|
|
173
|
+
this.watch.stop()
|
|
169
174
|
this.removeEventListener("click", this.handleLinkClick)
|
|
170
175
|
}
|
|
171
176
|
|
|
@@ -212,32 +217,14 @@ export class LtErrorSummary extends HostElement {
|
|
|
212
217
|
* nothing, so it counts as missing.
|
|
213
218
|
* `check-accessibility-tree.ts` checks the name a screen reader gets.
|
|
214
219
|
*/
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
const ours = labelledBy !== null && labelledBy === this.labelledByWritten
|
|
218
|
-
if (labelledBy !== null && !ours) this.labelledByWritten = null
|
|
219
|
-
if (title?.id && label === null && (labelledBy === null || ours)) {
|
|
220
|
-
setAttr(this, "aria-labelledby", title.id)
|
|
221
|
-
this.labelledByWritten = title.id
|
|
222
|
-
} else if (ours) {
|
|
223
|
-
// The heading is gone or lost its id, or the server named the box
|
|
224
|
-
// with `aria-label`, which `aria-labelledby` would otherwise win over.
|
|
225
|
-
this.removeAttribute("aria-labelledby")
|
|
226
|
-
this.labelledByWritten = null
|
|
227
|
-
}
|
|
228
|
-
if (title && !title.id && label === null && labelledBy === null)
|
|
220
|
+
const name = nameFromHeading(this.own, this, this, () => title?.id || null)
|
|
221
|
+
if (name === "none" && title && !title.id)
|
|
229
222
|
warnAboutMarkup(
|
|
230
223
|
this,
|
|
231
224
|
"has a heading without an id and no name, so it takes focus as an alert a screen reader cannot name. Give the heading an id and the box aria-labelledby pointing at it, as lt.errorSummary({ id }) does.",
|
|
232
225
|
() => {
|
|
233
226
|
const now = this.querySelector(LT_ERROR_SUMMARY_PARTS.title)
|
|
234
|
-
return (
|
|
235
|
-
this.isConnected &&
|
|
236
|
-
now !== null &&
|
|
237
|
-
!now.id &&
|
|
238
|
-
!this.getAttribute("aria-label")?.trim() &&
|
|
239
|
-
!this.getAttribute("aria-labelledby")?.trim()
|
|
240
|
-
)
|
|
227
|
+
return this.isConnected && now !== null && !now.id && !hasName(this)
|
|
241
228
|
},
|
|
242
229
|
)
|
|
243
230
|
}
|