@boostdev/design-system-components 1.2.0 → 1.2.1

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 (126) hide show
  1. package/AGENTS.md +25 -4
  2. package/dist/client.cjs +58 -54
  3. package/dist/client.css +503 -525
  4. package/dist/client.js +58 -54
  5. package/dist/index.cjs +58 -54
  6. package/dist/index.css +503 -525
  7. package/dist/index.js +58 -54
  8. package/dist/web-components/{chunk-6MH5UWUD.js → chunk-2FGATTGT.js} +5 -0
  9. package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
  10. package/dist/web-components/{chunk-DI46Q2EA.js → chunk-OCODKRVZ.js} +57 -82
  11. package/dist/web-components/index.d.ts +571 -1
  12. package/dist/web-components/index.js +1501 -4
  13. package/dist/web-components/interaction/bds-collapsible.js +1 -1
  14. package/dist/web-components/interaction/bds-drawer.js +1 -1
  15. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  16. package/package.json +1 -1
  17. package/src/components/interaction/Drawer/Drawer.module.css +37 -62
  18. package/src/components/interaction/Drawer/Drawer.tsx +21 -17
  19. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +8 -4
  20. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +13 -6
  21. package/src/stories/Introduction.mdx +2 -1
  22. package/src/web-components/index.ts +12 -0
  23. package/src/web-components/interaction/BdsAccordion.mdx +80 -28
  24. package/src/web-components/interaction/BdsAccordion.stories.tsx +67 -58
  25. package/src/web-components/interaction/BdsCollapsible.mdx +93 -23
  26. package/src/web-components/interaction/BdsCollapsible.stories.tsx +29 -48
  27. package/src/web-components/interaction/BdsDialog.mdx +88 -27
  28. package/src/web-components/interaction/BdsDialog.stories.tsx +129 -47
  29. package/src/web-components/interaction/BdsDrawer.mdx +85 -27
  30. package/src/web-components/interaction/BdsDrawer.stories.tsx +161 -31
  31. package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
  32. package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
  33. package/src/web-components/interaction/BdsSkipLink.mdx +72 -16
  34. package/src/web-components/interaction/BdsSkipLink.stories.tsx +47 -34
  35. package/src/web-components/interaction/BdsTabs.mdx +97 -27
  36. package/src/web-components/interaction/BdsTabs.stories.tsx +69 -55
  37. package/src/web-components/interaction/BdsTooltip.mdx +84 -18
  38. package/src/web-components/interaction/BdsTooltip.stories.tsx +49 -30
  39. package/src/web-components/interaction/bds-collapsible.ts +1 -0
  40. package/src/web-components/interaction/bds-drawer.ts +59 -82
  41. package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
  42. package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
  43. package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
  44. package/src/web-components/interaction/form/BdsCheckbox.mdx +57 -20
  45. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +35 -36
  46. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
  47. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
  48. package/src/web-components/interaction/form/BdsCombobox.mdx +52 -33
  49. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +55 -53
  50. package/src/web-components/interaction/form/BdsFileInput.mdx +59 -24
  51. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +38 -53
  52. package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
  53. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
  54. package/src/web-components/interaction/form/BdsNumberInput.mdx +51 -22
  55. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +24 -46
  56. package/src/web-components/interaction/form/BdsRadio.mdx +53 -22
  57. package/src/web-components/interaction/form/BdsRadio.stories.tsx +19 -51
  58. package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
  59. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
  60. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +76 -23
  61. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +64 -54
  62. package/src/web-components/interaction/form/BdsSelect.mdx +59 -21
  63. package/src/web-components/interaction/form/BdsSelect.stories.tsx +47 -56
  64. package/src/web-components/interaction/form/BdsSlider.mdx +55 -27
  65. package/src/web-components/interaction/form/BdsSlider.stories.tsx +23 -44
  66. package/src/web-components/interaction/form/BdsSwitch.mdx +53 -20
  67. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +22 -37
  68. package/src/web-components/interaction/form/BdsTextarea.mdx +54 -22
  69. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +35 -44
  70. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
  71. package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
  72. package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
  73. package/src/web-components/interaction/form/bds-form-input.ts +268 -0
  74. package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
  75. package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
  76. package/src/web-components/interaction/form/bds-segmented-control.ts +5 -0
  77. package/src/web-components/ui/BdsAvatar.mdx +29 -20
  78. package/src/web-components/ui/BdsAvatar.stories.tsx +14 -28
  79. package/src/web-components/ui/BdsBreadcrumb.mdx +22 -19
  80. package/src/web-components/ui/BdsBreadcrumb.stories.tsx +11 -21
  81. package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
  82. package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
  83. package/src/web-components/ui/BdsCalendar.mdx +93 -0
  84. package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
  85. package/src/web-components/ui/BdsCard.mdx +30 -25
  86. package/src/web-components/ui/BdsCard.stories.tsx +19 -42
  87. package/src/web-components/ui/BdsCarousel.mdx +83 -0
  88. package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
  89. package/src/web-components/ui/BdsDescriptionList.mdx +13 -16
  90. package/src/web-components/ui/BdsDescriptionList.stories.tsx +34 -49
  91. package/src/web-components/ui/BdsIconWrapper.mdx +20 -20
  92. package/src/web-components/ui/BdsIconWrapper.stories.tsx +46 -41
  93. package/src/web-components/ui/BdsLink.mdx +21 -17
  94. package/src/web-components/ui/BdsLink.stories.tsx +19 -20
  95. package/src/web-components/ui/BdsLoading.mdx +9 -13
  96. package/src/web-components/ui/BdsLoading.stories.tsx +8 -10
  97. package/src/web-components/ui/BdsNotificationBanner.mdx +32 -37
  98. package/src/web-components/ui/BdsNotificationBanner.stories.tsx +38 -47
  99. package/src/web-components/ui/BdsPagination.mdx +74 -0
  100. package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
  101. package/src/web-components/ui/BdsProgress.mdx +24 -17
  102. package/src/web-components/ui/BdsProgress.stories.tsx +55 -29
  103. package/src/web-components/ui/BdsProgressCircle.mdx +24 -18
  104. package/src/web-components/ui/BdsProgressCircle.stories.tsx +17 -29
  105. package/src/web-components/ui/BdsRating.mdx +22 -22
  106. package/src/web-components/ui/BdsRating.stories.tsx +19 -13
  107. package/src/web-components/ui/BdsSectionHeader.mdx +31 -28
  108. package/src/web-components/ui/BdsSectionHeader.stories.tsx +21 -23
  109. package/src/web-components/ui/BdsSeparator.mdx +22 -8
  110. package/src/web-components/ui/BdsSeparator.stories.tsx +27 -19
  111. package/src/web-components/ui/BdsSkeleton.mdx +20 -26
  112. package/src/web-components/ui/BdsSkeleton.stories.tsx +23 -23
  113. package/src/web-components/ui/BdsTable.mdx +81 -0
  114. package/src/web-components/ui/BdsTable.stories.tsx +83 -0
  115. package/src/web-components/ui/BdsTypography.mdx +20 -29
  116. package/src/web-components/ui/BdsTypography.stories.tsx +19 -18
  117. package/src/web-components/ui/bds-button-group.spec.ts +40 -0
  118. package/src/web-components/ui/bds-button-group.ts +78 -0
  119. package/src/web-components/ui/bds-calendar.spec.ts +91 -0
  120. package/src/web-components/ui/bds-calendar.ts +427 -0
  121. package/src/web-components/ui/bds-carousel.spec.ts +64 -0
  122. package/src/web-components/ui/bds-carousel.ts +296 -0
  123. package/src/web-components/ui/bds-pagination.spec.ts +67 -0
  124. package/src/web-components/ui/bds-pagination.ts +197 -0
  125. package/src/web-components/ui/bds-table.spec.ts +45 -0
  126. package/src/web-components/ui/bds-table.ts +96 -0
