@lmfaole/basics 0.3.0 → 0.5.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -350
  3. package/basic-components/basic-accordion/README.md +53 -0
  4. package/{components → basic-components}/basic-accordion/index.d.ts +5 -5
  5. package/basic-components/basic-accordion/index.js +413 -0
  6. package/basic-components/basic-alert/README.md +48 -0
  7. package/basic-components/basic-alert/index.d.ts +53 -0
  8. package/basic-components/basic-alert/index.js +189 -0
  9. package/basic-components/basic-alert/register.js +3 -0
  10. package/basic-components/basic-carousel/README.md +108 -0
  11. package/basic-components/basic-carousel/index.d.ts +73 -0
  12. package/basic-components/basic-carousel/index.js +255 -0
  13. package/basic-components/basic-carousel/register.js +3 -0
  14. package/basic-components/basic-dialog/README.md +57 -0
  15. package/basic-components/basic-popover/README.md +56 -0
  16. package/basic-components/basic-summary-table/README.md +93 -0
  17. package/{components → basic-components}/basic-summary-table/index.js +188 -42
  18. package/basic-components/basic-table/README.md +89 -0
  19. package/{components → basic-components}/basic-table/index.js +203 -145
  20. package/basic-components/basic-tabs/README.md +63 -0
  21. package/basic-components/basic-tabs/register.d.ts +1 -0
  22. package/basic-components/basic-toast/README.md +62 -0
  23. package/basic-components/basic-toast/index.d.ts +68 -0
  24. package/basic-components/basic-toast/index.js +690 -0
  25. package/basic-components/basic-toast/register.d.ts +1 -0
  26. package/basic-components/basic-toast/register.js +3 -0
  27. package/basic-components/basic-toc/README.md +43 -0
  28. package/basic-components/basic-toc/register.d.ts +1 -0
  29. package/basic-styling/components/basic-accordion.css +99 -0
  30. package/basic-styling/components/basic-alert.css +27 -0
  31. package/basic-styling/components/basic-carousel.css +183 -0
  32. package/basic-styling/components/basic-dialog.css +41 -0
  33. package/basic-styling/components/basic-popover.css +52 -0
  34. package/basic-styling/components/basic-summary-table.css +98 -0
  35. package/basic-styling/components/basic-table.css +66 -0
  36. package/basic-styling/components/basic-tabs.css +61 -0
  37. package/basic-styling/components/basic-toast.css +102 -0
  38. package/basic-styling/components/basic-toc.css +30 -0
  39. package/basic-styling/components.css +11 -0
  40. package/basic-styling/forms.css +55 -0
  41. package/basic-styling/global.css +62 -0
  42. package/basic-styling/index.css +2 -0
  43. package/basic-styling/interaction.css +90 -0
  44. package/basic-styling/tokens/base.css +19 -0
  45. package/basic-styling/tokens/palette.css +229 -0
  46. package/basic-styling/tokens/palette.tokens.json +1787 -0
  47. package/index.d.ts +10 -7
  48. package/index.js +10 -7
  49. package/package.json +61 -76
  50. package/components/basic-accordion/index.js +0 -387
  51. package/readme.mdx +0 -6
  52. /package/{components → basic-components}/basic-accordion/register.d.ts +0 -0
  53. /package/{components → basic-components}/basic-accordion/register.js +0 -0
  54. /package/{components/basic-dialog → basic-components/basic-alert}/register.d.ts +0 -0
  55. /package/{components/basic-popover → basic-components/basic-carousel}/register.d.ts +0 -0
  56. /package/{components → basic-components}/basic-dialog/index.d.ts +0 -0
  57. /package/{components → basic-components}/basic-dialog/index.js +0 -0
  58. /package/{components/basic-summary-table → basic-components/basic-dialog}/register.d.ts +0 -0
  59. /package/{components → basic-components}/basic-dialog/register.js +0 -0
  60. /package/{components → basic-components}/basic-popover/index.d.ts +0 -0
  61. /package/{components → basic-components}/basic-popover/index.js +0 -0
  62. /package/{components/basic-table → basic-components/basic-popover}/register.d.ts +0 -0
  63. /package/{components → basic-components}/basic-popover/register.js +0 -0
  64. /package/{components → basic-components}/basic-summary-table/index.d.ts +0 -0
  65. /package/{components/basic-tabs → basic-components/basic-summary-table}/register.d.ts +0 -0
  66. /package/{components → basic-components}/basic-summary-table/register.js +0 -0
  67. /package/{components → basic-components}/basic-table/index.d.ts +0 -0
  68. /package/{components/basic-toc → basic-components/basic-table}/register.d.ts +0 -0
  69. /package/{components → basic-components}/basic-table/register.js +0 -0
  70. /package/{components → basic-components}/basic-tabs/index.d.ts +0 -0
  71. /package/{components → basic-components}/basic-tabs/index.js +0 -0
  72. /package/{components → basic-components}/basic-tabs/register.js +0 -0
  73. /package/{components → basic-components}/basic-toc/index.d.ts +0 -0
  74. /package/{components → basic-components}/basic-toc/index.js +0 -0
  75. /package/{components → basic-components}/basic-toc/register.js +0 -0
