@lo-tek/interfaces 0.42.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 +82 -0
- package/agent/astro.md +1 -1
- package/agent/bundles.md +1 -1
- package/agent/datastar.md +50 -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/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,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
|
}
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
addClass,
|
|
5
5
|
defineElement,
|
|
6
6
|
HostElement,
|
|
7
|
-
|
|
7
|
+
ownership,
|
|
8
8
|
setAttr,
|
|
9
9
|
setFlag,
|
|
10
|
-
stillMarked,
|
|
11
10
|
uniqueId,
|
|
12
11
|
WRITTEN,
|
|
13
12
|
warnAboutCopiedMark,
|
|
14
13
|
warnAboutMarkup,
|
|
14
|
+
watcher,
|
|
15
15
|
} from "../../host-element.js"
|
|
16
16
|
import { computeFieldAttributes } from "./field-core.js"
|
|
17
17
|
|
|
@@ -69,58 +69,6 @@ const DERIVED_ATTRIBUTES = [
|
|
|
69
69
|
"style",
|
|
70
70
|
]
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
* What the component last wrote on a control, and what the server last said.
|
|
74
|
-
*
|
|
75
|
-
* Three attributes are both read from the control and written back to it:
|
|
76
|
-
* `aria-invalid`, `aria-describedby` and `disabled`. They are read because
|
|
77
|
-
* the server may have written the field with `lt.field()`, and then the
|
|
78
|
-
* answer is already there. But a naive read is the component's echo from the
|
|
79
|
-
* previous round. For `aria-invalid` that meant `field.invalid = false`
|
|
80
|
-
* removed the flag on the host while the red border stayed for good. For
|
|
81
|
-
* `aria-describedby` the list never shrank: the error text's id stayed after
|
|
82
|
-
* the error was gone. For `disabled` the field could not be enabled again
|
|
83
|
-
* from the host.
|
|
84
|
-
*
|
|
85
|
-
* The fix is not to remember what the host said last. That was tried, and
|
|
86
|
-
* made the component depend on its history: the same markup gave two
|
|
87
|
-
* different answers depending on whether the host had had `invalid` at some
|
|
88
|
-
* point.
|
|
89
|
-
*
|
|
90
|
-
* Instead the component notes the value it wrote, and on which control. If
|
|
91
|
-
* something else is there next time, someone else has touched the
|
|
92
|
-
* attribute, and that is the server's word. The read is therefore always
|
|
93
|
-
* derived from a change that actually happened, and the same document always
|
|
94
|
-
* gives the same answer.
|
|
95
|
-
*/
|
|
96
|
-
type ControlMemory = {
|
|
97
|
-
written: ControlWord
|
|
98
|
-
server: ControlWord
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The same memory for the markers on the label. The component both reads and
|
|
103
|
-
* writes `data-required` and `data-optional`, and without knowing what it
|
|
104
|
-
* wrote itself, it read its own answer back: `optional` and the required
|
|
105
|
-
* marker could not be turned off again.
|
|
106
|
-
*/
|
|
107
|
-
type LabelWord = { required: string | null; optional: boolean }
|
|
108
|
-
type LabelMemory = { written: LabelWord; server: LabelWord }
|
|
109
|
-
|
|
110
|
-
type ControlWord = {
|
|
111
|
-
invalid: string | null
|
|
112
|
-
describedBy: string | null
|
|
113
|
-
disabled: boolean
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function readControlWord(control: Element): ControlWord {
|
|
117
|
-
return {
|
|
118
|
-
invalid: control.getAttribute("aria-invalid"),
|
|
119
|
-
describedBy: control.getAttribute("aria-describedby"),
|
|
120
|
-
disabled: control.hasAttribute("disabled"),
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
72
|
/**
|
|
125
73
|
* Connects label, control, help text and error text in plain DOM.
|
|
126
74
|
*
|
|
@@ -153,7 +101,31 @@ function readControlWord(control: Element): ControlWord {
|
|
|
153
101
|
export class LtField extends HostElement {
|
|
154
102
|
static observedAttributes = ["disabled", "optional", "described-by"] as const
|
|
155
103
|
|
|
156
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The children and the derived attributes, watched while connected.
|
|
106
|
+
* `style` is observed for `display: none` on the error text; a style write
|
|
107
|
+
* elsewhere in the field, such as a popover positioning its panel, does not
|
|
108
|
+
* concern the wiring.
|
|
109
|
+
*/
|
|
110
|
+
private readonly watch = watcher(
|
|
111
|
+
(records) => {
|
|
112
|
+
const onlyForeignStyle = records.every(
|
|
113
|
+
(r) =>
|
|
114
|
+
r.type === "attributes" &&
|
|
115
|
+
r.attributeName === "style" &&
|
|
116
|
+
!(r.target as Element).matches?.(LT_FIELD_PARTS.error),
|
|
117
|
+
)
|
|
118
|
+
if (!onlyForeignStyle) this.sync()
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
childList: true,
|
|
122
|
+
subtree: true,
|
|
123
|
+
attributes: true,
|
|
124
|
+
attributeFilter: DERIVED_ATTRIBUTES,
|
|
125
|
+
// A morph can change the text of the error text without replacing the node.
|
|
126
|
+
characterData: true,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
157
129
|
/**
|
|
158
130
|
* The ids the help text and the error text last had, whether they came
|
|
159
131
|
* from the markup or from here, so a patch that removes them gets the same
|
|
@@ -165,20 +137,17 @@ export class LtField extends HostElement {
|
|
|
165
137
|
private lastErrorId?: string
|
|
166
138
|
private readonly managedIds = new Set<string>()
|
|
167
139
|
/**
|
|
168
|
-
* What the component wrote on
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
140
|
+
* What the component wrote on the control and the label, and the server's
|
|
141
|
+
* word it wrote against, per node. The memory has to survive the host
|
|
142
|
+
* being disconnected and connected again, as when React moves a field,
|
|
143
|
+
* and the control disappearing and coming back in a patch: a property
|
|
144
|
+
* reset in `disconnectedCallback` made the component read its own
|
|
145
|
+
* `aria-invalid="true"` as the server's word after a move, and the field
|
|
146
|
+
* could never become valid again. The component both reads and writes
|
|
147
|
+
* `data-required` and `data-optional` on the label too, and without the
|
|
148
|
+
* memory `optional` and the required marker could not be turned off.
|
|
177
149
|
*/
|
|
178
|
-
private readonly
|
|
179
|
-
private readonly labelMemory = new WeakMap<Element, LabelMemory>()
|
|
180
|
-
/** The `for` the component itself wrote on the label. */
|
|
181
|
-
private readonly forByMe = new WeakMap<Element, string>()
|
|
150
|
+
private readonly own = ownership()
|
|
182
151
|
/**
|
|
183
152
|
* Labels where the server wrote a `for` pointing to a different id than
|
|
184
153
|
* the control's. The component rewrites it, but without JavaScript the
|
|
@@ -202,8 +171,6 @@ export class LtField extends HostElement {
|
|
|
202
171
|
* detached node alive until the next `sync()`, on purpose.
|
|
203
172
|
*/
|
|
204
173
|
private lastControl?: HTMLElement
|
|
205
|
-
/** The controls where `data-state` was written by the component. */
|
|
206
|
-
private readonly stateByMe = new WeakSet<Element>()
|
|
207
174
|
|
|
208
175
|
/**
|
|
209
176
|
* The properties mirror the attributes.
|
|
@@ -253,32 +220,12 @@ export class LtField extends HostElement {
|
|
|
253
220
|
* Every write in `sync()` compares first. Without that the observer
|
|
254
221
|
* would trigger itself forever.
|
|
255
222
|
*/
|
|
256
|
-
this.
|
|
257
|
-
// `style` is observed for `display: none` on the error text. A style
|
|
258
|
-
// write elsewhere in the field, such as a popover positioning its
|
|
259
|
-
// panel, does not concern the wiring.
|
|
260
|
-
const onlyForeignStyle = records.every(
|
|
261
|
-
(r) =>
|
|
262
|
-
r.type === "attributes" &&
|
|
263
|
-
r.attributeName === "style" &&
|
|
264
|
-
!(r.target as Element).matches?.(LT_FIELD_PARTS.error),
|
|
265
|
-
)
|
|
266
|
-
if (!onlyForeignStyle) this.sync()
|
|
267
|
-
})
|
|
268
|
-
this.observer.observe(this, {
|
|
269
|
-
childList: true,
|
|
270
|
-
subtree: true,
|
|
271
|
-
attributes: true,
|
|
272
|
-
attributeFilter: DERIVED_ATTRIBUTES,
|
|
273
|
-
// A morph can change the text of the error text without replacing the node.
|
|
274
|
-
characterData: true,
|
|
275
|
-
})
|
|
223
|
+
this.watch.start(this)
|
|
276
224
|
this.sync()
|
|
277
225
|
}
|
|
278
226
|
|
|
279
227
|
disconnectedCallback(): void {
|
|
280
|
-
this.
|
|
281
|
-
this.observer = undefined
|
|
228
|
+
this.watch.stop()
|
|
282
229
|
}
|
|
283
230
|
|
|
284
231
|
attributeChangedCallback(): void {
|
|
@@ -301,7 +248,7 @@ export class LtField extends HostElement {
|
|
|
301
248
|
private noteFor(label: Element, id: string): void {
|
|
302
249
|
const current = label.getAttribute("for")
|
|
303
250
|
if (current === null || current === id) return
|
|
304
|
-
if (current !== this.
|
|
251
|
+
if (current !== this.own.written(label, "for")) this.wrongFor.add(label)
|
|
305
252
|
}
|
|
306
253
|
|
|
307
254
|
private resolveLabel(control: HTMLElement | null): HTMLLabelElement | null {
|
|
@@ -316,62 +263,6 @@ export class LtField extends HostElement {
|
|
|
316
263
|
)
|
|
317
264
|
}
|
|
318
265
|
|
|
319
|
-
/**
|
|
320
|
-
* The server's word about the control, derived from what is there now
|
|
321
|
-
* compared with what the component itself wrote last.
|
|
322
|
-
*
|
|
323
|
-
* A control the component has never written on holds only the server's
|
|
324
|
-
* word. Otherwise each attribute is the server's if it differs from what
|
|
325
|
-
* the component wrote, and unchanged if it is the same.
|
|
326
|
-
*/
|
|
327
|
-
private serverLabelWord(label: Element | null): LabelWord {
|
|
328
|
-
if (!label) return { required: null, optional: false }
|
|
329
|
-
const now: LabelWord = {
|
|
330
|
-
required: label.getAttribute("data-required"),
|
|
331
|
-
optional: label.hasAttribute("data-optional"),
|
|
332
|
-
}
|
|
333
|
-
const known = this.labelMemory.get(label)
|
|
334
|
-
if (!known) return now
|
|
335
|
-
|
|
336
|
-
const spoke = (name: string, changed: boolean) =>
|
|
337
|
-
changed || !stillMarked(label, name)
|
|
338
|
-
return {
|
|
339
|
-
required: spoke("data-required", now.required !== known.written.required)
|
|
340
|
-
? now.required
|
|
341
|
-
: known.server.required,
|
|
342
|
-
optional: spoke("data-optional", now.optional !== known.written.optional)
|
|
343
|
-
? now.optional
|
|
344
|
-
: known.server.optional,
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private serverWord(control: Element, now: ControlWord): ControlWord {
|
|
349
|
-
const known = this.memory.get(control)
|
|
350
|
-
if (!known) {
|
|
351
|
-
warnAboutCopiedMark(this, control)
|
|
352
|
-
return now
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// The server's when the value is another, or when a morph from the
|
|
356
|
-
// server has been there since the component wrote it (`WRITTEN`).
|
|
357
|
-
const spoke = (name: string, changed: boolean) =>
|
|
358
|
-
changed || !stillMarked(control, name)
|
|
359
|
-
return {
|
|
360
|
-
invalid: spoke("aria-invalid", now.invalid !== known.written.invalid)
|
|
361
|
-
? now.invalid
|
|
362
|
-
: known.server.invalid,
|
|
363
|
-
describedBy: spoke(
|
|
364
|
-
"aria-describedby",
|
|
365
|
-
now.describedBy !== known.written.describedBy,
|
|
366
|
-
)
|
|
367
|
-
? now.describedBy
|
|
368
|
-
: known.server.describedBy,
|
|
369
|
-
disabled: spoke("disabled", now.disabled !== known.written.disabled)
|
|
370
|
-
? now.disabled
|
|
371
|
-
: known.server.disabled,
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
266
|
/**
|
|
376
267
|
* The parts that belong to this field, and not to a field further down.
|
|
377
268
|
*
|
|
@@ -471,11 +362,8 @@ export class LtField extends HostElement {
|
|
|
471
362
|
"found no control to connect. Add an <input>, <textarea> or " +
|
|
472
363
|
"<select>, or the label, help text and error text have no field " +
|
|
473
364
|
"to be connected to.",
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
() =>
|
|
477
|
-
this.childElementCount > 0 &&
|
|
478
|
-
this.ownParts(CONTROL_SELECTOR).length === 0,
|
|
365
|
+
() => this.ownParts(CONTROL_SELECTOR).length === 0,
|
|
366
|
+
{ silentWhenEmpty: true },
|
|
479
367
|
)
|
|
480
368
|
return
|
|
481
369
|
}
|
|
@@ -604,16 +492,33 @@ export class LtField extends HostElement {
|
|
|
604
492
|
// looked at its own attributes would remove them again. In React that
|
|
605
493
|
// gave a hydration error: the server sent `data-required="symbol"`, the
|
|
606
494
|
// component removed it, and then React decided the HTML did not match.
|
|
607
|
-
const serverLabel =
|
|
495
|
+
const serverLabel = {
|
|
496
|
+
required: label ? this.own.serverWord(label, "data-required") : null,
|
|
497
|
+
optional: label
|
|
498
|
+
? this.own.serverWord(label, "data-optional", { presence: true }) !==
|
|
499
|
+
null
|
|
500
|
+
: false,
|
|
501
|
+
}
|
|
608
502
|
const marker = readMarker(serverLabel.required)
|
|
609
503
|
|
|
610
504
|
// The state is read from the markup, not just from an attribute on the
|
|
611
505
|
// host. If the server wrote the field with `lt.field()`, the answer is
|
|
612
506
|
// already on the control, and a component that computed its own would
|
|
613
|
-
// remove it again. The two halves of the API then fought each other.
|
|
614
|
-
// `
|
|
615
|
-
// component wrote
|
|
616
|
-
|
|
507
|
+
// remove it again. The two halves of the API then fought each other.
|
|
508
|
+
// `ownership()` in host-element.ts tells the server's word apart from
|
|
509
|
+
// what the component wrote: by the value and the node it wrote it on,
|
|
510
|
+
// never by what the host said last, which gave two answers for the same
|
|
511
|
+
// markup depending on what the host had been through.
|
|
512
|
+
// A control the component has never written on holds only the server's
|
|
513
|
+
// word, and a mark found there was copied into the template.
|
|
514
|
+
if (!this.own.knows(control, "aria-invalid"))
|
|
515
|
+
warnAboutCopiedMark(this, control)
|
|
516
|
+
const server = {
|
|
517
|
+
invalid: this.own.serverWord(control, "aria-invalid"),
|
|
518
|
+
describedBy: this.own.serverWord(control, "aria-describedby"),
|
|
519
|
+
disabled:
|
|
520
|
+
this.own.serverWord(control, "disabled", { flag: true }) !== null,
|
|
521
|
+
}
|
|
617
522
|
|
|
618
523
|
// `aria-invalid` has four valid values, and both `grammar` and
|
|
619
524
|
// `spelling` mean invalid. Absence, `false` and the empty string mean
|
|
@@ -693,29 +598,24 @@ export class LtField extends HostElement {
|
|
|
693
598
|
// the wiring is implicit.
|
|
694
599
|
if (control.id && !label.contains(control)) {
|
|
695
600
|
this.noteFor(label, control.id)
|
|
696
|
-
|
|
697
|
-
this.forByMe.set(label, control.id)
|
|
698
|
-
}
|
|
699
|
-
setAttr(label, "data-required", computed.label["data-required"])
|
|
700
|
-
setAttr(label, "data-optional", computed.label["data-optional"])
|
|
701
|
-
const writtenLabel: LabelWord = {
|
|
702
|
-
required: computed.label["data-required"] ?? null,
|
|
703
|
-
optional: computed.label["data-optional"] !== undefined,
|
|
601
|
+
this.own.write(label, "for", control.id)
|
|
704
602
|
}
|
|
705
|
-
|
|
603
|
+
this.own.write(
|
|
706
604
|
label,
|
|
707
605
|
"data-required",
|
|
708
|
-
|
|
606
|
+
computed.label["data-required"] ?? null,
|
|
607
|
+
{ server: serverLabel.required, mark: true },
|
|
709
608
|
)
|
|
710
|
-
|
|
609
|
+
this.own.write(
|
|
711
610
|
label,
|
|
712
611
|
"data-optional",
|
|
713
|
-
|
|
612
|
+
computed.label["data-optional"] ?? null,
|
|
613
|
+
{
|
|
614
|
+
server: serverLabel.optional ? "" : null,
|
|
615
|
+
mark: true,
|
|
616
|
+
presence: true,
|
|
617
|
+
},
|
|
714
618
|
)
|
|
715
|
-
this.labelMemory.set(label, {
|
|
716
|
-
written: writtenLabel,
|
|
717
|
-
server: serverLabel,
|
|
718
|
-
})
|
|
719
619
|
setAttr(label, "aria-disabled", computed.label["aria-disabled"])
|
|
720
620
|
}
|
|
721
621
|
|
|
@@ -730,8 +630,16 @@ export class LtField extends HostElement {
|
|
|
730
630
|
? "true"
|
|
731
631
|
: server.invalid
|
|
732
632
|
|
|
733
|
-
|
|
734
|
-
|
|
633
|
+
this.own.write(
|
|
634
|
+
control,
|
|
635
|
+
"aria-describedby",
|
|
636
|
+
computed.control["aria-describedby"] ?? null,
|
|
637
|
+
{ server: server.describedBy, mark: true },
|
|
638
|
+
)
|
|
639
|
+
this.own.write(control, "aria-invalid", ariaInvalid, {
|
|
640
|
+
server: server.invalid,
|
|
641
|
+
mark: true,
|
|
642
|
+
})
|
|
735
643
|
// `setFlag` and not `setAttr`: a template may have written
|
|
736
644
|
// `disabled="disabled"`, and it must stay as it is. `setAttr` would
|
|
737
645
|
// normalise the value to the empty string, and since `disabled` is among
|
|
@@ -739,33 +647,18 @@ export class LtField extends HostElement {
|
|
|
739
647
|
// would each write their own value on every patch. No `aria-disabled`
|
|
740
648
|
// alongside it: a real `disabled` is already visible to assistive
|
|
741
649
|
// technology, and `computeFieldAttributes` only writes it on the label.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
disabled,
|
|
747
|
-
}
|
|
748
|
-
markWritten(control, "aria-invalid", written.invalid !== server.invalid)
|
|
749
|
-
markWritten(
|
|
750
|
-
control,
|
|
751
|
-
"aria-describedby",
|
|
752
|
-
written.describedBy !== server.describedBy,
|
|
753
|
-
)
|
|
754
|
-
markWritten(control, "disabled", written.disabled !== server.disabled)
|
|
755
|
-
|
|
756
|
-
this.memory.set(control, {
|
|
757
|
-
written: {
|
|
758
|
-
invalid: written.invalid,
|
|
759
|
-
describedBy: written.describedBy,
|
|
760
|
-
disabled: written.disabled,
|
|
761
|
-
},
|
|
762
|
-
server,
|
|
650
|
+
this.own.write(control, "disabled", disabled ? "" : null, {
|
|
651
|
+
server: server.disabled ? "" : null,
|
|
652
|
+
mark: true,
|
|
653
|
+
flag: true,
|
|
763
654
|
})
|
|
764
655
|
|
|
765
656
|
// `data-state` is only set on the system's controls, and only when the
|
|
766
|
-
// consumer has not set it. It is only removed when the component set it
|
|
767
|
-
//
|
|
768
|
-
//
|
|
657
|
+
// consumer has not set it. It is only removed when the component set it,
|
|
658
|
+
// the field is no longer invalid, and the server has not sent the same
|
|
659
|
+
// value since: written with the mark, so a morph that sent it removes
|
|
660
|
+
// the mark. A `data-state` a template wrote by hand stays as the
|
|
661
|
+
// template wrote it.
|
|
769
662
|
const isSystemField =
|
|
770
663
|
control.classList.contains("lt-input") ||
|
|
771
664
|
control.classList.contains("lt-textarea") ||
|
|
@@ -773,15 +666,14 @@ export class LtField extends HostElement {
|
|
|
773
666
|
|
|
774
667
|
const state = computed.control["data-state"]
|
|
775
668
|
if (state && isSystemField && !control.hasAttribute("data-state")) {
|
|
776
|
-
|
|
777
|
-
this.stateByMe.add(control)
|
|
669
|
+
this.own.write(control, "data-state", state, { server: null, mark: true })
|
|
778
670
|
} else if (
|
|
779
671
|
!state &&
|
|
780
|
-
this.
|
|
781
|
-
control.getAttribute("data-state") === "invalid"
|
|
672
|
+
this.own.written(control, "data-state") != null &&
|
|
673
|
+
control.getAttribute("data-state") === "invalid" &&
|
|
674
|
+
this.own.serverWord(control, "data-state") === null
|
|
782
675
|
) {
|
|
783
|
-
|
|
784
|
-
this.stateByMe.delete(control)
|
|
676
|
+
this.own.release(control, "data-state")
|
|
785
677
|
}
|
|
786
678
|
}
|
|
787
679
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
setAttr,
|
|
9
9
|
setFlag,
|
|
10
10
|
warnAboutMarkup,
|
|
11
|
+
watcher,
|
|
11
12
|
} from "../../host-element.js"
|
|
12
13
|
import {
|
|
13
14
|
isPopoverPlacement,
|
|
@@ -80,7 +81,35 @@ export class LtPopover extends HostElement {
|
|
|
80
81
|
|
|
81
82
|
private panel?: HTMLElement
|
|
82
83
|
private triggerElement?: HTMLElement
|
|
83
|
-
private
|
|
84
|
+
private readonly watch = watcher(
|
|
85
|
+
(records) => {
|
|
86
|
+
// `type` concerns only the button. A password field in the panel that
|
|
87
|
+
// switches between `password` and `text` must not run all of `sync()`.
|
|
88
|
+
const onlyOtherTypes = records.every(
|
|
89
|
+
(record) =>
|
|
90
|
+
record.attributeName === "type" &&
|
|
91
|
+
record.target !== this.triggerElement,
|
|
92
|
+
)
|
|
93
|
+
if (!onlyOtherTypes) this.sync()
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
childList: true,
|
|
97
|
+
subtree: true,
|
|
98
|
+
attributes: true,
|
|
99
|
+
attributeFilter: [
|
|
100
|
+
"aria-expanded",
|
|
101
|
+
"style",
|
|
102
|
+
// The wiring the component fills in when the markup came without it.
|
|
103
|
+
"popover",
|
|
104
|
+
"id",
|
|
105
|
+
"aria-controls",
|
|
106
|
+
"class",
|
|
107
|
+
// A patch from a template without `type` removes it, and then the
|
|
108
|
+
// button submits the form again.
|
|
109
|
+
"type",
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
)
|
|
84
113
|
/**
|
|
85
114
|
* What the component was last asked for, through the `open` property.
|
|
86
115
|
*
|
|
@@ -133,33 +162,7 @@ export class LtPopover extends HostElement {
|
|
|
133
162
|
* out, so if a patch removes them, they must come back. Every write in
|
|
134
163
|
* `sync()` compares first, or the observer would trigger itself.
|
|
135
164
|
*/
|
|
136
|
-
this.
|
|
137
|
-
// `type` concerns only the button. A password field in the panel that
|
|
138
|
-
// switches between `password` and `text` must not run all of `sync()`.
|
|
139
|
-
const onlyOtherTypes = records.every(
|
|
140
|
-
(record) =>
|
|
141
|
-
record.attributeName === "type" &&
|
|
142
|
-
record.target !== this.triggerElement,
|
|
143
|
-
)
|
|
144
|
-
if (!onlyOtherTypes) this.sync()
|
|
145
|
-
})
|
|
146
|
-
this.observer.observe(this, {
|
|
147
|
-
childList: true,
|
|
148
|
-
subtree: true,
|
|
149
|
-
attributes: true,
|
|
150
|
-
attributeFilter: [
|
|
151
|
-
"aria-expanded",
|
|
152
|
-
"style",
|
|
153
|
-
// The wiring the component fills in when the markup came without it.
|
|
154
|
-
"popover",
|
|
155
|
-
"id",
|
|
156
|
-
"aria-controls",
|
|
157
|
-
"class",
|
|
158
|
-
// A patch from a template without `type` removes it, and then the
|
|
159
|
-
// button submits the form again.
|
|
160
|
-
"type",
|
|
161
|
-
],
|
|
162
|
-
})
|
|
165
|
+
this.watch.start(this)
|
|
163
166
|
this.sync()
|
|
164
167
|
}
|
|
165
168
|
|
|
@@ -168,8 +171,7 @@ export class LtPopover extends HostElement {
|
|
|
168
171
|
window.removeEventListener("scroll", this.reposition, true)
|
|
169
172
|
document.removeEventListener("click", this.handleOutsideClick, true)
|
|
170
173
|
document.removeEventListener("keydown", this.handleKeydown)
|
|
171
|
-
this.
|
|
172
|
-
this.observer = undefined
|
|
174
|
+
this.watch.stop()
|
|
173
175
|
this.triggerElement?.removeEventListener("click", this.handleTriggerClick)
|
|
174
176
|
this.panel?.removeEventListener("toggle", this.handlePanelToggle)
|
|
175
177
|
// The reference must be reset, or `sync()` sees that the button is the
|
|
@@ -269,16 +271,15 @@ export class LtPopover extends HostElement {
|
|
|
269
271
|
/*
|
|
270
272
|
* Two different faults, and a message for each, so the developer looks in
|
|
271
273
|
* the right place. An empty element is a region the server has not filled
|
|
272
|
-
* yet, and that is not a fault in the markup
|
|
274
|
+
* yet, and that is not a fault in the markup: `silentWhenEmpty`.
|
|
273
275
|
*/
|
|
274
|
-
const isEmpty = () => this.childElementCount === 0
|
|
275
|
-
|
|
276
276
|
if (!panel) {
|
|
277
277
|
warnAboutMarkup(
|
|
278
278
|
this,
|
|
279
279
|
"found no panel: an element with popover or the class " +
|
|
280
280
|
"lt-popover. Without one there is nothing to open or position.",
|
|
281
|
-
() =>
|
|
281
|
+
() => this.findPanel() === null,
|
|
282
|
+
{ silentWhenEmpty: true },
|
|
282
283
|
)
|
|
283
284
|
} else {
|
|
284
285
|
warnAboutMarkup(
|
|
@@ -288,8 +289,9 @@ export class LtPopover extends HostElement {
|
|
|
288
289
|
"component does not know what opens the popover.",
|
|
289
290
|
() => {
|
|
290
291
|
const found = this.findPanel()
|
|
291
|
-
return
|
|
292
|
+
return found !== null && !this.findTrigger(found)
|
|
292
293
|
},
|
|
294
|
+
{ silentWhenEmpty: true },
|
|
293
295
|
)
|
|
294
296
|
}
|
|
295
297
|
|