@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
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "$schema": "./manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "version": "0.37.0",
4
+ "version": "0.39.0",
5
+ "textLanguage": "en",
5
6
  "elements": {
6
7
  "lt-field": {
7
8
  "link": "https://lo-tek.sobernetics.no/components/field/",
@@ -31,7 +32,10 @@
31
32
  "description": "The control the field wires: an `<input>`, `<textarea>` or `<select>` with its `<option>`s. `type=\"hidden\"` does not count, and only the first one is wired. The template writes `for` on the label and `id` on the control, or wraps the control in the label.",
32
33
  "content": "none",
33
34
  "unique": true,
34
- "stableId": true
35
+ "stableId": true,
36
+ "name": {
37
+ "after": "label"
38
+ }
35
39
  },
36
40
  "help": {
37
41
  "selector": ".lt-help-text",
@@ -76,7 +80,8 @@
76
80
  "selector": "[role='tablist'], .lt-tabs__list",
77
81
  "description": "The tab list, a `<ul>` where each `<li>` gets `role=\"presentation\"`. It needs a name, such as `aria-label`. When the server has written `role=\"tab\"` on the links, the list can also hold other links.",
78
82
  "content": "free",
79
- "unique": true
83
+ "unique": true,
84
+ "name": {}
80
85
  },
81
86
  "tab": {
82
87
  "selector": "a[role='tab'][href^='#'], .lt-tabs__list a[href^='#']:not([role])",
@@ -88,7 +93,10 @@
88
93
  "selector": "[role='tabpanel'], .lt-tabs__panel:not([role])",
89
94
  "description": "The content of one tab, with the id the link points to. The panel has the same name as the tab: the heading at the top of the panel is the name when it says the same as the tab.",
90
95
  "content": "free",
91
- "multiple": true
96
+ "multiple": true,
97
+ "name": {
98
+ "component": true
99
+ }
92
100
  }
93
101
  },
94
102
  "events": {
@@ -128,6 +136,10 @@
128
136
  "content": "text",
129
137
  "multiple": true
130
138
  }
139
+ },
140
+ "name": {
141
+ "after": "title",
142
+ "id": true
131
143
  }
132
144
  },
133
145
  "lt-popover": {
@@ -221,13 +233,19 @@
221
233
  "description": "The field the user types in: `role=\"combobox\"`, otherwise the first `<input>`. The template writes `for` on the label and `id` on the field, or wraps the field in the label.",
222
234
  "content": "none",
223
235
  "unique": true,
224
- "stableId": true
236
+ "stableId": true,
237
+ "name": {
238
+ "after": "label"
239
+ }
225
240
  },
226
241
  "list": {
227
242
  "selector": "[role='listbox'], .lt-suggestion__list",
228
243
  "description": "The list of suggestions. It sits in the top layer when it is open, `popover=\"manual\"`, and has the label as its name.",
229
244
  "content": "parts",
230
- "unique": true
245
+ "unique": true,
246
+ "name": {
247
+ "component": true
248
+ }
231
249
  },
232
250
  "option": {
233
251
  "selector": "[role='option'], .lt-suggestion__list > li:not([role])",
@@ -253,6 +271,34 @@
253
271
  "content": "none",
254
272
  "unique": true,
255
273
  "optional": true
274
+ },
275
+ "label": {
276
+ "selector": "label",
277
+ "description": "The label. The component writes `for` on it from the field's id when it does not wrap the field, and names the list after it.",
278
+ "content": "text",
279
+ "optional": true
280
+ }
281
+ },
282
+ "texts": {
283
+ "suggestionNone": {
284
+ "text": "No matches",
285
+ "replacedBy": [
286
+ "count-none",
287
+ "count-other"
288
+ ]
289
+ },
290
+ "suggestionOne": {
291
+ "text": "One match",
292
+ "replacedBy": [
293
+ "count-one",
294
+ "count-other"
295
+ ]
296
+ },
297
+ "suggestionOther": {
298
+ "text": "{n} matches",
299
+ "replacedBy": [
300
+ "count-other"
301
+ ]
256
302
  }
257
303
  },
258
304
  "events": {
@@ -282,7 +328,10 @@
282
328
  "selector": ":scope > dialog",
283
329
  "description": "The `<dialog>` element, as a direct child of the host.",
284
330
  "content": "free",
285
- "unique": true
331
+ "unique": true,
332
+ "name": {
333
+ "after": "title"
334
+ }
286
335
  },
287
336
  "title": {
288
337
  "selector": "h1, h2, h3, h4, h5, h6",
@@ -325,6 +374,21 @@
325
374
  }
326
375
  },
