@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/agent/astro.md +1 -1
  3. package/agent/bundles.md +1 -1
  4. package/agent/datastar.md +57 -50
  5. package/agent/html.md +53 -53
  6. package/agent/kotlin.md +4 -4
  7. package/agent/react.md +1 -1
  8. package/agent/templates.md +50 -50
  9. package/dist/components/frame/dialog/lt-dialog.d.ts +11 -3
  10. package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -1
  11. package/dist/components/frame/dialog/lt-dialog.js +59 -46
  12. package/dist/components/frame/dialog/lt-dialog.js.map +1 -1
  13. package/dist/components/frame/error-summary/lt-error-summary.d.ts +9 -6
  14. package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -1
  15. package/dist/components/frame/error-summary/lt-error-summary.js +38 -57
  16. package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -1
  17. package/dist/components/frame/field/lt-field.d.ts +17 -26
  18. package/dist/components/frame/field/lt-field.d.ts.map +1 -1
  19. package/dist/components/frame/field/lt-field.js +81 -143
  20. package/dist/components/frame/field/lt-field.js.map +1 -1
  21. package/dist/components/frame/popover/lt-popover.d.ts +1 -1
  22. package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
  23. package/dist/components/frame/popover/lt-popover.js +31 -33
  24. package/dist/components/frame/popover/lt-popover.js.map +1 -1
  25. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +14 -2
  26. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -1
  27. package/dist/components/frame/session-timeout/lt-session-timeout.js +73 -46
  28. package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -1
  29. package/dist/components/frame/suggestion/lt-suggestion.d.ts +5 -7
  30. package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -1
  31. package/dist/components/frame/suggestion/lt-suggestion.js +57 -72
  32. package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -1
  33. package/dist/components/frame/tabs/lt-tabs.d.ts +14 -5
  34. package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -1
  35. package/dist/components/frame/tabs/lt-tabs.js +86 -106
  36. package/dist/components/frame/tabs/lt-tabs.js.map +1 -1
  37. package/dist/components/frame/term/lt-term.d.ts +5 -1
  38. package/dist/components/frame/term/lt-term.d.ts.map +1 -1
  39. package/dist/components/frame/term/lt-term.js +23 -21
  40. package/dist/components/frame/term/lt-term.js.map +1 -1
  41. package/dist/components/host-element.d.ts +137 -2
  42. package/dist/components/host-element.d.ts.map +1 -1
  43. package/dist/components/host-element.js +187 -4
  44. package/dist/components/host-element.js.map +1 -1
  45. package/dist/lotek.css +1 -1
  46. package/dist/react.d.ts +51 -437
  47. package/dist/react.d.ts.map +1 -1
  48. package/dist/react.js +48 -208
  49. package/dist/react.js.map +1 -1
  50. package/live-templates-kotlin.xml +4 -6
  51. package/manifest/manifest.json +1 -1
  52. package/package.json +7 -3
  53. package/src/components/frame/dialog/lt-dialog.ts +56 -50
  54. package/src/components/frame/error-summary/lt-error-summary.ts +33 -46
  55. package/src/components/frame/field/lt-field.ts +99 -207
  56. package/src/components/frame/popover/lt-popover.ts +37 -35
  57. package/src/components/frame/session-timeout/lt-session-timeout.ts +88 -46
  58. package/src/components/frame/suggestion/lt-suggestion.ts +59 -73
  59. package/src/components/frame/tabs/lt-tabs.ts +96 -122
  60. package/src/components/frame/term/lt-term.ts +27 -21
  61. package/src/components/host-element.ts +306 -3
  62. package/src/react.ts +125 -240
  63. package/web-types.json +1 -1
