@lmfaole/basics 0.3.0 → 0.4.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 (31) hide show
  1. package/README.md +182 -33
  2. package/basic-styling/components/basic-accordion.css +65 -0
  3. package/basic-styling/components/basic-alert.css +27 -0
  4. package/basic-styling/components/basic-dialog.css +41 -0
  5. package/basic-styling/components/basic-popover.css +54 -0
  6. package/basic-styling/components/basic-summary-table.css +76 -0
  7. package/basic-styling/components/basic-table.css +48 -0
  8. package/basic-styling/components/basic-tabs.css +45 -0
  9. package/basic-styling/components/basic-toast.css +102 -0
  10. package/basic-styling/components/basic-toc.css +30 -0
  11. package/basic-styling/components.css +9 -0
  12. package/basic-styling/global.css +61 -0
  13. package/basic-styling/index.css +2 -0
  14. package/basic-styling/tokens/base.css +19 -0
  15. package/basic-styling/tokens/palette.css +117 -0
  16. package/basic-styling/tokens/palette.tokens.json +1019 -0
  17. package/components/basic-accordion/index.d.ts +5 -5
  18. package/components/basic-accordion/index.js +169 -165
  19. package/components/basic-alert/index.d.ts +53 -0
  20. package/components/basic-alert/index.js +189 -0
  21. package/components/basic-alert/register.d.ts +1 -0
  22. package/components/basic-alert/register.js +3 -0
  23. package/components/basic-summary-table/index.js +188 -42
  24. package/components/basic-table/index.js +203 -145
  25. package/components/basic-toast/index.d.ts +65 -0
  26. package/components/basic-toast/index.js +429 -0
  27. package/components/basic-toast/register.d.ts +1 -0
  28. package/components/basic-toast/register.js +3 -0
  29. package/index.d.ts +2 -0
  30. package/index.js +2 -0
  31. package/package.json +22 -57
@@ -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,65 @@
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
+ */
53
+ export class ToastElement extends HTMLElement {
54
+ static observedAttributes: string[];
55
+ show(opener?: HTMLElement | null): boolean;
56
+ hide(): boolean;
57
+ toggle(opener?: HTMLElement | null): boolean;
58
+ }
59
+
60
+ /**
61
+ * Registers the `basic-toast` custom element if it is not already defined.
62
+ */
63
+ export function defineToast(
64
+ registry?: CustomElementRegistry,
65
+ ): typeof ToastElement;