327
376
  "parts": {},
377
+ "texts": {
378
+ "toastRegion": {
379
+ "text": "Notifications",
380
+ "replacedBy": [
381
+ "label",
382
+ "aria-label"
383
+ ]
384
+ },
385
+ "toastClose": {
386
+ "text": "Close message",
387
+ "replacedBy": [
388
+ "close-label"
389
+ ]
390
+ }
391
+ },
328
392
  "events": {
329
393
  "toast-dismiss": {}
330
394
  }
@@ -351,7 +415,10 @@
351
415
  "selector": "dialog",
352
416
  "description": "The `<dialog>` element with the warning.",
353
417
  "content": "free",
354
- "unique": true
418
+ "unique": true,
419
+ "name": {
420
+ "after": "title"
421
+ }
355
422
  },
356
423
  "title": {
357
424
  "selector": "h1, h2, h3, h4, h5, h6",
@@ -405,6 +472,20 @@
405
472
  }
406
473
  },
407
474
  "parts": {},
475
+ "texts": {
476
+ "connectionOffline": {
477
+ "text": "No connection. What you type is not being saved.",
478
+ "replacedBy": [
479
+ "offline-text"
480
+ ]
481
+ },
482
+ "connectionOnline": {
483
+ "text": "Connection restored.",
484
+ "replacedBy": [
485
+ "online-text"
486
+ ]
487
+ }
488
+ },
408
489
  "events": {
409
490
  "connection-lost": {},
410
491
  "connection-restored": {}
@@ -731,6 +812,7 @@
731
812
  "optional": true
732
813
  }
733
814
  },
815
+ "name": {},
734
816
  "attributes": {
735
817
  "data-state": {
736
818
  "values": [
@@ -749,6 +831,22 @@
749
831
  "link": "https://lo-tek.sobernetics.no/components/fieldset/",
750
832
  "content": "text",
751
833
  "partOf": "lt-fieldset",
834
+ "texts": [
835
+ {
836
+ "variable": "--lt-label-required-text",
837
+ "text": " (required)",
838
+ "attribute": "data-required",
839
+ "value": "text"
840
+ },
841
+ {
842
+ "variable": "--lt-label-optional-text",
843
+ "text": " (optional)",
844
+ "attribute": "data-optional",
845
+ "unless": [
846
+ "data-required"
847
+ ]
848
+ }
849
+ ],
752
850
  "attributes": {
753
851
  "data-required": {
754
852
  "values": [
@@ -879,6 +977,22 @@
879
977
  "description": "A label for form fields, with marking of required and optional. A CSS component.",
880
978
  "link": "https://lo-tek.sobernetics.no/components/label/",
881
979
  "content": "text",
980
+ "texts": [
981
+ {
982
+ "variable": "--lt-label-required-text",
983
+ "text": " (required)",
984
+ "attribute": "data-required",
985
+ "value": "text"
986
+ },
987
+ {
988
+ "variable": "--lt-label-optional-text",
989
+ "text": " (optional)",
990
+ "attribute": "data-optional",
991
+ "unless": [
992
+ "data-required"
993
+ ]
994
+ }
995
+ ],
882
996
  "attributes": {
883
997
  "data-required": {
884
998
  "values": [
@@ -1325,6 +1439,7 @@
1325
1439
  "multiple": true
1326
1440
  }
1327
1441
  },
1442
+ "name": {},
1328
1443
  "attributes": {}
1329
1444
  },
1330
1445
  "lt-toggle-group__option": {
@@ -1798,6 +1913,9 @@
1798
1913
  }
1799
1914
  }
1800
1915
  }
1916
+ },
1917
+ "classes": {
1918
+ "content": "lt-accordion__content"
1801
1919
  }
1802
1920
  },
1803
1921
  {
@@ -1846,6 +1964,9 @@
1846
1964
  }
1847
1965
  }
1848
1966
  }
1967
+ },
1968
+ "classes": {
1969
+ "title": "lt-alert__title"
1849
1970
  }
1850
1971
  },
1851
1972
  {
@@ -1908,6 +2029,9 @@
1908
2029
  }
1909
2030
  }
1910
2031
  }
2032
+ },
2033
+ "classes": {
2034
+ "stack": "lt-avatar-stack"
1911
2035
  }
1912
2036
  },
1913
2037
  {
@@ -2074,6 +2198,9 @@
2074
2198
  }
2075
2199
  }
2076
2200
  }
2201
+ },
2202
+ "classes": {
2203
+ "title": "lt-card__title"
2077
2204
  }
2078
2205
  },
2079
2206
  {
@@ -2118,6 +2245,9 @@
2118
2245
  }