@@ -3,44 +3,114 @@ import * as Stories from './BdsCollapsible.stories';
3
3
 
4
4
  <Meta of={Stories} />
5
5
 
6
- # Collapsible
6
+ # &lt;bds-collapsible&gt;
7
7
 
8
- `<bds-collapsible>` reveals or hides content on demand. Built on native `<details>/<summary>` semantics for built-in accessibility. Set the same `name` on multiple collapsibles to link them — opening one will close the others.
8
+ Framework-agnostic Collapsible custom element. Wraps a native `<details>`/`<summary>` pair for built-in toggle behaviour with design-system styling and a `bds-toggle` event.
9
9
 
10
- ## Usage
10
+ > **Status: experimental** — API may change before stable release.
11
+
12
+ ## Installation
13
+
14
+ ```js
15
+ import '@boostdev/components/web-components';
16
+ ```
17
+
18
+ Or in HTML without a bundler:
11
19
 
12
20
  ```html
13
- <bds-collapsible>
14
- <span slot="summary">Show more</span>
15
- <p>Hidden content revealed on click.</p>
16
- </bds-collapsible>
21
+ <script type="module">
22
+ import '@boostdev/components/web-components';
23
+ </script>
17
24
  ```
18
25
 
26
+ ## When to use
27
+
28
+ - Revealing secondary content on demand (e.g. "More details", "Advanced options")
29
+ - Standalone expandable sections that are independent of each other
30
+ - When accordion-style coordination is not required
31
+
32
+ ## When not to use
33
+
34
+ - When multiple related sections need to be coordinated (open one, close others) — use `<bds-accordion>` instead
35
+ - For navigation menus — use a dedicated nav component
36
+
37
+ ## Examples
38
+
39
+ ### Default
40
+ <Canvas of={Stories.Default} />
41
+
42
+ ### Open by default
43
+ <Canvas of={Stories.Open} />
44
+
45
+ ### Grouped (only one open at a time)
46
+ <Canvas of={Stories.Grouped} />
47
+
48
+ ## Props
49
+
50
+ <ArgTypes of={Stories} />
51
+
19
52
  ## Attributes
