@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
@@ -12,6 +12,22 @@ const GENERATED_ROW_HEADER_ATTRIBUTE = "data-basic-table-generated-row-header";
12
12
  const GENERATED_DESCRIPTION_ATTRIBUTE = "data-basic-table-generated-description";
13
13
  const MANAGED_HEADERS_ATTRIBUTE = "data-basic-table-managed-headers";
14
14
  const MANAGED_LABEL_ATTRIBUTE = "data-basic-table-managed-label";
15
+ const TABLE_OBSERVER_OPTIONS = {
16
+ childList: true,
17
+ subtree: true,
18
+ characterData: true,
19
+ attributes: true,
20
+ attributeFilter: [
21
+ "aria-describedby",
22
+ "aria-label",
23
+ "aria-labelledby",
24
+ "colspan",
25
+ "headers",
26
+ "id",
27
+ "rowspan",
28
+ "scope",
29
+ ],
30
+ };
15
31
 
16
32
  let nextTableInstanceId = 1;
17
33
 
@@ -198,6 +214,21 @@ function replaceCellTag(cell, tagName, generatedAttributeName) {
198
214
  return replacement;
199
215
  }
200
216
 
217
+ function demoteManagedHeaderCell(cell, generatedAttributeName) {
218
+ const replacement = document.createElement("td");
219
+
220
+ for (const attribute of Array.from(cell.attributes)) {
221
+ if (attribute.name === generatedAttributeName || attribute.name === "scope") {
222
+ continue;
223
+ }
224
+
225
+ replacement.setAttribute(attribute.name, attribute.value);
226
+ }
227
+
228
+ replacement.replaceChildren(...Array.from(cell.childNodes));
229
+ cell.parentElement?.replaceChild(replacement, cell);
230
+ }
231
+
201
232
  function promoteFirstRowCellsToHeaders(table) {
202
233
  const firstRow = table.rows[0];
203
234
 
@@ -214,38 +245,41 @@ function promoteFirstRowCellsToHeaders(table) {
214
245
  }
215
246
  }
216
247
 
217
- function promoteBodyCellsToRowHeaders(table, rowHeaderColumnIndex) {
218
- for (const section of Array.from(table.tBodies)) {
219
- for (const row of Array.from(section.rows)) {
220
- const targetCell = findCellAtColumnIndex(row, rowHeaderColumnIndex);
221
-
222
- if (!(targetCell instanceof HTMLTableCellElementBase) || targetCell.tagName === "TH") {
223
- continue;
224
- }
225
-
226
- replaceCellTag(targetCell, "th", GENERATED_ROW_HEADER_ATTRIBUTE);
227
- }
228
- }
229
- }
230
-
231
248
  function demoteManagedHeaders(table, generatedAttributeName) {
232
249
  for (const cell of Array.from(table.querySelectorAll(`th[${generatedAttributeName}]`))) {
233
250
  if (!(cell instanceof HTMLTableCellElementBase)) {
234
251
  continue;
235
252
  }
236
253
 
237
- const replacement = document.createElement("td");
254
+ demoteManagedHeaderCell(cell, generatedAttributeName);
255
+ }
256
+ }
238
257
 
