@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +160 -2
  2. package/README.md +4 -3
  3. package/agent/astro.md +1 -1
  4. package/agent/bundles.md +4 -2
  5. package/agent/datastar.md +48 -48
  6. package/agent/html.md +51 -51
  7. package/agent/react.md +6 -5
  8. package/agent/templates.md +49 -49
  9. package/core/lotek-core.wasm +0 -0
  10. package/core/lotek.wasm +0 -0
  11. package/dist/components/default-texts.d.ts +6 -0
  12. package/dist/components/default-texts.d.ts.map +1 -1
  13. package/dist/components/default-texts.js +6 -0
  14. package/dist/components/default-texts.js.map +1 -1
  15. package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -1
  16. package/dist/components/frame/dialog/lt-dialog.js +5 -2
  17. package/dist/components/frame/dialog/lt-dialog.js.map +1 -1
  18. package/dist/components/frame/error-summary/error-summary.d.ts +15 -2
  19. package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -1
  20. package/dist/components/frame/error-summary/error-summary.js +33 -11
  21. package/dist/components/frame/error-summary/error-summary.js.map +1 -1
  22. package/dist/components/frame/error-summary/lt-error-summary.d.ts +11 -0
  23. package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -1
  24. package/dist/components/frame/error-summary/lt-error-summary.js +114 -8
  25. package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -1
  26. package/dist/components/frame/field/lt-field.d.ts.map +1 -1
  27. package/dist/components/frame/field/lt-field.js +34 -14
  28. package/dist/components/frame/field/lt-field.js.map +1 -1
  29. package/dist/components/frame/popover/lt-popover.d.ts +6 -2
  30. package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
  31. package/dist/components/frame/popover/lt-popover.js +44 -14
  32. package/dist/components/frame/popover/lt-popover.js.map +1 -1
  33. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -1
  34. package/dist/components/frame/session-timeout/lt-session-timeout.js +43 -6
  35. package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -1
  36. package/dist/components/frame/suggestion/lt-suggestion.d.ts +8 -2
  37. package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -1
  38. package/dist/components/frame/suggestion/lt-suggestion.js +56 -37
  39. package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -1
  40. package/dist/components/frame/tabs/lt-tabs.d.ts +32 -10
  41. package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -1
  42. package/dist/components/frame/tabs/lt-tabs.js +85 -21
  43. package/dist/components/frame/tabs/lt-tabs.js.map +1 -1
  44. package/dist/components/host-element.d.ts +52 -0
  45. package/dist/components/host-element.d.ts.map +1 -1
  46. package/dist/components/host-element.js +164 -1
  47. package/dist/components/host-element.js.map +1 -1
  48. package/dist/components/standalone/connection-status/connection-status.d.ts +5 -1
  49. package/dist/components/standalone/connection-status/connection-status.d.ts.map +1 -1
  50. package/dist/components/standalone/connection-status/connection-status.js +5 -1
  51. package/dist/components/standalone/connection-status/connection-status.js.map +1 -1
  52. package/dist/components/standalone/connection-status/lt-connection-status.d.ts +11 -6
  53. package/dist/components/standalone/connection-status/lt-connection-status.d.ts.map +1 -1
  54. package/dist/components/standalone/connection-status/lt-connection-status.js +34 -10
  55. package/dist/components/standalone/connection-status/lt-connection-status.js.map +1 -1
  56. package/dist/diagnostics/core.d.ts +34 -0
  57. package/dist/diagnostics/core.d.ts.map +1 -1
  58. package/dist/diagnostics/core.js +14 -2
  59. package/dist/diagnostics/core.js.map +1 -1
  60. package/dist/lotek.css +1 -1
  61. package/dist/lt.d.ts +1 -1
  62. package/dist/react.d.ts +2 -0
  63. package/dist/react.d.ts.map +1 -1
  64. package/dist/vocabulary/classes.d.ts.map +1 -1
  65. package/dist/vocabulary/classes.js +34 -0
  66. package/dist/vocabulary/classes.js.map +1 -1
  67. package/dist/vocabulary/elements.d.ts.map +1 -1
  68. package/dist/vocabulary/elements.js +87 -7
  69. package/dist/vocabulary/elements.js.map +1 -1
  70. package/dist/vocabulary/types.d.ts +60 -0
  71. package/dist/vocabulary/types.d.ts.map +1 -1
  72. package/live-templates.xml +3 -2
  73. package/manifest/manifest.json +172 -8
  74. package/manifest/manifest.schema.json +80 -0
  75. package/package.json +2 -1
  76. package/src/components/default-texts.ts +7 -0
  77. package/src/components/frame/dialog/lt-dialog.ts +5 -2
  78. package/src/components/frame/error-summary/error-summary.ts +45 -11
  79. package/src/components/frame/error-summary/lt-error-summary.ts +121 -7
  80. package/src/components/frame/field/lt-field.ts +65 -28
  81. package/src/components/frame/popover/lt-popover.ts +43 -12
  82. package/src/components/frame/session-timeout/lt-session-timeout.ts +40 -6
  83. package/src/components/frame/suggestion/lt-suggestion.ts +57 -36
  84. package/src/components/frame/tabs/lt-tabs.ts +114 -38
  85. package/src/components/host-element.ts +190 -1
  86. package/src/components/standalone/connection-status/connection-status.ts +7 -1
  87. package/src/components/standalone/connection-status/lt-connection-status.ts +36 -10
  88. package/src/diagnostics/core.ts +19 -2
  89. package/src/vocabulary/classes.ts +34 -0
  90. package/src/vocabulary/elements.ts +87 -7
  91. package/src/vocabulary/types.ts +56 -0
  92. package/web-types.json +1 -1