2119
2246
  }
2120
2247
  }
2248
+ },
2249
+ "classes": {
2250
+ "row": "lt-checkbox-row"
2121
2251
  }
2122
2252
  },
2123
2253
  {
@@ -2148,6 +2278,8 @@
2148
2278
  "table": {
2149
2279
  "base": {
2150
2280
  "class": "lt-connection-status",
2281
+ "role": "status",
2282
+ "aria-live": "polite",
2151
2283
  "data-ignore-morph": ""
2152
2284
  },
2153
2285
  "options": {
@@ -2476,6 +2608,14 @@
2476
2608
  "kind": "text"
2477
2609
  },
2478
2610
  "description": "The id of the box, so the form can move focus here after submission."
2611
+ },
2612
+ {
2613
+ "name": "titleId",
2614
+ "required": false,
2615
+ "type": {
2616
+ "kind": "text"
2617
+ },
2618
+ "description": "The id of the heading. The box takes its name from it with `aria-labelledby`. Default: the box's `id` with `-title`, so the markup carries the wiring whenever the box has an id. An empty one counts as none; one with whitespace throws."
2479
2619
  }
2480
2620
  ],
2481
2621
  "returns": {
@@ -2775,6 +2915,9 @@
2775
2915
  }
2776
2916
  }
2777
2917
  }
2918
+ },
2919
+ "classes": {
2920
+ "list": "lt-file-upload-list"
2778
2921
  }
2779
2922
  },
2780
2923
  {
@@ -3184,6 +3327,9 @@
3184
3327
  },
3185
3328
  "options": {}
3186
3329
  }
3330
+ },
3331
+ "classes": {
3332
+ "gap": "lt-pagination__gap"
3187
3333
  }
3188
3334
  },
3189
3335
  {
@@ -3526,6 +3672,9 @@
3526
3672
  }
3527
3673
  }
3528
3674
  }
3675
+ },
3676
+ "classes": {
3677
+ "row": "lt-radio-row"
3529
3678
  }
3530
3679
  },
3531
3680
  {
@@ -3570,6 +3719,9 @@
3570
3719
  }
3571
3720
  }
3572
3721
  }
3722
+ },
3723
+ "classes": {
3724
+ "row": "lt-search-row"
3573
3725
  }
3574
3726
  },
3575
3727
  {
@@ -4229,6 +4381,9 @@
4229
4381
  }
4230
4382
  }
4231
4383
  }
4384
+ },
4385
+ "classes": {
4386
+ "row": "lt-switch-row"
4232
4387
  }
4233
4388
  },
4234
4389
  {
@@ -4283,6 +4438,9 @@
4283
4438
  }
4284
4439
  }
4285
4440
  }
4441
+ },
4442
+ "classes": {
4443
+ "scroll": "lt-table-scroll"
4286
4444
  }
4287
4445
  },
4288
4446
  {
@@ -4617,6 +4775,9 @@
4617
4775
  },
4618
4776
  "options": {}
4619
4777
  }
4778
+ },
4779
+ "classes": {
4780
+ "option": "lt-toggle-group__option"
4620
4781
  }
4621
4782
  },
4622
4783
  {
@@ -4633,6 +4794,9 @@
4633
4794
  },
4634
4795
  "options": {}
4635
4796
  }
4797
+ },
4798
+ "classes": {
4799
+ "bubble": "lt-tooltip__bubble"
4636
4800
  }
4637
4801
  }
4638
4802
  ],
@@ -24,6 +24,10 @@
24
24
  "type": "string",
25
25
  "description": "The version of @lo-tek/interfaces the manifest was written from."
26
26
  },
27
+ "textLanguage": {
28
+ "type": "string",
29
+ "description": "The language of the texts the components and stylesheets write themselves. lotek check warns about one of them on a page in another language."
30
+ },
27
31
  "elements": {
28
32
  "type": "object",
29
33
  "description": "Every <lt-…> element, with the attributes the component reads.",
@@ -51,6 +55,26 @@
51
55
  "$ref": "#/$defs/part"
52
56
  }
53
57
  },
