@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
|
@@ -265,6 +265,295 @@ export function stillMarked(element: Element, name: string): boolean {
|
|
|
265
265
|
return (element.getAttribute(WRITTEN) ?? "").split(/\s+/).includes(name)
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* What a web component wrote on a node, and what the server had said there.
|
|
270
|
+
*
|
|
271
|
+
* Some attributes are both the server's and the component's, and the
|
|
272
|
+
* component must read the server's word back without taking its own echo
|
|
273
|
+
* for it. The question was answered in seven ways across the components,
|
|
274
|
+
* and the model tests found bugs in five classes there. This is the rule
|
|
275
|
+
* for them to share, and field uses it: per node and attribute, the value
|
|
276
|
+
* the component wrote and the server's word it wrote it against. A
|
|
277
|
+
* `WeakMap`, so the memory survives the host being moved, and a node that is
|
|
278
|
+
* gone for good takes its memory with it.
|
|
279
|
+
*/
|
|
280
|
+
export interface Ownership {
|
|
281
|
+
/** Whether the component has written `name` on `element` before. */
|
|
282
|
+
knows(element: Element, name: string): boolean
|
|
283
|
+
/**
|
|
284
|
+
* The server's word for `name` on `element`: what stands there when the
|
|
285
|
+
* component has never written it, or when it differs from what the
|
|
286
|
+
* component wrote, or when a morph has removed the mark since; otherwise
|
|
287
|
+
* the server's word the component wrote against. With `presence` the
|
|
288
|
+
* attribute is compared by whether it is there, so `data-optional="true"`
|
|
289
|
+
* and `data-optional` are the same; a `flag` is compared so too.
|
|
290
|
+
*/
|
|
291
|
+
serverWord(
|
|
292
|
+
element: Element,
|
|
293
|
+
name: string,
|
|
294
|
+
options?: { flag?: boolean; presence?: boolean },
|
|
295
|
+
): string | null
|
|
296
|
+
/**
|
|
297
|
+
* Writes `value` for `name` on `element`, through `setAttr` or, for a
|
|
298
|
+
* `flag`, `setFlag`, and notes it against the server's word, compared by
|
|
299
|
+
* presence for a `flag` or with `presence`. With `mark`,
|
|
300
|
+
* the attribute is marked as the component's when the value differs from
|
|
301
|
+
* the server's, so a morph that removes the mark is seen as the server
|
|
302
|
+
* speaking.
|
|
303
|
+
*/
|
|
304
|
+
write(
|
|
305
|
+
element: Element,
|
|
306
|
+
name: string,
|
|
307
|
+
value: string | null,
|
|
308
|
+
options?: {
|
|
309
|
+
/**
|
|
310
|
+
* The server's word the value was written against. Left out for an
|
|
311
|
+
* attribute the component never reads back as the server's, such as
|
|
312
|
+
* `for` written from the control's id: then nothing is remembered as
|
|
313
|
+
* the server's, and a later read sees what stands there.
|
|
314
|
+
*/
|
|
315
|
+
server?: string | null
|
|
316
|
+
mark?: boolean
|
|
317
|
+
flag?: boolean
|
|
318
|
+
presence?: boolean
|
|
319
|
+
},
|
|
320
|
+
): void
|
|
321
|
+
/** What the component last wrote for `name` on `element`, or `undefined`. */
|
|
322
|
+
written(element: Element, name: string): string | null | undefined
|
|
323
|
+
/**
|
|
324
|
+
* Forgets what the component wrote for `name` on `element`: the server has
|
|
325
|
+
* taken the attribute over, and the same value later is the server's. The
|
|
326
|
+
* mark goes too, so the markup does not say a value the server owns is
|
|
327
|
+
* the component's.
|
|
328
|
+
*/
|
|
329
|
+
forget(element: Element, name: string): void
|
|
330
|
+
/**
|
|
331
|
+
* Removes `name` from `element` and forgets it in the same step, for an
|
|
332
|
+
* attribute the component wrote and no longer wants. Removing without
|
|
333
|
+
* forgetting left the component believing the attribute was its own, and
|
|
334
|
+
* it could later remove one the server had written.
|
|
335
|
+
*/
|
|
336
|
+
release(element: Element, name: string, options?: { flag?: boolean }): void
|
|
337
|
+
/**
|
|
338
|
+
* Whether a morph from the server has been there since the component
|
|
339
|
+
* wrote `name` on `element`: it marked a value that differed from the
|
|
340
|
+
* server's word, and the mark is gone. A value that did not differ had no
|
|
341
|
+
* mark to lose, and its absence says nothing.
|
|
342
|
+
*/
|
|
343
|
+
lost(element: Element, name: string): boolean
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function ownership(): Ownership {
|
|
347
|
+
const memory = new WeakMap<
|
|
348
|
+
Element,
|
|
349
|
+
Map<
|
|
350
|
+
string,
|
|
351
|
+
{
|
|
352
|
+
written: string | null
|
|
353
|
+
/** `undefined` when the write gave no server's word. */
|
|
354
|
+
server: string | null | undefined
|
|
355
|
+
marked: boolean
|
|
356
|
+
/** Compared by whether it is there, as the write said. */
|
|
357
|
+
presence: boolean
|
|
358
|
+
}
|
|
359
|
+
>
|
|
360
|
+
>()
|
|
361
|
+
// By presence, a value is "" when the attribute is there and null when not.
|
|
362
|
+
const byPresence = (value: string | null) => (value === null ? null : "")
|
|
363
|
+
const read = (element: Element, name: string, presence: boolean) =>
|
|
364
|
+
presence
|
|
365
|
+
? byPresence(element.getAttribute(name))
|
|
366
|
+
: element.getAttribute(name)
|
|
367
|
+
return {
|
|
368
|
+
knows: (element, name) => Boolean(memory.get(element)?.has(name)),
|
|
369
|
+
serverWord(element, name, { flag, presence } = {}) {
|
|
370
|
+
const known = memory.get(element)?.get(name)
|
|
371
|
+
// The comparison the write used, so a read that forgets the option
|
|
372
|
+
// still knows the component's own echo.
|
|
373
|
+
const now = read(
|
|
374
|
+
element,
|
|
375
|
+
name,
|
|
376
|
+
known?.presence ?? Boolean(flag || presence),
|
|
377
|
+
)
|
|
378
|
+
if (!known || known.server === undefined) return now
|
|
379
|
+
// The server's when the value is another, or, for an attribute written
|
|
380
|
+
// with the mark, when a morph from the server has been there since
|
|
381
|
+
// (`WRITTEN`). Without the mark, only a value that differs from the
|
|
382
|
+
// component's own says the server spoke.
|
|
383
|
+
return now !== known.written ||
|
|
384
|
+
(known.marked && !stillMarked(element, name))
|
|
385
|
+
? now
|
|
386
|
+
: known.server
|
|
387
|
+
},
|
|
388
|
+
write(element, name, value, { server, mark, flag, presence } = {}) {
|
|
389
|
+
if (flag) setFlag(element, name, value !== null)
|
|
390
|
+
else setAttr(element, name, value)
|
|
391
|
+
const compared = Boolean(flag || presence)
|
|
392
|
+
// Both sides in the same form, so `disabled="disabled"` from the
|
|
393
|
+
// server and the component's `disabled` are the same word.
|
|
394
|
+
const written = compared ? byPresence(value) : value
|
|
395
|
+
const said =
|
|
396
|
+
server === undefined
|
|
397
|
+
? undefined
|
|
398
|
+
: compared
|
|
399
|
+
? byPresence(server)
|
|
400
|
+
: server
|
|
401
|
+
// The mark says the value differs from the server's word, so without
|
|
402
|
+
// one there is nothing to mark against.
|
|
403
|
+
if (mark && said !== undefined)
|
|
404
|
+
markWritten(element, name, written !== said)
|
|
405
|
+
let attributes = memory.get(element)
|
|
406
|
+
if (!attributes) {
|
|
407
|
+
attributes = new Map()
|
|
408
|
+
memory.set(element, attributes)
|
|
409
|
+
}
|
|
410
|
+
attributes.set(name, {
|
|
411
|
+
written,
|
|
412
|
+
server: said,
|
|
413
|
+
marked: Boolean(mark),
|
|
414
|
+
presence: compared,
|
|
415
|
+
})
|
|
416
|
+
},
|
|
417
|
+
written: (element, name) => memory.get(element)?.get(name)?.written,
|
|
418
|
+
forget(element, name) {
|
|
419
|
+
if (memory.get(element)?.get(name)?.marked)
|
|
420
|
+
markWritten(element, name, false)
|
|
421
|
+
memory.get(element)?.delete(name)
|
|
422
|
+
},
|
|
423
|
+
release(element, name, { flag } = {}) {
|
|
424
|
+
if (flag) setFlag(element, name, false)
|
|
425
|
+
else setAttr(element, name, null)
|
|
426
|
+
if (memory.get(element)?.get(name)?.marked)
|
|
427
|
+
markWritten(element, name, false)
|
|
428
|
+
memory.get(element)?.delete(name)
|
|
429
|
+
},
|
|
430
|
+
lost(element, name) {
|
|
431
|
+
const known = memory.get(element)?.get(name)
|
|
432
|
+
return Boolean(
|
|
433
|
+
known?.marked &&
|
|
434
|
+
known.server !== undefined &&
|
|
435
|
+
known.written !== known.server &&
|
|
436
|
+
!stillMarked(element, name),
|
|
437
|
+
)
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Whether `element` has a name from `aria-labelledby` or `aria-label`. A
|
|
444
|
+
* blank one names nothing. The rule `nameFromHeading()` reads by, for the
|
|
445
|
+
* warnings about an element without a name.
|
|
446
|
+
*/
|
|
447
|
+
export function hasName(element: Element): boolean {
|
|
448
|
+
return Boolean(
|
|
449
|
+
element.getAttribute("aria-labelledby")?.trim() ||
|
|
450
|
+
element.getAttribute("aria-label")?.trim(),
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Names `element` from a heading with `aria-labelledby`, unless the server
|
|
456
|
+
* named it, and says how it is named.
|
|
457
|
+
*
|
|
458
|
+
* - `"server"`: an `aria-labelledby` the component did not write, or wrote
|
|
459
|
+
* and the server has sent since. It stays, and the component forgets its
|
|
460
|
+
* own.
|
|
461
|
+
* - `"label"`: the server gave an `aria-label`, which `aria-labelledby` would
|
|
462
|
+
* otherwise win over. The component lets go of its own.
|
|
463
|
+
* - `"heading"`: `heading()` gave an id, and `aria-labelledby` points at it,
|
|
464
|
+
* written with the mark, so a morph that sends the same id makes it the
|
|
465
|
+
* server's. It follows a heading a patch replaced.
|
|
466
|
+
* - `"none"`: no name and no heading. The component lets go of its own, and
|
|
467
|
+
* the caller says so with `warnAboutMarkup`.
|
|
468
|
+
*
|
|
469
|
+
* A blank `aria-labelledby` or `aria-label` names nothing, so it counts as
|
|
470
|
+
* missing. `heading()` is only called when the server has not named the
|
|
471
|
+
* element, so a caller that gives the heading an id does so only then.
|
|
472
|
+
* dialog, session-timeout and error-summary name themselves through it,
|
|
473
|
+
* and `host` is the web component that says so when the markup came with
|
|
474
|
+
* the mark copied in.
|
|
475
|
+
*/
|
|
476
|
+
export function nameFromHeading(
|
|
477
|
+
own: Ownership,
|
|
478
|
+
host: Element,
|
|
479
|
+
element: Element,
|
|
480
|
+
heading: () => string | null,
|
|
481
|
+
): "server" | "label" | "heading" | "none" {
|
|
482
|
+
const NAME = "aria-labelledby"
|
|
483
|
+
if (!own.knows(element, NAME)) warnAboutCopiedMark(host, element)
|
|
484
|
+
const labelledBy = element.getAttribute(NAME)?.trim() || null
|
|
485
|
+
const label = element.getAttribute("aria-label")?.trim() || null
|
|
486
|
+
const ours =
|
|
487
|
+
labelledBy !== null &&
|
|
488
|
+
own.knows(element, NAME) &&
|
|
489
|
+
own.serverWord(element, NAME) === null
|
|
490
|
+
if (labelledBy !== null && !ours) {
|
|
491
|
+
own.forget(element, NAME)
|
|
492
|
+
return "server"
|
|
493
|
+
}
|
|
494
|
+
if (label !== null) {
|
|
495
|
+
if (ours) own.release(element, NAME)
|
|
496
|
+
return "label"
|
|
497
|
+
}
|
|
498
|
+
const id = heading()
|
|
499
|
+
if (!id) {
|
|
500
|
+
if (ours) own.release(element, NAME)
|
|
501
|
+
return "none"
|
|
502
|
+
}
|
|
503
|
+
own.write(element, NAME, id, { server: null, mark: true })
|
|
504
|
+
return "heading"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A `MutationObserver` a web component starts on a node and stops again.
|
|
509
|
+
*
|
|
510
|
+
* Started again on another node, it stops watching the old one first, so a
|
|
511
|
+
* node the server has replaced does not go on calling the component. field
|
|
512
|
+
* watches through it.
|
|
513
|
+
*/
|
|
514
|
+
export function watcher(
|
|
515
|
+
callback: (records: MutationRecord[]) => void,
|
|
516
|
+
init: MutationObserverInit,
|
|
517
|
+
): {
|
|
518
|
+
start(target: Node): void
|
|
519
|
+
stop(): void
|
|
520
|
+
/** Whether it was started and not stopped since. */
|
|
521
|
+
readonly watching: boolean
|
|
522
|
+
} {
|
|
523
|
+
let observer: MutationObserver | undefined
|
|
524
|
+
return {
|
|
525
|
+
get watching() {
|
|
526
|
+
return observer !== undefined
|
|
527
|
+
},
|
|
528
|
+
start(target) {
|
|
529
|
+
observer?.disconnect()
|
|
530
|
+
observer = new MutationObserver(callback)
|
|
531
|
+
observer.observe(target, init)
|
|
532
|
+
},
|
|
533
|
+
stop() {
|
|
534
|
+
observer?.disconnect()
|
|
535
|
+
observer = undefined
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Puts `open` back on a `<dialog>` that is still in the top layer, when a
|
|
542
|
+
* morph took the attribute the server never sent, and says whether it did.
|
|
543
|
+
*
|
|
544
|
+
* The condition is `:modal` and not `open`: a dialog the user has closed
|
|
545
|
+
* leaves the top layer, and one a morph took `open` from stays there,
|
|
546
|
+
* invisible, with the rest of the page inert, and `close()` does nothing on
|
|
547
|
+
* it until the attribute is back.
|
|
548
|
+
*/
|
|
549
|
+
export function restoreModalOpen(
|
|
550
|
+
dialog: HTMLDialogElement | null | undefined,
|
|
551
|
+
): boolean {
|
|
552
|
+
if (!dialog?.matches(":modal") || dialog.open) return false
|
|
553
|
+
setAttr(dialog, "open", "")
|
|
554
|
+
return true
|
|
555
|
+
}
|
|
556
|
+
|
|
268
557
|
/**
|
|
269
558
|
* Turns a boolean attribute on or off, without writing when it is already so.
|
|
270
559
|
*
|
|
@@ -382,14 +671,16 @@ export function derivedParts<T extends Element>(
|
|
|
382
671
|
host: Element,
|
|
383
672
|
selector: string,
|
|
384
673
|
role: string,
|
|
385
|
-
|
|
674
|
+
/** The `ownership()` the component wrote the roles through. */
|
|
675
|
+
roledByHost: Pick<Ownership, "written">,
|
|
386
676
|
): T[] {
|
|
677
|
+
const mine = (part: Element) => roledByHost.written(part, "role") === role
|
|
387
678
|
const own = [...host.querySelectorAll<T>(selector)].filter(
|
|
388
679
|
(part) => part.closest(host.localName) === host,
|
|
389
680
|
)
|
|
390
681
|
const bare = own
|
|
391
682
|
.filter((part) => part.getAttribute("role") === role)
|
|
392
|
-
.every(
|
|
683
|
+
.every(mine)
|
|
393
684
|
return bare ? own : own.filter((part) => part.getAttribute("role") === role)
|
|
394
685
|
}
|
|
395
686
|
|
|
@@ -456,6 +747,16 @@ export function warnAboutMarkup(
|
|
|
456
747
|
element: Element,
|
|
457
748
|
message: string,
|
|
458
749
|
stillBroken: () => boolean,
|
|
750
|
+
{
|
|
751
|
+
silentWhenEmpty = false,
|
|
752
|
+
}: {
|
|
753
|
+
/**
|
|
754
|
+
* An element with no children is a region the server has not filled
|
|
755
|
+
* yet, which is not an error in the markup. field uses it, in place of
|
|
756
|
+
* `childElementCount > 0` in its `stillBroken`.
|
|
757
|
+
*/
|
|
758
|
+
silentWhenEmpty?: boolean
|
|
759
|
+
} = {},
|
|
459
760
|
): void {
|
|
460
761
|
if (typeof console === "undefined") return
|
|
461
762
|
if (reported.get(element)?.has(message)) return
|
|
@@ -470,7 +771,9 @@ export function warnAboutMarkup(
|
|
|
470
771
|
|
|
471
772
|
whenSettled(() => {
|
|
472
773
|
pending.delete(message)
|
|
473
|
-
if (!element.isConnected
|
|
774
|
+
if (!element.isConnected) return
|
|
775
|
+
if (silentWhenEmpty && element.childElementCount === 0) return
|
|
776
|
+
if (!stillBroken()) return
|
|
474
777
|
|
|
475
778
|
let said = reported.get(element)
|
|
476
779
|
if (!said) {
|