@@ -3,15 +3,15 @@ import {
3
3
  derivedParts,
4
4
  HostElement,
5
5
  isServerControlled,
6
- markWritten,
6
+ type Ownership,
7
+ ownership,
7
8
  SERVER_CONTROLLED,
8
9
  setAttr,
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
  export const LT_TABS_TAG = "lt-tabs" as const
17
17
 
@@ -158,32 +158,6 @@ function nameAsReferenced(element: Element): string {
158
158
  return flat(textOf(element, hiddenFromAT(element), true))
159
159
  }
160
160
 
161
- /**
162
- * What the web component last wrote on a node, and what the server last said.
163
- *
164
- * `aria-selected` on the tabs and `hidden` on the panels are both read and
165
- * written. A naive read is the web component's echo of itself: bare markup
166
- * got `aria-selected="true"` on the first tab from the web component, and
167
- * when the server switched tab with a patch that only touched the panels,
168
- * the web component read its earlier selection back and hid the panel the
169
- * server had just shown. If something other than what the web component
170
- * wrote is there, the server has touched it, and it is the server's word. If
171
- * the same is there, what the server said last applies.
172
- */
173
- type Word = { written: string | null; server: string | null }
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
-
187
161
  /**
188
162
  * Keyboard behaviour in a tab list, and the wiring when the markup comes
189
163
  * without it.
@@ -253,7 +227,43 @@ export class LtTabs extends HostElement {
253
227
  */
254
228
  static observedAttributes = [SERVER_CONTROLLED, "activation"] as const
255
229
 
256
- private observer?: MutationObserver
230
+ private readonly watch = watcher(
231
+ (records) => {
232
+ if (this.concerns(records)) this.sync()
233
+ },
234
+ {
235
+ childList: true,
236
+ subtree: true,
237
+ attributes: true,
238
+ // The panel's name is the tab's. idiomorph changes the text in a text
239
+ // node, with nothing else the observer sees.
240
+ characterData: true,
241
+ // `disabled` is included because the web component reads it: if a patch
242
+ // disables the selected tab, the tab stop moves to one that can take
243
+ // focus. `alt`, `aria-hidden` and `title` change the name of a tab.
244
+ attributeFilter: [
245
+ // A morph from the server removes the mark, and the web component
246
+ // puts it back: the same markup must give the same page.
247
+ WRITTEN,
248
+ "aria-selected",
249
+ "tabindex",
250
+ "hidden",
251
+ "disabled",
252
+ "aria-disabled",
253
+ // The wiring the web component sets when the markup came without it,
254
+ // and restores when a patch removes it.
255
+ "role",
256
+ "id",
257
+ "href",
258
+ "aria-controls",
259
+ "aria-labelledby",
260
+ "aria-label",
261
+ "alt",
262
+ "aria-hidden",
263
+ "title",
264
+ ],
265
+ },
266
+ )
257
267
  /**
258
268
  * The panel the user's tab points to, remembered so a patch cannot take
259
269
  * the selection.
@@ -288,9 +298,18 @@ export class LtTabs extends HostElement {
288
298
  * link is a tab, from one where the server wrote the roles, where a link
289
299
  * without a role is something else, such as a help link.
290
300
  */
291
- private readonly roledByMe = new WeakSet<Element>()
292
- private selectedMemory = new WeakMap<Element, Word>()
293
- private hiddenMemory = new WeakMap<Element, Word>()
301
+ private readonly roledByMe = ownership()
302
+ /**
303
+ * What the web component wrote for `aria-selected` on each tab and `hidden`
304
+ * on each panel, and the server's word it wrote against. If something other
305
+ * than what it wrote is there, the server has touched it; if the same is
306
+ * there, what the server said last applies. A patch that only touched the
307
+ * panels once made the component read its earlier selection back and hide
308
+ * the panel the server had just shown. Each is replaced whole when the
309
+ * server's answer makes what is in the markup the starting point.
310
+ */
311
+ private selectedOwn = ownership()
312
+ private hiddenOwn = ownership()
294
313
  /**
295
314
  * The form the server hides panels in, the last time it hid something:
296
315
  * `""` from `lt.tabs()`, `"until-found"` or something else. The web
@@ -310,7 +329,7 @@ export class LtTabs extends HostElement {
310
329
  */
311
330
  private readonly headingIds = new WeakMap<Element, string>()
312
331
  /** The name the web component wrote in `aria-label` on a panel without a heading. */
313
- private readonly labelByMe = new WeakMap<Element, string>()
332
+ private readonly labelOwn = ownership()
314
333
  /**
315
334
  * The tab the user clicked under `server-controlled`, until the server
316
335
  * answers.
@@ -374,41 +393,7 @@ export class LtTabs extends HostElement {
374
393
  root.addEventListener("beforematch", this.handleBeforematch, true)
375
394
  }
376
395
  window.addEventListener("hashchange", this.handleHashchange)
377
- this.observer = new MutationObserver((records) => {
378
- if (this.concerns(records)) this.sync()
379
- })
380
- this.observer.observe(this, {
381
- childList: true,
382
- subtree: true,
383
- attributes: true,
384
- // The panel's name is the tab's. idiomorph changes the text in a text
385
- // node, with nothing else the observer sees.
386
- characterData: true,
387
- // `disabled` is included because the web component reads it: if a patch
388
- // disables the selected tab, the tab stop moves to one that can take
389
- // focus. `alt`, `aria-hidden` and `title` change the name of a tab.
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,
394
- "aria-selected",
395
- "tabindex",
396
- "hidden",
397
- "disabled",
398
- "aria-disabled",
399
- // The wiring the web component sets when the markup came without it,
400
- // and restores when a patch removes it.
401
- "role",
402
- "id",
403
- "href",
404
- "aria-controls",
405
- "aria-labelledby",
406
- "aria-label",
407
- "alt",
408
- "aria-hidden",
409
- "title",
410
- ],
411
- })
396
+ this.watch.start(this)
412
397
  this.sync()
413
398
  }
414
399
 
@@ -428,8 +413,7 @@ export class LtTabs extends HostElement {
428
413
  }
429
414
 
430
415
  disconnectedCallback(): void {
431
- this.observer?.disconnect()
432
- this.observer = undefined
416
+ this.watch.stop()
433
417
  this.removeEventListener("click", this.handleClick, true)
434
418
  this.removeEventListener("keydown", this.handleKeydown, true)
435
419
  document.removeEventListener("beforematch", this.handleBeforematch, true)
@@ -492,41 +476,32 @@ export class LtTabs extends HostElement {
492
476
 
493
477
  /** The server's word about an attribute, against what the web component last wrote. */
494
478
  private serverWord(
495
- memory: WeakMap<Element, Word>,
479
+ own: Ownership,
496
480
  element: Element,
497
481
  name: string,
498
482
  ): string | null {
499
- const now = element.getAttribute(name)
500
- const known = memory.get(element)
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
483
+ if (!own.knows(element, name)) warnAboutCopiedMark(this, element)
484
+ return own.serverWord(element, name)
510
485
  }
511
486
 
512
487
  /** Is there something on a tab or a panel other than what the web component wrote? */
513
488
  private touchedByOthers(): boolean {
514
489
  return this.tabs.some((tab) => {
515
- const selected = this.selectedMemory.get(tab)
516
490
  if (
517
- !selected ||
518
- tab.getAttribute("aria-selected") !== selected.written ||
519
- markGone(tab, "aria-selected", selected)
491
+ !this.selectedOwn.knows(tab, "aria-selected") ||
492
+ tab.getAttribute("aria-selected") !==
493
+ this.selectedOwn.written(tab, "aria-selected") ||
494
+ this.selectedOwn.lost(tab, "aria-selected")
520
495
  ) {
521
496
  return true
522
497
  }
523
498
  const panel = this.panelFor(tab)
524
499
  if (!panel) return false
525
- const hidden = this.hiddenMemory.get(panel)
526
500
  return (
527
- !hidden ||
528
- panel.getAttribute("hidden") !== hidden.written ||
529
- markGone(panel, "hidden", hidden)
501
+ !this.hiddenOwn.knows(panel, "hidden") ||
502
+ panel.getAttribute("hidden") !==
503
+ this.hiddenOwn.written(panel, "hidden") ||
504
+ this.hiddenOwn.lost(panel, "hidden")
530
505
  )
531
506
  })
532
507
  }
@@ -552,33 +527,37 @@ export class LtTabs extends HostElement {
552
527
  const tabs = this.tabs
553
528
  const serverSpoke = tabs.some((tab) => {
554
529
  const now = tab.getAttribute("aria-selected")
555
- const known = this.selectedMemory.get(tab)
556
530
  // A new, bare tab has said nothing. Had it counted, the click's
557
531
  // `aria-selected` was read as the server's, and a later move with
558
532
  // `hidden` alone was undone.
559
- return known
560
- ? now !== known.written || markGone(tab, "aria-selected", known)
533
+ return this.selectedOwn.knows(tab, "aria-selected")
534
+ ? now !== this.selectedOwn.written(tab, "aria-selected") ||
535
+ this.selectedOwn.lost(tab, "aria-selected")
561
536
  : now !== null
562
537
  })
563
- this.hiddenMemory = new WeakMap()
538
+ this.hiddenOwn = ownership()
564
539
  if (serverSpoke) {
565
- this.selectedMemory = new WeakMap()
540
+ this.selectedOwn = ownership()
566
541
  return
567
542
  }
568
- for (const tab of tabs) {
569
- const written = tab.getAttribute("aria-selected")
570
- this.selectedMemory.set(tab, { written, server: null })
571
- markWritten(tab, "aria-selected", written !== null)
572
- }
543
+ // What stands there is the component's, against a server that said
544
+ // nothing: written again as it is, which changes no value, and marked.
545
+ for (const tab of tabs)
546
+ this.selectedOwn.write(
547
+ tab,
548
+ "aria-selected",
549
+ tab.getAttribute("aria-selected"),
550
+ { server: null, mark: true },
551
+ )
573
552
  }
574
553
 
575
554
  private serverSelected(tab: Element): string | null {
576
- return this.serverWord(this.selectedMemory, tab, "aria-selected")
555
+ return this.serverWord(this.selectedOwn, tab, "aria-selected")
577
556
  }
578
557
 
579
558
  /** The server's word about `hidden` on the panel: the form, or `null` for visible. */
580
559
  private serverHidden(panel: Element): string | null {
581
- return this.serverWord(this.hiddenMemory, panel, "hidden")
560
+ return this.serverWord(this.hiddenOwn, panel, "hidden")
582
561
  }
583
562
 
584
563
  /**
@@ -810,16 +789,14 @@ export class LtTabs extends HostElement {
810
789
  const named = new Set<HTMLElement>()
811
790
  for (const tab of this.tabs) {
812
791
  if (!tab.hasAttribute("role")) {
813
- setAttr(tab, "role", "tab")
814
- this.roledByMe.add(tab)
792
+ this.roledByMe.write(tab, "role", "tab")
815
793
  }
816
794
  const panel = this.panelFor(tab)
817
795
  setAttr(tab, "aria-controls", panel?.id ?? linkTarget(tab))
818
796
  if (!panel) continue
819
797
 
820
798
  if (!panel.hasAttribute("role")) {
821
- setAttr(panel, "role", "tabpanel")
822
- this.roledByMe.add(panel)
799
+ this.roledByMe.write(panel, "role", "tabpanel")
823
800
  }
824
801
  if (named.has(panel)) continue
825
802
  named.add(panel)
@@ -844,7 +821,7 @@ export class LtTabs extends HostElement {
844
821
  * `aria-label` applies.
845
822
  */
846
823
  private name(panel: HTMLElement, tab: HTMLElement): void {
847
- const ownLabel = this.labelByMe.get(panel)
824
+ const ownLabel = this.labelOwn.written(panel, "aria-label")
848
825
  const label = panel.getAttribute("aria-label")
849
826
  if (label !== null && label !== ownLabel) return
850
827
 
@@ -887,15 +864,13 @@ export class LtTabs extends HostElement {
887
864
 
888
865
  /** Writes the tab's name as `aria-label`, and remembers that the web component wrote it. */
889
866
  private setOwnLabel(panel: HTMLElement, text: string): void {
890
- setAttr(panel, "aria-label", text)
891
- this.labelByMe.set(panel, text)
867
+ this.labelOwn.write(panel, "aria-label", text)
892
868
  }
893
869
 
894
870
  /** Removes the fallback, when a name that exists has taken over. */
895
871
  private dropOwnLabel(panel: HTMLElement): void {
896
- if (!this.labelByMe.has(panel)) return
897
- setAttr(panel, "aria-label", null)
898
- this.labelByMe.delete(panel)
872
+ if (!this.labelOwn.knows(panel, "aria-label")) return
873
+ this.labelOwn.release(panel, "aria-label")
899
874
  }
900
875
 
901
876
  /** Does at least one of the ids point to an element that exists? */
@@ -911,7 +886,7 @@ export class LtTabs extends HostElement {
911
886
  * anything.
912
887
  *
913
888
  * The server's word, not what is there: what the web component wrote does
914
- * not count, see `Word`. `aria-selected` first. When that is missing, as in
889
+ * not count, see `selectedOwn`. `aria-selected` first. When that is missing, as in
915
890
  * markup written without JavaScript, `hidden` on the panels says the same:
916
891
  * the first panel the server shows belongs to the selected tab. If the
917
892
  * markup says nothing, or hides every panel, it is the first, which is what
@@ -1037,7 +1012,8 @@ export class LtTabs extends HostElement {
1037
1012
  'selected", and the arrow keys do nothing.',
1038
1013
  // An empty `<lt-tabs>` is a region the server has not filled yet,
1039
1014
  // and that is not a mistake in the markup.
1040
- () => this.childElementCount > 0 && this.tabs.length === 0,
1015
+ () => this.tabs.length === 0,
1016
+ { silentWhenEmpty: true },
1041
1017
  )
1042
1018
  return
1043
1019
  }
@@ -1347,9 +1323,10 @@ export class LtTabs extends HostElement {
1347
1323
  // The server's word is noted before the write, or the write was read as
1348
1324
  // the server's in the next round.
1349
1325
  const server = this.serverSelected(tab)
1350
- setAttr(tab, "aria-selected", String(chosen))
1351
- this.selectedMemory.set(tab, { written: String(chosen), server })
1352
- markWritten(tab, "aria-selected", String(chosen) !== server)
1326
+ this.selectedOwn.write(tab, "aria-selected", String(chosen), {
1327
+ server,
1328
+ mark: true,
1329
+ })
1353
1330
  setAttr(tab, "tabindex", i === stop ? "0" : "-1")
1354
1331
 
1355
1332
  const panel = this.panelFor(tab)
@@ -1371,10 +1348,7 @@ export class LtTabs extends HostElement {
1371
1348
  const server = this.serverHidden(panel)
1372
1349
  const written = shown ? null : this.hiddenForm
1373
1350
  if (panel !== this.revealing) {
1374
- if (written === null) setFlag(panel, "hidden", false)
1375
- else setAttr(panel, "hidden", written)
1376
- this.hiddenMemory.set(panel, { written, server })
1377
- markWritten(panel, "hidden", written !== server)
1351
+ this.hiddenOwn.write(panel, "hidden", written, { server, mark: true })
1378
1352
  }
1379
1353
  /*
1380
1354
  * Only the selected panel is a tab stop, so Tab from the tab list goes
@@ -2,9 +2,11 @@ import {
2
2
  addClass,
3
3
  defineElement,
4
4
  HostElement,
5
+ ownership,
5
6
  place,
6
7
  setAttr,
7
8
  warnAboutMarkup,
9
+ watcher,
8
10
  } from "../../host-element.js"
9
11
  import { TERM_CLASS, TERM_ENTRY_CLASS } from "./term.js"
10
12
 
@@ -66,11 +68,12 @@ function fragmentsOf(word: Element): string[] {
66
68
  }
67
69
 
68
70
  /**
69
- * The `aria-details` the component wrote, per link. A value that is still
70
- * there is the component's to change when `href` points elsewhere; any
71
- * other value is the server's word, and stays.
71
+ * The `aria-details` the component wrote, per link, shared by every term as
72
+ * the link may be. A value that is still there is the component's to change
73
+ * when `href` points elsewhere; any other value is the server's word, and
74
+ * stays.
72
75
  */
73
- const detailsWritten = new WeakMap<Element, string>()
76
+ const detailsOwn = ownership()
74
77
 
75
78
  type Shown = "no" | "hover" | "pinned"
76
79
 
@@ -114,7 +117,16 @@ export class LtTerm extends HostElement {
114
117
  /** The entry this component shows as the card right now. */
115
118
  private entry?: HTMLElement
116
119
  private shown: Shown = "no"
117
- private observer?: MutationObserver
120
+ /**
121
+ * The link and the entry it points at. `class` too: the component sets
122
+ * `lt-term` on a link without it, and a morph takes it again.
123
+ */
124
+ private readonly observe = watcher(() => this.sync(), {
125
+ childList: true,
126
+ subtree: true,
127
+ attributes: true,
128
+ attributeFilter: ["href", "class", "aria-details", "aria-expanded"],
129
+ })
118
130
  /** Watches the entry while the card is open: a patch can strip what the component set on it. */
119
131
  private outside?: MutationObserver
120
132
  private visibility?: IntersectionObserver
@@ -183,23 +195,14 @@ export class LtTerm extends HostElement {
183
195
  }
184
196
 
185
197
  connectedCallback(): void {
186
- this.observer = new MutationObserver(() => this.sync())
187
- this.observer.observe(this, {
188
- childList: true,
189
- subtree: true,
190
- attributes: true,
191
- // `class` too: the component sets `lt-term` on a link without it, and
192
- // a morph takes it again.
193
- attributeFilter: ["href", "class", "aria-details", "aria-expanded"],
194
- })
198
+ this.observe.start(this)
195
199
  LtTerm.watch(this)
196
200
  this.sync()
197
201
  }
198
202
 
199
203
  disconnectedCallback(): void {
200
204
  this.close()
201
- this.observer?.disconnect()
202
- this.observer = undefined
205
+ this.observe.stop()
203
206
  LtTerm.unwatch(this)
204
207
  this.detachWord()
205
208
  }
@@ -241,7 +244,8 @@ export class LtTerm extends HostElement {
241
244
  warnAboutMarkup(
242
245
  this,
243
246
  'found no link: an <a> with an href to the glossary entry, such as href="#term-signal". Without it there is no word to explain.',
244
- () => this.childElementCount > 0 && this.findWord() === null,
247
+ () => this.findWord() === null,
248
+ { silentWhenEmpty: true },
245
249
  )
246
250
  this.close()
247
251
  this.detachWord()
@@ -290,10 +294,12 @@ export class LtTerm extends HostElement {
290
294
  // `aria-details` the server wrote is its word, also when it points
291
295
  // elsewhere. What the component wrote follows the entry, and goes with it.
292
296
  const details = word.getAttribute("aria-details")
293
- if (details === null || details === detailsWritten.get(word)) {
294
- setAttr(word, "aria-details", entry?.id ?? null)
295
- if (entry) detailsWritten.set(word, entry.id)
296
- else detailsWritten.delete(word)
297
+ if (
298
+ details === null ||
299
+ details === detailsOwn.written(word, "aria-details")
300
+ ) {
301
+ if (entry) detailsOwn.write(word, "aria-details", entry.id)
302
+ else detailsOwn.release(word, "aria-details")
297
303
  }
298
304
 
299
305
  if (!entry) {