58
+ "name": {
59
+ "$ref": "#/$defs/name"
60
+ },
61
+ "texts": {
62
+ "type": "object",
63
+ "description": "The texts the element writes itself, by their name in DEFAULT_TEXTS.",
64
+ "additionalProperties": {
65
+ "type": "object",
66
+ "required": ["text", "replacedBy"],
67
+ "properties": {
68
+ "text": { "type": "string" },
69
+ "replacedBy": {
70
+ "type": "array",
71
+ "description": "The attributes that replace the text. Any one of them is enough.",
72
+ "items": { "type": "string" }
73
+ }
74
+ },
75
+ "additionalProperties": false
76
+ }
77
+ },
54
78
  "events": {
55
79
  "type": "object",
56
80
  "description": "The events the element dispatches. All of them bubble and cross shadow boundaries. Absent when the element dispatches nothing.",
@@ -104,6 +128,29 @@
104
128
  "type": "string",
105
129
  "description": "The class this one is part of, when it is only a part of another component's markup, like lt-card__title in lt-card. The parent has a part that matches it."
106
130
  },
131
+ "name": {
132
+ "$ref": "#/$defs/name"
133
+ },
134
+ "texts": {
135
+ "type": "array",
136
+ "description": "The texts the stylesheet shows on the element with the class and the attribute, and the CSS variable that replaces each.",
137
+ "items": {
138
+ "type": "object",
139
+ "required": ["variable", "text", "attribute"],
140
+ "properties": {
141
+ "variable": { "type": "string" },
142
+ "text": { "type": "string" },
143
+ "attribute": { "type": "string" },
144
+ "value": { "type": "string" },
145
+ "unless": {
146
+ "type": "array",
147
+ "description": "Attributes that keep the text from showing.",
148
+ "items": { "type": "string" }
149
+ }
150
+ },
151
+ "additionalProperties": false
152
+ }
153
+ },
107
154
  "parts": {
108
155
  "type": "object",
109
156
  "description": "The parts the markup consists of, for the classes with a fixed structure, with the selector relative to the element with the class. Absent for the classes without one.",
@@ -192,6 +239,13 @@
192
239
  "returns": {
193
240
  "$ref": "#/$defs/returns"
194
241
  },
242
+ "classes": {
243
+ "type": "object",
244
+ "description": "The classes the builder function carries for the parts the template writes itself, as lt.card.title in TypeScript: the name on the function and the class. Only the parts no set of the function's own answer writes.",
245
+ "additionalProperties": {
246
+ "type": "string"
247
+ }
248
+ },
195
249
  "table": {
196
250
  "description": "When the options act independently: the base set and what each option adds, derived from the results and checked against every builder case. Another language can build the builder function from it alone.",
197
251
  "oneOf": [
@@ -505,6 +559,32 @@
505
559
  "optional": {
506
560
  "const": true,
507
561
  "description": "The element works without the part."
562
+ },
563
+ "stableId": {
564
+ "const": true,
565
+ "description": "The part needs an id from the server: the component never writes one."
566
+ },
567
+ "name": {
568
+ "$ref": "#/$defs/name"
569
+ }
570
+ },
571
+ "additionalProperties": false
572
+ },
573
+ "name": {
574
+ "type": "object",
575
+ "description": "A name LO-TEK demands where WAI-ARIA does not, or one the component gives itself. lotek check reports an element the markup gives no name.",
576
+ "properties": {
577
+ "after": {
578
+ "type": "string",
579
+ "description": "The part the component points aria-labelledby at when the markup has neither aria-label nor aria-labelledby."
580
+ },
581
+ "id": {
582
+ "const": true,
583
+ "description": "That part must have an id from the template: the component does not write one."
584
+ },
585
+ "component": {
586
+ "const": true,
587
+ "description": "The component names it itself, and no rule judges its name in the markup."
508
588
  }
509
589
  },
510
590
  "additionalProperties": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lo-tek/interfaces",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Framework-agnostic design system: tokens, CSS components and web components.",
5
5
  "license": "MIT",
6
6
  "author": "August Søberg-Klyver",
@@ -367,6 +367,7 @@
367
367
  "typecheck": "tsc --noEmit",
368
368
  "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
369
369
  "test:browser": "vitest --config vitest.config.ts --run",
370
+ "test:model": "vitest --config vitest.model.config.ts --run",
370
371
  "check:exports": "bun scripts/check-exports.ts",
371
372
  "check:server": "bun scripts/check-server-import.ts",
372
373
  "check:types": "bun scripts/check-types.ts",
@@ -16,6 +16,13 @@
16
16
  * read from an `--lt-` variable with the text as the fallback, and
17
17
  * `check-texts.ts` requires that.
18
18
  */
19
+ /**
20
+ * The language the texts are in. `lotek check` warns about a component that
21
+ * shows one of them on a page in another language, without the attribute or
22
+ * CSS variable that replaces it.
23
+ */
24
+ export const DEFAULT_LANGUAGE = "en"
25
+
19
26
  export const DEFAULT_TEXTS = {
20
27
  /** `aria-label` on the `<lt-toast>` region. Replaced with `label`. */
21
28
  toastRegion: "Notifications",
@@ -159,7 +159,9 @@ export class LtDialog extends HostElement {
159
159
  childList: true,
160
160
  subtree: true,
161
161
  attributes: true,
162
- attributeFilter: ["class", "id", "aria-labelledby"],
162
+ // `aria-label` too: removed, the dialog is named after its title
163
+ // again, and without it here the dialog was left without a name.
164
+ attributeFilter: ["class", "id", "aria-labelledby", "aria-label"],
163
165
  })
164
166
  this.sync()
165
167
  }
@@ -284,7 +286,8 @@ export class LtDialog extends HostElement {
284
286
  }
285
287
 
286
288
  private notify(open: boolean, returnValue = ""): void {
287
- if (open === this.lastNotified) return
289
+ // A dialog starts closed: one that never opened reported a close.
290
+ if (open === (this.lastNotified ?? false)) return
288
291
  this.lastNotified = open
289
292
  this.dispatchEvent(
290
293
  new CustomEvent("dialog-toggle", {
@@ -8,6 +8,13 @@ export type ErrorSummaryOptions = {
8
8
  count?: number
9
9
  /** The id of the box, so the form can move focus here after submission. */
10
10
  id?: string
11
+ /**
12
+ * The id of the heading. The box takes its name from it with
13
+ * `aria-labelledby`. Default: the box's `id` with `-title`, so the markup
14
+ * carries the wiring whenever the box has an id. An empty one counts as
15
+ * none; one with whitespace throws.
16
+ */
17
+ titleId?: string
11
18
  }
12
19
 
13
20
  export type ErrorSummaryAttributes = {
@@ -17,9 +24,11 @@ export type ErrorSummaryAttributes = {
17
24
  tabindex: "-1"
18
25
  id?: string
19
26
  hidden?: true
27
+ "aria-labelledby"?: string
20
28
  }
21
29
  title: {
22
30
  class: typeof ERROR_SUMMARY_TITLE_CLASS
31
+ id?: string
23
32
  }
24
33
  }
25
34
 
@@ -32,7 +41,11 @@ export type ErrorSummaryAttributes = {
32
41
  * every morph in Datastar.
33
42
  *
34
43
  * ```ts
35
- * const summary = lt.errorSummary({ count: errors.length, id: "errors" })
44
+ * const summary = lt.errorSummary({
45
+ * count: errors.length,
46
+ * id: "errors",
47
+ * titleId: "errors-title",
48
+ * })
36
49
  * ```
37
50
  * ```html
38
51
  * <lt-error-summary {...summary.host}>
@@ -44,13 +57,34 @@ export type ErrorSummaryAttributes = {
44
57
  export const errorSummary = ({
45
58
  count = 0,
46
59
  id,
47
- }: ErrorSummaryOptions = {}): ErrorSummaryAttributes => ({
48
- host: attributes({
49
- class: ERROR_SUMMARY_CLASS,
50
- role: "alert" as const,
51
- tabindex: "-1" as const,
52
- id,
53
- hidden: count === 0 ? (true as const) : undefined,
54
- }),
55
- title: attributes({ class: ERROR_SUMMARY_TITLE_CLASS }),
56
- })
60
+ titleId: givenTitleId,
61
+ }: ErrorSummaryOptions = {}): ErrorSummaryAttributes => {
62
+ /*
63
+ * The heading's id is written here, by whoever renders, never by the web
64
+ * component: Datastar and idiomorph replace an element whose id the server
65
+ * did not send, so an id from the component gave the heading a new one on
66
+ * every patch. An empty one names nothing.
67
+ */
68
+ const titleId = givenTitleId?.trim()
69
+ ? givenTitleId
70
+ : id?.trim()
71
+ ? `${id}-title`
72
+ : undefined
73
+ // `aria-labelledby` splits on whitespace, so the box would point at two
74
+ // ids, neither of them the heading's.
75
+ if (titleId !== undefined && /\s/.test(titleId))
76
+ throw new Error(
77
+ `lt.errorSummary(): the titleId "${titleId}" contains whitespace. aria-labelledby splits on whitespace, so the box would point at ids that are not the heading's.`,
78
+ )
79
+ return {
80
+ host: attributes({
81
+ class: ERROR_SUMMARY_CLASS,
82
+ role: "alert" as const,
83
+ tabindex: "-1" as const,
84
+ id,
85
+ hidden: count === 0 ? (true as const) : undefined,
86
+ "aria-labelledby": titleId,
87
+ }),
88
+ title: attributes({ class: ERROR_SUMMARY_TITLE_CLASS, id: titleId }),
89
+ }
90
+ }