20
53
 
21
- | Attribute | Type | Default | Description |
22
- |-----------|-----------|-------------|----------------------------------------------|
23
- | `open` | `boolean` | `false` | Controls the open/closed state |
24
- | `name` | `string` | `undefined` | Links collapsibles into a mutually-exclusive group |
54
+ | Attribute | Type | Default | Description |
55
+ |-----------|------|---------|-------------|
56
+ | `open` | boolean | `false` | Whether the panel is currently expanded |
57
+ | `name` | string | `""` | Groups collapsibles so only one in a group is open at a time (native `<details name>` behaviour) |
25
58
 
26
59
  ## Slots
27
60
 
28
- | Slot | Description |
29
- |-----------|----------------------------------|
30
- | `summary` | The always-visible clickable label |
31
- | (default) | The collapsible body content |
61
+ | Slot | Description |
62
+ |------|-------------|
63
+ | `summary` | The always-visible trigger text |
64
+ | `(default)` | The content revealed when expanded |
32
65
 
33
66
  ## Events
34
67
 
35
- | Event | Detail | Description |
36
- |--------------|---------------------------------|--------------------------------|
37
- | `bds-toggle` | `{ open: boolean }` | Fired when open state changes |
68
+ | Event | Bubbles | Detail | Description |
69
+ |-------|---------|--------|-------------|
70
+ | `bds-toggle` | Yes | `{ open: boolean }` | Fired when the collapsible opens or closes; `detail.open` reflects the new state |
38
71
 
39
- ## Stories
72
+ ## Usage in plain HTML
40
73
 
41
- <Canvas of={Stories.Default} />
42
- <Canvas of={Stories.Grouped} />
74
+ ```html
75
+ <bds-collapsible>
76
+ <span slot="summary">Show details</span>
77
+ <p>Hidden content revealed on toggle.</p>
78
+ </bds-collapsible>
79
+
80
+ <!-- Open by default -->
81
+ <bds-collapsible open>
82
+ <span slot="summary">Open section</span>
83
+ <p>This is visible immediately.</p>
84
+ </bds-collapsible>
85
+
86
+ <!-- Grouped: only one open at a time -->
87
+ <bds-collapsible name="faq">
88
+ <span slot="summary">Question 1</span>
89
+ <p>Answer 1</p>
90
+ </bds-collapsible>
91
+ <bds-collapsible name="faq">
92
+ <span slot="summary">Question 2</span>
93
+ <p>Answer 2</p>
94
+ </bds-collapsible>
95
+ ```
43
96
 
