@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,466 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon-button/ds-icon-button.js';
3
+ import '../ds-icon/ds-icon.js';
4
+
5
+ /**
6
+ * Dialog component for the Design System
7
+ *
8
+ * @element ds-dialog
9
+ *
10
+ * @prop {boolean} open - Whether the dialog is open
11
+ * @prop {string} heading - The title of the dialog
12
+ * @prop {boolean} preventClose - If true, clicking backdrop or Esc won't close it
13
+ * @prop {string} size - 'sm' | 'md' | 'lg' | 'fullscreen' (default: 'md')
14
+ *
15
+ * @slot header - Content for the header (overrides default title/close)
16
+ * @slot body - Main content area
17
+ * @slot footer - Footer area (usually for ds-action-bar)
18
+ * @slot - Default slot, rendering into the body area
19
+ *
20
+ * @event ds-close - Fired when the dialog is requested to close
21
+ * @event ds-open - Fired when the dialog opens
22
+ */
23
+ export class DsDialog extends LitElement {
24
+ static properties = {
25
+ open: { type: Boolean, reflect: true },
26
+ heading: { type: String },
27
+ preventClose: { type: Boolean, attribute: 'prevent-close' },
28
+ size: { type: String, reflect: true },
29
+ modal: { type: Boolean, reflect: true },
30
+ position: { type: String, reflect: true }
31
+ };
32
+
33
+ static styles = css`
34
+ :host {
35
+ display: contents;
36
+ }
37
+
38
+ /* Native Dialog Reset & Base Styles */
39
+ dialog {
40
+ padding: 0;
41
+ border: none;
42
+ border-radius: var(--ds-radius-container, 0px);
43
+ background: var(--ds-color-bg-surface, #ffffff);
44
+ color: var(--ds-color-text-default);
45
+
46
+ /* Tokens */
47
+ box-shadow: var(--ds-elevation-floating, 0 2px 12px 0 #e6e2dc);
48
+ z-index: var(--ds-zindex-modal, 900);
49
+
50
+ box-sizing: border-box;
51
+
52
+ /* Flex Column Layout for Sticky Header/Footer */
53
+ display: flex;
54
+ flex-direction: column;
55
+
56
+ /* Height Best Practice:
57
+ Use fit-content so it only takes what it needs,
58
+ up to the max-height constraint.
59
+ */
60
+ height: fit-content;
61
+ max-height: calc(100vh - 48px); /* Safety margin */
62
+ max-width: 100vw;
63
+
64
+ /* Centering via native behavior */
65
+ margin: auto;
66
+ }
67
+
68
+ /*
69
+ * NON-MODAL (Floating Window) BEHAVIOR
70
+ * Ensure it stays fixed on screen and doesn't block clicks outside.
71
+ */
72
+ dialog:not(.is-modal) {
73
+ position: fixed;
74
+ pointer-events: none; /* Let clicks pass through the "glass" */
75
+ }
76
+
77
+ /* POSITIONS for Non-Modal */
78
+ /* Center (Default) */
79
+ :host([position="center"]) dialog:not(.is-modal),
80
+ :host(:not([position])) dialog:not(.is-modal) {
81
+ inset: 0;
82
+ margin: auto;
83
+ }
84
+
85
+ /* Top Left */
86
+ :host([position="top-left"]) dialog:not(.is-modal) {
87
+ inset: auto;
88
+ top: 24px;
89
+ left: 24px;
90
+ margin: 0;
91
+ }
92
+
93
+ /* Top Right */
94
+ :host([position="top-right"]) dialog:not(.is-modal) {
95
+ inset: auto;
96
+ top: 24px;
97
+ right: 24px;
98
+ margin: 0;
99
+ }
100
+
101
+ /* Bottom Left */
102
+ :host([position="bottom-left"]) dialog:not(.is-modal) {
103
+ inset: auto;
104
+ bottom: 24px;
105
+ left: 24px;
106
+ margin: 0;
107
+ }
108
+
109
+ /* Bottom Right */
110
+ :host([position="bottom-right"]) dialog:not(.is-modal) {
111
+ inset: auto;
112
+ bottom: 24px;
113
+ right: 24px;
114
+ margin: 0;
115
+ }
116
+
117
+ /* Re-enable pointer events on the dialog content itself */
118
+ dialog:not(.is-modal) > * {
119
+ pointer-events: auto;
120
+ }
121
+
122
+ /* Ensure styles apply when polyfilled or if display is overridden */
123
+ dialog:not([open]) {
124
+ display: none;
125
+ }
126
+
127
+ /* Remove default focus ring from the dialog container itself */
128
+ dialog:focus,
129
+ dialog:focus-visible {
130
+ outline: none;
131
+ }
132
+
133
+ /* Only show backdrop for modal dialogs */
134
+ dialog.is-modal::backdrop {
135
+ background: var(--ds-color-bg-backdrop, #fbfaf9cc);
136
+ backdrop-filter: blur(2px);
137
+ z-index: var(--ds-zindex-backdrop, 800);
138
+ }
139
+
140
+ /* Fallback for browsers/polyfills where ::backdrop might not work as expected or for extra safety */
141
+ dialog:not([open])::backdrop {
142
+ display: none;
143
+ }
144
+
145
+ /*
146
+ * SIZES
147
+ */
148
+
149
+ /* SM: 300px - 520px */
150
+ :host([size="sm"]) dialog {
151
+ width: 100%;
152
+ min-width: 300px;
153
+ max-width: 520px;
154
+ }
155
+
156
+ /* MD: 720px */
157
+ :host([size="md"]) dialog {
158
+ width: 720px;
159
+ max-width: calc(100vw - 32px); /* Safety margin */
160
+ }
161
+
162
+ /* LG: 1080px */
163
+ :host([size="lg"]) dialog {
164
+ width: 1080px;
165
+ max-width: calc(100vw - 32px); /* Safety margin */
166
+ }
167
+
168
+ /* Fullscreen */
169
+ :host([size="fullscreen"]) dialog {
170
+ width: 100vw;
171
+ height: 100vh;
172
+ max-width: 100%;
173
+ max-height: 100%;
174
+ border-radius: 0;
175
+ }
176
+
177
+ /*
178
+ * HEADER
179
+ * Padding: 16px Top, 16px Horizontal (0 Bottom, handled by Body gap)
180
+ * Sticky Top (Flex item doesn't shrink)
181
+ */
182
+ .header {
183
+ flex-shrink: 0;
184
+ display: flex;
185
+ justify-content: space-between;
186
+ align-items: center;
187
+ padding: 16px 16px 0 16px;
188
+ }
189
+
190
+ .header-title {
191
+ /* Token: title-s -> typo_heading_sm */
192
+ font: var(--ds-typo-heading-sm, 700 18px 'Noto Sans', sans-serif);
193
+ margin: 0;
194
+ color: var(--ds-color-text-default);
195
+ }
196
+
197
+ /*
198
+ * BODY
199
+ * Scrollable area
200
+ * Padding: 8px Top, 24px Bottom, 16px Horizontal
201
+ * Token: body-default -> typo_content_body_regular
202
+ */
203
+ .body {
204
+ flex-grow: 1;
205
+ overflow-y: auto;
206
+ padding: 8px 16px 24px 16px;
207
+ font: var(--ds-typo-content-body-regular, 400 14px 'Noto Sans', sans-serif);
208
+ color: var(--ds-color-text-default);
209
+ }
210
+
211
+ /* Remove default outline and apply custom one if needed,
212
+ but typically internal content should manage its own focus styles.
213
+ User requested to remove the "native aspect".
214
+ We'll remove the outline from the container and rely on children-level focus
215
+ or specific focus-visible style if strictly required.
216
+ Reverting the previous .body:focus-visible to simple outline: none
217
+ unless User explicitly asks for a specific focus style there again.
218
+ The request was "focus now looks internal and keeps native aspect... remove that border".
219
+ This likely referred to the DIALOG border.
220
+ We will keep the tokenized focus style for the body content purely for accessibility
221
+ but ensure it's clean (no double borders).
222
+ */
223
+ .body:focus,
224
+ .body:focus-visible {
225
+ outline: none; /* Let inner elements handle focus, or use a very specific subtle style */
226
+ }
227
+
228
+ /*
229
+ * FOOTER
230
+ * Sticky Bottom (Flex item doesn't shrink)
231
+ */
232
+ .footer {
233
+ flex-shrink: 0;
234
+ }
235
+ `;
236
+
237
+ constructor() {
238
+ super();
239
+ this.open = false;
240
+ this.heading = '';
241
+ this.preventClose = false;
242
+ this.size = 'md';
243
+ this.modal = false; // Default to non-modal (safer)
244
+ this.position = 'center';
245
+ }
246
+
247
+ disconnectedCallback() {
248
+ super.disconnectedCallback();
249
+ this._unlockScroll();
250
+ }
251
+
252
+ updated(changedProps) {
253
+ // If 'open' changed OR 'modal' changed while open
254
+ if (changedProps.has('open') || (this.open && changedProps.has('modal'))) {
255
+ const dialog = this.shadowRoot.querySelector('dialog');
256
+
257
+ // If we are switching modes (modal <-> non-modal) while open, we might need to reset
258
+ if (changedProps.has('modal') && this.open && dialog.open) {
259
+ dialog.close(); // Close first to reset mode
260
+ this._unlockScroll();
261
+ }
262
+
263
+ if (this.open) {
264
+ // Enforce explicit class for CSS styling (backdrop)
265
+ if (this.modal) {
266
+ dialog.classList.add('is-modal');
267
+ } else {
268
+ dialog.classList.remove('is-modal');
269
+ }
270
+
271
+ if (!dialog.open) {
272
+ // Modal vs Non-Modal
273
+ if (this.modal) {
274
+ dialog.showModal();
275
+ this._lockScroll();
276
+ } else {
277
+ dialog.show();
278
+ // Ensure scroll is unlocked if we switched to non-modal
279
+ this._unlockScroll();
280
+ }
281
+ // Only dispatch if it was a fresh open, but logic here might dispatch on mode switch.
282
+ // Assuming typical usage doesn't toggle modal prop often while open, it's acceptable.
283
+ if (changedProps.has('open')) {
284
+ this.dispatchEvent(new CustomEvent('ds-open', { bubbles: true, composed: true }));
285
+ }
286
+ }
287
+ } else {
288
+ if (dialog.open) {
289
+ dialog.close();
290
+ this._unlockScroll();
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // Helper to get all focusable elements including those inside slots
297
+ _getFocusableElements() {
298
+ const selector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable], ds-button:not([disabled]), ds-icon-button:not([disabled]), ds-input:not([disabled]), ds-link, ds-textarea, ds-select, ds-checkbox, ds-radio';
299
+
300
+ const getAllFocusables = (parent) => {
301
+ let elements = [];
302
+
303
+ // Check children
304
+ const nodes = parent.querySelectorAll ? parent.querySelectorAll(selector) : [];
305
+ elements = [...nodes];
306
+
307
+ // Check slots
308
+ const slots = parent.querySelectorAll ? parent.querySelectorAll('slot') : [];
309
+ slots.forEach(slot => {
310
+ const assigned = slot.assignedElements({ flatten: true });
311
+ assigned.forEach(node => {
312
+ if (node.matches && node.matches(selector)) {
313
+ elements.push(node);
314
+ }
315
+ // Recursively find focusables inside the slotted element
316
+ if (node.querySelectorAll) {
317
+ elements = [...elements, ...node.querySelectorAll(selector)];
318
+ }
319
+ });
320
+ });
321
+
322
+ return elements;
323
+ };
324
+
325
+ // Start from the dialog element in shadow root
326
+ const dialog = this.shadowRoot.querySelector('dialog');
327
+ return getAllFocusables(dialog).filter(el => {
328
+ // Filter out invisible elements if needed, though 'display:none' is usually handled by not being focusable.
329
+ // Native 'inert' or 'hidden' check:
330
+ return el.offsetParent !== null;
331
+ });
332
+ }
333
+
334
+ _lockScroll() {
335
+ if (this.modal) {
336
+ document.body.style.overflow = 'hidden';
337
+ }
338
+ }
339
+
340
+ _unlockScroll() {
341
+ document.body.style.overflow = '';
342
+ }
343
+
344
+ // Internal method to trigger close event (explicit action)
345
+ _requestClose() {
346
+ // Unlock scroll BEFORE emitting close event so UI feels responsive
347
+ // Actually, better to do it when the prop changes to false, but
348
+ // if the user handles the event by removing the element or setting open=false,
349
+ // the updated() or disconnectedCallback() will handle it.
350
+ // However, to be safe against edge cases (like simple removal):
351
+
352
+ this.dispatchEvent(new CustomEvent('ds-close', { bubbles: true, composed: true }));
353
+ }
354
+
355
+ _handleDialogCancel(e) {
356
+ e.preventDefault(); // Prevent native close immediately
357
+
358
+ if (!this.preventClose) {
359
+ this._requestClose();
360
+ }
361
+ }
362
+
363
+ _handleBackdropClick(e) {
364
+ // Only applies to modal
365
+ if (!this.modal) return;
366
+
367
+ if (e.target.tagName === 'DIALOG') {
368
+ const rect = e.target.getBoundingClientRect();
369
+ const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.top + rect.height &&
370
+ rect.left <= e.clientX && e.clientX <= rect.left + rect.width);
371
+
372
+ if (!isInDialog) {
373
+ if (!this.preventClose) {
374
+ this._requestClose();
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ _handleKeyDown(e) {
381
+ if (!this.open) return;
382
+
383
+ // Close on Escape (for non-modal mostly, as native handles modal Esc)
384
+ if (e.key === 'Escape') {
385
+ if (!this.modal && !this.preventClose) {
386
+ e.preventDefault();
387
+ this._requestClose();
388
+ }
389
+ return;
390
+ }
391
+
392
+ // Explicit Tab Trap for Modal
393
+ if (e.key === 'Tab' && this.modal) {
394
+ const focusables = this._getFocusableElements();
395
+ if (focusables.length === 0) return;
396
+
397
+ const first = focusables[0];
398
+ const last = focusables[focusables.length - 1];
399
+
400
+ // Check active element in both contexts
401
+ const isFirstFocused = (document.activeElement === first) || (this.shadowRoot.activeElement === first);
402
+ const isLastFocused = (document.activeElement === last) || (this.shadowRoot.activeElement === last);
403
+
404
+ if (e.shiftKey) { // Backward
405
+ if (isFirstFocused) {
406
+ e.preventDefault();
407
+ last.focus();
408
+ }
409
+ } else { // Forward
410
+ if (isLastFocused) {
411
+ e.preventDefault();
412
+ first.focus();
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ render() {
419
+ return html`
420
+ <dialog
421
+ @cancel=${this._handleDialogCancel}
422
+ @click=${this._handleBackdropClick}
423
+ @keydown=${this._handleKeyDown}
424
+ >
425
+ <div class="header">
426
+ <slot name="header">
427
+ <h2 class="header-title">${this.heading}</h2>
428
+ <ds-icon-button
429
+ variant="action"
430
+ size="m"
431
+ icon="close"
432
+ @click=${this._requestClose}
433
+ label="Close dialog"
434
+ ></ds-icon-button>
435
+ </slot>
436
+ </div>
437
+
438
+ <div class="body">
439
+ <slot name="body">
440
+ <slot></slot>
441
+ </slot>
442
+ </div>
443
+
444
+ <div class="footer">
445
+ <slot name="footer" @slotchange=${this._handleFooterSlotChange}></slot>
446
+ </div>
447
+ </dialog>
448
+ `;
449
+ }
450
+
451
+
452
+
453
+ _handleFooterSlotChange(e) {
454
+ if (this.size === 'fullscreen') {
455
+ const slot = e.target;
456
+ const nodes = slot.assignedElements({ flatten: true });
457
+ nodes.forEach(node => {
458
+ if (node.tagName.toLowerCase() === 'ds-action-bar') {
459
+ node.setAttribute('variant', 'page');
460
+ }
461
+ });
462
+ }
463
+ }
464
+ }
465
+
466
+ customElements.define('ds-dialog', DsDialog);