@lmfaole/basics 0.4.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -499
  3. package/basic-components/basic-accordion/README.md +53 -0
  4. package/{components → basic-components}/basic-accordion/index.js +59 -37
  5. package/basic-components/basic-alert/README.md +48 -0
  6. package/basic-components/basic-carousel/README.md +108 -0
  7. package/basic-components/basic-carousel/index.d.ts +73 -0
  8. package/basic-components/basic-carousel/index.js +255 -0
  9. package/basic-components/basic-carousel/register.js +3 -0
  10. package/basic-components/basic-dialog/README.md +57 -0
  11. package/basic-components/basic-popover/README.md +56 -0
  12. package/basic-components/basic-summary-table/README.md +93 -0
  13. package/basic-components/basic-table/README.md +89 -0
  14. package/basic-components/basic-tabs/README.md +63 -0
  15. package/basic-components/basic-toast/README.md +62 -0
  16. package/{components → basic-components}/basic-toast/index.d.ts +3 -0
  17. package/{components → basic-components}/basic-toast/index.js +264 -3
  18. package/basic-components/basic-toc/README.md +43 -0
  19. package/basic-components/basic-toc/register.d.ts +1 -0
  20. package/basic-styling/components/basic-accordion.css +38 -4
  21. package/basic-styling/components/basic-carousel.css +183 -0
  22. package/basic-styling/components/basic-popover.css +2 -4
  23. package/basic-styling/components/basic-summary-table.css +27 -5
  24. package/basic-styling/components/basic-table.css +22 -4
  25. package/basic-styling/components/basic-tabs.css +26 -10
  26. package/basic-styling/components.css +2 -0
  27. package/basic-styling/forms.css +55 -0
  28. package/basic-styling/global.css +1 -0
  29. package/basic-styling/interaction.css +90 -0
  30. package/basic-styling/tokens/palette.css +112 -0
  31. package/basic-styling/tokens/palette.tokens.json +768 -0
  32. package/index.d.ts +10 -9
  33. package/index.js +10 -9
  34. package/package.json +49 -29
  35. package/readme.mdx +0 -6
  36. /package/{components → basic-components}/basic-accordion/index.d.ts +0 -0
  37. /package/{components → basic-components}/basic-accordion/register.d.ts +0 -0
  38. /package/{components → basic-components}/basic-accordion/register.js +0 -0
  39. /package/{components → basic-components}/basic-alert/index.d.ts +0 -0
  40. /package/{components → basic-components}/basic-alert/index.js +0 -0
  41. /package/{components → basic-components}/basic-alert/register.d.ts +0 -0
  42. /package/{components → basic-components}/basic-alert/register.js +0 -0
  43. /package/{components/basic-dialog → basic-components/basic-carousel}/register.d.ts +0 -0
  44. /package/{components → basic-components}/basic-dialog/index.d.ts +0 -0
  45. /package/{components → basic-components}/basic-dialog/index.js +0 -0
  46. /package/{components/basic-popover → basic-components/basic-dialog}/register.d.ts +0 -0
  47. /package/{components → basic-components}/basic-dialog/register.js +0 -0
  48. /package/{components → basic-components}/basic-popover/index.d.ts +0 -0
  49. /package/{components → basic-components}/basic-popover/index.js +0 -0
  50. /package/{components/basic-summary-table → basic-components/basic-popover}/register.d.ts +0 -0
  51. /package/{components → basic-components}/basic-popover/register.js +0 -0
  52. /package/{components → basic-components}/basic-summary-table/index.d.ts +0 -0
  53. /package/{components → basic-components}/basic-summary-table/index.js +0 -0
  54. /package/{components/basic-table → basic-components/basic-summary-table}/register.d.ts +0 -0
  55. /package/{components → basic-components}/basic-summary-table/register.js +0 -0
  56. /package/{components → basic-components}/basic-table/index.d.ts +0 -0
  57. /package/{components → basic-components}/basic-table/index.js +0 -0
  58. /package/{components/basic-tabs → basic-components/basic-table}/register.d.ts +0 -0
  59. /package/{components → basic-components}/basic-table/register.js +0 -0
  60. /package/{components → basic-components}/basic-tabs/index.d.ts +0 -0
  61. /package/{components → basic-components}/basic-tabs/index.js +0 -0
  62. /package/{components/basic-toast → basic-components/basic-tabs}/register.d.ts +0 -0
  63. /package/{components → basic-components}/basic-tabs/register.js +0 -0
  64. /package/{components/basic-toc → basic-components/basic-toast}/register.d.ts +0 -0
  65. /package/{components → basic-components}/basic-toast/register.js +0 -0
  66. /package/{components → basic-components}/basic-toc/index.d.ts +0 -0
  67. /package/{components → basic-components}/basic-toc/index.js +0 -0
  68. /package/{components → basic-components}/basic-toc/register.js +0 -0