239
- for (const attribute of Array.from(cell.attributes)) {
240
- if (attribute.name === generatedAttributeName || attribute.name === "scope") {
258
+ function syncBodyRowHeaders(table, rowHeaderColumnIndex, rowHeadersEnabled) {
259
+ for (const section of Array.from(table.tBodies)) {
260
+ for (const row of Array.from(section.rows)) {
261
+ const targetCell = rowHeadersEnabled
262
+ ? findCellAtColumnIndex(row, rowHeaderColumnIndex)
263
+ : null;
264
+
265
+ for (const cell of Array.from(row.cells)) {
266
+ if (
267
+ !(cell instanceof HTMLTableCellElementBase)
268
+ || !cell.hasAttribute(GENERATED_ROW_HEADER_ATTRIBUTE)
269
+ || cell === targetCell
270
+ ) {
271
+ continue;
272
+ }
273
+
274
+ demoteManagedHeaderCell(cell, GENERATED_ROW_HEADER_ATTRIBUTE);
275
+ }
276
+
277
+ if (!(targetCell instanceof HTMLTableCellElementBase) || targetCell.tagName === "TH") {
241
278
  continue;
242
279
  }
243
280
 
244
- replacement.setAttribute(attribute.name, attribute.value);
281
+ replaceCellTag(targetCell, "th", GENERATED_ROW_HEADER_ATTRIBUTE);
245
282
  }
246
-
247
- replacement.replaceChildren(...Array.from(cell.childNodes));
248
- cell.parentElement?.replaceChild(replacement, cell);
249
283
  }
250
284
  }
251
285
 
@@ -256,6 +290,12 @@ function createGeneratedCaption(table) {
256
290
  return caption;
257
291
  }
258
292
 
293
+ function syncTextContent(node, text) {
294
+ if (node.textContent !== text) {
295
+ node.textContent = text;
296
+ }
297
+ }
298
+
259
299
  function syncTableCaption(table, captionText) {
260
300
  const existingCaption = table.caption;
261
301
  const generatedCaption = existingCaption?.hasAttribute(GENERATED_CAPTION_ATTRIBUTE)
@@ -272,18 +312,12 @@ function syncTableCaption(table, captionText) {
272
312
  }
273
313
 
274
314
  const caption = generatedCaption ?? createGeneratedCaption(table);
275
- caption.textContent = captionText;
315
+ syncTextContent(caption, captionText);
276
316
  }
277
317
 
278
318
  function syncFallbackAccessibleName(table, label) {
279
319
  const hasCaption = Boolean(table.caption);
280
- let hasManagedLabel = table.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
281
-
282
- if (hasManagedLabel && table.getAttribute("aria-label") !== label) {
283
- table.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
284
- hasManagedLabel = false;
285
- }
286
-
320
+ const hasManagedLabel = table.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
287
321
  const hasOwnAriaLabel = table.hasAttribute("aria-label") && !hasManagedLabel;
288
322
  const hasOwnLabelledBy = table.hasAttribute("aria-labelledby");
289
323
 
@@ -296,8 +330,13 @@ function syncFallbackAccessibleName(table, label) {
296
330
  return;
297
331
  }
298
332
 
299
- table.setAttribute("aria-label", label);
300
- table.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
333
+ if (table.getAttribute("aria-label") !== label) {
334
+ table.setAttribute("aria-label", label);
335
+ }
336
+
337
+ if (!hasManagedLabel) {
338
+ table.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
339
+ }
301
340
  }
302
341
 
303
342
  function getGeneratedDescription(root) {
@@ -321,7 +360,11 @@ function syncTableDescription(root, table, descriptionText, baseId) {
321
360
  );
322
361
 
323
362
  if (tokens.length > 0) {
324
- table.setAttribute("aria-describedby", tokens.join(" "));
363
+ const nextValue = tokens.join(" ");
364
+
365
+ if (table.getAttribute("aria-describedby") !== nextValue) {
366
+ table.setAttribute("aria-describedby", nextValue);
367
+ }
325
368
  } else {
326
369
  table.removeAttribute("aria-describedby");
327
370
  }
@@ -343,13 +386,17 @@ function syncTableDescription(root, table, descriptionText, baseId) {
343
386
  description.id = `${baseId}-description`;
344
387
  }
345
388
 
346
- description.textContent = descriptionText;
389
+ syncTextContent(description, descriptionText);
347
390
 
348
391
  const tokens = getAriaReferenceTokens(table.getAttribute("aria-describedby")).filter(
349
392
  (token) => token !== description.id,
350
393
  );
351
394
  tokens.push(description.id);
352
- table.setAttribute("aria-describedby", tokens.join(" "));
395
+ const nextValue = tokens.join(" ");
396
+
397
+ if (table.getAttribute("aria-describedby") !== nextValue) {
398
+ table.setAttribute("aria-describedby", nextValue);
399
+ }
353
400
  }
354
401
 
355
402
  export class TableElement extends HTMLElementBase {
@@ -364,6 +411,7 @@ export class TableElement extends HTMLElementBase {
364
411
 
365
412
  #instanceId = `${TABLE_TAG_NAME}-${nextTableInstanceId++}`;
366
413
  #observer = null;
414
+ #observing = false;
367
415
  #scheduledFrame = 0;
368
416
 
369
417
  connectedCallback() {
@@ -372,7 +420,7 @@ export class TableElement extends HTMLElementBase {
372
420
  }
373
421
 
374
422
  disconnectedCallback() {
375
- this.#observer?.disconnect();
423
+ this.#stopObserving();
376
424
  this.#observer = null;
377
425
 
378
426
  if (this.#scheduledFrame !== 0 && typeof window !== "undefined") {
@@ -386,119 +434,126 @@ export class TableElement extends HTMLElementBase {
386
434
  }
387
435
 
388
436
  refresh() {
389
- const table = collectOwnedTables(this)[0] ?? null;
437
+ this.#stopObserving();
390
438
 
391
- if (!(table instanceof HTMLTableElementBase)) {
392
- return;
393
- }
439
+ try {
440
+ const table = collectOwnedTables(this)[0] ?? null;
394
441
 
395
- const label = normalizeTableLabel(this.getAttribute("data-label"));
396
- const caption = normalizeTableCaption(this.getAttribute("data-caption"));
397
- const description = normalizeTableDescription(this.getAttribute("data-description"));
398
- const columnHeadersEnabled = normalizeTableColumnHeaders(
399
- this.getAttribute("data-column-headers") ?? this.hasAttribute("data-column-headers"),
400
- );
401
- const rowHeaderColumnIndex = normalizeTableRowHeaderColumn(
402
- this.getAttribute("data-row-header-column"),
403
- ) - 1;
404
- const rowHeadersEnabled = normalizeTableRowHeaders(
405
- this.getAttribute("data-row-headers") ?? this.hasAttribute("data-row-header-column"),
406
- );
407
- const baseId = this.id || this.#instanceId;
408
-
409
- syncTableCaption(table, caption);
410
- syncFallbackAccessibleName(table, label);
411
- syncTableDescription(this, table, description, baseId);
412
-
413
- if (columnHeadersEnabled) {
414
- promoteFirstRowCellsToHeaders(table);
415
- } else {
416
- demoteManagedHeaders(table, GENERATED_COLUMN_HEADER_ATTRIBUTE);
417
- }
442
+ if (!(table instanceof HTMLTableElementBase)) {
443
+ return;
444
+ }
418
445
 
419
- if (rowHeadersEnabled) {
420
- demoteManagedHeaders(table, GENERATED_ROW_HEADER_ATTRIBUTE);
421
- promoteBodyCellsToRowHeaders(table, rowHeaderColumnIndex);
422
- } else {
423
- demoteManagedHeaders(table, GENERATED_ROW_HEADER_ATTRIBUTE);
424
- }
446
+ const label = normalizeTableLabel(this.getAttribute("data-label"));
447
+ const caption = normalizeTableCaption(this.getAttribute("data-caption"));
448
+ const description = normalizeTableDescription(this.getAttribute("data-description"));
449
+ const columnHeadersEnabled = normalizeTableColumnHeaders(
450
+ this.getAttribute("data-column-headers") ?? this.hasAttribute("data-column-headers"),
451
+ );
452
+ const rowHeaderColumnIndex = normalizeTableRowHeaderColumn(
453
+ this.getAttribute("data-row-header-column"),
454
+ ) - 1;
455
+ const rowHeadersEnabled = normalizeTableRowHeaders(
456
+ this.getAttribute("data-row-headers") ?? this.hasAttribute("data-row-header-column"),
457
+ );
458
+ const baseId = this.id || this.#instanceId;
425
459
 
426
- const placements = buildTablePlacements(table);
427
- const headerPlacements = [];
428
- let nextHeaderIndex = 1;
460
+ syncTableCaption(table, caption);
461
+ syncFallbackAccessibleName(table, label);
462
+ syncTableDescription(this, table, description, baseId);
429
463
 
430
- for (const placement of placements) {
431
- if (placement.cell.tagName !== "TH") {
432
- continue;
464
+ if (columnHeadersEnabled) {
465
+ promoteFirstRowCellsToHeaders(table);
466
+ } else {
467
+ demoteManagedHeaders(table, GENERATED_COLUMN_HEADER_ATTRIBUTE);
433
468
  }
434
469
 
435
- const scope = inferHeaderScope(placement.cell, placement, {
436
- columnHeadersEnabled,
437
- rowHeadersEnabled,
438
- rowHeaderColumnIndex,
439
- });
470
+ syncBodyRowHeaders(table, rowHeaderColumnIndex, rowHeadersEnabled);
440
471
 
441
- if (scope) {
442
- placement.cell.setAttribute("scope", scope);
443
- }
472
+ const placements = buildTablePlacements(table);
473
+ const headerPlacements = [];
474
+ let nextHeaderIndex = 1;
444
475
 
445
- headerPlacements.push({
446
- ...placement,
447
- scope,
448
- id: ensureHeaderId(placement.cell, this.id || this.#instanceId, nextHeaderIndex),
449
- });
450
- nextHeaderIndex += 1;
451
- }
476
+ for (const placement of placements) {
477
+ if (placement.cell.tagName !== "TH") {
478
+ continue;
479
+ }
452
480
 
453
- headerPlacements.sort(sortPlacementsInDocumentOrder);
481
+ const scope = inferHeaderScope(placement.cell, placement, {
482
+ columnHeadersEnabled,
483
+ rowHeadersEnabled,
484
+ rowHeaderColumnIndex,
485
+ });
454
486
 
455
- for (const placement of placements) {
456
- if (placement.cell.tagName !== "TD") {
457
- continue;
487
+ if (scope) {
488
+ if (placement.cell.getAttribute("scope") !== scope) {
489
+ placement.cell.setAttribute("scope", scope);
490
+ }
491
+ }
492
+
493
+ headerPlacements.push({
494
+ ...placement,
495
+ scope,
496
+ id: ensureHeaderId(placement.cell, this.id || this.#instanceId, nextHeaderIndex),
497
+ });
498
+ nextHeaderIndex += 1;
458
499
  }
459
500
 
460
- const associatedHeaders = headerPlacements.filter((header) => {
461
- switch (header.scope) {
462
- case "col":
463
- case "colgroup":
464
- return (
465
- header.rowIndex < placement.rowIndex
466
- && header.columnIndex < placement.columnIndex + placement.colSpan
467
- && header.columnIndex + header.colSpan > placement.columnIndex
468
- );
469
- case "row":
470
- case "rowgroup":
471
- return (
472
- header.columnIndex < placement.columnIndex
473
- && header.rowIndex < placement.rowIndex + placement.rowSpan
474
- && header.rowIndex + header.rowSpan > placement.rowIndex
475
- );
476
- default:
477
- return false;
501
+ headerPlacements.sort(sortPlacementsInDocumentOrder);
502
+
503
+ for (const placement of placements) {
504
+ if (placement.cell.tagName !== "TD") {
505
+ continue;
478
506
  }
479
- });
480
507
 
481
- if (associatedHeaders.length === 0) {
482
- if (placement.cell.hasAttribute(MANAGED_HEADERS_ATTRIBUTE)) {
483
- placement.cell.removeAttribute("headers");
484
- placement.cell.removeAttribute(MANAGED_HEADERS_ATTRIBUTE);
508
+ const associatedHeaders = headerPlacements.filter((header) => {
509
+ switch (header.scope) {
510
+ case "col":
511
+ case "colgroup":
512
+ return (
513
+ header.rowIndex < placement.rowIndex
514
+ && header.columnIndex < placement.columnIndex + placement.colSpan
515
+ && header.columnIndex + header.colSpan > placement.columnIndex
516
+ );
517
+ case "row":
518
+ case "rowgroup":
519
+ return (
520
+ header.columnIndex < placement.columnIndex
521
+ && header.rowIndex < placement.rowIndex + placement.rowSpan
522
+ && header.rowIndex + header.rowSpan > placement.rowIndex
523
+ );
524
+ default:
525
+ return false;
526
+ }
527
+ });
528
+
529
+ if (associatedHeaders.length === 0) {
530
+ if (placement.cell.hasAttribute(MANAGED_HEADERS_ATTRIBUTE)) {
531
+ placement.cell.removeAttribute("headers");
532
+ placement.cell.removeAttribute(MANAGED_HEADERS_ATTRIBUTE);
533
+ }
534
+
535
+ continue;
485
536
  }
486
537
 
487
- continue;
488
- }
538
+ if (
539
+ placement.cell.hasAttribute("headers")
540
+ && !placement.cell.hasAttribute(MANAGED_HEADERS_ATTRIBUTE)
541
+ ) {
542
+ continue;
543
+ }
489
544
 
490
- if (
491
- placement.cell.hasAttribute("headers")
492
- && !placement.cell.hasAttribute(MANAGED_HEADERS_ATTRIBUTE)
493
- ) {
494
- continue;
495
- }
545
+ const nextHeaders = associatedHeaders.map((header) => header.id).join(" ");
496
546
 
497
- placement.cell.setAttribute(
498
- "headers",
499
- associatedHeaders.map((header) => header.id).join(" "),
500
- );
501
- placement.cell.setAttribute(MANAGED_HEADERS_ATTRIBUTE, "");
547
+ if (placement.cell.getAttribute("headers") !== nextHeaders) {
548
+ placement.cell.setAttribute("headers", nextHeaders);
549
+ }
550
+
551
+ if (!placement.cell.hasAttribute(MANAGED_HEADERS_ATTRIBUTE)) {
552
+ placement.cell.setAttribute(MANAGED_HEADERS_ATTRIBUTE, "");
553
+ }
554
+ }
555
+ } finally {
556
+ this.#startObserving();
502
557
  }
503
558
  }
504
559
 
@@ -522,22 +577,25 @@ export class TableElement extends HTMLElementBase {
522
577
  this.#scheduleRefresh();
523
578
  });
524
579
 
525
- this.#observer.observe(this, {
526
- childList: true,
527
- subtree: true,
528
- characterData: true,
529
- attributes: true,
530
- attributeFilter: [
531
- "aria-describedby",
532
- "aria-label",
533
- "aria-labelledby",
534
- "colspan",
535
- "headers",
536
- "id",
537
- "rowspan",
538
- "scope",
539
- ],
540
- });
580
+ this.#startObserving();
581
+ }
582
+
583
+ #startObserving() {
584
+ if (!this.#observer || this.#observing) {
585
+ return;
586
+ }
587
+
588
+ this.#observer.observe(this, TABLE_OBSERVER_OPTIONS);
589
+ this.#observing = true;
590
+ }
591
+
592
+ #stopObserving() {
593
+ if (!this.#observer || !this.#observing) {
594
+ return;
595
+ }
596
+
597
+ this.#observer.disconnect();
598
+ this.#observing = false;
541
599
  }
542
600
  }
543
601
 
@@ -0,0 +1,63 @@
1
+ # `basic-tabs`
2
+
3
+ Accessible tablists and panels from existing markup.
4
+
5
+ ## Register
6
+
7
+ ```js
8
+ import "@lmfaole/basics/basic-components/basic-tabs/register";
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```html
14
+ <basic-tabs data-label="Eksempelkode">
15
+ <div data-tabs-list>
16
+ <button type="button" data-tab>Oversikt</button>
17
+ <button type="button" data-tab>Implementasjon</button>
18
+ <button type="button" data-tab>Tilgjengelighet</button>
19
+ </div>
20
+
21
+ <section data-tab-panel>
22
+ <p>Viser en kort oppsummering.</p>
23
+ </section>
24
+ <section data-tab-panel>
25
+ <p>Viser implementasjonsdetaljer.</p>
26
+ </section>
27
+ <section data-tab-panel>
28
+ <p>Viser tilgjengelighetsnotater.</p>
29
+ </section>
30
+ </basic-tabs>
31
+ ```
32
+
33
+ ## Props
34
+
35
+ | Prop | Description | Type | Default | Options |
36
+ | --- | --- | --- | --- | --- |
37
+ | `data-label` | Accessible name for the generated tablist when it does not already have one. | string | `Faner` | any string |
38
+ | `data-activation` | Chooses whether arrow-key focus changes also activate the selected panel. | enum string | `automatic` | `automatic`, `manual` |
39
+ | `data-selected-index` | Sets the initially selected tab by index. Invalid values fall back to the first enabled tab. | zero-based integer | first enabled tab | zero-based integer |
40
+
41
+ ## Markup Hooks
42
+
43
+ | Hook | Description | Type | Default | Options |
44
+ | --- | --- | --- | --- | --- |
45
+ | `data-tabs-list` | Holds the interactive tab controls. | descendant container attribute | required | present on one descendant container |
46
+ | `data-tab` | Marks each tab control. Prefer `<button>` elements. | descendant control attribute | required | present on matching descendant controls |
47
+ | `data-tab-panel` | Marks each panel in the same order as the tabs. | descendant panel attribute | required | present on matching descendant panels |
48
+ | `disabled` on a `[data-tab]` control | Removes that tab from keyboard navigation and selection. | boolean attribute | off | `present`, `omitted` |
49
+
50
+ ## Behavior
51
+
52
+ - Generates missing tab and panel ids
53
+ - Keeps `aria-selected`, `aria-controls`, `aria-labelledby`, `hidden`, and `data-selected` in sync
54
+ - Supports click, `ArrowLeft`, `ArrowRight`, `Home`, and `End`
55
+ - Skips disabled tabs during keyboard navigation
56
+ - In `manual` mode, arrow keys move focus and `Enter` or `Space` activates
57
+
58
+ ## Markup Contract
59
+
60
+ - Provide one descendant element with `data-tabs-list`
61
+ - Provide matching counts of `[data-tab]` and `[data-tab-panel]` in the same order
62
+ - Prefer `<button>` elements for tabs
63
+ - Keep layout and styling outside the package
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ # `basic-toast`
2
+
3
+ Toast notifications with managed open state and live-region announcements.
4
+
5
+ ## Register
6
+
7
+ ```js
8
+ import "@lmfaole/basics/basic-components/basic-toast/register";
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```html
14
+ <basic-toast data-label="Lagring fullfort" data-duration="5000">
15
+ <button type="button" data-toast-open>Show toast</button>
16
+
17
+ <section data-toast-panel>
18
+ <h2 data-toast-title>Lagret</h2>
19
+ <p>Meldingen ble lagret uten feil.</p>
20
+ </section>
21
+ </basic-toast>
22
+ ```
23
+
24
+ ## Props
25
+
26
+ | Prop | Description | Type | Default | Options |
27
+ | --- | --- | --- | --- | --- |
28
+ | `data-label` | Fallback accessible name when the toast panel has no `aria-label`, `aria-labelledby`, or `[data-toast-title]`. | string | `Toast` | any string |
29
+ | `data-live` | Chooses whether the announcement behaves like `status` or `alert`. | enum string | `polite` | `polite`, `assertive` |
30
+ | `data-duration` | Auto-dismiss timeout for the toast. | non-negative integer | `5000` | non-negative integer milliseconds, `0` disables auto-dismiss |
31
+ | `data-open` | Optional initial open state for the managed toast panel. | boolean-ish attribute | closed | `present`, `omitted`, `false` |
32
+
33
+ ## Starter Styling Prop
34
+
35
+ | Prop | Description | Type | Default | Options |
36
+ | --- | --- | --- | --- | --- |
37
+ | `data-toast-position` | Optional starter-CSS hook for viewport placement when you import `basic-styling`. | enum string | none | `top-left`, `top-center`, `top-right`, `center-left`, `center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` |
38
+
39
+ ## Markup Hooks
40
+
41
+ | Hook | Description | Type | Default | Options |
42
+ | --- | --- | --- | --- | --- |
43
+ | `data-toast-open` | Shows or toggles the toast. | descendant control attribute | none | present on a descendant button or control |
44
+ | `data-toast-panel` | Marks the message container managed by the component. | descendant element attribute | required | present on one descendant element |
45
+ | `data-toast-title` | Makes the visible heading the toast's accessible name. | descendant heading attribute | none | present on a descendant heading |
46
+ | `data-toast-close` | Closes the toast when activated. | descendant control attribute | none | present on a descendant control outside `[data-toast-panel]` |
47
+
48
+ ## Behavior
49
+
50
+ - Uses the Popover API in manual mode when available so the toast can render in the top layer
51
+ - Announces the current toast text through an internal live region whenever the toast opens
52
+ - Syncs `hidden` and `data-open` on the panel and root element
53
+ - Uses `[data-toast-title]` as the accessible name when present, otherwise falls back to `data-label`
54
+ - Supports `show()`, `hide()`, and `toggle()`
55
+
56
+ ## Markup Contract
57
+
58
+ - Provide one descendant `[data-toast-panel]`
59
+ - Keep `[data-toast-panel]` to non-interactive message content
60
+ - Use `[data-toast-open]` on buttons that should show or toggle the toast
61
+ - If you need an explicit dismiss control, place `[data-toast-close]` outside `[data-toast-panel]`
62
+ - Keep layout and styling outside the package
@@ -0,0 +1,68 @@
1
+ export const TOAST_TAG_NAME: "basic-toast";
2
+
3
+ /**
4
+ * Normalizes unsupported or empty labels back to the default `"Toast"`.
5
+ */
6
+ export function normalizeToastLabel(
7
+ value?: string | null,
8
+ ): string;
9
+
10
+ /**
11
+ * Normalizes unsupported live-region values back to `"polite"`.
12
+ */
13
+ export function normalizeToastLive(
14
+ value?: string | null,
15
+ ): "assertive" | "polite";
16
+
17
+ /**
18
+ * Maps a toast live setting to the matching ARIA role.
19
+ */
20
+ export function getToastRoleForLive(
21
+ value?: string | null,
22
+ ): "alert" | "status";
23
+
24
+ /**
25
+ * Normalizes the optional `data-duration` attribute into milliseconds.
26
+ * A value of `0` disables auto-dismiss.
27
+ */
28
+ export function normalizeToastDuration(
29
+ value?: string | null,
30
+ ): number;
31
+
32
+ /**
33
+ * Normalizes the optional `data-open` attribute into a boolean flag.
34
+ */
35
+ export function normalizeToastOpen(
36
+ value?: string | null,
37
+ ): boolean;
38
+
39
+ /**
40
+ * Custom element that upgrades trigger-and-panel markup into a toast
41
+ * notification flow with optional auto-dismiss.
42
+ *
43
+ * Attributes:
44
+ * - `data-label`: fallback accessible name when the toast has no title
45
+ * - `data-live`: chooses between `status` and `alert` semantics
46
+ * - `data-duration`: auto-dismiss timeout in milliseconds, `0` disables it
47
+ * - `data-open`: optional initial open state
48
+ *
49
+ * Behavior:
50
+ * - uses the Popover API in manual mode when available so the toast panel can
51
+ * render in the top layer
52
+ * - announces the current toast text through an internal live region whenever
53
+ * the toast opens
54
+ * - expects `[data-toast-panel]` to contain non-interactive message content
55
+ */
56
+ export class ToastElement extends HTMLElement {
57
+ static observedAttributes: string[];
58
+ show(opener?: HTMLElement | null): boolean;
59
+ hide(): boolean;
60
+ toggle(opener?: HTMLElement | null): boolean;
61
+ }
62
+
63
+ /**
64
+ * Registers the `basic-toast` custom element if it is not already defined.
65
+ */
66
+ export function defineToast(
67
+ registry?: CustomElementRegistry,
68
+ ): typeof ToastElement;