44
- ## Controls
97
+ ## CSS Custom Properties
45
98
 
46
- <ArgTypes of={Stories} />
99
+ | Property | Default | Description |
100
+ |----------|---------|-------------|
101
+ | `--collapsible_bg` | `var(--bds-color_bg)` | Summary (header) background colour |
102
+ | `--collapsible_bg--hover` | `var(--bds-color_bg--subtle)` | Summary background on hover |
103
+ | `--collapsible_content-bg` | `var(--bds-color_bg)` | Content panel background colour |
104
+ | `--collapsible_color` | `var(--bds-color_on-bg)` | Summary text colour |
105
+ | `--collapsible_on-color` | `var(--bds-color_on-bg)` | Content text colour |
106
+ | `--collapsible_border-color` | `var(--bds-color_bg--subtle)` | Border colour |
107
+ | `--collapsible_border-width` | `1px` | Border width |
108
+ | `--collapsible_border-radius` | `var(--bds-border_radius--m)` | Border radius |
109
+
110
+ ## Accessibility
111
+
112
+ - Built on native `<details>`/`<summary>`, which is keyboard accessible out of the box
113
+ - The `summary` slot element is focusable and responds to Enter/Space
114
+ - Focus outline follows design system tokens
115
+ - Chevron icon is `aria-hidden="true"`
116
+ - `prefers-reduced-motion` disables the chevron transition
@@ -1,25 +1,24 @@
1
1
  import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
- import '../index';
3
+ import '../index'; // auto-registers all custom elements
4
4
 
5
+ // Thin React wrapper so Storybook controls work with the custom element's props
5
6
  function BdsCollapsible({
6
- open,
7
+ open = false,
7
8
  name,
8
9
  summary,
9
10
  children,
10
- onToggle,
11
11
  }: {
12
12
  open?: boolean;
13
13
  name?: string;
14
14
  summary?: string;
15
15
  children?: React.ReactNode;
16
- onToggle?: (e: Event) => void;
17
16
  }) {
18
17
  return React.createElement(
19
18
  'bds-collapsible',
20
- { open: open || undefined, name, onbdstoggle: onToggle },
19
+ { open: open || undefined, name: name || undefined },
21
20
  React.createElement('span', { slot: 'summary' }, summary ?? 'Toggle section'),
22
- children ?? React.createElement('p', { style: { margin: 0 } }, 'Collapsible content goes here.'),
21
+ children ?? React.createElement('p', { style: { margin: 0 } }, 'Collapsible panel content.'),
23
22
  );
24
23
  }
25
24
 
@@ -30,58 +29,40 @@ const meta = {
30
29
  parameters: { layout: 'padded' },
31
30
  argTypes: {
32
31
  open: { control: 'boolean' },
32
+ name: { control: 'text' },
33
+ summary: { control: 'text' },
33
34
  },
34
35
  } satisfies Meta<typeof BdsCollapsible>;
35
36
 
36
37
  export default meta;
37
38
  type Story = StoryObj<typeof meta>;
38
39
 
39
- export const Default: Story = { args: { summary: 'Show more details' } };
40
- export const OpenByDefault: Story = { args: { summary: 'Open by default', open: true } };
40
+ export const Default: Story = {
41
+ args: {
42
+ summary: 'Show details',
43
+ open: false,
44
+ },
45
+ };
41
46
 