@@ -0,0 +1,53 @@
1
+ # `basic-accordion`
2
+
3
+ Coordinates direct child `<details>` items into an accordion.
4
+
5
+ ## Register
6
+
7
+ ```js
8
+ import "@lmfaole/basics/basic-components/basic-accordion/register";
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```html
14
+ <basic-accordion>
15
+ <details open>
16
+ <summary>Oversikt</summary>
17
+ <p>Viser en kort oppsummering.</p>
18
+ </details>
19
+
20
+ <details>
21
+ <summary>Implementasjon</summary>
22
+ <p>Viser implementasjonsdetaljer.</p>
23
+ </details>
24
+ </basic-accordion>
25
+ ```
26
+
27
+ ## Props
28
+
29
+ | Prop | Description | Type | Default | Options |
30
+ | --- | --- | --- | --- | --- |
31
+ | `data-multiple` | Allows more than one accordion item to stay open at the same time. | boolean attribute | off | `present`, `omitted` |
32
+ | `data-collapsible` | Allows the last open item in single-open mode to close. | boolean attribute | off | `present`, `omitted` |
33
+
34
+ ## Item Hooks
35
+
36
+ | Hook | Description | Type | Default | Options |
37
+ | --- | --- | --- | --- | --- |
38
+ | `open` on a child `<details>` | Sets an item's initial expanded state before the accordion normalizes it. | boolean attribute | closed | `present`, `omitted` |
39
+ | `data-disabled` on a child `<details>` | Removes the item from toggle behavior and arrow-key navigation. | boolean attribute | off | `present`, `omitted` |
40
+
41
+ ## Behavior
42
+
43
+ - Preserves native `details` and `summary` semantics
44
+ - Keeps `data-open` in sync with the normalized open state
45
+ - Supports `ArrowUp`, `ArrowDown`, `Home`, and `End` between enabled summaries
46
+ - In single-open mode, keeps one enabled item open unless `data-collapsible` is set
47
+
48
+ ## Markup Contract
49
+
50
+ - Provide direct child `<details>` items, each with a first-child `<summary>`
51
+ - Add `open` to any item that should start expanded
52
+ - Add `data-disabled` to a `<details>` item when it should be skipped by arrow-key navigation and blocked from toggling
53
+ - Keep layout and styling outside the package
@@ -175,13 +175,9 @@ export class AccordionElement extends HTMLElementBase {
175
175
  return;
176
176
  }
177
177
 
178
- if (
179
- !this.#isMultiple()
180
- && !this.#isCollapsible()
181
- && itemStates[summaryIndex]?.open
182
- && getOpenAccordionIndexes(itemStates).length === 1
183
- ) {
178
+ if (!this.#isMultiple()) {
184
179
  event.preventDefault();
180
+ this.#toggleItem(summaryIndex, itemStates);
185
181
  }
186
182
  };
187
183
 
@@ -215,21 +211,7 @@ export class AccordionElement extends HTMLElementBase {
215
211
  case " ":
216
212
  case "Enter":
217
213
  event.preventDefault();
218
-
219
- if (itemStates[currentIndex]?.disabled) {
220
- return;
221
- }
222
-
223
- if (
224
- !this.#isMultiple()
225
- && !this.#isCollapsible()
226
- && itemStates[currentIndex]?.open
227
- && getOpenAccordionIndexes(itemStates).length === 1
228
- ) {
229
- return;
230
- }
231
-
232
- this.#items[currentIndex].details.open = !this.#items[currentIndex].details.open;
214
+ this.#toggleItem(currentIndex, itemStates);
233
215
  return;
234
216
  default:
235
217
  return;
@@ -338,22 +320,7 @@ export class AccordionElement extends HTMLElementBase {
338
320
  }
339
321
  }
340
322
 
341
- const openIndexSet = new Set(openIndexes);
342
-
343
- this.#isSyncingState = true;
344
-
345
- try {
346
- for (let index = 0; index < this.#items.length; index += 1) {
347
- const details = this.#items[index].details;
348
- const open = !itemStates[index]?.disabled && openIndexSet.has(index);
349
-
350
- if (details.open !== open) {
351
- details.open = open;
352
- }
353
- }
354
- } finally {
355
- this.#isSyncingState = false;
356
- }
323
+ this.#applyOpenState(openIndexes, itemStates);
357
324
  }
358
325
 
359
326
  #applyState() {
@@ -376,6 +343,61 @@ export class AccordionElement extends HTMLElementBase {
376
343
  }
377
344
  }
378
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
+ }
379
401
  }
380
402
 
381
403
  export function defineAccordion(registry = globalThis.customElements) {
@@ -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,108 @@
1
+ # `basic-carousel`
2
+
3
+ Named carousel regions built around a native scroll-snap track.
4
+
5
+ ## Register
6
+
7
+ ```js
8
+ import "@lmfaole/basics/basic-components/basic-carousel/register";
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```html
14
+ <basic-carousel
15
+ data-label="Featured stories"
16
+ data-controls="both"
17
+ data-snapping="center"
18
+ >
19
+ <div data-carousel-track>
20
+ <article>
21
+ <h2>Launch Week</h2>
22
+ <p>Three product updates shipping across the design system.</p>
23
+ </article>
24
+
25
+ <article data-carousel-marker-label="Go to the accessibility slide">
26
+ <h2>Accessibility</h2>
27
+ <p>Keyboard and announcement details for the next release.</p>
28
+ </article>
29
+
30
+ <article>
31
+ <h2>Change Freeze Window</h2>
32
+ <p>Friday deployment holds and rollback owners are published for the April migration.</p>
33
+ </article>
34
+
35
+ <article>
36
+ <h2>Signup Funnel Feedback</h2>
37
+ <p>Eight research sessions highlighted friction around plan limits and account handoff.</p>
38
+ </article>
39
+
40
+ <article>
41
+ <h2>Tokens</h2>
42
+ <p>New surface and border tokens for interaction states.</p>
43
+ </article>
44
+
45
+ <article>
46
+ <h2>Migration Guides Updated</h2>
47
+ <p>Upgrade notes now include copy-paste examples and QA checklists for current component consumers.</p>
48
+ </article>
49
+
50
+ <article>
51
+ <h2>Bundle Audit Review</h2>
52
+ <p>A new audit flags duplicate helper code across package entry points and the next trim targets.</p>
53
+ </article>
54
+
55
+ <article>
56
+ <h2>Permission Model Review</h2>
57
+ <p>Security notes now spell out which stories need elevated browser APIs and which ones stay sandbox-safe.</p>
58
+ </article>
59
+
60
+ <article>
61
+ <h2>Pilot Teams Onboarded</h2>
62
+ <p>Three product squads have moved their prototypes onto the package and started filing integration feedback.</p>
63
+ </article>
64
+
65
+ <article>
66
+ <h2>Regression Sweep</h2>
67
+ <p>A targeted browser sweep caught contrast regressions, stale labels, and one broken close action before release cut.</p>
68
+ </article>
69
+ </div>
70
+ </basic-carousel>
71
+ ```
72
+
73
+ Import the optional starter carousel CSS when you want native `::scroll-button()` and `::scroll-marker` controls where the browser supports them.
74
+
75
+ ## Props
76
+
77
+ | Prop | Description | Type | Default | Options |
78
+ | --- | --- | --- | --- | --- |
79
+ | `data-label` | Accessible name for the carousel region when no own `aria-label` or `aria-labelledby` is present. | string | `Carousel` | any string |
80
+ | `data-controls` | Chooses which native scroll controls to expose when the browser supports them. | string | `both` | `both`, `markers`, `arrows`, `none` |
81
+ | `data-snapping` | Chooses where each slide snaps within the scrollport and where `scrollToItem()` aligns it. | string | `center` | `start`, `center`, `end` |
82
+
83
+ ## Markup Hooks
84
+
85
+ | Hook | Description | Type | Default | Options |
86
+ | --- | --- | --- | --- | --- |
87
+ | `data-carousel-track` | Marks the scroll container that owns the slides and native CSS controls. | descendant container attribute | required | present on one descendant scroll container |
88
+ | direct children of `[data-carousel-track]` | Each direct child becomes one carousel slide and one generated scroll marker. | descendant item | required | present on one or more direct child elements |
89
+ | `data-carousel-marker-label` | Optional per-slide label used for the generated marker's accessible name. | string attribute on a direct slide child | auto-generated | any string |
90
+
91
+ ## Behavior
92
+
93
+ - Applies `role="region"` and a fallback accessible label on the root element
94
+ - Normalizes `data-controls` to `both`, `markers`, `arrows`, or `none`
95
+ - Normalizes `data-snapping` to `start`, `center`, or `end`
96
+ - Annotates each slide with generated marker text and marker labels for CSS `content: attr(...)`
97
+ - Exposes `refresh()` when the slide structure changes after connection
98
+ - Exposes `scrollToItem(index, options)` for programmatic navigation
99
+
100
+ ## Markup Contract
101
+
102
+ - Provide one descendant element with `data-carousel-track`
103
+ - Keep each slide as a direct child of that track
104
+ - Add your own slide semantics inside each item, for example `article`, `li`, or `section`
105
+ - Use `data-controls="none"` for no generated controls, `data-controls="markers"` for numbered markers only, `data-controls="arrows"` for arrows only, or `data-controls="both"` for both controls
106
+ - Use `data-snapping="start"`, `center`, or `end` to align the active slide consistently
107
+ - Import starter CSS if you want generated scroll buttons and scroll markers
108
+ - Browsers without native scroll controls still get the scroll-snap track and manual scrolling
@@ -0,0 +1,73 @@
1
+ export const CAROUSEL_TAG_NAME: "basic-carousel";
2
+ export type CarouselControls = "both" | "markers" | "arrows" | "none";
3
+ export type CarouselSnapping = "start" | "center" | "end";
4
+
5
+ export interface CarouselScrollOptions {
6
+ behavior?: ScrollBehavior | null;
7
+ snapping?: CarouselSnapping | string | null;
8
+ }
9
+
10
+ /**
11
+ * Normalizes unsupported or empty labels back to the default `"Carousel"`.
12
+ */
13
+ export function normalizeCarouselLabel(
14
+ value?: string | null,
15
+ ): string;
16
+
17
+ /**
18
+ * Normalizes carousel controls to `"both"`, `"markers"`, `"arrows"`, or `"none"`.
19
+ */
20
+ export function normalizeCarouselControls(
21
+ value?: string | null,
22
+ ): CarouselControls;
23
+
24
+ /**
25
+ * Normalizes scroll behaviors to `"auto"` or `"smooth"`.
26
+ */
27
+ export function normalizeCarouselScrollBehavior(
28
+ value?: string | null,
29
+ ): ScrollBehavior;
30
+
31
+ /**
32
+ * Normalizes snap positions to `"start"`, `"center"`, or `"end"`.
33
+ */
34
+ export function normalizeCarouselSnapping(
35
+ value?: string | null,
36
+ ): CarouselSnapping;
37
+
38
+ /**
39
+ * Clamps a requested item index into the available carousel range.
40
+ */
41
+ export function clampCarouselIndex(
42
+ index: number,
43
+ itemCount: number,
44
+ ): number;
45
+
46
+ /**
47
+ * Custom element that upgrades a scroll-snap track into a named carousel region
48
+ * and annotates each slide for CSS-native scroll buttons and markers.
49
+ *
50
+ * Attributes:
51
+ * - `data-label`: fallback accessible name for the carousel region
52
+ * - `data-controls`: show generated markers, arrows, both, or no generated controls where supported
53
+ * - `data-snapping`: align slides to the start, center, or end of the scrollport
54
+ *
55
+ * Descendant hooks:
56
+ * - one `[data-carousel-track]` scroll container
57
+ * - direct child items inside that track
58
+ * - optional `data-carousel-marker-label` on each item for custom marker names
59
+ */
60
+ export class CarouselElement extends HTMLElement {
61
+ static observedAttributes: string[];
62
+ get track(): HTMLElement | null;
63
+ get items(): HTMLElement[];
64
+ refresh(): number;
65
+ scrollToItem(index: number, options?: CarouselScrollOptions): boolean;
66
+ }
67
+
68
+ /**
69
+ * Registers the `basic-carousel` custom element if it is not already defined.
70
+ */
71
+ export function defineCarousel(
72
+ registry?: CustomElementRegistry,
73
+ ): typeof CarouselElement;
@@ -0,0 +1,255 @@
1
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
2
+
3
+ export const CAROUSEL_TAG_NAME = "basic-carousel";
4
+
5
+ const DEFAULT_LABEL = "Carousel";
6
+ const DEFAULT_CONTROLS = "both";
7
+ const DEFAULT_SNAPPING = "center";
8
+ const TRACK_SELECTOR = "[data-carousel-track]";
9
+ const MANAGED_LABEL_ATTRIBUTE = "data-basic-carousel-managed-label";
10
+ const MANAGED_READY_ATTRIBUTE = "data-basic-carousel-ready";
11
+
12
+ let nextCarouselInstanceId = 1;
13
+
14
+ function collectOwnedElements(root, scope, selector) {
15
+ return Array.from(scope.querySelectorAll(selector)).filter(
16
+ (element) => element instanceof HTMLElementBase && element.closest(CAROUSEL_TAG_NAME) === root,
17
+ );
18
+ }
19
+
20
+ function collectCarouselItems(track) {
21
+ return Array.from(track.children).filter(
22
+ (element) => element instanceof HTMLElementBase,
23
+ );
24
+ }
25
+
26
+ function normalizeMarkerLabel(value, index, total) {
27
+ const normalized = value?.trim();
28
+
29
+ if (normalized) {
30
+ return normalized;
31
+ }
32
+
33
+ return `Go to slide ${index} of ${total}`;
34
+ }
35
+
36
+ export function normalizeCarouselLabel(value) {
37
+ return value?.trim() || DEFAULT_LABEL;
38
+ }
39
+
40
+ export function normalizeCarouselControls(value) {
41
+ const normalized = value?.trim().toLowerCase();
42
+
43
+ if (normalized === "none") {
44
+ return "none";
45
+ }
46
+
47
+ if (
48
+ normalized === "markers"
49
+ || normalized === "numbers"
50
+ || normalized === "number-buttons"
51
+ ) {
52
+ return "markers";
53
+ }
54
+
55
+ if (normalized === "arrows") {
56
+ return "arrows";
57
+ }
58
+
59
+ return DEFAULT_CONTROLS;
60
+ }
61
+
62
+ export function normalizeCarouselScrollBehavior(value) {
63
+ const normalized = value?.trim().toLowerCase();
64
+
65
+ return normalized === "smooth" ? "smooth" : "auto";
66
+ }
67
+
68
+ export function normalizeCarouselSnapping(value) {
69
+ const normalized = value?.trim().toLowerCase();
70
+
71
+ if (normalized === "start" || normalized === "end") {
72
+ return normalized;
73
+ }
74
+
75
+ return DEFAULT_SNAPPING;
76
+ }
77
+
78
+ export function clampCarouselIndex(index, itemCount) {
79
+ if (!Number.isInteger(index) || itemCount <= 0) {
80
+ return -1;
81
+ }
82
+
83
+ return Math.min(Math.max(index, 0), itemCount - 1);
84
+ }
85
+
86
+ function clampScrollOffset(offset, maxOffset) {
87
+ return Math.min(Math.max(offset, 0), Math.max(maxOffset, 0));
88
+ }
89
+
90
+ function getItemInlineMetrics(track, item) {
91
+ const trackRect = track.getBoundingClientRect?.();
92
+ const itemRect = item.getBoundingClientRect?.();
93
+
94
+ if (trackRect?.width > 0 && itemRect?.width > 0) {
95
+ const start = itemRect.left - trackRect.left + track.scrollLeft;
96
+
97
+ return {
98
+ start,
99
+ width: itemRect.width,
100
+ };
101
+ }
102
+
103
+ return {
104
+ start: item.offsetLeft || 0,
105
+ width: item.offsetWidth || track.clientWidth || 0,
106
+ };
107
+ }
108
+
109
+ function getTrackViewportWidth(track) {
110
+ return track.clientWidth || track.getBoundingClientRect?.().width || 0;
111
+ }
112
+
113
+ function getScrollOffsetForItem(track, item, snapping) {
114
+ const viewportWidth = getTrackViewportWidth(track);
115
+ const { start, width } = getItemInlineMetrics(track, item);
116
+
117
+ if (snapping === "end") {
118
+ return start - (viewportWidth - width);
119
+ }
120
+
121
+ if (snapping === "center") {
122
+ return start - ((viewportWidth - width) / 2);
123
+ }
124
+
125
+ return start;
126
+ }
127
+
128
+ export class CarouselElement extends HTMLElementBase {
129
+ static observedAttributes = ["data-controls", "data-label", "data-snapping"];
130
+
131
+ #instanceId = `${CAROUSEL_TAG_NAME}-${nextCarouselInstanceId++}`;
132
+ #track = null;
133
+ #items = [];
134
+
135
+ connectedCallback() {
136
+ this.refresh();
137
+ }
138
+
139
+ attributeChangedCallback() {
140
+ this.#applyState();
141
+ }
142
+
143
+ get track() {
144
+ return this.#track;
145
+ }
146
+
147
+ get items() {
148
+ return [...this.#items];
149
+ }
150
+
151
+ refresh() {
152
+ this.#track = collectOwnedElements(this, this, TRACK_SELECTOR)[0] ?? null;
153
+ this.#items = this.#track ? collectCarouselItems(this.#track) : [];
154
+ this.#applyState();
155
+ return this.#items.length;
156
+ }
157
+
158
+ scrollToItem(index, options = {}) {
159
+ const nextIndex = clampCarouselIndex(index, this.#items.length);
160
+ const item = nextIndex === -1 ? null : this.#items[nextIndex];
161
+ const track = this.#track;
162
+
163
+ if (!(item instanceof HTMLElementBase) || !(track instanceof HTMLElementBase)) {
164
+ return false;
165
+ }
166
+
167
+ const behavior = normalizeCarouselScrollBehavior(options.behavior);
168
+ const snapping = normalizeCarouselSnapping(options.snapping ?? this.getAttribute("data-snapping"));
169
+ const viewportWidth = getTrackViewportWidth(track);
170
+ const maxOffset = Math.max(track.scrollWidth - viewportWidth, 0);
171
+ const left = clampScrollOffset(getScrollOffsetForItem(track, item, snapping), maxOffset);
172
+
173
+ if (typeof track.scrollTo === "function") {
174
+ track.scrollTo({
175
+ left,
176
+ behavior,
177
+ });
178
+ } else {
179
+ track.scrollLeft = left;
180
+ }
181
+
182
+ return true;
183
+ }
184
+
185
+ #applyState() {
186
+ const baseId = this.id || this.#instanceId;
187
+ const controls = normalizeCarouselControls(this.getAttribute("data-controls"));
188
+ const snapping = normalizeCarouselSnapping(this.getAttribute("data-snapping"));
189
+
190
+ if (this.#track instanceof HTMLElementBase && !this.#track.id) {
191
+ this.#track.id = `${baseId}-track`;
192
+ }
193
+
194
+ this.dataset.basicCarouselControls = controls;
195
+ this.dataset.basicCarouselSnapping = snapping;
196
+ this.toggleAttribute(MANAGED_READY_ATTRIBUTE, this.#track instanceof HTMLElementBase && this.#items.length > 0);
197
+ this.setAttribute("role", "region");
198
+ this.#syncAccessibleLabel();
199
+
200
+ const total = this.#items.length;
201
+
202
+ for (const [index, item] of this.#items.entries()) {
203
+ item.dataset.basicCarouselMarker = String(index + 1);
204
+ item.dataset.basicCarouselMarkerLabel = normalizeMarkerLabel(
205
+ item.getAttribute("data-carousel-marker-label"),
206
+ index + 1,
207
+ total,
208
+ );
209
+
210
+ if (!item.id) {
211
+ item.id = `${baseId}-item-${index + 1}`;
212
+ }
213
+ }
214
+ }
215
+
216
+ #syncAccessibleLabel() {
217
+ const nextLabel = normalizeCarouselLabel(this.getAttribute("data-label"));
218
+ const hasManagedLabel = this.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
219
+
220
+ if (hasManagedLabel && this.getAttribute("aria-label") !== nextLabel) {
221
+ this.removeAttribute("aria-label");
222
+ this.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
223
+ }
224
+
225
+ if (this.hasAttribute("aria-labelledby")) {
226
+ if (hasManagedLabel) {
227
+ this.removeAttribute("aria-label");
228
+ this.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
229
+ }
230
+
231
+ return;
232
+ }
233
+
234
+ const hasOwnAriaLabel = this.hasAttribute("aria-label") && !hasManagedLabel;
235
+
236
+ if (hasOwnAriaLabel) {
237
+ return;
238
+ }
239
+
240
+ this.setAttribute("aria-label", nextLabel);
241
+ this.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
242
+ }
243
+ }
244
+
245
+ export function defineCarousel(registry = globalThis.customElements) {
246
+ if (!registry?.get || !registry?.define) {
247
+ return CarouselElement;
248
+ }
249
+
250
+ if (!registry.get(CAROUSEL_TAG_NAME)) {
251
+ registry.define(CAROUSEL_TAG_NAME, CarouselElement);
252
+ }
253
+
254
+ return CarouselElement;
255
+ }
@@ -0,0 +1,3 @@
1
+ import { defineCarousel } from "./index.js";
2
+
3
+ defineCarousel();