accented 0.0.2 → 1.0.1

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 (217) hide show
  1. package/NOTICE +14 -0
  2. package/README.md +44 -187
  3. package/dist/accented.d.ts +8 -8
  4. package/dist/accented.d.ts.map +1 -1
  5. package/dist/accented.js +37 -30
  6. package/dist/accented.js.map +1 -1
  7. package/dist/common/strings.d.ts +2 -0
  8. package/dist/common/strings.d.ts.map +1 -0
  9. package/dist/common/strings.js +2 -0
  10. package/dist/common/strings.js.map +1 -0
  11. package/dist/common/tokens.d.ts +7 -0
  12. package/dist/common/tokens.d.ts.map +1 -0
  13. package/dist/common/tokens.js +8 -0
  14. package/dist/common/tokens.js.map +1 -0
  15. package/dist/constants.d.ts +2 -1
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +2 -1
  18. package/dist/constants.js.map +1 -1
  19. package/dist/dom-updater.d.ts +1 -1
  20. package/dist/dom-updater.d.ts.map +1 -1
  21. package/dist/dom-updater.js +73 -31
  22. package/dist/dom-updater.js.map +1 -1
  23. package/dist/elements/accented-dialog.d.ts +18 -10
  24. package/dist/elements/accented-dialog.d.ts.map +1 -1
  25. package/dist/elements/accented-dialog.js +116 -95
  26. package/dist/elements/accented-dialog.js.map +1 -1
  27. package/dist/elements/accented-trigger.d.ts +14 -9
  28. package/dist/elements/accented-trigger.d.ts.map +1 -1
  29. package/dist/elements/accented-trigger.js +83 -24
  30. package/dist/elements/accented-trigger.js.map +1 -1
  31. package/dist/fullscreen-listener.d.ts +2 -0
  32. package/dist/fullscreen-listener.d.ts.map +1 -0
  33. package/dist/fullscreen-listener.js +17 -0
  34. package/dist/fullscreen-listener.js.map +1 -0
  35. package/dist/intersection-observer.d.ts +1 -1
  36. package/dist/intersection-observer.d.ts.map +1 -1
  37. package/dist/intersection-observer.js +12 -6
  38. package/dist/intersection-observer.js.map +1 -1
  39. package/dist/log-and-rethrow.d.ts +1 -1
  40. package/dist/log-and-rethrow.d.ts.map +1 -1
  41. package/dist/log-and-rethrow.js +2 -3
  42. package/dist/log-and-rethrow.js.map +1 -1
  43. package/dist/logger.d.ts +4 -1
  44. package/dist/logger.d.ts.map +1 -1
  45. package/dist/logger.js +6 -3
  46. package/dist/logger.js.map +1 -1
  47. package/dist/register-elements.d.ts +1 -1
  48. package/dist/register-elements.d.ts.map +1 -1
  49. package/dist/register-elements.js +6 -7
  50. package/dist/register-elements.js.map +1 -1
  51. package/dist/resize-listener.d.ts +1 -1
  52. package/dist/resize-listener.d.ts.map +1 -1
  53. package/dist/resize-listener.js +3 -4
  54. package/dist/resize-listener.js.map +1 -1
  55. package/dist/scanner.d.ts +2 -2
  56. package/dist/scanner.d.ts.map +1 -1
  57. package/dist/scanner.js +76 -43
  58. package/dist/scanner.js.map +1 -1
  59. package/dist/scroll-listeners.d.ts +1 -1
  60. package/dist/scroll-listeners.d.ts.map +1 -1
  61. package/dist/scroll-listeners.js +3 -4
  62. package/dist/scroll-listeners.js.map +1 -1
  63. package/dist/state.d.ts +3 -2
  64. package/dist/state.d.ts.map +1 -1
  65. package/dist/state.js +5 -3
  66. package/dist/state.js.map +1 -1
  67. package/dist/task-queue.d.ts +4 -4
  68. package/dist/task-queue.d.ts.map +1 -1
  69. package/dist/task-queue.js +3 -2
  70. package/dist/task-queue.js.map +1 -1
  71. package/dist/types.d.ts +140 -49
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js.map +1 -1
  74. package/dist/utils/are-elements-with-issues-equal.d.ts +3 -0
  75. package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -0
  76. package/dist/utils/are-elements-with-issues-equal.js +5 -0
  77. package/dist/utils/are-elements-with-issues-equal.js.map +1 -0
  78. package/dist/utils/are-issue-sets-equal.d.ts +2 -2
  79. package/dist/utils/are-issue-sets-equal.d.ts.map +1 -1
  80. package/dist/utils/are-issue-sets-equal.js +3 -3
  81. package/dist/utils/are-issue-sets-equal.js.map +1 -1
  82. package/dist/utils/containing-blocks.d.ts +3 -0
  83. package/dist/utils/containing-blocks.d.ts.map +1 -0
  84. package/dist/utils/containing-blocks.js +46 -0
  85. package/dist/utils/containing-blocks.js.map +1 -0
  86. package/dist/utils/contains.d.ts +2 -0
  87. package/dist/utils/contains.d.ts.map +1 -0
  88. package/dist/utils/contains.js +19 -0
  89. package/dist/utils/contains.js.map +1 -0
  90. package/dist/utils/deduplicate-nodes.d.ts +2 -0
  91. package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
  92. package/dist/utils/deduplicate-nodes.js +4 -0
  93. package/dist/utils/deduplicate-nodes.js.map +1 -0
  94. package/dist/utils/deep-merge.d.ts +1 -1
  95. package/dist/utils/deep-merge.d.ts.map +1 -1
  96. package/dist/utils/deep-merge.js +8 -5
  97. package/dist/utils/deep-merge.js.map +1 -1
  98. package/dist/utils/dom-helpers.d.ts +9 -0
  99. package/dist/utils/dom-helpers.d.ts.map +1 -0
  100. package/dist/utils/dom-helpers.js +34 -0
  101. package/dist/utils/dom-helpers.js.map +1 -0
  102. package/dist/utils/ensure-non-empty.d.ts +2 -0
  103. package/dist/utils/ensure-non-empty.d.ts.map +1 -0
  104. package/dist/utils/ensure-non-empty.js +7 -0
  105. package/dist/utils/ensure-non-empty.js.map +1 -0
  106. package/dist/utils/get-element-html.d.ts +1 -1
  107. package/dist/utils/get-element-html.d.ts.map +1 -1
  108. package/dist/utils/get-element-html.js +4 -2
  109. package/dist/utils/get-element-html.js.map +1 -1
  110. package/dist/utils/get-element-position.d.ts +10 -2
  111. package/dist/utils/get-element-position.d.ts.map +1 -1
  112. package/dist/utils/get-element-position.js +64 -16
  113. package/dist/utils/get-element-position.js.map +1 -1
  114. package/dist/utils/get-parent.d.ts +2 -0
  115. package/dist/utils/get-parent.d.ts.map +1 -0
  116. package/dist/utils/get-parent.js +12 -0
  117. package/dist/utils/get-parent.js.map +1 -0
  118. package/dist/utils/get-scan-context.d.ts +3 -0
  119. package/dist/utils/get-scan-context.d.ts.map +1 -0
  120. package/dist/utils/get-scan-context.js +28 -0
  121. package/dist/utils/get-scan-context.js.map +1 -0
  122. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  123. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  124. package/dist/utils/get-scrollable-ancestors.js +10 -6
  125. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  126. package/dist/utils/is-node-in-scan-context.d.ts +3 -0
  127. package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
  128. package/dist/utils/is-node-in-scan-context.js +26 -0
  129. package/dist/utils/is-node-in-scan-context.js.map +1 -0
  130. package/dist/utils/is-non-empty.d.ts +2 -0
  131. package/dist/utils/is-non-empty.d.ts.map +1 -0
  132. package/dist/utils/is-non-empty.js +4 -0
  133. package/dist/utils/is-non-empty.js.map +1 -0
  134. package/dist/utils/normalize-context.d.ts +3 -0
  135. package/dist/utils/normalize-context.d.ts.map +1 -0
  136. package/dist/utils/normalize-context.js +59 -0
  137. package/dist/utils/normalize-context.js.map +1 -0
  138. package/dist/utils/recalculate-positions.d.ts +1 -1
  139. package/dist/utils/recalculate-positions.d.ts.map +1 -1
  140. package/dist/utils/recalculate-positions.js +5 -5
  141. package/dist/utils/recalculate-positions.js.map +1 -1
  142. package/dist/utils/recalculate-scrollable-ancestors.d.ts +1 -1
  143. package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -1
  144. package/dist/utils/recalculate-scrollable-ancestors.js +4 -4
  145. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
  146. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +10 -0
  147. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -0
  148. package/dist/utils/shadow-dom-aware-mutation-observer.js +61 -0
  149. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -0
  150. package/dist/utils/supports-anchor-positioning.d.ts +1 -1
  151. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
  152. package/dist/utils/supports-anchor-positioning.js +15 -2
  153. package/dist/utils/supports-anchor-positioning.js.map +1 -1
  154. package/dist/utils/transform-violations.d.ts +2 -2
  155. package/dist/utils/transform-violations.d.ts.map +1 -1
  156. package/dist/utils/transform-violations.js +25 -10
  157. package/dist/utils/transform-violations.js.map +1 -1
  158. package/dist/utils/update-elements-with-issues.d.ts +11 -5
  159. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  160. package/dist/utils/update-elements-with-issues.js +56 -24
  161. package/dist/utils/update-elements-with-issues.js.map +1 -1
  162. package/dist/validate-options.d.ts +2 -2
  163. package/dist/validate-options.d.ts.map +1 -1
  164. package/dist/validate-options.js +91 -4
  165. package/dist/validate-options.js.map +1 -1
  166. package/package.json +16 -8
  167. package/src/accented.test.ts +2 -2
  168. package/src/accented.ts +45 -34
  169. package/src/common/strings.ts +2 -0
  170. package/src/common/tokens.ts +10 -0
  171. package/src/constants.ts +2 -1
  172. package/src/dom-updater.ts +87 -34
  173. package/src/elements/accented-dialog.ts +163 -123
  174. package/src/elements/accented-trigger.ts +128 -50
  175. package/src/fullscreen-listener.ts +21 -0
  176. package/src/intersection-observer.ts +27 -16
  177. package/src/log-and-rethrow.ts +2 -3
  178. package/src/logger.ts +14 -4
  179. package/src/register-elements.ts +7 -7
  180. package/src/resize-listener.ts +15 -11
  181. package/src/scanner.ts +113 -57
  182. package/src/scroll-listeners.ts +27 -19
  183. package/src/state.ts +27 -16
  184. package/src/task-queue.test.ts +5 -4
  185. package/src/task-queue.ts +8 -6
  186. package/src/types.ts +179 -76
  187. package/src/utils/are-elements-with-issues-equal.ts +11 -0
  188. package/src/utils/are-issue-sets-equal.test.ts +6 -7
  189. package/src/utils/are-issue-sets-equal.ts +8 -6
  190. package/src/utils/containing-blocks.ts +60 -0
  191. package/src/utils/contains.test.ts +54 -0
  192. package/src/utils/contains.ts +19 -0
  193. package/src/utils/deduplicate-nodes.ts +3 -0
  194. package/src/utils/deep-merge.test.ts +8 -1
  195. package/src/utils/deep-merge.ts +14 -8
  196. package/src/utils/dom-helpers.ts +42 -0
  197. package/src/utils/ensure-non-empty.ts +6 -0
  198. package/src/utils/get-element-html.ts +4 -2
  199. package/src/utils/get-element-position.ts +84 -16
  200. package/src/utils/get-parent.ts +14 -0
  201. package/src/utils/get-scan-context.test.ts +85 -0
  202. package/src/utils/get-scan-context.ts +36 -0
  203. package/src/utils/get-scrollable-ancestors.ts +15 -7
  204. package/src/utils/is-node-in-scan-context.test.ts +70 -0
  205. package/src/utils/is-node-in-scan-context.ts +29 -0
  206. package/src/utils/is-non-empty.ts +3 -0
  207. package/src/utils/normalize-context.test.ts +105 -0
  208. package/src/utils/normalize-context.ts +65 -0
  209. package/src/utils/recalculate-positions.ts +5 -5
  210. package/src/utils/recalculate-scrollable-ancestors.ts +4 -4
  211. package/src/utils/shadow-dom-aware-mutation-observer.ts +75 -0
  212. package/src/utils/supports-anchor-positioning.ts +19 -3
  213. package/src/utils/transform-violations.test.ts +29 -25
  214. package/src/utils/transform-violations.ts +32 -12
  215. package/src/utils/update-elements-with-issues.test.ts +145 -53
  216. package/src/utils/update-elements-with-issues.ts +123 -54
  217. package/src/validate-options.ts +154 -14