42
- export const Grouped: Story = {
43
- render: () => (
44
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', maxWidth: '480px' }}>
45
- <BdsCollapsible name="faq" summary="What is this component?">
46
- <p style={{ margin: 0 }}>A Collapsible reveals or hides content on demand.</p>
47
- </BdsCollapsible>
48
- <BdsCollapsible name="faq" summary="How does grouping work?">
49
- <p style={{ margin: 0 }}>Setting the same `name` on multiple collapsibles links them — opening one closes the others.</p>
50
- </BdsCollapsible>
51
- <BdsCollapsible name="faq" summary="Is it accessible?">
52
- <p style={{ margin: 0 }}>Yes — built on native `&lt;details&gt;/&lt;summary&gt;` semantics.</p>
53
- </BdsCollapsible>
54
- </div>
55
- ),
47
+ export const Open: Story = {
48
+ args: {
49
+ summary: 'Open by default',
50
+ open: true,
51
+ children: React.createElement('p', { style: { margin: 0 } }, 'This collapsible is open by default via the open attribute.'),
52
+ },
56
53
  };
57
54
 
58
- export const AllVariants: Story = {
55
+ export const Grouped: Story = {
59
56
  render: () => (
60
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: '480px' }}>
61
- <BdsCollapsible summary="Closed by default">
62
- <p style={{ margin: 0 }}>This starts closed.</p>
63
- </BdsCollapsible>
64
- <BdsCollapsible summary="Open by default" open>
65
- <p style={{ margin: 0 }}>This starts open.</p>
66
- </BdsCollapsible>
57
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: '480px' }}>
58
+ {(['Section A', 'Section B', 'Section C'] as const).map((label, i) =>
59
+ React.createElement(
60
+ 'bds-collapsible',
61
+ { key: label, name: 'group', open: i === 0 ? '' : undefined },
62
+ React.createElement('span', { slot: 'summary' }, label),
63
+ React.createElement('p', { style: { margin: 0 } }, `Content for ${label}. Only one section in this group can be open at a time.`),
64
+ ),
65
+ )}
67
66
  </div>
68
67
  ),
69
68
  };
70
-
71
- export const Interactive: Story = {
72
- render: () => {
73
- const [isOpen, setIsOpen] = React.useState(false);
74
- return (
75
- <div style={{ maxWidth: '480px' }}>
76
- <p style={{ margin: '0 0 8px', fontSize: '0.875rem' }}>State: {isOpen ? 'open' : 'closed'}</p>
77
- <BdsCollapsible
78
- summary="Controlled collapsible"
79
- open={isOpen}
80
- onToggle={(e: Event) => setIsOpen((e as CustomEvent<{ open: boolean }>).detail.open)}
81
- >
82
- <p style={{ margin: 0 }}>Controlled via React state. The bds-toggle event updates the parent.</p>
83
- </BdsCollapsible>
84
- </div>
85
- );
86
- },
87
- };
@@ -3,47 +3,108 @@ import * as Stories from './BdsDialog.stories';
3
3
 
4
4
  <Meta of={Stories} />
5
5
 
6
- # Dialog
6
+ # &lt;bds-dialog&gt;
7
7
 
8
- `<bds-dialog>` is a modal dialog built on the native `<dialog>` element. It traps focus while open, dismisses on Escape and backdrop click, and restores focus to the trigger element on close. Supports the native Invoker Commands API.
8
+ Framework-agnostic Dialog custom element. Wraps a native `<dialog>` element with `showModal()` / `close()` lifecycle, backdrop-click to dismiss, Escape key handling, Tab focus trapping, and the Invoker Commands API.
9
9
 
10
- ## Usage
10
+ > **Status: experimental** — API may change before stable release.
11
11
 