@@ -7,11 +7,11 @@ import {
7
7
  HostElement,
8
8
  isServerControlled,
9
9
  languageOf,
10
+ rememberedId,
10
11
  SERVER_CONTROLLED,
11
12
  setAttr,
12
13
  setFlag,
13
14
  setText,
14
- uniqueId,
15
15
  warnAboutMarkup,
16
16
  } from "../../host-element.js"
17
17
  import {
@@ -53,6 +53,8 @@ export const LT_SUGGESTION_PARTS = {
53
53
  status: "[role='status']",
54
54
  /** The hidden field that receives the code of the option the user chose. */
55
55
  value: `input.${SUGGESTION_VALUE_CLASS}`,
56
+ /** The label, which gets `for` from the field's id. */
57
+ label: "label",
56
58
  } as const
57
59
 
58
60
  const CONTROL_SELECTOR = LT_SUGGESTION_PARTS.control
@@ -270,11 +272,23 @@ export class LtSuggestion extends HostElement {
270
272
  * callback also fires during the upgrade, before the children exist.
271
273
  */
272
274
  if (name === "prefiltered" && this.observer) {
273
- // The count present when the attribute is set is the starting point,
274
- // and nothing to announce. Without this the first change afterwards
275
- // was taken as the starting point, and was lost.
276
- this.announcedCount = this.prefiltered ? this.visible.length : undefined
277
- this.filter()
275
+ /*
276
+ * After the whole update, as the other repairs: a morph sets the
277
+ * attribute before it has replaced the options, and the count was
278
+ * the old list's. And through `repair()`, which filters only a list
279
+ * that stays open: the filter announces, and a closed list said "One
280
+ * match" to nobody's search.
281
+ */
282
+ queueMicrotask(() => {
283
+ if (!this.observer) return
284
+ // The count present when the attribute is set is the starting point,
285
+ // and nothing to announce. Without this the first change afterwards
286
+ // was taken as the starting point, and was lost.
287
+ this.announcedCount = this.prefiltered ? this.visible.length : undefined
288
+ // The whole repair, not the filter alone: the same update can lock
289
+ // the field, and `repair()` closes the list before it filters.
290
+ this.sync()
291
+ })
278
292
  }
279
293
  }
280
294
 
@@ -388,7 +402,7 @@ export class LtSuggestion extends HostElement {
388
402
  setAttr(
389
403
  labelElement,
390
404
  "id",
391
- this.rememberedId(labelElement, "lt-suggestion-label"),
405
+ rememberedId(this.ids, labelElement, "lt-suggestion-label"),
392
406
  )
393
407
  }
394
408
  if (label !== null) setAttr(list, "aria-label", null)
@@ -399,17 +413,6 @@ export class LtSuggestion extends HostElement {
399
413
  })