@@ -0,0 +1,413 @@
1
+ const ElementBase = globalThis.Element ?? class {};
2
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
3
+ const HTMLDetailsElementBase = globalThis.HTMLDetailsElement ?? class {};
4
+
5
+ export const ACCORDION_TAG_NAME = "basic-accordion";
6
+
7
+ const SUMMARY_TAG_NAME = "SUMMARY";
8
+
9
+ function findDirectAccordionSummary(details) {
10
+ return Array.from(details.children).find(
11
+ (child) => child instanceof HTMLElementBase && child.tagName === SUMMARY_TAG_NAME,
12
+ ) ?? null;
13
+ }
14
+
15
+ function collectOwnedAccordionItems(root) {
16
+ return Array.from(root.children).flatMap((child) => {
17
+ if (!(child instanceof HTMLDetailsElementBase)) {
18
+ return [];
19
+ }
20
+
21
+ const summary = findDirectAccordionSummary(child);
22
+
23
+ if (!(summary instanceof HTMLElementBase)) {
24
+ return [];
25
+ }
26
+
27
+ return [{ details: child, summary }];
28
+ });
29
+ }
30
+
31
+ function isAccordionItemDisabled(details) {
32
+ return details.hasAttribute("data-disabled")
33
+ || details.hasAttribute("disabled")
34
+ || details.getAttribute("aria-disabled") === "true";
35
+ }
36
+
37
+ function getOpenAccordionIndexes(itemStates) {
38
+ const openIndexes = [];
39
+
40
+ for (let index = 0; index < itemStates.length; index += 1) {
41
+ if (itemStates[index]?.open && !itemStates[index]?.disabled) {
42
+ openIndexes.push(index);
43
+ }
44
+ }
45
+
46
+ return openIndexes;
47
+ }
48
+
49
+ function findFirstEnabledAccordionIndex(itemStates) {
50
+ for (let index = 0; index < itemStates.length; index += 1) {
51
+ if (!itemStates[index]?.disabled) {
52
+ return index;
53
+ }
54
+ }
55
+
56
+ return -1;
57
+ }
58
+
59
+ function findLastEnabledAccordionIndex(itemStates) {
60
+ for (let index = itemStates.length - 1; index >= 0; index -= 1) {
61
+ if (!itemStates[index]?.disabled) {
62
+ return index;
63
+ }
64
+ }
65
+
66
+ return -1;
67
+ }
68
+
69
+ export function getInitialOpenAccordionIndexes(
70
+ itemStates,
71
+ { multiple = false, collapsible = false } = {},
72
+ ) {
73
+ const explicitOpenIndexes = [];
74
+
75
+ for (let index = 0; index < itemStates.length; index += 1) {
76
+ const itemState = itemStates[index];
77
+
78
+ if (itemState?.open && !itemState.disabled) {
79
+ explicitOpenIndexes.push(index);
80
+ }
81
+ }
82
+
83
+ if (multiple) {
84
+ return explicitOpenIndexes;
85
+ }
86
+
87
+ if (explicitOpenIndexes.length > 0) {
88
+ return [explicitOpenIndexes[0]];
89
+ }
90
+
91
+ if (collapsible) {
92
+ return [];
93
+ }
94
+
95
+ const firstEnabledIndex = findFirstEnabledAccordionIndex(itemStates);
96
+ return firstEnabledIndex === -1 ? [] : [firstEnabledIndex];
97
+ }
98
+
99
+ export function findNextEnabledAccordionIndex(itemStates, startIndex, direction) {
100
+ if (itemStates.length === 0) {
101
+ return -1;
102
+ }
103
+
104
+ const step = direction < 0 ? -1 : 1;
105
+ let nextIndex = startIndex;
106
+
107
+ for (let checked = 0; checked < itemStates.length; checked += 1) {
108
+ nextIndex += step;
109
+
110
+ if (nextIndex < 0) {
111
+ nextIndex = itemStates.length - 1;
112
+ } else if (nextIndex >= itemStates.length) {
113
+ nextIndex = 0;
114
+ }
115
+
116
+ if (!itemStates[nextIndex]?.disabled) {
117
+ return nextIndex;
118
+ }
119
+ }
120
+
121
+ return -1;
122
+ }
123
+
124
+ export class AccordionElement extends HTMLElementBase {
125
+ static observedAttributes = ["data-collapsible", "data-multiple"];
126
+
127
+ #items = [];
128
+ #eventsBound = false;
129
+ #isSyncingState = false;
130
+
131
+ connectedCallback() {
132
+ if (!this.#eventsBound) {
133
+ this.addEventListener("click", this.#handleClick);
134
+ this.addEventListener("keydown", this.#handleKeyDown);
135
+ this.#eventsBound = true;
136
+ }
137
+
138
+ this.#sync({ resetOpen: true });
139
+ }
140
+
141
+ disconnectedCallback() {
142
+ if (this.#eventsBound) {
143
+ this.removeEventListener("click", this.#handleClick);
144
+ this.removeEventListener("keydown", this.#handleKeyDown);
145
+ this.#eventsBound = false;
146
+ }
147
+
148
+ for (const { details } of this.#items) {
149
+ details.removeEventListener("toggle", this.#handleToggle);
150
+ }
151
+
152
+ this.#items = [];
153
+ }
154
+
155
+ attributeChangedCallback() {
156
+ this.#sync({ resetOpen: true });
157
+ }
158
+
159
+ #handleClick = (event) => {
160
+ if (!(event.target instanceof ElementBase)) {
161
+ return;
162
+ }
163
+
164
+ const summary = event.target.closest("summary");
165
+ const summaryIndex = this.#findSummaryIndex(summary);
166
+
167
+ if (summaryIndex === -1) {
168
+ return;
169
+ }
170
+
171
+ const itemStates = this.#getItemStates();
172
+
173
+ if (itemStates[summaryIndex]?.disabled) {
174
+ event.preventDefault();
175
+ return;
176
+ }
177
+
178
+ if (!this.#isMultiple()) {
179
+ event.preventDefault();
180
+ this.#toggleItem(summaryIndex, itemStates);
181
+ }
182
+ };
183
+
184
+ #handleKeyDown = (event) => {
185
+ if (!(event.target instanceof ElementBase)) {
186
+ return;
187
+ }
188
+
189
+ const currentSummary = event.target.closest("summary");
190
+ const currentIndex = this.#findSummaryIndex(currentSummary);
191
+ const itemStates = this.#getItemStates();
192
+ let nextIndex = -1;
193
+
194
+ if (currentIndex === -1 || currentIndex >= itemStates.length) {
195
+ return;
196
+ }
197
+
198
+ switch (event.key) {
199
+ case "ArrowDown":
200
+ nextIndex = findNextEnabledAccordionIndex(itemStates, currentIndex, 1);
201
+ break;
202
+ case "ArrowUp":
203
+ nextIndex = findNextEnabledAccordionIndex(itemStates, currentIndex, -1);
204
+ break;
205
+ case "Home":
206
+ nextIndex = findFirstEnabledAccordionIndex(itemStates);
207
+ break;
208
+ case "End":
209
+ nextIndex = findLastEnabledAccordionIndex(itemStates);
210
+ break;
211
+ case " ":
212
+ case "Enter":
213
+ event.preventDefault();
214
+ this.#toggleItem(currentIndex, itemStates);
215
+ return;
216
+ default:
217
+ return;
218
+ }
219
+
220
+ if (nextIndex === -1) {
221
+ return;
222
+ }
223
+
224
+ event.preventDefault();
225
+ this.#items[nextIndex]?.summary.focus();
226
+ };
227
+
228
+ #handleToggle = (event) => {
229
+ if (this.#isSyncingState) {
230
+ return;
231
+ }
232
+
233
+ const details = event.currentTarget;
234
+ const preferredIndex = this.#items.findIndex((item) => item.details === details);
235
+
236
+ if (preferredIndex === -1) {
237
+ return;
238
+ }
239
+
240
+ this.#sync({ preferredIndex });
241
+ };
242
+
243
+ #findSummaryIndex(summary) {
244
+ if (!(summary instanceof HTMLElementBase)) {
245
+ return -1;
246
+ }
247
+
248
+ return this.#items.findIndex((item) => item.summary === summary);
249
+ }
250
+
251
+ #isCollapsible() {
252
+ return this.hasAttribute("data-collapsible");
253
+ }
254
+
255
+ #isMultiple() {
256
+ return this.hasAttribute("data-multiple");
257
+ }
258
+
259
+ #getItemStates({ includeDataOpen = false } = {}) {
260
+ return this.#items.map(({ details }) => ({
261
+ disabled: isAccordionItemDisabled(details),
262
+ open: details.hasAttribute("open")
263
+ || (includeDataOpen && details.hasAttribute("data-open")),
264
+ }));
265
+ }
266
+
267
+ #sync({ resetOpen = false, preferredIndex = -1 } = {}) {
268
+ const previousDetails = this.#items.map((item) => item.details);
269
+ const nextItems = collectOwnedAccordionItems(this);
270
+ const nextDetails = nextItems.map((item) => item.details);
271
+
272
+ for (const details of previousDetails) {
273
+ if (!nextDetails.includes(details)) {
274
+ details.removeEventListener("toggle", this.#handleToggle);
275
+ }
276
+ }
277
+
278
+ for (const details of nextDetails) {
279
+ if (!previousDetails.includes(details)) {
280
+ details.addEventListener("toggle", this.#handleToggle);
281
+ }
282
+ }
283
+
284
+ this.#items = nextItems;
285
+ this.#normalizeOpenState({
286
+ itemStates: this.#getItemStates({ includeDataOpen: resetOpen }),
287
+ resetOpen,
288
+ preferredIndex,
289
+ });
290
+ this.#applyState();
291
+ }
292
+
293
+ #normalizeOpenState({ itemStates, resetOpen = false, preferredIndex = -1 }) {
294
+ let openIndexes = [];
295
+
296
+ if (resetOpen) {
297
+ openIndexes = getInitialOpenAccordionIndexes(itemStates, {
298
+ multiple: this.#isMultiple(),
299
+ collapsible: this.#isCollapsible(),
300
+ });
301
+ } else if (this.#isMultiple()) {
302
+ openIndexes = getOpenAccordionIndexes(itemStates);
303
+ } else {
304
+ openIndexes = getOpenAccordionIndexes(itemStates);
305
+
306
+ if (
307
+ preferredIndex !== -1
308
+ && openIndexes.includes(preferredIndex)
309
+ && !itemStates[preferredIndex]?.disabled
310
+ ) {
311
+ openIndexes = [preferredIndex];
312
+ } else if (openIndexes.length > 0) {
313
+ openIndexes = [openIndexes[0]];
314
+ } else if (!this.#isCollapsible()) {
315
+ const fallbackIndex = findFirstEnabledAccordionIndex(itemStates);
316
+
317
+ if (fallbackIndex !== -1) {
318
+ openIndexes = [fallbackIndex];
319
+ }
320
+ }
321
+ }
322
+
323
+ this.#applyOpenState(openIndexes, itemStates);
324
+ }
325
+
326
+ #applyState() {
327
+ for (const { details, summary } of this.#items) {
328
+ const disabled = isAccordionItemDisabled(details);
329
+ const open = !disabled && details.open;
330
+
331
+ if (disabled && details.open) {
332
+ details.open = false;
333
+ }
334
+
335
+ details.toggleAttribute("data-open", open);
336
+
337
+ if (disabled) {
338
+ summary.setAttribute("aria-disabled", "true");
339
+ summary.tabIndex = -1;
340
+ } else {
341
+ summary.removeAttribute("aria-disabled");
342
+ summary.tabIndex = 0;
343
+ }
344
+ }
345
+ }
346
+
347
+ #toggleItem(index, itemStates = this.#getItemStates()) {
348
+ if (index < 0 || index >= this.#items.length || itemStates[index]?.disabled) {
349
+ return;
350
+ }
351
+
352
+ if (this.#isMultiple()) {
353
+ this.#items[index].details.open = !this.#items[index].details.open;
354
+ return;
355
+ }
356
+
357
+ const openIndexes = getOpenAccordionIndexes(itemStates);
358
+
359
+ if (itemStates[index]?.open) {
360
+ if (!this.#isCollapsible() && openIndexes.length === 1) {
361
+ return;
362
+ }
363
+
364
+ this.#applyOpenState([], itemStates);
365
+ this.#applyState();
366
+ return;
367
+ }
368
+
369
+ this.#applyOpenState([index], itemStates);
370
+ this.#applyState();
371
+ }
372
+
373
+ #applyOpenState(openIndexes, itemStates = this.#getItemStates()) {
374
+ const openIndexSet = new Set(openIndexes);
375
+
376
+ this.#isSyncingState = true;
377
+
378
+ try {
379
+ // Close panels first so single-open accordions do not briefly expose two bodies.
380
+ for (let index = 0; index < this.#items.length; index += 1) {
381
+ const details = this.#items[index].details;
382
+ const open = !itemStates[index]?.disabled && openIndexSet.has(index);
383
+
384
+ if (!open && details.open) {
385
+ details.open = false;
386
+ }
387
+ }
388
+
389
+ for (let index = 0; index < this.#items.length; index += 1) {
390
+ const details = this.#items[index].details;
391
+ const open = !itemStates[index]?.disabled && openIndexSet.has(index);
392
+
393
+ if (open && !details.open) {
394
+ details.open = true;
395
+ }
396
+ }
397
+ } finally {
398
+ this.#isSyncingState = false;
399
+ }
400
+ }
401
+ }
402
+
403
+ export function defineAccordion(registry = globalThis.customElements) {
404
+ if (!registry?.get || !registry?.define) {
405
+ return AccordionElement;
406
+ }
407
+
408
+ if (!registry.get(ACCORDION_TAG_NAME)) {
409
+ registry.define(ACCORDION_TAG_NAME, AccordionElement);
410
+ }
411
+
412
+ return AccordionElement;
413
+ }
@@ -0,0 +1,48 @@
1
+ # `basic-alert`
2
+
3
+ Inline live-region alert content without opinionated styling.
4
+
5
+ ## Register
6
+
7
+ ```js
8
+ import "@lmfaole/basics/basic-components/basic-alert/register";
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```html
14
+ <basic-alert data-label="Lagring fullfort" data-live="polite">
15
+ <h2 data-alert-title>Endringer lagret</h2>
16
+ <p>Meldingen ble lagret uten feil.</p>
17
+ <button type="button" data-alert-close>Dismiss</button>
18
+ </basic-alert>
19
+ ```
20
+
21
+ ## Props
22
+
23
+ | Prop | Description | Type | Default | Options |
24
+ | --- | --- | --- | --- | --- |
25
+ | `data-label` | Fallback accessible name when the alert has no `aria-label`, `aria-labelledby`, or `[data-alert-title]`. | string | `Alert` | any string |
26
+ | `data-live` | Chooses whether the alert announces as `role="alert"` or `role="status"`. | enum string | `assertive` | `assertive`, `polite` |
27
+ | `data-open` | Managed visibility flag. If omitted, the alert stays visible unless `hidden` is set. | boolean-ish attribute | visible | `present`, `omitted`, `false` |
28
+
29
+ ## Markup Hooks
30
+
31
+ | Hook | Description | Type | Default | Options |
32
+ | --- | --- | --- | --- | --- |
33
+ | `data-alert-title` | Makes the visible heading the alert's accessible name. | descendant heading attribute | none | present on a descendant heading |
34
+ | `data-alert-close` | Dismisses the alert when activated. | descendant control attribute | none | present on a descendant control |
35
+
36
+ ## Behavior
37
+
38
+ - Applies the matching live-region role, `aria-live`, and `aria-atomic="true"` on the root element
39
+ - Uses `[data-alert-title]` as the accessible name when present, otherwise falls back to `data-label`
40
+ - `[data-alert-close]` hides the alert and removes its managed `data-open` state
41
+ - `show()` and `hide()` support programmatic visibility changes
42
+
43
+ ## Markup Contract
44
+
45
+ - Put the content directly inside `<basic-alert>`
46
+ - Use `[data-alert-title]` when the alert should have a visible accessible name
47
+ - Use `[data-alert-close]` when the alert should be dismissible
48
+ - Keep layout and styling outside the package
@@ -0,0 +1,53 @@
1
+ export const ALERT_TAG_NAME: "basic-alert";
2
+
3
+ /**
4
+ * Normalizes unsupported or empty labels back to the default `"Alert"`.
5
+ */
6
+ export function normalizeAlertLabel(
7
+ value?: string | null,
8
+ ): string;
9
+
10
+ /**
11
+ * Normalizes unsupported live-region values back to `"assertive"`.
12
+ */
13
+ export function normalizeAlertLive(
14
+ value?: string | null,
15
+ ): "assertive" | "polite";
16
+
17
+ /**
18
+ * Maps an alert live setting to the matching ARIA role.
19
+ */
20
+ export function getAlertRoleForLive(
21
+ value?: string | null,
22
+ ): "alert" | "status";
23
+
24
+ /**
25
+ * Normalizes the optional `data-open` attribute into a boolean flag, using the
26
+ * current `hidden` state as a fallback.
27
+ */
28
+ export function normalizeAlertOpen(
29
+ value?: string | null,
30
+ hidden?: boolean,
31
+ ): boolean;
32
+
33
+ /**
34
+ * Custom element that upgrades inline content into a dismissible live-region
35
+ * alert.
36
+ *
37
+ * Attributes:
38
+ * - `data-label`: fallback accessible name when the alert has no title
39
+ * - `data-live`: chooses between `alert` and `status` semantics
40
+ * - `data-open`: optional managed visibility flag
41
+ */
42
+ export class AlertElement extends HTMLElement {
43
+ static observedAttributes: string[];
44
+ show(): boolean;
45
+ hide(): boolean;
46
+ }
47
+
48
+ /**
49
+ * Registers the `basic-alert` custom element if it is not already defined.
50
+ */
51
+ export function defineAlert(
52
+ registry?: CustomElementRegistry,
53
+ ): typeof AlertElement;
@@ -0,0 +1,189 @@
1
+ const ElementBase = globalThis.Element ?? class {};
2
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
3
+ const HTMLButtonElementBase = globalThis.HTMLButtonElement ?? class {};
4
+
5
+ export const ALERT_TAG_NAME = "basic-alert";
6
+
7
+ const DEFAULT_LABEL = "Alert";
8
+ const DEFAULT_LIVE = "assertive";
9
+ const TITLE_SELECTOR = "[data-alert-title]";
10
+ const CLOSE_SELECTOR = "[data-alert-close]";
11
+ const MANAGED_LABEL_ATTRIBUTE = "data-basic-alert-managed-label";
12
+ const MANAGED_LABELLEDBY_ATTRIBUTE = "data-basic-alert-managed-labelledby";
13
+
14
+ let nextAlertInstanceId = 1;
15
+
16
+ function collectOwnedElements(root, scope, selector) {
17
+ return Array.from(scope.querySelectorAll(selector)).filter(
18
+ (element) => element instanceof HTMLElementBase && element.closest(ALERT_TAG_NAME) === root,
19
+ );
20
+ }
21
+
22
+ export function normalizeAlertLabel(value) {
23
+ return value?.trim() || DEFAULT_LABEL;
24
+ }
25
+
26
+ export function normalizeAlertLive(value) {
27
+ const normalized = value?.trim().toLowerCase();
28
+ return normalized === "polite" ? "polite" : DEFAULT_LIVE;
29
+ }
30
+
31
+ export function getAlertRoleForLive(value) {
32
+ return normalizeAlertLive(value) === "polite" ? "status" : "alert";
33
+ }
34
+
35
+ export function normalizeAlertOpen(value, hidden = false) {
36
+ if (hidden) {
37
+ return false;
38
+ }
39
+
40
+ if (value == null) {
41
+ return true;
42
+ }
43
+
44
+ const normalized = value.trim().toLowerCase();
45
+ return normalized === "" || normalized === "true" || normalized === "1";
46
+ }
47
+
48
+ export class AlertElement extends HTMLElementBase {
49
+ static observedAttributes = ["data-label", "data-live", "data-open", "hidden"];
50
+
51
+ #instanceId = `${ALERT_TAG_NAME}-${nextAlertInstanceId++}`;
52
+ #title = null;
53
+ #closeButtons = [];
54
+ #eventsBound = false;
55
+
56
+ connectedCallback() {
57
+ if (!this.#eventsBound) {
58
+ this.addEventListener("click", this.#handleClick);
59
+ this.#eventsBound = true;
60
+ }
61
+
62
+ this.#sync();
63
+ }
64
+
65
+ disconnectedCallback() {
66
+ if (!this.#eventsBound) {
67
+ return;
68
+ }
69
+
70
+ this.removeEventListener("click", this.#handleClick);
71
+ this.#eventsBound = false;
72
+ }
73
+
74
+ attributeChangedCallback() {
75
+ this.#sync();
76
+ }
77
+
78
+ show() {
79
+ this.hidden = false;
80
+ this.toggleAttribute("data-open", true);
81
+ this.#sync();
82
+ return true;
83
+ }
84
+
85
+ hide() {
86
+ this.hidden = true;
87
+ this.toggleAttribute("data-open", false);
88
+ this.#sync();
89
+ return true;
90
+ }
91
+
92
+ #handleClick = (event) => {
93
+ if (!(event.target instanceof ElementBase)) {
94
+ return;
95
+ }
96
+
97
+ const closeButton = event.target.closest(CLOSE_SELECTOR);
98
+
99
+ if (
100
+ closeButton instanceof HTMLElementBase
101
+ && closeButton.closest(ALERT_TAG_NAME) === this
102
+ ) {
103
+ event.preventDefault();
104
+ this.hide();
105
+ }
106
+ };
107
+
108
+ #sync() {
109
+ const nextTitle = collectOwnedElements(this, this, TITLE_SELECTOR)[0] ?? null;
110
+
111
+ this.#title = nextTitle instanceof HTMLElementBase ? nextTitle : null;
112
+ this.#closeButtons = collectOwnedElements(this, this, CLOSE_SELECTOR);
113
+ this.#applyState();
114
+ }
115
+
116
+ #applyState() {
117
+ for (const button of this.#closeButtons) {
118
+ if (button instanceof HTMLButtonElementBase && !button.hasAttribute("type")) {
119
+ button.type = "button";
120
+ }
121
+ }
122
+
123
+ const open = normalizeAlertOpen(this.getAttribute("data-open"), this.hidden);
124
+ const baseId = this.id || this.#instanceId;
125
+
126
+ if (this.#title instanceof HTMLElementBase && !this.#title.id) {
127
+ this.#title.id = `${baseId}-title`;
128
+ }
129
+
130
+ this.hidden = !open;
131
+ this.toggleAttribute("data-open", open);
132
+ this.setAttribute("role", getAlertRoleForLive(this.getAttribute("data-live")));
133
+ this.setAttribute("aria-live", normalizeAlertLive(this.getAttribute("data-live")));
134
+ this.setAttribute("aria-atomic", "true");
135
+ this.#syncAccessibleLabel();
136
+ }
137
+
138
+ #syncAccessibleLabel() {
139
+ const nextLabel = normalizeAlertLabel(this.getAttribute("data-label"));
140
+ const hasManagedLabel = this.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
141
+ const hasManagedLabelledBy = this.hasAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
142
+
143
+ if (hasManagedLabel && this.getAttribute("aria-label") !== nextLabel) {
144
+ this.removeAttribute("aria-label");
145
+ this.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
146
+ }
147
+
148
+ if (this.#title?.id) {
149
+ if (this.hasAttribute(MANAGED_LABEL_ATTRIBUTE)) {
150
+ this.removeAttribute("aria-label");
151
+ this.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
152
+ }
153
+
154
+ if (!this.hasAttribute("aria-labelledby") || hasManagedLabelledBy) {
155
+ this.setAttribute("aria-labelledby", this.#title.id);
156
+ this.setAttribute(MANAGED_LABELLEDBY_ATTRIBUTE, "");
157
+ }
158
+
159
+ return;
160
+ }
161
+
162
+ if (hasManagedLabelledBy) {
163
+ this.removeAttribute("aria-labelledby");
164
+ this.removeAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
165
+ }
166
+
167
+ const hasOwnAriaLabel = this.hasAttribute("aria-label") && !this.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
168
+ const hasOwnLabelledBy = this.hasAttribute("aria-labelledby");
169
+
170
+ if (hasOwnAriaLabel || hasOwnLabelledBy) {
171
+ return;
172
+ }
173
+
174
+ this.setAttribute("aria-label", nextLabel);
175
+ this.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
176
+ }
177
+ }
178
+
179
+ export function defineAlert(registry = globalThis.customElements) {
180
+ if (!registry?.get || !registry?.define) {
181
+ return AlertElement;
182
+ }
183
+
184
+ if (!registry.get(ALERT_TAG_NAME)) {
185
+ registry.define(ALERT_TAG_NAME, AlertElement);
186
+ }
187
+
188
+ return AlertElement;
189
+ }
@@ -0,0 +1,3 @@
1
+ import { defineAlert } from "./index.js";
2
+
3
+ defineAlert();