@@ -1,22 +1,24 @@
1
- import type { Issue } from '../types';
2
1
  import type { Signal } from '@preact/signals-core';
3
- import { effect } from '@preact/signals-core';
4
- import getElementHtml from '../utils/get-element-html.js';
2
+ import { colorDark, colorLight, fontSystemMono, fontSystemSans } from '../common/tokens.js';
5
3
  import { accentedUrl } from '../constants.js';
6
- import logAndRethrow from '../log-and-rethrow.js';
4
+ import { logAndRethrow } from '../log-and-rethrow.js';
5
+ import type { Issue } from '../types.ts';
6
+ import { getElementHtml } from '../utils/get-element-html.js';
7
+ import { isNonEmpty } from '../utils/is-non-empty.js';
7
8
 
8
9
  export interface AccentedDialog extends HTMLElement {
9
10
  issues: Signal<Array<Issue>> | undefined;
10
11
  element: Element | undefined;
11
12
  showModal: () => void;
13
+ open: boolean;
12
14
  }
13
15
 
14
16
  // We want Accented to not throw an error in Node, and use static imports,
15
17
  // so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
16
- export default () => {
18
+ export const getAccentedDialog = () => {
17
19
  const dialogTemplate = document.createElement('template');
18
20
  dialogTemplate.innerHTML = `
19
- <dialog dir="ltr" lang="en" aria-labelledby="title">
21
+ <dialog dir="ltr" lang="en" aria-labelledby="title" closedby="any">
20
22
  <div id="button-container">
21
23
  <button id="close" aria-label="Close">✕</button>
22
24
  </div>
@@ -56,20 +58,45 @@ export default () => {
56
58
  :host {
57
59
  all: initial !important;
58
60
 
59
- --light-color: white;
60
- --dark-color: black;
61
- --focus-color: #0078d4; /* Contrasts with both white and black. */
61
+ /* OKLCH stuff: https://oklch.com/ */
62
+ --light-color: ${colorLight};
63
+ --dark-color: ${colorDark};
62
64
 
63
- --impact-minor-color: lightgray;
64
- --impact-moderate-color: gold;
65
- --impact-serious-color: #ff9e00;
66
- --impact-critical-color: #f883ec;
65
+ --background-color: light-dark(var(--light-color), var(--dark-color));
66
+ --text-color: light-dark(var(--dark-color), var(--light-color));
67
+
68
+ --impact-lightness: 0.80;
69
+ --focus-lightness: 0.45;
70
+ @media (prefers-color-scheme: dark) {
71
+ --impact-lightness: 0.45;
72
+ --focus-lightness: 0.80;
73
+ }
74
+
75
+ --blue-hue: 230;
76
+ --gold-hue: 90;
77
+ --red-hue: 0;
78
+
79
+ /* Contrasts with background. */
80
+ --focus-color: oklch(var(--focus-lightness) 0.25 var(--blue-hue));
81
+
82
+ --impact-chroma: 0.16;
83
+
84
+ --impact-moderate-hue: var(--blue-hue);
85
+ --impact-serious-hue: var(--gold-hue);
86
+ --impact-critical-hue: var(--red-hue);
87
+
88
+ --impact-minor-color: oklch(var(--impact-lightness) 0 0);
89
+ --impact-moderate-color: oklch(var(--impact-lightness) var(--impact-chroma) var(--impact-moderate-hue));
90
+ --impact-serious-color: oklch(var(--impact-lightness) var(--impact-chroma) var(--impact-serious-hue));
91
+ --impact-critical-color: oklch(var(--impact-lightness) var(--impact-chroma) var(--impact-critical-hue));
92
+
93
+ --base-size: max(1rem, 16px);
67
94
 
68
95
  /* Spacing and typography custom props, inspired by https://utopia.fyi (simplified). */
69
96
 
70
97
  /* @link https://utopia.fyi/type/calculator?c=320,16,1.2,1240,16,1.2,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
71
98
  --ratio: 1.2;
72
- --step-0: 1rem;
99
+ --step-0: var(--base-size);
73
100
  --step-1: calc(var(--step-0) * var(--ratio));
74
101
  --step-2: calc(var(--step-1) * var(--ratio));
75
102
  --step-3: calc(var(--step-2) * var(--ratio));
@@ -77,15 +104,15 @@ export default () => {
77
104
  --step--1: calc(var(--step-0) / var(--ratio));
78
105
 
79
106
  /* @link https://utopia.fyi/space/calculator?c=320,16,1.2,1240,16,1.2,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
80
- --space-3xs: 0.25rem;
81
- --space-2xs: 0.5rem;
82
- --space-xs: 0.75rem;
83
- --space-s: 1rem;
84
- --space-m: 1.5rem;
85
- --space-l: 2rem;
86
- --space-xl: 3rem;
87
- --space-2xl: 4rem;
88
- --space-3xl: 6rem;
107
+ --space-3xs: calc(0.25 * var(--base-size));
108
+ --space-2xs: calc(0.5 * var(--base-size));
109
+ --space-xs: calc(0.75 * var(--base-size));
110
+ --space-s: var(--base-size);
111
+ --space-m: calc(1.5 * var(--base-size));
112
+ --space-l: calc(2 * var(--base-size));
113
+ --space-xl: calc(3 * var(--base-size));
114
+ --space-2xl: calc(4 * var(--base-size));
115
+ --space-3xl: calc(6 * var(--base-size));
89
116
  }
90
117
 
91
118
  a[href], button {
@@ -98,6 +125,7 @@ export default () => {
98
125
  outline-style: solid;
99
126
  }
100
127
 
128
+ /* We should probably be comfortable with showing these styles on non-hover devices. */
101
129
  &:hover:not(:focus-visible) {
102
130
  outline-style: dashed;
103
131
  }
@@ -114,14 +142,17 @@ export default () => {
114
142
  dialog {
115
143
  box-sizing: border-box;
116
144
  overflow-wrap: break-word;
117
- font-family: system-ui;
145
+ font-family: ${fontSystemSans};
118
146
  line-height: 1.5;
119
- background-color: var(--light-color);
120
- color: var(--dark-color);
147
+ text-wrap: pretty;
148
+ background-color: var(--background-color);
149
+ color: var(--text-color);
121
150
  border: 2px solid currentColor;
122
151
  padding: var(--space-l);
123
152
  inline-size: min(90ch, calc(100% - var(--space-s)* 2));
124
153
  max-block-size: calc(100% - var(--space-s) * 2);
154
+
155
+ color-scheme: light dark;
125
156
  }
126
157
 
127
158
  #button-container {
@@ -129,8 +160,8 @@ export default () => {
129
160
  }
130
161
 
131
162
  #close {
132
- background-color: var(--light-color);
133
- color: var(--dark-color);
163
+ background-color: var(--background-color);
164
+ color: var(--text-color);
134
165
  border: 2px solid currentColor;
135
166
  padding-inline: var(--space-2xs);
136
167
  aspect-ratio: 1 / 1;
@@ -151,8 +182,7 @@ export default () => {
151
182
  }
152
183
 
153
184
  code {
154
- /* https://systemfontstack.com/ */
155
- font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
185
+ font-family: ${fontSystemMono};
156
186
  font-size: var(--step--1);
157
187
  }
158
188
 
@@ -167,7 +197,7 @@ export default () => {
167
197
  }
168
198
 
169
199
  a {
170
- font-weight: bold;
200
+ font-weight: 500;
171
201
  }
172
202
  }
173
203
 
@@ -213,19 +243,17 @@ export default () => {
213
243
  `);
214
244
 
215
245
  return class extends HTMLElement implements AccentedDialog {
216
- #disposeOfEffect: (() => void) | undefined;
217
-
218
246
  #abortController: AbortController | undefined;
219
247
 
220
248
  issues: Signal<Array<Issue>> | undefined;
221
249
 
222
250
  element: Element | undefined;
223
251
 
224
- #elementMutationObserver: MutationObserver | undefined;
252
+ open = false;
225
253
 
226
254
  constructor() {
255
+ super();
227
256
  try {
228
- super();
229
257
  this.attachShadow({ mode: 'open' });
230
258
  const content = dialogTemplate.content.cloneNode(true);
231
259
  if (this.shadowRoot) {
@@ -244,96 +272,104 @@ export default () => {
244
272
  const dialog = shadowRoot.querySelector('dialog');
245
273
  const closeButton = shadowRoot.querySelector('#close');
246
274
  this.#abortController = new AbortController();
247
- closeButton?.addEventListener('click', () => {
248
- try {
249
- dialog?.close();
250
- } catch (error) {
251
- logAndRethrow(error);
252
- }
253
- }, { signal: this.#abortController.signal });
254
-
255
- dialog?.addEventListener('click', (event) => {
256
- try {
257
- this.#onDialogClick(event);
258
- } catch (error) {
259
- logAndRethrow(error);
260
- }
261
- }, { signal: this.#abortController.signal });
262
-
263
- this.#disposeOfEffect = effect(() => {
264
- if (this.issues) {
265
- const issues = this.issues.value;
266
- const issuesList = shadowRoot.getElementById('issues');
267
- if (issuesList) {
268
- issuesList.innerHTML = '';
269
- for (const issue of issues) {
270
- const issueContent = issueTemplate.content.cloneNode(true) as Element;
271
- const title = issueContent.querySelector('a');
272
- const impact = issueContent.querySelector('.impact');
273
- const description = issueContent.querySelector('.description');
274
- if (title && impact && description) {
275
- title.textContent = issue.title + ' (' + issue.id + ')';
276
- title.href = issue.url;
277
-
278
- impact.textContent = 'User impact: ' + issue.impact;
279
- impact.setAttribute('data-impact', String(issue.impact));
280
-
281
- const descriptionItems = issue.description.split(/\n\s*/);
282
- const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
283
- const descriptionTitle = descriptionContent.querySelector('span');
284
- const descriptionList = descriptionContent.querySelector('ul');
285
- if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
286
- descriptionTitle.textContent = descriptionItems[0]!;
287
- for (const descriptionItem of descriptionItems.slice(1)) {
288
- const li = document.createElement('li');
289
- li.textContent = descriptionItem;
290
- descriptionList.appendChild(li);
291
- }
292
- description.appendChild(descriptionContent);
275
+ closeButton?.addEventListener(
276
+ 'click',
277
+ () => {
278
+ try {
279
+ dialog?.close();
280
+ } catch (error) {
281
+ logAndRethrow(error);
282
+ }
283
+ },
284
+ { signal: this.#abortController.signal },
285
+ );
286
+
287
+ dialog?.addEventListener(
288
+ 'click',
289
+ (event) => {
290
+ try {
291
+ this.#onDialogClick(event);
292
+ } catch (error) {
293
+ logAndRethrow(error);
294
+ }
295
+ },
296
+ { signal: this.#abortController.signal },
297
+ );
298
+
299
+ dialog?.addEventListener(
300
+ 'keydown',
301
+ (event) => {
302
+ try {
303
+ if (event.key === 'Escape') {
304
+ event.stopPropagation();
305
+ }
306
+ } catch (error) {
307
+ logAndRethrow(error);
308
+ }
309
+ },
310
+ { signal: this.#abortController.signal },
311
+ );
312
+
313
+ if (this.issues) {
314
+ const issues = this.issues.value;
315
+ const issuesList = shadowRoot.getElementById('issues');
316
+ if (issuesList) {
317
+ issuesList.innerHTML = '';
318
+ for (const issue of issues) {
319
+ const issueContent = issueTemplate.content.cloneNode(true) as Element;
320
+ const title = issueContent.querySelector('a');
321
+ const impact = issueContent.querySelector('.impact');
322
+ const description = issueContent.querySelector('.description');
323
+ if (title && impact && description) {
324
+ title.textContent = `${issue.title} (${issue.id})`;
325
+ title.href = issue.url;
326
+
327
+ impact.textContent = `User impact: ${issue.impact}`;
328
+ impact.setAttribute('data-impact', String(issue.impact));
329
+
330
+ const descriptionItems = issue.description.split(/\n\s*/);
331
+ const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
332
+ const descriptionTitle = descriptionContent.querySelector('span');
333
+ const descriptionList = descriptionContent.querySelector('ul');
334
+ if (
335
+ descriptionTitle &&
336
+ descriptionList &&
337
+ isNonEmpty(descriptionItems) &&
338
+ descriptionItems.length > 1
339
+ ) {
340
+ descriptionTitle.textContent = descriptionItems[0];
341
+ for (const descriptionItem of descriptionItems.slice(1)) {
342
+ const li = document.createElement('li');
343
+ li.textContent = descriptionItem;
344
+ descriptionList.appendChild(li);
293
345
  }
346
+ description.appendChild(descriptionContent);
294
347
  }
295
- issuesList.appendChild(issueContent);
296
348
  }
349
+ issuesList.appendChild(issueContent);
297
350
  }
298
351
  }
299
- });
300
-
301
- const updateElementHtml = () => {
302
- if (this.element) {
303
- const elementHtmlContainer = shadowRoot.getElementById('element-html');
304
- if (elementHtmlContainer) {
305
- elementHtmlContainer.textContent = getElementHtml(this.element);
306
- }
307
- }
308
- };
309
-
310
- updateElementHtml();
352
+ }
311
353
 
312
- this.#elementMutationObserver = new MutationObserver(() => {
313
- try {
314
- updateElementHtml();
315
- } catch (error) {
316
- logAndRethrow(error);
317
- }
318
- });
319
354
  if (this.element) {
320
- // We're only outputting the element itself, not its subtree.
321
- // However, we're still listening for childList changes, because
322
- // we display an ellipsis if the element has innerHTML,
323
- // and we leave it empty if the element is empty.
324
- this.#elementMutationObserver.observe(this.element, {
325
- attributes: true,
326
- childList: true
327
- });
355
+ const elementHtmlContainer = shadowRoot.getElementById('element-html');
356
+ if (elementHtmlContainer) {
357
+ elementHtmlContainer.textContent = getElementHtml(this.element);
358
+ }
328
359
  }
329
360
 
330
- dialog?.addEventListener('close', () => {
331
- try {
332
- this.dispatchEvent(new Event('close'));
333
- } catch (error) {
334
- logAndRethrow(error);
335
- }
336
- }, { signal: this.#abortController.signal });
361
+ dialog?.addEventListener(
362
+ 'close',
363
+ () => {
364
+ try {
365
+ this.open = false;
366
+ this.dispatchEvent(new Event('close'));
367
+ } catch (error) {
368
+ logAndRethrow(error);
369
+ }
370
+ },
371
+ { signal: this.#abortController.signal },
372
+ );
337
373
  }
338
374
  } catch (error) {
339
375
  logAndRethrow(error);
@@ -342,15 +378,9 @@ export default () => {
342
378
 
343
379
  disconnectedCallback() {
344
380
  try {
345
- if (this.#disposeOfEffect) {
346
- this.#disposeOfEffect();
347
- }
348
381
  if (this.#abortController) {
349
382
  this.#abortController.abort();
350
383
  }
351
- if (this.#elementMutationObserver) {
352
- this.#elementMutationObserver.disconnect();
353
- }
354
384
  } catch (error) {
355
385
  logAndRethrow(error);
356
386
  }
@@ -361,13 +391,23 @@ export default () => {
361
391
  const dialog = this.shadowRoot.querySelector('dialog');
362
392
  if (dialog) {
363
393
  dialog.showModal();
394
+ this.open = true;
364
395
  }
365
396
  }
366
397
  }
367
398
 
399
+ /**
400
+ * This can be removed once the closedBy attribute
401
+ * is available in all supported browsers:
402
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/closedBy
403
+ */
368
404
  #onDialogClick(event: MouseEvent) {
369
405
  const dialog = event.currentTarget as HTMLDialogElement;
370
- if (!dialog || typeof dialog.getBoundingClientRect !== 'function' || typeof dialog.close !== 'function') {
406
+ if (
407
+ !dialog ||
408
+ typeof dialog.getBoundingClientRect !== 'function' ||
409
+ typeof dialog.close !== 'function'
410
+ ) {
371
411
  return;
372
412
  }
373
413
  const rect = dialog.getBoundingClientRect();