400
414
  }
401
415
 
402
- /** The id an element should have: the one it has, otherwise the one the web component created last. */
403
- private rememberedId(element: Element, prefix: string): string {
404
- if (element.id) return element.id
405
- let id = this.ids.get(element)
406
- if (!id) {
407
- id = uniqueId(prefix)
408
- this.ids.set(element, id)
409
- }
410
- return id
411
- }
412
-
413
416
  /**
414
417
  * Fills in the wiring where the markup arrived without it. Only what is
415
418
  * missing.
@@ -430,7 +433,7 @@ export class LtSuggestion extends HostElement {
430
433
  if (!control.hasAttribute("aria-autocomplete")) {
431
434
  setAttr(control, "aria-autocomplete", "list")
432
435
  }
433
- const label = this.querySelector("label")
436
+ const label = this.querySelector(LT_SUGGESTION_PARTS.label)
434
437
  const root = this.getRootNode() as Document | ShadowRoot
435
438
 
436
439
  if (label) {
@@ -547,7 +550,7 @@ export class LtSuggestion extends HostElement {
547
550
  const wanted = control.getAttribute("aria-controls")
548
551
  const free = wanted && !root.getElementById?.(wanted)
549
552
  if (free) this.ids.set(list, wanted)
550
- setAttr(list, "id", this.rememberedId(list, "lt-suggestion-list"))
553
+ setAttr(list, "id", rememberedId(this.ids, list, "lt-suggestion-list"))
551
554
  }
552
555
  if (!control.hasAttribute("aria-controls")) {
553
556
  setAttr(control, "aria-controls", list.id)
@@ -567,7 +570,11 @@ export class LtSuggestion extends HostElement {
567
570
  this.roledByMe.add(option)
568
571
  }
569
572
  if (!option.id) {
570
- setAttr(option, "id", this.rememberedId(option, "lt-suggestion-option"))
573
+ setAttr(
574
+ option,
575
+ "id",
576
+ rememberedId(this.ids, option, "lt-suggestion-option"),
577
+ )
571
578
  }
572
579
  // From the start, not only when the list closes: otherwise the options
573
580
  // the server sent were without it until then, while the rest of the list
@@ -600,6 +607,12 @@ export class LtSuggestion extends HostElement {
600
607
  this.writeCode(valueField, this.chosen.value)
601
608
  }
602
609
 
610
+ // A field that became `readonly` or `disabled` while the list was open
611
+ // closes it. Otherwise the repair kept it open, and Escape does nothing in
612
+ // a locked field. First, so nothing below writes for a list that closes.
613
+ if (!isServerControlled(this) && this.locked && this.wantOpen)
614
+ this.setOpen(false)
615
+
603
616
  // An emptied status element gets its text back while the list is open,
604
617
  // where the count is what the user is working on. Not otherwise: every
605
618
  // patch of the region would announce the old count again. And not when
@@ -616,12 +629,17 @@ export class LtSuggestion extends HostElement {
616
629
  setText(status, this.announcedText)
617
630
  }
618
631
 
619
- if (isServerControlled(this)) return
620
-
621
- // A field that became `readonly` or `disabled` while the list was open
622
- // closes it. Otherwise the repair kept it open, and Escape does nothing in
623
- // a locked field.
624
- if (this.locked && this.wantOpen) this.setOpen(false)
632
+ if (isServerControlled(this)) {
633
+ /*
634
+ * The server decides whether the list is open and what is
635
+ * highlighted, not what matches: an update that changes an open list
636
+ * is filtered here too. Before, the list showed every option the
637
+ * server sent, whatever the user had typed, and an emptied list kept
638
+ * the empty message hidden.
639
+ */
640
+ if (this.listElement?.hidden === false) this.filter()
641
+ return
642
+ }
625
643
 
