@nuvia-ui/components 4.0.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 (230) hide show
  1. package/package.json +27 -0
  2. package/src/ds-accordion/ds-accordion-item.js +288 -0
  3. package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
  4. package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
  5. package/src/ds-accordion/ds-accordion.js +68 -0
  6. package/src/ds-accordion/ds-accordion.stories.js +118 -0
  7. package/src/ds-accordion/ds-accordion.test.js +146 -0
  8. package/src/ds-accordion/index.js +2 -0
  9. package/src/ds-action-bar/ds-action-bar.js +116 -0
  10. package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
  11. package/src/ds-action-bar/ds-action-bar.test.js +64 -0
  12. package/src/ds-action-bar/index.js +1 -0
  13. package/src/ds-alert/ds-alert.a11y.test.js +151 -0
  14. package/src/ds-alert/ds-alert.js +223 -0
  15. package/src/ds-alert/ds-alert.mdx +142 -0
  16. package/src/ds-alert/ds-alert.stories.js +166 -0
  17. package/src/ds-alert/ds-alert.test.js +256 -0
  18. package/src/ds-alert/index.js +1 -0
  19. package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
  20. package/src/ds-avatar/ds-avatar.js +216 -0
  21. package/src/ds-avatar/ds-avatar.stories.js +120 -0
  22. package/src/ds-avatar/ds-avatar.test.js +83 -0
  23. package/src/ds-avatar/index.js +1 -0
  24. package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
  25. package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
  26. package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
  27. package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
  28. package/src/ds-avatar-extended/index.js +1 -0
  29. package/src/ds-banner/ds-banner.a11y.test.js +51 -0
  30. package/src/ds-banner/ds-banner.js +233 -0
  31. package/src/ds-banner/ds-banner.stories.js +185 -0
  32. package/src/ds-banner/ds-banner.test.js +116 -0
  33. package/src/ds-banner/index.js +1 -0
  34. package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
  35. package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
  36. package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
  37. package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
  38. package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
  39. package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
  40. package/src/ds-button/ds-button.a11y.test.js +49 -0
  41. package/src/ds-button/ds-button.js +205 -0
  42. package/src/ds-button/ds-button.mdx +141 -0
  43. package/src/ds-button/ds-button.stories.js +152 -0
  44. package/src/ds-button/ds-button.test.js +62 -0
  45. package/src/ds-button/index.js +1 -0
  46. package/src/ds-button-group/ds-button-group.js +82 -0
  47. package/src/ds-button-group/ds-button-group.mdx +39 -0
  48. package/src/ds-button-group/ds-button-group.stories.js +47 -0
  49. package/src/ds-button-group/ds-button-group.test.js +47 -0
  50. package/src/ds-button-group/index.js +1 -0
  51. package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
  52. package/src/ds-checkbox/ds-checkbox.js +271 -0
  53. package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
  54. package/src/ds-checkbox/ds-checkbox.test.js +191 -0
  55. package/src/ds-checkbox/index.js +1 -0
  56. package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
  57. package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
  58. package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
  59. package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
  60. package/src/ds-checkbox-group/index.js +1 -0
  61. package/src/ds-dialog/ds-dialog.js +466 -0
  62. package/src/ds-dialog/ds-dialog.stories.js +274 -0
  63. package/src/ds-dialog/ds-dialog.test.js +441 -0
  64. package/src/ds-dialog/index.js +1 -0
  65. package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
  66. package/src/ds-dropdown/ds-dropdown.js +891 -0
  67. package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
  68. package/src/ds-dropdown/ds-dropdown.test.js +268 -0
  69. package/src/ds-dropdown/index.js +1 -0
  70. package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
  71. package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
  72. package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
  73. package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
  74. package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
  75. package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
  76. package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
  77. package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
  78. package/src/ds-file-uploader/ds-file-uploader.js +442 -0
  79. package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
  80. package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
  81. package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
  82. package/src/ds-header/ds-header.a11y.test.js +38 -0
  83. package/src/ds-header/ds-header.js +149 -0
  84. package/src/ds-header/ds-header.stories.js +63 -0
  85. package/src/ds-header/ds-header.test.js +52 -0
  86. package/src/ds-header/index.js +1 -0
  87. package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
  88. package/src/ds-header-nav/ds-header-nav.js +114 -0
  89. package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
  90. package/src/ds-header-nav/ds-header-nav.test.js +93 -0
  91. package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
  92. package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
  93. package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
  94. package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
  95. package/src/ds-icon/ds-icon.a11y.test.js +49 -0
  96. package/src/ds-icon/ds-icon.js +75 -0
  97. package/src/ds-icon/ds-icon.mdx +36 -0
  98. package/src/ds-icon/ds-icon.stories.js +88 -0
  99. package/src/ds-icon/ds-icon.test.js +97 -0
  100. package/src/ds-icon/index.js +1 -0
  101. package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
  102. package/src/ds-icon-button/ds-icon-button.js +224 -0
  103. package/src/ds-icon-button/ds-icon-button.mdx +131 -0
  104. package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
  105. package/src/ds-icon-button/ds-icon-button.test.js +90 -0
  106. package/src/ds-icon-button/index.js +1 -0
  107. package/src/ds-input/ds-input.a11y.test.js +145 -0
  108. package/src/ds-input/ds-input.js +645 -0
  109. package/src/ds-input/ds-input.mdx +251 -0
  110. package/src/ds-input/ds-input.stories.js +298 -0
  111. package/src/ds-input/ds-input.test.js +792 -0
  112. package/src/ds-input/index.js +1 -0
  113. package/src/ds-link/ds-link.js +111 -0
  114. package/src/ds-link/ds-link.stories.js +56 -0
  115. package/src/ds-link/ds-link.test.js +74 -0
  116. package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
  117. package/src/ds-list-item/ds-list-item.js +292 -0
  118. package/src/ds-list-item/ds-list-item.stories.js +101 -0
  119. package/src/ds-list-item/ds-list-item.test.js +63 -0
  120. package/src/ds-menu/ds-menu.js +30 -0
  121. package/src/ds-menu/ds-menu.stories.js +120 -0
  122. package/src/ds-menu/ds-menu.test.js +123 -0
  123. package/src/ds-menu-group/ds-menu-group.js +101 -0
  124. package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
  125. package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
  126. package/src/ds-nav-item/ds-nav-item.js +307 -0
  127. package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
  128. package/src/ds-nav-item/ds-nav-item.test.js +169 -0
  129. package/src/ds-nav-item/index.js +1 -0
  130. package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
  131. package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
  132. package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
  133. package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
  134. package/src/ds-nav-vertical/index.js +1 -0
  135. package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
  136. package/src/ds-pagination/ds-pagination.js +232 -0
  137. package/src/ds-pagination/ds-pagination.stories.js +63 -0
  138. package/src/ds-pagination/ds-pagination.test.js +141 -0
  139. package/src/ds-pagination/index.js +1 -0
  140. package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
  141. package/src/ds-progress-bar/ds-progress-bar.js +81 -0
  142. package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
  143. package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
  144. package/src/ds-radio/ds-radio.a11y.test.js +69 -0
  145. package/src/ds-radio/ds-radio.js +240 -0
  146. package/src/ds-radio/ds-radio.stories.js +102 -0
  147. package/src/ds-radio/ds-radio.test.js +114 -0
  148. package/src/ds-radio/index.js +1 -0
  149. package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
  150. package/src/ds-radio-group/ds-radio-group.js +257 -0
  151. package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
  152. package/src/ds-radio-group/ds-radio-group.test.js +194 -0
  153. package/src/ds-radio-group/index.js +1 -0
  154. package/src/ds-rich-list/ds-rich-list.js +246 -0
  155. package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
  156. package/src/ds-rich-list/ds-rich-list.test.js +293 -0
  157. package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
  158. package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
  159. package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
  160. package/src/ds-slider/ds-slider.js +399 -0
  161. package/src/ds-slider/ds-slider.stories.js +107 -0
  162. package/src/ds-slider/ds-slider.test.js +308 -0
  163. package/src/ds-spinner/ds-spinner.js +173 -0
  164. package/src/ds-spinner/ds-spinner.stories.js +52 -0
  165. package/src/ds-spinner/ds-spinner.test.js +50 -0
  166. package/src/ds-status-border/ds-status-border.js +88 -0
  167. package/src/ds-status-border/ds-status-border.stories.js +242 -0
  168. package/src/ds-status-border/ds-status-border.test.js +168 -0
  169. package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
  170. package/src/ds-stepper/ds-stepper.js +207 -0
  171. package/src/ds-stepper/ds-stepper.stories.js +530 -0
  172. package/src/ds-stepper/ds-stepper.test.js +311 -0
  173. package/src/ds-stepper-item/ds-stepper-item.js +485 -0
  174. package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
  175. package/src/ds-switch/ds-switch.js +348 -0
  176. package/src/ds-switch/ds-switch.stories.js +145 -0
  177. package/src/ds-switch/ds-switch.test.js +226 -0
  178. package/src/ds-switch/index.js +1 -0
  179. package/src/ds-tab-item/ds-tab-item.js +341 -0
  180. package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
  181. package/src/ds-tabs/ds-tab-panel.js +48 -0
  182. package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
  183. package/src/ds-tabs/ds-tabs.js +180 -0
  184. package/src/ds-tabs/ds-tabs.stories.js +152 -0
  185. package/src/ds-tabs/ds-tabs.test.js +306 -0
  186. package/src/ds-tabs/index.js +3 -0
  187. package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
  188. package/src/ds-tag-action/ds-tag-action.js +185 -0
  189. package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
  190. package/src/ds-tag-action/ds-tag-action.test.js +44 -0
  191. package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
  192. package/src/ds-tag-removable/ds-tag-removable.js +146 -0
  193. package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
  194. package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
  195. package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
  196. package/src/ds-tag-status/ds-tag-status.js +164 -0
  197. package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
  198. package/src/ds-tag-status/ds-tag-status.test.js +140 -0
  199. package/src/ds-tag-status/index.js +1 -0
  200. package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
  201. package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
  202. package/src/ds-textarea/ds-textarea.js +505 -0
  203. package/src/ds-textarea/ds-textarea.stories.js +335 -0
  204. package/src/ds-textarea/ds-textarea.test.js +218 -0
  205. package/src/ds-textarea/index.js +1 -0
  206. package/src/ds-thumbnail/ds-thumbnail.js +207 -0
  207. package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
  208. package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
  209. package/src/ds-toast/ds-toast-provider.js +110 -0
  210. package/src/ds-toast/ds-toast.a11y.test.js +34 -0
  211. package/src/ds-toast/ds-toast.js +243 -0
  212. package/src/ds-toast/ds-toast.stories.js +143 -0
  213. package/src/ds-toast/ds-toast.test.js +93 -0
  214. package/src/ds-toast/index.js +2 -0
  215. package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
  216. package/src/ds-tooltip/ds-tooltip.js +217 -0
  217. package/src/ds-tooltip/ds-tooltip.mdx +75 -0
  218. package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
  219. package/src/ds-tooltip/ds-tooltip.test.js +191 -0
  220. package/src/ds-tooltip/index.js +1 -0
  221. package/src/ds-tooltip/positioner.js +117 -0
  222. package/src/index.js +50 -0
  223. package/src/mixins/field-label.mixin.js +113 -0
  224. package/src/mixins/field-message.mixin.js +66 -0
  225. package/src/token-provider/index.js +1 -0
  226. package/src/token-provider/token-provider.a11y.test.js +44 -0
  227. package/src/token-provider/token-provider.js +85 -0
  228. package/src/token-provider/token-provider.stories.js +105 -0
  229. package/src/token-provider/token-provider.test.js +134 -0
  230. package/src/utils/number-input.utils.js +42 -0
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-icon-button.js';
4
+
5
+ describe('ds-icon-button accessibility', () => {
6
+ let container;
7
+
8
+ beforeEach(() => {
9
+ container = document.createElement('div');
10
+ document.body.appendChild(container);
11
+ });
12
+
13
+ afterEach(() => {
14
+ container.remove();
15
+ });
16
+
17
+ it('should pass axe checks with aria-label', async () => {
18
+ container.innerHTML = '<ds-icon-button icon="add" aria-label="Add item"></ds-icon-button>';
19
+
20
+ await new Promise(resolve => setTimeout(resolve, 100));
21
+
22
+ const results = await axe.run(container, {
23
+ rules: {
24
+ 'color-contrast': { enabled: false },
25
+ 'button-name': { enabled: false }, // Handled by aria-label on host
26
+ 'nested-interactive': { enabled: false } // Shadow DOM boundary issue
27
+ }
28
+ });
29
+
30
+ if (results.violations.length > 0) {
31
+ console.log('Violations:', JSON.stringify(results.violations, null, 2));
32
+ }
33
+ expect(results.violations).toHaveLength(0);
34
+ });
35
+
36
+ it('should pass axe checks for all variants', async () => {
37
+ const variants = ['primary', 'secondary', 'outline', 'action', 'tertiary'];
38
+
39
+ container.innerHTML = variants.map(v =>
40
+ `<ds-icon-button icon="star" variant="${v}" aria-label="Star ${v}"></ds-icon-button>`
41
+ ).join('');
42
+
43
+ await new Promise(resolve => setTimeout(resolve, 100));
44
+
45
+ const results = await axe.run(container, {
46
+ rules: {
47
+ 'color-contrast': { enabled: false },
48
+ 'button-name': { enabled: false },
49
+ 'nested-interactive': { enabled: false }
50
+ }
51
+ });
52
+
53
+ expect(results.violations).toHaveLength(0);
54
+ });
55
+ });
@@ -0,0 +1,224 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon/ds-icon.js';
3
+
4
+
5
+ /**
6
+ * Icon-only button component for the Design System
7
+ *
8
+ * @element ds-icon-button
9
+ *
10
+ * @prop {string} icon - Icon name (required)
11
+ * @prop {string} variant - Button style variant: 'primary' | 'secondary' | 'outline' | 'action' | 'tertiary' (default: 'primary')
12
+ * @prop {string} size - Button size: 'm' (32px) | 's' (24px) (default: 'm')
13
+ * @prop {boolean} disabled - Whether the button is disabled
14
+ *
15
+ * @example
16
+ * <ds-icon-button icon="add" aria-label="Add item"></ds-icon-button>
17
+ * <ds-icon-button icon="close" variant="secondary" size="s" aria-label="Close"></ds-icon-button>
18
+ */
19
+ export class DsIconButton extends LitElement {
20
+ static properties = {
21
+ icon: { type: String },
22
+ variant: { type: String, reflect: true },
23
+ size: { type: String, reflect: true },
24
+ disabled: { type: Boolean, reflect: true }
25
+ };
26
+
27
+ static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
28
+
29
+ static styles = css`
30
+ :host {
31
+ display: inline-block;
32
+ vertical-align: middle;
33
+ }
34
+
35
+ button {
36
+ appearance: none;
37
+ display: inline-flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+
41
+ /* Square layout - width equals height */
42
+ aspect-ratio: 1;
43
+ padding: 0;
44
+ border: 1px solid transparent;
45
+ border-radius: var(--ds-radius-action, 999px);
46
+ background: transparent;
47
+ color: inherit;
48
+
49
+ cursor: pointer;
50
+ box-sizing: border-box;
51
+ transition: background-color 0.2s, color 0.2s, border-color 0.2s;
52
+ }
53
+
54
+ /* SIZE M (default): 32px */
55
+ :host([size="m"]) button,
56
+ :host(:not([size])) button {
57
+ width: var(--ds-space-xl, 32px);
58
+ height: var(--ds-space-xl, 32px);
59
+ }
60
+ :host([size="m"]) ds-icon,
61
+ :host(:not([size])) ds-icon {
62
+ --size: var(--ds-icon-size-sm, 20px);
63
+ }
64
+
65
+ /* SIZE S: 24px */
66
+ :host([size="s"]) button {
67
+ width: var(--ds-space-lg, 24px);
68
+ height: var(--ds-space-lg, 24px);
69
+ }
70
+ :host([size="s"]) ds-icon {
71
+ --size: var(--ds-icon-size-xs, 16px);
72
+ }
73
+
74
+ /* Disabled state */
75
+ :host([disabled]) button {
76
+ cursor: not-allowed;
77
+ pointer-events: none;
78
+ }
79
+
80
+ :host([disabled]) {
81
+ pointer-events: none;
82
+ }
83
+
84
+ /* Focus */
85
+ button:focus-visible {
86
+ outline: 2px solid var(--ds-color-border-focus);
87
+ outline-offset: 2px;
88
+ }
89
+
90
+ /*
91
+ * VARIANTS
92
+ */
93
+
94
+ /* PRIMARY */
95
+ :host([variant="primary"]) button {
96
+ background: var(--ds-color-bg-brand);
97
+ color: var(--ds-color-text-inverse);
98
+ }
99
+ :host([variant="primary"]:not([disabled])) button:hover {
100
+ background: var(--ds-color-bg-brand-hover);
101
+ }
102
+ :host([variant="primary"]:not([disabled])) button:active {
103
+ background: var(--ds-color-bg-brand-pressed);
104
+ }
105
+
106
+ /* SECONDARY */
107
+ :host([variant="secondary"]) button {
108
+ background: var(--ds-color-bg-secondary);
109
+ color: var(--ds-color-text-default);
110
+ border-color: var(--ds-color-border-strong);
111
+ }
112
+ :host([variant="secondary"]:not([disabled])) button:hover {
113
+ background: var(--ds-color-bg-hover);
114
+ }
115
+ :host([variant="secondary"]:not([disabled])) button:active {
116
+ background: var(--ds-color-bg-pressed);
117
+ }
118
+
119
+ /* OUTLINE */
120
+ :host([variant="outline"]) button {
121
+ background: transparent;
122
+ color: var(--ds-color-text-brand);
123
+ border-color: var(--ds-color-border-brand);
124
+ }
125
+ :host([variant="outline"]:not([disabled])) button:hover {
126
+ background: var(--ds-color-bg-hover);
127
+ color: var(--ds-color-text-brand-hover);
128
+ border-color: var(--ds-color-border-brand-hover);
129
+ }
130
+ :host([variant="outline"]:not([disabled])) button:active {
131
+ background: var(--ds-color-bg-pressed);
132
+ color: var(--ds-color-text-brand-pressed);
133
+ border-color: var(--ds-color-border-brand-pressed);
134
+ }
135
+
136
+ /* ACTION */
137
+ :host([variant="action"]) button {
138
+ background: transparent;
139
+ color: var(--ds-color-text-default);
140
+ }
141
+ :host([variant="action"]:not([disabled])) button:hover {
142
+ background: var(--ds-color-bg-hover);
143
+ }
144
+ :host([variant="action"]:not([disabled])) button:active {
145
+ background: var(--ds-color-bg-pressed);
146
+ }
147
+
148
+ /* TERTIARY */
149
+ :host([variant="tertiary"]) button {
150
+ background: transparent;
151
+ color: var(--ds-color-text-brand);
152
+ }
153
+ :host([variant="tertiary"]:not([disabled])) button:hover {
154
+ background: var(--ds-color-bg-hover);
155
+ color: var(--ds-color-text-brand-hover);
156
+ }
157
+ :host([variant="tertiary"]:not([disabled])) button:active {
158
+ background: var(--ds-color-bg-pressed);
159
+ color: var(--ds-color-text-brand-pressed);
160
+ }
161
+
162
+ /* DISABLED STATE for all variants */
163
+ :host([disabled]) button {
164
+ background: var(--ds-color-bg-disabled);
165
+ color: var(--ds-color-text-disabled);
166
+ border-color: transparent;
167
+ }
168
+ :host([variant="secondary"][disabled]) button {
169
+ border-color: var(--ds-color-border-disabled);
170
+ }
171
+ :host([variant="outline"][disabled]) button {
172
+ border-color: var(--ds-color-border-disabled);
173
+ }
174
+
175
+ :host([variant="action"][disabled]) button,
176
+ :host([variant="tertiary"][disabled]) button {
177
+ background: transparent;
178
+ }
179
+ `;
180
+
181
+ constructor() {
182
+ super();
183
+ this.icon = '';
184
+ this.variant = 'primary';
185
+ this.size = 'm';
186
+ this.disabled = false;
187
+ }
188
+
189
+ connectedCallback() {
190
+ super.connectedCallback();
191
+
192
+ // Set role for accessibility (allows aria-label on custom element)
193
+ // Set role for accessibility (allows aria-label on custom element)
194
+ // Removed: Host should not have role="button" because it contains a native button (Nested Interactive Controls violation)
195
+
196
+ // Accessibility warning
197
+ if (!this.hasAttribute('aria-label')) {
198
+ console.warn('ds-icon-button: "aria-label" is required for accessibility');
199
+ }
200
+ }
201
+
202
+ disconnectedCallback() {
203
+ super.disconnectedCallback();
204
+ // No external listeners to clean up currently
205
+ // This method is here for lifecycle consistency and future extensions
206
+ }
207
+
208
+ render() {
209
+ if (!this.icon) {
210
+ console.warn('ds-icon-button: "icon" prop is required');
211
+ return html``;
212
+ }
213
+
214
+ const label = this.getAttribute('aria-label') || '';
215
+
216
+ return html`
217
+ <button ?disabled=${this.disabled} part="button" aria-label="${label}">
218
+ <ds-icon name=${this.icon}></ds-icon>
219
+ </button>
220
+ `;
221
+ }
222
+ }
223
+
224
+ customElements.define('ds-icon-button', DsIconButton);
@@ -0,0 +1,131 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/blocks';
2
+ import * as DsIconButtonStories from './ds-icon-button.stories';
3
+
4
+ <Meta of={DsIconButtonStories} />
5
+
6
+ # Icon Button
7
+
8
+ Compact buttons that use an icon to represent an action, without a text label.
9
+
10
+ ## Playground
11
+ Interact with the button properties using the controls below.
12
+
13
+ <Canvas of={DsIconButtonStories.Primary} />
14
+ <Controls of={DsIconButtonStories.Primary} />
15
+
16
+ ## Usage
17
+
18
+ ### When to use
19
+ - To display more actions (e.g., hamburger menu, overflow menu).
20
+ - To navigate within the same screen (e.g., back to top, close modal).
21
+ - When space is limited and the icon is universally understood (e.g., trash for delete).
22
+
23
+ ### When not to use
24
+ - To display static or read-only information.
25
+ - For bulk selection actions.
26
+ - When the icon meaning is ambiguous without strict context.
27
+
28
+ ## Best Practices
29
+
30
+ ### Accessibility & Clarity
31
+ - ✅ **Tooltips:** Icon-only buttons **must** show a tooltip label on hover for clarity and accessibility.
32
+ - ✅ **Aria-label:** Always provide a descriptive `aria-label` since there is no visible text.
33
+
34
+ ### Layout & Sizing
35
+ - ✅ **Exceptions:** Unlike standard buttons, icon buttons do **not** need to adhere to the 72px minimum width rule.
36
+ - ✅ **Touch Target:** Ensure the touch target remains accessible (minimum 44x44px recommended for mobile) even if the visual size is smaller.
37
+
38
+ ## Types
39
+
40
+ ### Primary
41
+ The primary button should draw attention to the most important action on a screen, so reserve it for actions that are essential to the experience. This helps create a clear visual hierarchy and keeps users focused on what matters most.
42
+
43
+ <Canvas of={DsIconButtonStories.Primary} />
44
+
45
+ ### Secondary
46
+ The secondary button supports less critical actions and complements the primary button.
47
+
48
+ <Canvas of={DsIconButtonStories.Secondary} />
49
+
50
+ ### Outline
51
+ Outline buttons have less prominence than a primary button and slightly more prominence than a tertiary button. It can be used on its own or when there is no clear distinction between multiple important actions on a screen.
52
+
53
+ <Canvas of={DsIconButtonStories.Outline} />
54
+
55
+ ### Tertiary
56
+ Tertiary buttons have the lowest prominence, so they should be used for low impact actions and/or actions that are not directly related with the primary button.
57
+
58
+ <Canvas of={DsIconButtonStories.Tertiary} />
59
+
60
+ ### Action
61
+ Action buttons let users complete routine tasks or make selections within a workflow. They’re designed to be subtle controls and usually appear inside containers like action bars, so they don’t compete with primary call-to-action elements.
62
+
63
+ <Canvas of={DsIconButtonStories.Action} />
64
+
65
+ ## Functionality & States
66
+
67
+ ### Sizes
68
+ Icon buttons support different sizes to fit various contexts.
69
+
70
+ <Canvas of={DsIconButtonStories.SizeSmall} />
71
+
72
+ ### Disabled
73
+ Use the disabled state to indicate that an action is currently unavailable.
74
+
75
+ <Canvas of={DsIconButtonStories.Disabled} />
76
+
77
+ ## Accessibility
78
+
79
+ <table style={{ width: '100%' }}>
80
+ <thead>
81
+ <tr>
82
+ <th>Attribute</th>
83
+ <th>Value</th>
84
+ <th>Notes</th>
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ <tr>
89
+ <td><code>role</code></td>
90
+ <td><code>button</code></td>
91
+ <td>Native button element provides this</td>
92
+ </tr>
93
+ <tr>
94
+ <td><code>aria-label</code></td>
95
+ <td><strong>Required</strong></td>
96
+ <td>Must describe the action (e.g., "Close", "Add item")</td>
97
+ </tr>
98
+ <tr>
99
+ <td><code>disabled</code></td>
100
+ <td><code>true/false</code></td>
101
+ <td>Disables interaction and updates <code>aria-disabled</code></td>
102
+ </tr>
103
+ </tbody>
104
+ </table>
105
+
106
+ > ⚠️ **Important:** Icon-only buttons have no visible label, so `aria-label` is mandatory for screen reader users.
107
+
108
+ ### Keyboard Support
109
+
110
+ <table>
111
+ <thead>
112
+ <tr>
113
+ <th>Key</th>
114
+ <th>Action</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ <tr>
119
+ <td><kbd>Enter</kbd> / <kbd>Space</kbd></td>
120
+ <td>Activates the button</td>
121
+ </tr>
122
+ <tr>
123
+ <td><kbd>Tab</kbd></td>
124
+ <td>Moves focus to next focusable element</td>
125
+ </tr>
126
+ <tr>
127
+ <td><kbd>Shift</kbd> + <kbd>Tab</kbd></td>
128
+ <td>Moves focus to previous focusable element</td>
129
+ </tr>
130
+ </tbody>
131
+ </table>
@@ -0,0 +1,128 @@
1
+ import './ds-icon-button.js';
2
+
3
+ export default {
4
+ title: 'Components/Icon Button',
5
+ component: 'ds-icon-button',
6
+ argTypes: {
7
+ icon: { control: 'text' },
8
+ variant: {
9
+ control: 'select',
10
+ options: ['primary', 'secondary', 'outline', 'action', 'tertiary'],
11
+ },
12
+ size: {
13
+ control: 'select',
14
+ options: ['m', 's'],
15
+ },
16
+ disabled: { control: 'boolean' },
17
+ 'aria-label': { control: 'text' }
18
+ },
19
+ };
20
+
21
+ const createIconButton = ({ icon, variant, size, disabled, 'aria-label': ariaLabel }) => {
22
+ const btn = document.createElement('ds-icon-button');
23
+
24
+ if (icon) btn.setAttribute('icon', icon);
25
+ if (variant) btn.setAttribute('variant', variant);
26
+ if (size) btn.setAttribute('size', size);
27
+ if (disabled) btn.setAttribute('disabled', '');
28
+ if (ariaLabel) btn.setAttribute('aria-label', ariaLabel);
29
+
30
+ return btn;
31
+ };
32
+
33
+ export const Primary = {
34
+ args: {
35
+ icon: 'add',
36
+ variant: 'primary',
37
+ size: 'm',
38
+ 'aria-label': 'Add item'
39
+ },
40
+ render: createIconButton
41
+ };
42
+
43
+ export const Secondary = {
44
+ args: {
45
+ icon: 'close',
46
+ variant: 'secondary',
47
+ size: 'm',
48
+ 'aria-label': 'Close'
49
+ },
50
+ render: createIconButton
51
+ };
52
+
53
+ export const Outline = {
54
+ args: {
55
+ icon: 'info',
56
+ variant: 'outline',
57
+ size: 'm',
58
+ 'aria-label': 'More Information'
59
+ },
60
+ render: createIconButton
61
+ };
62
+
63
+ export const Action = {
64
+ args: {
65
+ icon: 'more-vert',
66
+ variant: 'action',
67
+ size: 'm',
68
+ 'aria-label': 'More Options'
69
+ },
70
+ render: createIconButton
71
+ };
72
+
73
+ export const Tertiary = {
74
+ args: {
75
+ icon: 'delete',
76
+ variant: 'tertiary',
77
+ size: 'm',
78
+ 'aria-label': 'Delete'
79
+ },
80
+ render: createIconButton
81
+ };
82
+
83
+ export const SizeSmall = {
84
+ args: {
85
+ icon: 'add',
86
+ variant: 'primary',
87
+ size: 's',
88
+ 'aria-label': 'Add item'
89
+ },
90
+ render: createIconButton
91
+ };
92
+
93
+ export const Disabled = {
94
+ args: {
95
+ icon: 'add',
96
+ variant: 'primary',
97
+ size: 'm',
98
+ disabled: true,
99
+ 'aria-label': 'Add item'
100
+ },
101
+ render: createIconButton
102
+ };
103
+
104
+ export const AllVariants = {
105
+ render: () => {
106
+ const container = document.createElement('div');
107
+ container.style.cssText = 'display: flex; gap: 16px; align-items: center;';
108
+
109
+ ['primary', 'secondary', 'outline', 'action', 'tertiary'].forEach(variant => {
110
+ container.appendChild(createIconButton({ icon: 'star', variant, size: 'm', 'aria-label': `Star ${variant}` }));
111
+ });
112
+
113
+ return container;
114
+ }
115
+ };
116
+
117
+ export const AllSizes = {
118
+ render: () => {
119
+ const container = document.createElement('div');
120
+ container.style.cssText = 'display: flex; gap: 16px; align-items: center;';
121
+
122
+ ['m', 's'].forEach(size => {
123
+ container.appendChild(createIconButton({ icon: 'add', variant: 'primary', size, 'aria-label': `Add (${size})` }));
124
+ });
125
+
126
+ return container;
127
+ }
128
+ };
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import './ds-icon-button.js';
3
+
4
+ describe('ds-icon-button', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ });
15
+
16
+ it('should render with default props', async () => {
17
+ container.innerHTML = '<ds-icon-button icon="add" aria-label="Add"></ds-icon-button>';
18
+ const iconButton = container.querySelector('ds-icon-button');
19
+
20
+ await new Promise(resolve => setTimeout(resolve, 50));
21
+
22
+ expect(iconButton.shadowRoot).toBeTruthy();
23
+ expect(iconButton.variant).toBe('primary');
24
+ expect(iconButton.size).toBe('m');
25
+ expect(iconButton.disabled).toBe(false);
26
+ });
27
+
28
+ it('should reflect variant attribute', async () => {
29
+ container.innerHTML = '<ds-icon-button icon="close" variant="secondary" aria-label="Close"></ds-icon-button>';
30
+ const iconButton = container.querySelector('ds-icon-button');
31
+
32
+ await new Promise(resolve => setTimeout(resolve, 50));
33
+
34
+ expect(iconButton.getAttribute('variant')).toBe('secondary');
35
+ });
36
+
37
+ it('should reflect size attribute', async () => {
38
+ container.innerHTML = '<ds-icon-button icon="add" size="s" aria-label="Add"></ds-icon-button>';
39
+ const iconButton = container.querySelector('ds-icon-button');
40
+
41
+ await new Promise(resolve => setTimeout(resolve, 50));
42
+
43
+ expect(iconButton.getAttribute('size')).toBe('s');
44
+ });
45
+
46
+ it('should reflect disabled attribute', async () => {
47
+ container.innerHTML = '<ds-icon-button icon="add" disabled aria-label="Add"></ds-icon-button>';
48
+ const iconButton = container.querySelector('ds-icon-button');
49
+
50
+ await new Promise(resolve => setTimeout(resolve, 50));
51
+
52
+ expect(iconButton.hasAttribute('disabled')).toBe(true);
53
+ const button = iconButton.shadowRoot.querySelector('button');
54
+ expect(button.disabled).toBe(true);
55
+ });
56
+
57
+ it('should render ds-icon with correct name', async () => {
58
+ container.innerHTML = '<ds-icon-button icon="star" aria-label="Star"></ds-icon-button>';
59
+ const iconButton = container.querySelector('ds-icon-button');
60
+
61
+ await new Promise(resolve => setTimeout(resolve, 50));
62
+
63
+ const icon = iconButton.shadowRoot.querySelector('ds-icon');
64
+ expect(icon).toBeTruthy();
65
+ expect(icon.getAttribute('name')).toBe('star');
66
+ });
67
+
68
+ it('should warn and render nothing when icon prop is missing', async () => {
69
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
70
+ container.innerHTML = '<ds-icon-button aria-label="Test"></ds-icon-button>';
71
+ const iconButton = container.querySelector('ds-icon-button');
72
+
73
+ await new Promise(resolve => setTimeout(resolve, 50));
74
+
75
+ expect(warnSpy).toHaveBeenCalledWith('ds-icon-button: "icon" prop is required');
76
+ const button = iconButton.shadowRoot.querySelector('button');
77
+ expect(button).toBeFalsy();
78
+ warnSpy.mockRestore();
79
+ });
80
+
81
+ it('should warn when aria-label is missing', async () => {
82
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
83
+ container.innerHTML = '<ds-icon-button icon="add"></ds-icon-button>';
84
+
85
+ await new Promise(resolve => setTimeout(resolve, 50));
86
+
87
+ expect(warnSpy).toHaveBeenCalledWith('ds-icon-button: "aria-label" is required for accessibility');
88
+ warnSpy.mockRestore();
89
+ });
90
+ });
@@ -0,0 +1 @@
1
+ export { DsIconButton } from './ds-icon-button.js';