@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
@@ -0,0 +1,429 @@
1
+ const ElementBase = globalThis.Element ?? class {};
2
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
3
+ const HTMLButtonElementBase = globalThis.HTMLButtonElement ?? class {};
4
+
5
+ export const TOAST_TAG_NAME = "basic-toast";
6
+
7
+ const DEFAULT_DURATION = 5000;
8
+ const DEFAULT_LABEL = "Toast";
9
+ const DEFAULT_LIVE = "polite";
10
+ const PANEL_SELECTOR = "[data-toast-panel]";
11
+ const TITLE_SELECTOR = "[data-toast-title]";
12
+ const OPEN_SELECTOR = "[data-toast-open]";
13
+ const CLOSE_SELECTOR = "[data-toast-close]";
14
+ const MANAGED_LABEL_ATTRIBUTE = "data-basic-toast-managed-label";
15
+ const MANAGED_LABELLEDBY_ATTRIBUTE = "data-basic-toast-managed-labelledby";
16
+ const MANAGED_POPOVER_ATTRIBUTE = "data-basic-toast-managed-popover";
17
+
18
+ let nextToastInstanceId = 1;
19
+
20
+ function collectOwnedElements(root, scope, selector) {
21
+ return Array.from(scope.querySelectorAll(selector)).filter(
22
+ (element) => element instanceof HTMLElementBase && element.closest(TOAST_TAG_NAME) === root,
23
+ );
24
+ }
25
+
26
+ function supportsToastPopover(panel) {
27
+ return panel instanceof HTMLElementBase
28
+ && typeof panel.showPopover === "function"
29
+ && typeof panel.hidePopover === "function";
30
+ }
31
+
32
+ function isToastPanelOpen(panel) {
33
+ if (!(panel instanceof HTMLElementBase)) {
34
+ return false;
35
+ }
36
+
37
+ try {
38
+ return panel.matches(":popover-open");
39
+ } catch {
40
+ return panel.hasAttribute("data-open") || !panel.hidden;
41
+ }
42
+ }
43
+
44
+ export function normalizeToastLabel(value) {
45
+ return value?.trim() || DEFAULT_LABEL;
46
+ }
47
+
48
+ export function normalizeToastLive(value) {
49
+ const normalized = value?.trim().toLowerCase();
50
+ return normalized === "assertive" ? "assertive" : DEFAULT_LIVE;
51
+ }
52
+
53
+ export function getToastRoleForLive(value) {
54
+ return normalizeToastLive(value) === "assertive" ? "alert" : "status";
55
+ }
56
+
57
+ export function normalizeToastDuration(value) {
58
+ if (value == null || value.trim() === "") {
59
+ return DEFAULT_DURATION;
60
+ }
61
+
62
+ const parsed = Number.parseInt(value, 10);
63
+
64
+ if (Number.isInteger(parsed) && parsed >= 0) {
65
+ return parsed;
66
+ }
67
+
68
+ return DEFAULT_DURATION;
69
+ }
70
+
71
+ export function normalizeToastOpen(value) {
72
+ if (value == null) {
73
+ return false;
74
+ }
75
+
76
+ const normalized = value.trim().toLowerCase();
77
+ return normalized === "" || normalized === "true" || normalized === "1";
78
+ }
79
+
80
+ export class ToastElement extends HTMLElementBase {
81
+ static observedAttributes = ["data-duration", "data-label", "data-live", "data-open"];
82
+
83
+ #instanceId = `${TOAST_TAG_NAME}-${nextToastInstanceId++}`;
84
+ #panel = null;
85
+ #panelWithEvents = null;
86
+ #title = null;
87
+ #openButtons = [];
88
+ #closeButtons = [];
89
+ #restoreFocusTo = null;
90
+ #dismissTimer = 0;
91
+ #eventsBound = false;
92
+
93
+ connectedCallback() {
94
+ if (!this.#eventsBound) {
95
+ this.addEventListener("click", this.#handleClick);
96
+ this.addEventListener("keydown", this.#handleKeyDown);
97
+ this.#eventsBound = true;
98
+ }
99
+
100
+ this.#sync();
101
+ }
102
+
103
+ disconnectedCallback() {
104
+ if (!this.#eventsBound) {
105
+ return;
106
+ }
107
+
108
+ this.removeEventListener("click", this.#handleClick);
109
+ this.removeEventListener("keydown", this.#handleKeyDown);
110
+ this.#eventsBound = false;
111
+ this.#syncPanelEvents(null);
112
+ this.#clearDismissTimer();
113
+ }
114
+
115
+ attributeChangedCallback() {
116
+ this.#sync();
117
+ }
118
+
119
+ show(opener = null) {
120
+ this.#sync();
121
+
122
+ if (!(this.#panel instanceof HTMLElementBase)) {
123
+ return false;
124
+ }
125
+
126
+ const fallbackOpener = opener instanceof HTMLElementBase
127
+ ? opener
128
+ : this.ownerDocument?.activeElement instanceof HTMLElementBase
129
+ ? this.ownerDocument.activeElement
130
+ : null;
131
+
132
+ this.#restoreFocusTo = fallbackOpener;
133
+ this.toggleAttribute("data-open", true);
134
+ this.#syncOpenState();
135
+ return true;
136
+ }
137
+
138
+ hide() {
139
+ if (!(this.#panel instanceof HTMLElementBase) || !this.#isOpen()) {
140
+ return false;
141
+ }
142
+
143
+ const shouldRestoreFocus = this.#panel.contains(this.ownerDocument?.activeElement);
144
+
145
+ this.toggleAttribute("data-open", false);
146
+ this.#syncOpenState();
147
+
148
+ if (
149
+ shouldRestoreFocus
150
+ && this.#restoreFocusTo instanceof HTMLElementBase
151
+ && this.#restoreFocusTo.isConnected
152
+ ) {
153
+ this.#restoreFocusTo.focus();
154
+ }
155
+
156
+ this.#restoreFocusTo = null;
157
+ return true;
158
+ }
159
+
160
+ toggle(opener = null) {
161
+ if (this.#isOpen()) {
162
+ return this.hide();
163
+ }
164
+
165
+ return this.show(opener);
166
+ }
167
+
168
+ #handleClick = (event) => {
169
+ if (!(event.target instanceof ElementBase)) {
170
+ return;
171
+ }
172
+
173
+ const openButton = event.target.closest(OPEN_SELECTOR);
174
+
175
+ if (
176
+ openButton instanceof HTMLElementBase
177
+ && openButton.closest(TOAST_TAG_NAME) === this
178
+ ) {
179
+ event.preventDefault();
180
+ this.toggle(openButton);
181
+ return;
182
+ }
183
+
184
+ const closeButton = event.target.closest(CLOSE_SELECTOR);
185
+
186
+ if (
187
+ closeButton instanceof HTMLElementBase
188
+ && closeButton.closest(TOAST_TAG_NAME) === this
189
+ ) {
190
+ event.preventDefault();
191
+ this.hide();
192
+ }
193
+ };
194
+
195
+ #handleKeyDown = (event) => {
196
+ if (
197
+ event.key !== "Escape"
198
+ || !(event.target instanceof ElementBase)
199
+ || event.target.closest(TOAST_TAG_NAME) !== this
200
+ || !(this.#panel instanceof HTMLElementBase)
201
+ || !this.#isOpen()
202
+ ) {
203
+ return;
204
+ }
205
+
206
+ event.preventDefault();
207
+ this.hide();
208
+ };
209
+
210
+ #handlePanelToggle = () => {
211
+ this.#syncStateFromPanel();
212
+ };
213
+
214
+ #sync() {
215
+ const nextPanel = collectOwnedElements(this, this, PANEL_SELECTOR)[0] ?? null;
216
+ const nextTitle = collectOwnedElements(this, this, TITLE_SELECTOR)[0] ?? null;
217
+
218
+ this.#syncPanelEvents(nextPanel instanceof HTMLElementBase ? nextPanel : null);
219
+ this.#panel = nextPanel instanceof HTMLElementBase ? nextPanel : null;
220
+ this.#title = nextTitle instanceof HTMLElementBase ? nextTitle : null;
221
+ this.#openButtons = collectOwnedElements(this, this, OPEN_SELECTOR);
222
+ this.#closeButtons = collectOwnedElements(this, this, CLOSE_SELECTOR);
223
+ this.#applyState();
224
+ }
225
+
226
+ #syncPanelEvents(nextPanel) {
227
+ if (this.#panelWithEvents === nextPanel) {
228
+ return;
229
+ }
230
+
231
+ if (this.#panelWithEvents instanceof HTMLElementBase) {
232
+ this.#panelWithEvents.removeEventListener("toggle", this.#handlePanelToggle);
233
+ }
234
+
235
+ if (nextPanel instanceof HTMLElementBase) {
236
+ nextPanel.addEventListener("toggle", this.#handlePanelToggle);
237
+ }
238
+
239
+ this.#panelWithEvents = nextPanel;
240
+ }
241
+
242
+ #shouldBeOpen() {
243
+ return normalizeToastOpen(this.getAttribute("data-open"));
244
+ }
245
+
246
+ #isOpen() {
247
+ if (!(this.#panel instanceof HTMLElementBase)) {
248
+ return false;
249
+ }
250
+
251
+ if (supportsToastPopover(this.#panel)) {
252
+ return isToastPanelOpen(this.#panel);
253
+ }
254
+
255
+ return !this.#panel.hidden;
256
+ }
257
+
258
+ #applyState() {
259
+ for (const button of [...this.#openButtons, ...this.#closeButtons]) {
260
+ if (button instanceof HTMLButtonElementBase && !button.hasAttribute("type")) {
261
+ button.type = "button";
262
+ }
263
+ }
264
+
265
+ if (!(this.#panel instanceof HTMLElementBase)) {
266
+ this.#clearDismissTimer();
267
+ return;
268
+ }
269
+
270
+ const baseId = this.id || this.#instanceId;
271
+
272
+ if (this.#title instanceof HTMLElementBase && !this.#title.id) {
273
+ this.#title.id = `${baseId}-title`;
274
+ }
275
+
276
+ this.#panel.setAttribute("role", getToastRoleForLive(this.getAttribute("data-live")));
277
+ this.#panel.setAttribute("aria-live", normalizeToastLive(this.getAttribute("data-live")));
278
+ this.#panel.setAttribute("aria-atomic", "true");
279
+ this.#syncAccessibleLabel();
280
+ this.#syncPopoverState();
281
+ this.#syncOpenState();
282
+ }
283
+
284
+ #scheduleDismiss() {
285
+ this.#clearDismissTimer();
286
+
287
+ if (typeof window === "undefined") {
288
+ return;
289
+ }
290
+
291
+ const duration = normalizeToastDuration(this.getAttribute("data-duration"));
292
+
293
+ if (duration === 0) {
294
+ return;
295
+ }
296
+
297
+ this.#dismissTimer = window.setTimeout(() => {
298
+ this.#dismissTimer = 0;
299
+ this.hide();
300
+ }, duration);
301
+ }
302
+
303
+ #clearDismissTimer() {
304
+ if (this.#dismissTimer === 0 || typeof window === "undefined") {
305
+ return;
306
+ }
307
+
308
+ window.clearTimeout(this.#dismissTimer);
309
+ this.#dismissTimer = 0;
310
+ }
311
+
312
+ #syncPopoverState() {
313
+ if (!(this.#panel instanceof HTMLElementBase) || !supportsToastPopover(this.#panel)) {
314
+ return;
315
+ }
316
+
317
+ if (!this.#panel.hasAttribute("popover")) {
318
+ this.#panel.setAttribute("popover", "manual");
319
+ this.#panel.setAttribute(MANAGED_POPOVER_ATTRIBUTE, "");
320
+ }
321
+ }
322
+
323
+ #syncOpenState() {
324
+ if (!(this.#panel instanceof HTMLElementBase)) {
325
+ this.#clearDismissTimer();
326
+ return;
327
+ }
328
+
329
+ const open = this.#shouldBeOpen();
330
+
331
+ if (supportsToastPopover(this.#panel)) {
332
+ this.#panel.hidden = false;
333
+
334
+ try {
335
+ if (open && !isToastPanelOpen(this.#panel)) {
336
+ this.#panel.showPopover();
337
+ } else if (!open && isToastPanelOpen(this.#panel)) {
338
+ this.#panel.hidePopover();
339
+ }
340
+ } catch {
341
+ this.#panel.hidden = !open;
342
+ }
343
+ } else {
344
+ this.#panel.hidden = !open;
345
+ }
346
+
347
+ this.#syncStateFromPanel();
348
+ }
349
+
350
+ #syncStateFromPanel() {
351
+ if (!(this.#panel instanceof HTMLElementBase)) {
352
+ this.#clearDismissTimer();
353
+ return;
354
+ }
355
+
356
+ const open = supportsToastPopover(this.#panel)
357
+ ? isToastPanelOpen(this.#panel)
358
+ : !this.#panel.hidden;
359
+
360
+ this.#panel.hidden = !open;
361
+ this.#panel.toggleAttribute("data-open", open);
362
+ this.toggleAttribute("data-open", open);
363
+
364
+ if (open) {
365
+ this.#scheduleDismiss();
366
+ } else {
367
+ this.#clearDismissTimer();
368
+ }
369
+ }
370
+
371
+ #syncAccessibleLabel() {
372
+ if (!(this.#panel instanceof HTMLElementBase)) {
373
+ return;
374
+ }
375
+
376
+ const nextLabel = normalizeToastLabel(this.getAttribute("data-label"));
377
+ const hasManagedLabelledBy = this.#panel.hasAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
378
+
379
+ if (
380
+ this.#panel.hasAttribute(MANAGED_LABEL_ATTRIBUTE)
381
+ && this.#panel.getAttribute("aria-label") !== nextLabel
382
+ ) {
383
+ this.#panel.removeAttribute("aria-label");
384
+ this.#panel.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
385
+ }
386
+
387
+ if (this.#title?.id) {
388
+ if (this.#panel.hasAttribute(MANAGED_LABEL_ATTRIBUTE)) {
389
+ this.#panel.removeAttribute("aria-label");
390
+ this.#panel.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
391
+ }
392
+
393
+ if (!this.#panel.hasAttribute("aria-labelledby") || hasManagedLabelledBy) {
394
+ this.#panel.setAttribute("aria-labelledby", this.#title.id);
395
+ this.#panel.setAttribute(MANAGED_LABELLEDBY_ATTRIBUTE, "");
396
+ }
397
+
398
+ return;
399
+ }
400
+
401
+ if (hasManagedLabelledBy) {
402
+ this.#panel.removeAttribute("aria-labelledby");
403
+ this.#panel.removeAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
404
+ }
405
+
406
+ const hasOwnAriaLabel = this.#panel.hasAttribute("aria-label")
407
+ && !this.#panel.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
408
+ const hasOwnLabelledBy = this.#panel.hasAttribute("aria-labelledby");
409
+
410
+ if (hasOwnAriaLabel || hasOwnLabelledBy) {
411
+ return;
412
+ }
413
+
414
+ this.#panel.setAttribute("aria-label", nextLabel);
415
+ this.#panel.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
416
+ }
417
+ }
418
+
419
+ export function defineToast(registry = globalThis.customElements) {
420
+ if (!registry?.get || !registry?.define) {
421
+ return ToastElement;
422
+ }
423
+
424
+ if (!registry.get(TOAST_TAG_NAME)) {
425
+ registry.define(TOAST_TAG_NAME, ToastElement);
426
+ }
427
+
428
+ return ToastElement;
429
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { defineToast } from "./index.js";
2
+
3
+ defineToast();
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./components/basic-alert";
1
2
  export * from "./components/basic-accordion";
2
3
  export * from "./components/basic-dialog";
3
4
  export * from "./components/basic-popover";
@@ -5,3 +6,4 @@ export * from "./components/basic-summary-table";
5
6
  export * from "./components/basic-table";
6
7
  export * from "./components/basic-toc";
7
8
  export * from "./components/basic-tabs";
9
+ export * from "./components/basic-toast";
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./components/basic-alert/index.js";
1
2
  export * from "./components/basic-accordion/index.js";
2
3
  export * from "./components/basic-dialog/index.js";
3
4
  export * from "./components/basic-popover/index.js";
@@ -5,3 +6,4 @@ export * from "./components/basic-summary-table/index.js";
5
6
  export * from "./components/basic-table/index.js";
6
7
  export * from "./components/basic-toc/index.js";
7
8
  export * from "./components/basic-tabs/index.js";
9
+ export * from "./components/basic-toast/index.js";
package/package.json CHANGED
@@ -1,92 +1,57 @@
1
1
  {
2
2
  "name": "@lmfaole/basics",
3
- "version": "0.3.0",
4
- "description": "Simple unstyled custom elements and DOM helpers.",
3
+ "version": "0.4.0",
4
+ "description": "Simple custom elements and DOM helpers with optional starter styles.",
5
5
  "type": "module",
6
6
  "sideEffects": [
7
+ "./basic-styling/**/*.css",
8
+ "./components/basic-alert/register.js",
7
9
  "./components/basic-accordion/register.js",
8
10
  "./components/basic-dialog/register.js",
9
11
  "./components/basic-popover/register.js",
10
12
  "./components/basic-summary-table/register.js",
11
13
  "./components/basic-table/register.js",
12
14
  "./components/basic-toc/register.js",
13
- "./components/basic-tabs/register.js"
15
+ "./components/basic-tabs/register.js",
16
+ "./components/basic-toast/register.js"
14
17
  ],
15
18
  "exports": {
16
19
  ".": {
17
20
  "types": "./index.d.ts",
18
21
  "import": "./index.js"
19
22
  },
20
- "./components/basic-accordion": {
21
- "types": "./components/basic-accordion/index.d.ts",
22
- "import": "./components/basic-accordion/index.js"
23
+ "./basic-styling": "./basic-styling/index.css",
24
+ "./basic-styling/*.css": "./basic-styling/*.css",
25
+ "./basic-styling/components/*.css": "./basic-styling/components/*.css",
26
+ "./basic-styling/tokens/*.css": "./basic-styling/tokens/*.css",
27
+ "./basic-styling/tokens/*.json": "./basic-styling/tokens/*.json",
28
+ "./components/*/register": {
29
+ "types": "./components/*/register.d.ts",
30
+ "import": "./components/*/register.js"
23
31
  },
24
- "./components/basic-accordion/register": {
25
- "types": "./components/basic-accordion/register.d.ts",
26
- "import": "./components/basic-accordion/register.js"
27
- },
28
- "./components/basic-dialog": {
29
- "types": "./components/basic-dialog/index.d.ts",
30
- "import": "./components/basic-dialog/index.js"
31
- },
32
- "./components/basic-dialog/register": {
33
- "types": "./components/basic-dialog/register.d.ts",
34
- "import": "./components/basic-dialog/register.js"
35
- },
36
- "./components/basic-popover": {
37
- "types": "./components/basic-popover/index.d.ts",
38
- "import": "./components/basic-popover/index.js"
39
- },
40
- "./components/basic-popover/register": {
41
- "types": "./components/basic-popover/register.d.ts",
42
- "import": "./components/basic-popover/register.js"
43
- },
44
- "./components/basic-summary-table": {
45
- "types": "./components/basic-summary-table/index.d.ts",
46
- "import": "./components/basic-summary-table/index.js"
47
- },
48
- "./components/basic-summary-table/register": {
49
- "types": "./components/basic-summary-table/register.d.ts",
50
- "import": "./components/basic-summary-table/register.js"
51
- },
52
- "./components/basic-table": {
53
- "types": "./components/basic-table/index.d.ts",
54
- "import": "./components/basic-table/index.js"
55
- },
56
- "./components/basic-table/register": {
57
- "types": "./components/basic-table/register.d.ts",
58
- "import": "./components/basic-table/register.js"
59
- },
60
- "./components/basic-toc": {
61
- "types": "./components/basic-toc/index.d.ts",
62
- "import": "./components/basic-toc/index.js"
63
- },
64
- "./components/basic-toc/register": {
65
- "types": "./components/basic-toc/register.d.ts",
66
- "import": "./components/basic-toc/register.js"
67
- },
68
- "./components/basic-tabs": {
69
- "types": "./components/basic-tabs/index.d.ts",
70
- "import": "./components/basic-tabs/index.js"
71
- },
72
- "./components/basic-tabs/register": {
73
- "types": "./components/basic-tabs/register.d.ts",
74
- "import": "./components/basic-tabs/register.js"
32
+ "./components/*": {
33
+ "types": "./components/*/index.d.ts",
34
+ "import": "./components/*/index.js"
75
35
  }
76
36
  },
77
37
  "files": [
78
38
  "README.md",
79
39
  "index.js",
80
40
  "index.d.ts",
41
+ "basic-styling/**/*.css",
42
+ "basic-styling/**/*.json",
81
43
  "components/*/index.js",
82
44
  "components/*/index.d.ts",
83
45
  "components/*/register.js",
84
46
  "components/*/register.d.ts"
85
47
  ],
86
48
  "keywords": [
49
+ "alert",
87
50
  "accordion",
88
51
  "custom-element",
89
52
  "dialog",
53
+ "toast",
54
+ "notification",
90
55
  "basic-summary-table",
91
56
  "basic-tabs",
92
57
  "basic-table",