626
644
  if (this.wantOpen !== undefined) {
627
645
  /*
@@ -1276,8 +1294,17 @@ export class LtSuggestion extends HostElement {
1276
1294
 
1277
1295
  private handleFocus = (): void => {
1278
1296
  if (this.locked) return
1279
- this.filter()
1297
+ this.openFiltered()
1298
+ }
1299
+
1300
+ /**
1301
+ * Opens the list, and filters and announces only when it opens. Filtering
1302
+ * first announced "No matches" for a list that `setOpen` then kept closed,
1303
+ * since there was nothing to show.
1304
+ */
1305
+ private openFiltered(): void {
1280
1306
  this.setOpen(true)
1307
+ if (this.wantOpen) this.filter()
1281
1308
  }
1282
1309
 
1283
1310
  private handleFocusOut = (event: FocusEvent): void => {
@@ -1297,10 +1324,7 @@ export class LtSuggestion extends HostElement {
1297
1324
 
1298
1325
  if (event.key === "ArrowDown") {
1299
1326
  event.preventDefault()
1300
- if (!open) {
1301
- this.filter()
1302
- this.setOpen(true)
1303
- }
1327
+ if (!open) this.openFiltered()
1304
1328
  this.markActive(this.activeIndex + 1)
1305
1329
  } else if (event.key === "ArrowUp") {
1306
1330
  event.preventDefault()
@@ -1308,10 +1332,7 @@ export class LtSuggestion extends HostElement {
1308
1332
  // `aria-activedescendant` pointed to an option in a list the field at the
1309
1333
  // same time reported as closed, and the screen reader announced something
1310
1334
  // that was not on screen.
1311
- if (!open) {
1312
- this.filter()
1313
- this.setOpen(true)
1314
- }
1335
+ if (!open) this.openFiltered()
1315
1336
  // If nothing is highlighted, arrow up goes to the last one.
1316
1337
  // `activeIndex - 1` would have given the second to last, since nothing
1317
1338
  // highlighted is -1.
@@ -3,10 +3,14 @@ import {
3
3
  derivedParts,
4
4
  HostElement,
5
5
  isServerControlled,
6
+ markWritten,
6
7
  SERVER_CONTROLLED,
7
8
  setAttr,
8
9
  setFlag,
10
+ stillMarked,
9
11
  uniqueId,
12
+ WRITTEN,
13
+ warnAboutCopiedMark,
10
14
  warnAboutMarkup,
11
15
  } from "../../host-element.js"
12
16
  export const LT_TABS_TAG = "lt-tabs" as const
@@ -168,6 +172,18 @@ function nameAsReferenced(element: Element): string {
168
172
  */
169
173
  type Word = { written: string | null; server: string | null }
170
174
 
175
+ /**
176
+ * Whether a morph from the server has been there since the web component
177
+ * wrote the attribute: the mark it wrote is gone. Only a value that differed
178
+ * from the server's word was marked (`markWritten`); one that did not has
179
+ * no mark to lose, and its absence says nothing. Read as a sign of the
180
+ * server, every unmarked tab in a row of three released a click under
181
+ * `server-controlled` at once.
182
+ */
183
+ function markGone(element: Element, name: string, word: Word): boolean {
184
+ return word.written !== word.server && !stillMarked(element, name)
185
+ }
186
+
171
187
  /**
172
188
  * Keyboard behaviour in a tab list, and the wiring when the markup comes
173
189
  * without it.
@@ -239,22 +255,34 @@ export class LtTabs extends HostElement {
239
255
 
240
256
  private observer?: MutationObserver
241
257
  /**
242
- * The tab the user selected, remembered so a patch cannot take it.
258
+ * The panel the user's tab points to, remembered so a patch cannot take
259
+ * the selection.
243
260
  *
244
- * The node itself, not the index and not `href`. An index is not an
245
- * identity: if the server inserts a tab first in the list, the same index
246
- * points to something else, and the selection jumps to a tab the user never
247
- * pressed.
261
+ * Not the index and not the node. An index is not an identity: if the
262
+ * server inserts a tab first in the list, the same index points to
263
+ * something else. The node is not one either: the links have no id, so
264
+ * Datastar and idiomorph match them by position, and the node the user
265
+ * pressed became the inserted tab. The panel's id is the server's and
266
+ * stays; a tab pointing to another panel is another tab.
248
267
  *
249
- * A morph keeps the links, since they have no id. If the patch replaces the
250
- * link itself, as `innerHTML` does, the tab is a different one, and then
251
- * the web component forgets the selection rather than guessing; the address
252
- * still has it, see `selectFromAddress`.
268
+ * Remembered with the link, `chosenTab`.
253
269
  *
254
270
  * `undefined` means the user has not selected anything yet, and then the
255
271
  * server's markup applies.
256
272
  */
273
+ private chosenTarget?: string
274
+ /**
275
+ * The link the user pressed, with `chosenTarget`. While it is in the tab
276
+ * list and points to the same panel, it is the user's tab, also when
277
+ * another tab points to that panel too. When a morph has turned it into
278
+ * another tab, or dropped it while it kept other links, the tab that points
279
+ * to the panel is found. When the links were replaced, none of the row is
280
+ * left, as `innerHTML` does, and the selection is forgotten rather than
281
+ * guessed; the address still has it, see `selectFromAddress`.
282
+ */
257
283
  private chosenTab?: HTMLElement
284
+ /** The tab list as it was when the user's tab was last found. */
285
+ private chosenRow: HTMLElement[] = []
258
286
  /**
259
287
  * Roles the web component set itself. Tells a bare tab list, where every
260
288
  * link is a tab, from one where the server wrote the roles, where a link
@@ -287,7 +315,7 @@ export class LtTabs extends HostElement {
287
315
  * The tab the user clicked under `server-controlled`, until the server
288
316
  * answers.
289
317
  *
290
- * It is not the user's selection in the sense of `chosenTab`: it is never
318
+ * It is not the user's selection in the sense of `chosenTarget`: it is never
291
319
  * restored against the server's word. It only lasts until someone else has
292
320
  * touched `aria-selected` or `hidden`, added a tab or a panel, removed the
293
321
  * tab, or removed `server-controlled`. Without it, `repair()` read the
@@ -360,6 +388,9 @@ export class LtTabs extends HostElement {
360
388
  // disables the selected tab, the tab stop moves to one that can take
361
389
  // focus. `alt`, `aria-hidden` and `title` change the name of a tab.
362
390
  attributeFilter: [
391
+ // A morph from the server removes the mark, and the web component
392
+ // puts it back: the same markup must give the same page.
393
+ WRITTEN,
363
394
  "aria-selected",
364
395
  "tabindex",
365
396
  "hidden",
@@ -385,7 +416,7 @@ export class LtTabs extends HostElement {
385
416
  // `server-controlled` switched on midway: the web component lets go, and
386
417
  // the next patch decides.
387
418
  if (name === SERVER_CONTROLLED && isServerControlled(this)) {
388
- this.chosenTab = undefined
419
+ this.choose(undefined)
389
420
  // What is there now is the starting point. Without this the tab list
390
421
  // jumped back to the tab the server last named, before the user chose
391
422
  // another. But as when the server answers a click: `aria-selected` the
@@ -426,28 +457,77 @@ export class LtTabs extends HostElement {
426
457
  ).filter((tab) => linkTarget(tab) !== null)
427
458
  }
428
459
 
460
+ /** Remembers the user's tab: the link, and the panel it points to. */
461
+ private choose(tab: HTMLElement | undefined): void {
462
+ this.chosenTab = tab
463
+ this.chosenTarget = tab ? (linkTarget(tab) ?? undefined) : undefined
464
+ this.chosenRow = tab ? this.tabs : []
465
+ }
466
+
467
+ /**
468
+ * Where the user's tab is now, or -1: the link while it points to the same
469
+ * panel; the tab that points to that panel when a morph has turned the
470
+ * link into another tab, or dropped it and kept others (the morphers match
471
+ * links without ids by position, and look ahead when one is removed);
472
+ * none when the links were replaced, with nothing of the row left.
473
+ */
474
+ private chosenIndex(): number {
475
+ const tabs = this.tabs
476
+ const link = this.chosenTab
477
+ if (!link || this.chosenTarget === undefined) return -1
478
+ const at = tabs.indexOf(link)
479
+ if (at >= 0 && linkTarget(link) === this.chosenTarget) {
480
+ this.chosenRow = tabs
481
+ return at
482
+ }
483
+ const morphed = at >= 0 || this.chosenRow.some((tab) => tabs.includes(tab))
484
+ if (!morphed) return -1
485
+ const moved = tabs.findIndex((tab) => linkTarget(tab) === this.chosenTarget)
486
+ if (moved >= 0) {
487
+ this.chosenTab = tabs[moved]
488
+ this.chosenRow = tabs
489
+ }
490
+ return moved
491
+ }
492
+
429
493
  /** The server's word about an attribute, against what the web component last wrote. */
430
494
  private serverWord(
431
495
  memory: WeakMap<Element, Word>,
432
496
  element: Element,
433
- now: string | null,
497
+ name: string,
434
498
  ): string | null {
499
+ const now = element.getAttribute(name)
435
500
  const known = memory.get(element)
436
- if (!known) return now
437
- return now !== known.written ? now : known.server
501
+ if (!known) {
502
+ warnAboutCopiedMark(this, element)
503
+ return now
504
+ }
505
+ // The server's when the value is another, or when a morph from the
506
+ // server has been there since the web component wrote it (`WRITTEN`).
507
+ return now !== known.written || !stillMarked(element, name)
508
+ ? now
509
+ : known.server
438
510
  }
439
511
 
440
512
  /** Is there something on a tab or a panel other than what the web component wrote? */
441
513
  private touchedByOthers(): boolean {
442
514
  return this.tabs.some((tab) => {
443
515
  const selected = this.selectedMemory.get(tab)
444
- if (!selected || tab.getAttribute("aria-selected") !== selected.written) {
516
+ if (
517
+ !selected ||
518
+ tab.getAttribute("aria-selected") !== selected.written ||
519
+ markGone(tab, "aria-selected", selected)
520
+ ) {
445
521
  return true
446
522
  }
447
523
  const panel = this.panelFor(tab)
448
524
  if (!panel) return false
449
525
  const hidden = this.hiddenMemory.get(panel)
450
- return !hidden || panel.getAttribute("hidden") !== hidden.written
526
+ return (
527
+ !hidden ||
528
+ panel.getAttribute("hidden") !== hidden.written ||
529
+ markGone(panel, "hidden", hidden)
530
+ )
451
531
  })
452
532
  }
453
533
 
@@ -476,7 +556,9 @@ export class LtTabs extends HostElement {
476
556
  // A new, bare tab has said nothing. Had it counted, the click's
477
557
  // `aria-selected` was read as the server's, and a later move with
478
558
  // `hidden` alone was undone.
479
- return known ? now !== known.written : now !== null
559
+ return known
560
+ ? now !== known.written || markGone(tab, "aria-selected", known)
561
+ : now !== null
480
562
  })
481
563
  this.hiddenMemory = new WeakMap()
482
564
  if (serverSpoke) {
@@ -484,28 +566,19 @@ export class LtTabs extends HostElement {
484
566
  return
485
567
  }
486
568
  for (const tab of tabs) {
487
- this.selectedMemory.set(tab, {
488
- written: tab.getAttribute("aria-selected"),
489
- server: null,
490
- })
569
+ const written = tab.getAttribute("aria-selected")
570
+ this.selectedMemory.set(tab, { written, server: null })
571
+ markWritten(tab, "aria-selected", written !== null)
491
572
  }
492
573
  }
493
574
 
494
575
  private serverSelected(tab: Element): string | null {
495
- return this.serverWord(
496
- this.selectedMemory,
497
- tab,
498
- tab.getAttribute("aria-selected"),
499
- )
576
+ return this.serverWord(this.selectedMemory, tab, "aria-selected")
500
577
  }
501
578
 
502
579
  /** The server's word about `hidden` on the panel: the form, or `null` for visible. */
503
580
  private serverHidden(panel: Element): string | null {
504
- return this.serverWord(
505
- this.hiddenMemory,
506
- panel,
507
- panel.getAttribute("hidden"),
508
- )
581
+ return this.serverWord(this.hiddenMemory, panel, "hidden")
509
582
  }
510
583
 
511
584
  /**
@@ -879,7 +952,7 @@ export class LtTabs extends HostElement {
879
952
  this.releasePending()
880
953
  }
881
954
 
882
- if (!this.chosenTab || isServerControlled(this)) {
955
+ if (this.chosenTarget === undefined || isServerControlled(this)) {
883
956
  /*
884
957
  * No user selection to restore, so the server's word decides, and it
885
958
  * is written out in full: `aria-selected`, the tab stop and `hidden` in
@@ -898,7 +971,8 @@ export class LtTabs extends HostElement {
898
971
  return
899
972
  }
900
973
 
901
- const index = this.tabs.indexOf(this.chosenTab)
974
+ const chosen = this.chosenTarget
975
+ const index = this.chosenIndex()
902
976
  if (index < 0) {
903
977
  /*
904
978
  * The tab no longer exists. The server has sent something else, and
@@ -917,8 +991,8 @@ export class LtTabs extends HostElement {
917
991
  * loads the panel's content or writes the selection to the address
918
992
  * would never learn that it moved.
919
993
  */
920
- const lostPanel = this.panelFor(this.chosenTab)
921
- this.chosenTab = undefined
994
+ const lostPanel = chosen === undefined ? null : this.byFragment(chosen)
995
+ this.choose(undefined)
922
996
  // The server marked a tab itself. Then that applies, and the panels and
923
997
  // the tab stop are written to match it, even when nothing else gave the
924
998
  // observer another round.
@@ -938,10 +1012,10 @@ export class LtTabs extends HostElement {
938
1012
  const tab = this.tabs[index]
939
1013
  this.apply(index)
940
1014
  if (this.panelFor(tab) === lostPanel) {
941
- this.chosenTab = tab
1015
+ this.choose(tab)
942
1016
  return
943
1017
  }
944
- if (fromAddress >= 0) this.chosenTab = tab
1018
+ if (fromAddress >= 0) this.choose(tab)
945
1019
  this.showInAddress(index)
946
1020
  this.notify(index)
947
1021
  }
@@ -1225,7 +1299,7 @@ export class LtTabs extends HostElement {
1225
1299
  // No memory when the server decides the selection. Without this a
1226
1300
  // selection made while the attribute was there would be restored the
1227
1301
  // moment someone removed it again.
1228
- if (!isServerControlled(this)) this.chosenTab = tabs[index]
1302
+ if (!isServerControlled(this)) this.choose(tabs[index])
1229
1303
  this.apply(index)
1230
1304
  if (isServerControlled(this)) this.pendingTab = tabs[index]
1231
1305
  this.showInAddress(index)
@@ -1275,6 +1349,7 @@ export class LtTabs extends HostElement {
1275
1349
  const server = this.serverSelected(tab)
1276
1350
  setAttr(tab, "aria-selected", String(chosen))
1277
1351
  this.selectedMemory.set(tab, { written: String(chosen), server })
1352
+ markWritten(tab, "aria-selected", String(chosen) !== server)
1278
1353
  setAttr(tab, "tabindex", i === stop ? "0" : "-1")
1279
1354
 
1280
1355
  const panel = this.panelFor(tab)
@@ -1299,6 +1374,7 @@ export class LtTabs extends HostElement {
1299
1374
  if (written === null) setFlag(panel, "hidden", false)
1300
1375
  else setAttr(panel, "hidden", written)
1301
1376
  this.hiddenMemory.set(panel, { written, server })
1377
+ markWritten(panel, "hidden", written !== server)
1302
1378
  }
1303
1379
  /*
1304
1380
  * Only the selected panel is a tab stop, so Tab from the tab list goes