12
- ```html
13
- <bds-dialog id="my-dialog">
14
- <h2>Dialog title</h2>
15
- <p>Dialog body content.</p>
16
- <button type="button">Close</button>
17
- </bds-dialog>
12
+ ## Installation
18
13
 
19
- <!-- Declarative open via Invoker API -->
20
- <button type="button" commandfor="my-dialog" command="show-modal">Open</button>
14
+ ```js
15
+ import '@boostdev/components/web-components';
21
16
  ```
22
17
 
23
- ## Attributes
18
+ Or in HTML without a bundler:
24
19
 
25
- | Attribute | Type | Default | Description |
26
- |-----------|-----------|---------|-----------------------------|
27
- | `open` | `boolean` | `false` | Controls the open state |
20
+ ```html
21
+ <script type="module">
22
+ import '@boostdev/components/web-components';
23
+ </script>
24
+ ```
28
25
 
29
- ## Events
26
+ ## When to use
30
27
 
31
- | Event | Description |
32
- |-------------|---------------------------------------|
33
- | `bds-open` | Fired when the dialog opens |
34
- | `bds-close` | Fired when the dialog closes |
28
+ - Confirmations that require a decision before continuing
29
+ - Short forms (edit profile, create item) that interrupt the current flow
30
+ - Alerts or notices that must be acknowledged
35
31
 
36
- ## Slots
32
+ ## When not to use
37
33
 
38
- | Slot | Description |
39
- |-----------|-------------------------------------|
40
- | (default) | Dialog content (title, body, actions) |
34
+ - Complex multi-step flows — consider a dedicated page instead
35
+ - Non-blocking panels — use `<bds-drawer>` or `<bds-popover>` instead
36
+ - Displaying lots of scrollable content prefer a full page or `<bds-drawer>`
41
37
 
42
- ## Stories
38
+ ## Examples
43
39
 
40
+ ### Default
44
41
  <Canvas of={Stories.Default} />
45
- <Canvas of={Stories.InvokerAPI} />
46
42
 
47
- ## Controls
43
+ ### With form content
44
+ <Canvas of={Stories.WithForm} />
45
+
46
+ ### Invoker Commands API
47
+ <Canvas of={Stories.InvokerCommands} />
48
+
49
+ ## Props
48
50
 
49
51
  <ArgTypes of={Stories} />
52
+
53
+ ## Attributes
54
+
55
+ | Attribute | Type | Default | Description |
56
+ |-----------|------|---------|-------------|
57
+ | `open` | boolean | `false` | Controlled open state — syncs to native `showModal()` / `close()` |
58
+
59
+ ## Slots
60
+
61
+ | Slot | Description |
62
+ |------|-------------|
63
+ | `(default)` | Dialog content |
64
+
65
+ ## Events
66
+
67
+ | Event | Bubbles | Description |
68
+ |-------|---------|-------------|
69
+ | `bds-open` | Yes | Fired when the dialog opens |
70
+ | `bds-close` | Yes | Fired when the dialog closes (backdrop click, Escape, or close button) |
71
+
72
+ ## Invoker Commands API
73
+
74
+ Open or close the dialog declaratively using the Invoker Commands API:
75
+
76
+ ```html
77
+ <button commandfor="my-dialog" command="show-modal">Open</button>
78
+ <button commandfor="my-dialog" command="close">Close</button>
79
+
80
+ <bds-dialog id="my-dialog">
81
+ <p>Dialog content</p>
82
+ </bds-dialog>
83
+ ```
84
+
85
+ The Invoker Commands API is a native browser feature. For browsers that do not support it yet, use the `open` attribute or JavaScript.
86
+
87
+ ## Usage in plain HTML
88
+
89
+ ```html
90
+ <bds-dialog id="my-dialog">
91
+ <h2>Confirm deletion</h2>
92
+ <p>This action cannot be undone.</p>
93
+ <div>
94
+ <button onclick="document.getElementById('my-dialog').open = false">Cancel</button>
95
+ <button>Delete</button>
96
+ </div>
97
+ </bds-dialog>
98
+
99
+ <button onclick="document.getElementById('my-dialog').open = true">Delete item</button>
100
+ ```
101
+
102
+ ## Accessibility
103
+
104
+ - Renders a native `<dialog>` element with `aria-modal="true"`
105
+ - Focus is trapped inside the dialog while open (Tab / Shift+Tab cycle)
106
+ - Escape key closes the dialog
107
+ - Clicking the backdrop closes the dialog
108
+ - Focus returns to the triggering element on close
109
+ - A visually positioned close button is provided with `aria-label="Close dialog"`
110
+ - `document.body.overflow` is set to `hidden` while the dialog is open to prevent background scroll
@@ -1,75 +1,157 @@
1
1
  import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
- import '../index';
3
+ import '../index'; // auto-registers all custom elements
4
+
5
+ // Thin React wrapper for bds-dialog + its trigger button
6
+ function BdsDialog({
7
+ open,
8
+ triggerLabel = 'Open dialog',
9
+ children,
10
+ }: {
11
+ open?: boolean;
12
+ triggerLabel?: string;
13
+ children?: React.ReactNode;
14
+ }) {
15
+ const id = React.useId();
16
+ const [isOpen, setIsOpen] = React.useState(open ?? false);
17
+ const dialogRef = React.useRef<HTMLElement>(null);
18
+
19
+ React.useEffect(() => {
20
+ setIsOpen(open ?? false);
21
+ }, [open]);
22
+
23
+ React.useEffect(() => {
24
+ const el = dialogRef.current;
25
+ if (!el) return;
26
+ const handler = () => setIsOpen(false);
27
+ el.addEventListener('bds-close', handler);
28
+ return () => el.removeEventListener('bds-close', handler);
29
+ }, []);
4
30
 
5
- function DialogDemo({ title = 'Dialog title', withForm = false }: { title?: string; withForm?: boolean }) {
6
- const [open, setOpen] = React.useState(false);
7
31
  return (
8
32
  <div>
9
- <button type="button" onClick={() => setOpen(true)}>Open dialog</button>
33
+ <button
34
+ type="button"
35
+ onClick={() => setIsOpen(true)}
36
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
37
+ >
38
+ {triggerLabel}
39
+ </button>
10
40
  {React.createElement(
11
41
  'bds-dialog',
12
- { open: open || undefined, onbdsclose: () => setOpen(false) },
13
- React.createElement('h2', { style: { margin: '0 0 12px' } }, title),
14
- withForm
15
- ? React.createElement(
16
- 'form',
17
- { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
18
- React.createElement('label', null,
19
- 'Name',
20
- React.createElement('input', { type: 'text', placeholder: 'Your name', style: { display: 'block', width: '100%', marginTop: '4px' } }),
21
- ),
22
- React.createElement('label', null,
23
- 'Email',
24
- React.createElement('input', { type: 'email', placeholder: 'you@example.com', style: { display: 'block', width: '100%', marginTop: '4px' } }),
25
- ),
26
- )
27
- : React.createElement('p', { style: { margin: '0 0 12px' } }, 'This is the dialog body. Press Escape or click the × button to close.'),
28
- React.createElement(
29
- 'div',
30
- { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '12px' } },
31
- React.createElement('button', { type: 'button', onClick: () => setOpen(false) }, 'Cancel'),
32
- React.createElement('button', { type: 'button', onClick: () => setOpen(false) }, 'Confirm'),
33
- ),
42
+ {
43
+ id,
44
+ ref: dialogRef,
45
+ open: isOpen || undefined,
46
+ },
47
+ children ?? React.createElement('p', { style: { margin: 0 } }, 'Dialog content goes here.'),
34
48
  )}
35
49
  </div>
36
50
  );
37
51
  }
38
52
 
39
- function BdsDialogStory({ withForm }: { withForm?: boolean }) {
40
- return React.createElement(DialogDemo, { withForm });
41
- }
42
-
43
53
  const meta = {
44
54
  title: 'Web Components/Interaction/Dialog',
45
- component: BdsDialogStory,
55
+ component: BdsDialog,
46
56
  tags: ['!stable', 'experimental'],
47
57
  parameters: { layout: 'centered' },
48
- } satisfies Meta<typeof BdsDialogStory>;
58
+ argTypes: {
59
+ triggerLabel: { control: 'text' },
60
+ },
61
+ } satisfies Meta<typeof BdsDialog>;
49
62
 
50
63
  export default meta;
51
64
  type Story = StoryObj<typeof meta>;
52
65
 
53
- export const Default: Story = { args: { withForm: false } };
54
- export const WithForm: Story = { args: { withForm: true } };
66
+ export const Default: Story = {
67
+ args: { triggerLabel: 'Open dialog' },
68
+ };
69
+
70
+ function WithFormDialog() {
71
+ const id = React.useId();
72
+ const [isOpen, setIsOpen] = React.useState(false);
73
+ const dialogRef = React.useRef<HTMLElement>(null);
55
74
 
56
- export const InvokerAPI: Story = {
57
- render: () => (
58
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '16px' }}>
59
- <p style={{ margin: 0, fontSize: '0.875rem', color: '#666' }}>
60
- Uses native Invoker Commands API — no JavaScript needed.
61
- </p>
62
- {React.createElement('button', { type: 'button', commandfor: 'invoker-dialog', command: 'show-modal' }, 'Open via commandfor')}
75
+ React.useEffect(() => {
76
+ const el = dialogRef.current;
77
+ if (!el) return;
78
+ const handler = () => setIsOpen(false);
79
+ el.addEventListener('bds-close', handler);
80
+ return () => el.removeEventListener('bds-close', handler);
81
+ }, []);
82
+
83
+ return (
84
+ <div>
85
+ <button
86
+ type="button"
87
+ onClick={() => setIsOpen(true)}
88
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
89
+ >
90
+ Open form dialog
91
+ </button>
63
92
  {React.createElement(
64
93
  'bds-dialog',
65
- { id: 'invoker-dialog' },
66
- React.createElement('h2', { style: { margin: '0 0 12px' } }, 'Opened via Invoker API'),
67
- React.createElement('p', { style: { margin: 0 } }, 'This dialog was opened declaratively using commandfor and command attributes.'),
94
+ {
95
+ id,
96
+ ref: dialogRef,
97
+ open: isOpen || undefined,
98
+ },
99
+ React.createElement(
100
+ 'form',
101
+ {
102
+ style: { display: 'flex', flexDirection: 'column', gap: '1rem', minWidth: '280px' },
103
+ onSubmit: (e: React.FormEvent) => { e.preventDefault(); setIsOpen(false); },
104
+ },
105
+ React.createElement('h2', { style: { margin: 0 } }, 'Edit profile'),
106
+ React.createElement(
107
+ 'label',
108
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
109
+ 'Name',
110
+ React.createElement('input', { type: 'text', defaultValue: 'Jane Doe', style: { padding: '0.5rem' } }),
111
+ ),
112
+ React.createElement(
113
+ 'label',
114
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
115
+ 'Email',
116
+ React.createElement('input', { type: 'email', defaultValue: 'jane@example.com', style: { padding: '0.5rem' } }),
117
+ ),
118
+ React.createElement(
119
+ 'div',
120
+ { style: { display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' } },
121
+ React.createElement('button', { type: 'button', onClick: () => setIsOpen(false) }, 'Cancel'),
122
+ React.createElement('button', { type: 'submit' }, 'Save'),
123
+ ),
124
+ ),
68
125
  )}
69
126
  </div>
70
- ),
127
+ );
128
+ }
129
+
130
+ export const WithForm: Story = {
131
+ render: () => React.createElement(WithFormDialog),
71
132
  };
72
133
 
73
- export const AllVariants: Story = {
74
- render: () => React.createElement(DialogDemo, { title: 'Confirmation' }),
134
+ export const InvokerCommands: Story = {
135
+ render: () => {
136
+ const dialogId = 'story-dialog-invoker';
137
+ return (
138
+ <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
139
+ {React.createElement(
140
+ 'button',
141
+ { commandFor: dialogId, command: 'show-modal', style: { padding: '0.5rem 1rem', cursor: 'pointer' } },
142
+ 'Open with Invoker Command',
143
+ )}
144
+ {React.createElement(
145
+ 'bds-dialog',
146
+ { id: dialogId },
147
+ React.createElement(
148
+ 'div',
149
+ { style: { minWidth: '260px' } },
150
+ React.createElement('h2', { style: { margin: '0 0 0.5rem' } }, 'Invoker Commands'),
151
+ React.createElement('p', { style: { margin: 0 } }, 'This dialog was opened using the Invoker Commands API (commandfor + command="show-modal").'),
152
+ ),
153
+ )}
154
+ </div>
155
+ );
156
+ },
75
157
  };