@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,891 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { FieldMessageMixin, fieldMessageStyles } from '../mixins/field-message.mixin.js';
3
+ import { FieldLabelMixin, fieldLabelStyles } from '../mixins/field-label.mixin.js';
4
+ import { PositionerController } from '../ds-tooltip/positioner.js';
5
+ import '../ds-icon-button/ds-icon-button.js';
6
+ import '../ds-icon/ds-icon.js';
7
+ import '../ds-tooltip/ds-tooltip.js';
8
+ import '../ds-dropdown-panel/ds-dropdown-panel.js';
9
+ import '../ds-list-item/ds-list-item.js';
10
+
11
+ /**
12
+ * Dropdown component using ds-menu and floating-ui for robust selection.
13
+ *
14
+ * @element ds-dropdown
15
+ *
16
+ * @slot - Default slot for ds-list-item elements
17
+ *
18
+ * @prop {string} label - Label text
19
+ * @prop {string} info - Info tooltip text
20
+ * @prop {string} labelPosition - Label position: 'top' | 'inline-start' (default: 'top')
21
+ * @prop {string} labelWidth - Fixed label width for inline layout (e.g., '100px')
22
+ * @prop {string} placeholder - Placeholder text
23
+ * @prop {string} value - Selected value(s). Comma separated string for simplicity in basic usage, or can be bound as array in complex apps.
24
+ * @prop {string} helper - Help text
25
+ * @prop {string} validationStatus - 'error' | 'success'
26
+ * @prop {string} validationMessage - Error message
27
+ * @prop {boolean} disabled - Disabled state
28
+ * @prop {boolean} required - Required field
29
+ * @prop {boolean} clearable - Show clear button
30
+ * @prop {boolean} multiple - Enable multiple selection
31
+ * @prop {boolean} selectAll - Enable "Select All" option (only for multiple)
32
+ *
33
+ * @fires change - Fired when selection changes
34
+ * @fires info-click - Fired when info button is clicked
35
+ */
36
+ export class DsDropdown extends FieldLabelMixin(FieldMessageMixin(LitElement)) {
37
+ static properties = {
38
+ label: { type: String },
39
+ info: { type: String },
40
+ labelPosition: { type: String, attribute: 'label-position' },
41
+ labelWidth: { type: String, attribute: 'label-width' },
42
+ placeholder: { type: String },
43
+ value: { type: String }, // Storing transparently. For multi, likely a joined string or managed externally.
44
+ helper: { type: String },
45
+ validationStatus: { type: String, reflect: true, attribute: 'validation-status' },
46
+ validationMessage: { type: String, attribute: 'validation-message' },
47
+ disabled: { type: Boolean, reflect: true },
48
+ required: { type: Boolean, reflect: true },
49
+ clearable: { type: Boolean },
50
+ multiple: { type: Boolean, reflect: true },
51
+ selectAll: { type: Boolean, attribute: 'select-all' },
52
+ open: { type: Boolean, reflect: true },
53
+ _displayValue: { type: String, state: true },
54
+
55
+ _isAllSelected: { type: Boolean, state: true },
56
+ _isIndeterminate: { type: Boolean, state: true },
57
+ _highlightedIndex: { type: Number, state: true },
58
+ _focusZone: { type: String, state: true }
59
+ };
60
+
61
+ static styles = css`
62
+ :host {
63
+ display: block;
64
+ box-sizing: border-box;
65
+ position: relative; /* For the controller anchor reference */
66
+ }
67
+
68
+ .field-wrapper {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: 4px;
72
+ }
73
+
74
+ /* Label styles */
75
+ ${fieldLabelStyles}
76
+
77
+ /* Trigger Button - Mimics Input */
78
+ .trigger {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ height: 32px;
83
+ padding: 5px 8px; /* 32px height total */
84
+ border: 1px solid var(--ds-color-border-strong);
85
+ border-radius: var(--ds-radius-input);
86
+ background: var(--ds-color-bg-default);
87
+ box-sizing: border-box;
88
+ cursor: pointer;
89
+ user-select: none;
90
+ transition: background-color 0.2s, border-color 0.2s;
91
+ font: var(--ds-typo-content-body-regular);
92
+ color: var(--ds-color-text-default);
93
+ width: 100%;
94
+ outline: none;
95
+ }
96
+
97
+ /* Trigger States */
98
+ :host([disabled]) .trigger {
99
+ background: var(--ds-color-bg-disabled);
100
+ color: var(--ds-color-text-disabled);
101
+ cursor: not-allowed;
102
+ border-color: transparent;
103
+ }
104
+
105
+ :host(:not([disabled])) .trigger:hover {
106
+ background: var(--ds-color-bg-hover);
107
+ }
108
+
109
+ /* Header Zone */
110
+ .header-zone {
111
+ border-bottom: 1px solid var(--ds-color-border-default);
112
+ padding: 16px; /* Direct padding */
113
+ flex-shrink: 0;
114
+ }
115
+
116
+ /* Header ds-list-item needs less padding since zone has it */
117
+ .header-zone ds-list-item {
118
+ padding: 0 !important;
119
+ }
120
+
121
+ /* List Zone - Scrollable container */
122
+ .list-zone {
123
+ flex: 1;
124
+ overflow-y: auto;
125
+ scroll-behavior: smooth;
126
+ padding: 16px; /* Vertical padding for spacing */
127
+ scrollbar-width: thin;
128
+ scrollbar-color: var(--ds-color-border-strong) transparent;
129
+ }
130
+
131
+ /* Items inside list-zone */
132
+ .list-zone ::slotted(ds-list-item) {
133
+ margin: 0 -16px; /* Counteract zone padding for full-width items */
134
+ padding-left: 16px;
135
+ padding-right: 16px;
136
+ }
137
+
138
+ /* Footer Zone */
139
+ .footer-zone {
140
+ border-top: 1px solid var(--ds-color-border-default);
141
+ padding: 16px; /* Direct padding */
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ /* Disabled Label and Message */
146
+ :host([disabled]) .label-row label {
147
+ color: var(--ds-color-text-disabled);
148
+ cursor: not-allowed;
149
+ }
150
+
151
+ /* Select All Header */
152
+ .select-all-header {
153
+ border-bottom: 1px solid var(--ds-color-border-default);
154
+ padding: 8px 16px; /* Restored header padding to match content */
155
+ margin: 0;
156
+ flex-shrink: 0;
157
+ }
158
+ /* Disabled Label and Message */
159
+ :host([disabled]) .label-row label {
160
+ color: var(--ds-color-text-disabled);
161
+ cursor: not-allowed;
162
+ }
163
+
164
+ :host([disabled]) .field-message {
165
+ color: var(--ds-color-text-disabled);
166
+ }
167
+
168
+ :host([open]) .trigger {
169
+ border-color: var(--ds-color-border-focus);
170
+ outline: 2px solid var(--ds-color-border-focus);
171
+ outline-offset: -1px;
172
+ }
173
+
174
+ .trigger:focus-visible {
175
+ border-color: var(--ds-color-border-focus);
176
+ outline: 2px solid var(--ds-color-border-focus);
177
+ outline-offset: -1px;
178
+ }
179
+
180
+
181
+ /* Validation Status on Trigger */
182
+ :host([validation-status="error"]) .trigger {
183
+ border-color: var(--ds-color-border-error);
184
+ }
185
+ :host([validation-status="success"]) .trigger {
186
+ border-color: var(--ds-color-border-success);
187
+ }
188
+
189
+ .value-text {
190
+ white-space: nowrap;
191
+ overflow: hidden;
192
+ text-overflow: ellipsis;
193
+ flex: 1;
194
+ margin-right: 8px;
195
+ }
196
+
197
+ .placeholder {
198
+ color: var(--ds-color-text-secondary);
199
+ }
200
+
201
+ .actions {
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 4px;
205
+ flex-shrink: 0;
206
+ }
207
+
208
+ .chevron {
209
+ transition: transform 0.2s;
210
+ color: var(--ds-color-icon-default);
211
+ }
212
+
213
+ :host([disabled]) .chevron {
214
+ color: var(--ds-color-text-disabled);
215
+ }
216
+
217
+ :host([open]) .chevron {
218
+ transform: rotate(180deg);
219
+ }
220
+
221
+ /* Popup */
222
+ .popup {
223
+ display: none;
224
+ position: fixed; /* Fixed strategy for floating-ui usually */
225
+ z-index: 1000;
226
+ width: max-content;
227
+ /* Width is managed by JS to match trigger or content */
228
+ box-sizing: border-box;
229
+ }
230
+
231
+ :host([open]) .popup {
232
+ display: block;
233
+ }
234
+
235
+ /* Message styles */
236
+ ${fieldMessageStyles}
237
+ `;
238
+
239
+ constructor() {
240
+ super();
241
+ this.label = '';
242
+ this.info = '';
243
+ this.labelPosition = 'top';
244
+ this.labelWidth = '';
245
+ this.placeholder = 'Select...';
246
+ this.value = '';
247
+ this.helper = '';
248
+ this.validationStatus = '';
249
+ this.validationMessage = '';
250
+ this.disabled = false;
251
+ this.required = false;
252
+ this.clearable = false;
253
+ this.multiple = false;
254
+ this.selectAll = false;
255
+ this.open = false;
256
+ this._displayValue = '';
257
+ this._highlightedIndex = -1;
258
+ this._itemIds = new Map(); // Store generated IDs for items
259
+
260
+ // Setup positioner
261
+ this.positioner = new PositionerController(this);
262
+
263
+ // Generate unique ID for accessibility
264
+ this._labelId = `dropdown-label-${Math.random().toString(36).substr(2, 9)}`;
265
+ this._focusZone = 'list'; // 'list' | 'header'
266
+ this._keyboardNavActive = false;
267
+ }
268
+
269
+ connectedCallback() {
270
+ super.connectedCallback();
271
+ // Initialize positioner
272
+ this.positioner.strategy = 'fixed';
273
+ this.positioner.placement = 'bottom-start';
274
+ // We defer target/floating assignment to first render/updated
275
+ document.addEventListener('click', this._handleOutsideClick.bind(this));
276
+ }
277
+
278
+ disconnectedCallback() {
279
+ super.disconnectedCallback();
280
+ document.removeEventListener('click', this._handleOutsideClick.bind(this));
281
+ }
282
+
283
+ updated(changedProperties) {
284
+ if (changedProperties.has('open')) {
285
+ if (this.open) {
286
+ this._highlightedIndex = -1; // Reset highlight on open
287
+ this._focusZone = 'list'; // Default to list or header? Usually list first. User says "tab only works between select all and options". It implies start at list? or start at nothing? Let's start at list (autoHighlight handles it).
288
+
289
+ // Ensure positioner is active
290
+ const trigger = this.shadowRoot.querySelector('.trigger');
291
+ const popup = this.shadowRoot.querySelector('.popup');
292
+ if (trigger && popup) {
293
+ this.positioner.target = trigger;
294
+ this.positioner.floating = popup;
295
+ popup.style.width = `${trigger.offsetWidth}px`;
296
+ }
297
+
298
+ // Wait for components to possibly upgrade/render
299
+ this.updateComplete.then(() => {
300
+ this._autoHighlight();
301
+ });
302
+ } else {
303
+ this._clearHighlight();
304
+ }
305
+ }
306
+
307
+ if (changedProperties.has('value')) {
308
+ const oldValue = changedProperties.get('value');
309
+ const isInitialRun = oldValue === undefined;
310
+
311
+ // If initial run and value is empty, do NOT sync items.
312
+ // Allow _syncValueFromItems to infer value from selected items instead.
313
+ if (isInitialRun && !this.value) {
314
+ // Skip
315
+ } else {
316
+ // When value prop changes from outside, we need to sync items
317
+ this._syncItemsFromValue();
318
+ }
319
+ }
320
+
321
+ if (changedProperties.has('multiple')) {
322
+ this._updateChildrenVariants();
323
+ }
324
+ }
325
+
326
+ _updateChildrenVariants() {
327
+ const listZone = this.shadowRoot?.querySelector('.list-zone');
328
+ const slot = listZone?.querySelector('slot:not([name])');
329
+ if (!slot) return;
330
+
331
+ const items = slot.assignedElements({ flatten: true }).filter(el => el.localName === 'ds-list-item');
332
+
333
+ items.forEach(item => {
334
+ if (item.getAttribute('variant') === 'section') return;
335
+
336
+ const targetVariant = this.multiple ? 'select-checkbox' : 'select-simple';
337
+ if (item.getAttribute('variant') !== targetVariant) {
338
+ item.setAttribute('variant', targetVariant);
339
+ }
340
+ });
341
+ }
342
+
343
+ _handleOutsideClick(e) {
344
+ if (!this.open) return;
345
+
346
+ // If click is inside simple-dropdown, ignore
347
+ if (this.contains(e.target)) return;
348
+ // If click is inside the popup in shadowroot (unlikely to propagate up as outside click, but just in case)
349
+
350
+ this.open = false;
351
+ }
352
+
353
+ _toggleOpen() {
354
+ if (this.disabled) return;
355
+ this.open = !this.open;
356
+ }
357
+
358
+ _handleTriggerKeydown(e) {
359
+ if (this.disabled) return;
360
+
361
+ // Zonal Navigation Logic for Select All
362
+ const hasSelectAll = this.multiple && this.selectAll;
363
+
364
+ if (e.key === 'Tab' && this.open && hasSelectAll) {
365
+ e.preventDefault();
366
+ // Tab resets keyboard mode if needed? Actually focus style is enough.
367
+ // Toggle Zone
368
+ if (this._focusZone === 'list') {
369
+ this._focusZone = 'header';
370
+ this._highlightedIndex = -1; // Clear list highlight
371
+ this._updateItemHighlights();
372
+ this.requestUpdate();
373
+ } else {
374
+ this._focusZone = 'list';
375
+ this._highlightedIndex = 0; // Restore list highlight (start at top)
376
+ this._updateItemHighlights();
377
+ }
378
+ return;
379
+ }
380
+
381
+ if (e.key === 'ArrowDown') {
382
+ e.preventDefault();
383
+ this._keyboardNavActive = true;
384
+ if (!this.open) {
385
+ this.open = true;
386
+ } else {
387
+ if (this._focusZone === 'list') {
388
+ this._moveHighlight(1);
389
+ }
390
+ // If sticky in header, do nothing (or move to list?) User implies sticky options only.
391
+ }
392
+ } else if (e.key === 'ArrowUp') {
393
+ e.preventDefault();
394
+ this._keyboardNavActive = true;
395
+ if (!this.open) {
396
+ this.open = true;
397
+ } else {
398
+ if (this._focusZone === 'list') {
399
+ this._moveHighlight(-1);
400
+ }
401
+ }
402
+ } else if (e.key === 'Enter' || (e.key === ' ' && !this.open)) {
403
+ e.preventDefault();
404
+ if (!this.open) {
405
+ this.open = true;
406
+ } else {
407
+ if (this._focusZone === 'header') {
408
+ this._handleSelectAll();
409
+ } else if (this._highlightedIndex >= 0) {
410
+ this._selectHighlighted();
411
+ }
412
+ }
413
+ } else if (e.key === 'Escape') {
414
+ if (this.open) {
415
+ e.preventDefault();
416
+ this.open = false;
417
+ }
418
+ } else if (e.key === 'Tab') {
419
+ // Normal Tab behavior (close and move on) ONLY if not trapping in SelectAll
420
+ // If SelectAll is NOT present, we close and let event bubble default
421
+ this.open = false;
422
+ }
423
+ }
424
+
425
+ _autoHighlight() {
426
+ const items = this._getEnabledItems();
427
+ if (items.length === 0) return;
428
+
429
+ const selectedIndex = items.findIndex(el => el.selected || el.hasAttribute('selected'));
430
+ this._highlightedIndex = selectedIndex >= 0 ? selectedIndex : 0;
431
+ this._updateItemHighlights();
432
+ }
433
+
434
+ _moveHighlight(delta) {
435
+ const items = this._getEnabledItems();
436
+ if (items.length === 0) return;
437
+
438
+ let nextIndex = this._highlightedIndex + delta;
439
+ if (nextIndex < 0) nextIndex = items.length - 1;
440
+ if (nextIndex >= items.length) nextIndex = 0;
441
+
442
+ this._highlightedIndex = nextIndex;
443
+ this._updateItemHighlights();
444
+ this._scrollHighlightedIntoView();
445
+ }
446
+
447
+ _clearHighlight() {
448
+ this._highlightedIndex = -1;
449
+ this._updateItemHighlights();
450
+ }
451
+
452
+ _updateItemHighlights() {
453
+ const items = this._getEnabledItems();
454
+ items.forEach((item, index) => {
455
+ const isHighlighted = (index === this._highlightedIndex);
456
+ item.highlighted = isHighlighted;
457
+
458
+ // Ensure ID for accessibility
459
+ if (!item.id) {
460
+ item.id = `ds-item-${Math.random().toString(36).substr(2, 9)}`;
461
+ }
462
+ });
463
+
464
+ // Force trigger update to reflect new aria-activedescendant
465
+ this.requestUpdate();
466
+ }
467
+
468
+ _scrollHighlightedIntoView() {
469
+ const items = this._getEnabledItems();
470
+ const highlighted = items[this._highlightedIndex];
471
+ if (highlighted) {
472
+ highlighted.scrollIntoView({
473
+ block: 'nearest',
474
+ behavior: 'smooth' // Smooth scrolling!
475
+ });
476
+ }
477
+ }
478
+
479
+ _selectHighlighted() {
480
+ const items = this._getEnabledItems();
481
+ const item = items[this._highlightedIndex];
482
+ if (item && item.getAttribute('variant') !== 'section') {
483
+ item.click(); // Trigger the item select logic
484
+ }
485
+ }
486
+
487
+ _getEnabledItems() {
488
+ // Items are now inside .list-zone, get them from the default slot
489
+ const listZone = this.shadowRoot.querySelector('.list-zone');
490
+ const slot = listZone?.querySelector('slot:not([name])');
491
+ if (!slot) return [];
492
+
493
+ // Slotted items (in the default slot, which is inside list-zone)
494
+ const lightItems = slot.assignedElements({ flatten: true }).filter(el => el.localName === 'ds-list-item' && !el.hasAttribute('disabled'));
495
+
496
+ // Filter out decorative items (sections/dividers)
497
+ return lightItems.filter(item => {
498
+ const variant = item.getAttribute('variant');
499
+ return variant !== 'section' && !item.hasAttribute('hide-label');
500
+ });
501
+ }
502
+
503
+ _handleItemSelect(e) {
504
+ // With flat architecture, we need to ensure we caught a list item click.
505
+ const path = e.composedPath();
506
+ const item = path.find(el => el.localName === 'ds-list-item');
507
+
508
+ if (!item || item.hasAttribute('disabled') || item.getAttribute('variant') === 'section') return;
509
+
510
+ // Toggle logic
511
+ if (this.multiple) {
512
+ item.selected = !item.selected;
513
+ } else {
514
+ // Single select: Select this, deselect others
515
+ item.selected = true;
516
+ const allItems = this._getEnabledItems();
517
+ allItems.forEach(el => {
518
+ if (el !== item) el.selected = false;
519
+ });
520
+ }
521
+
522
+ // Process change
523
+ setTimeout(() => {
524
+ this._syncValueFromItems();
525
+
526
+ if (!this.multiple) {
527
+ this.open = false;
528
+ this.shadowRoot.querySelector('.trigger')?.focus();
529
+ }
530
+ }, 0);
531
+ }
532
+
533
+ _handleMouseMove(e) {
534
+ // Standard detection: Only disable keyboard mode if mouse actually moved.
535
+ // movementX/Y is supported in modern browsers.
536
+ if (e.movementX !== 0 || e.movementY !== 0) {
537
+ this._keyboardNavActive = false;
538
+ }
539
+ }
540
+
541
+ _handleMouseOver(e) {
542
+ // If keyboard mode is active, completely ignore hover events
543
+ // (which might be synthetic due to scroll)
544
+ if (this._keyboardNavActive) return;
545
+
546
+ const path = e.composedPath();
547
+ const item = path.find(el => el.localName === 'ds-list-item');
548
+ if (!item) return;
549
+
550
+ const items = this._getEnabledItems();
551
+ const index = items.indexOf(item);
552
+
553
+ if (index >= 0 && index !== this._highlightedIndex) {
554
+ this._highlightedIndex = index;
555
+ this._focusZone = 'list'; // Ensure keyboard knows we are in list
556
+ this._updateItemHighlights();
557
+ }
558
+ }
559
+
560
+ _handleSlotChange(e) {
561
+ this._updateChildrenVariants();
562
+ if (this.value) {
563
+ this._syncItemsFromValue();
564
+ } else {
565
+ this._syncValueFromItems();
566
+ }
567
+ }
568
+
569
+ _syncItemsFromValue() {
570
+ const listZone = this.shadowRoot.querySelector('.list-zone');
571
+ const slot = listZone?.querySelector('slot:not([name])');
572
+ if (!slot) return;
573
+
574
+ const items = slot.assignedElements({ flatten: true }).filter(el => el.localName === 'ds-list-item');
575
+ if (!items.length) return;
576
+
577
+ const currentValues = this.value ? this.value.split(',').map(v => v.trim()) : [];
578
+
579
+ items.forEach(item => {
580
+ const itemValue = item.getAttribute('value') || item.getAttribute('label');
581
+ if (currentValues.includes(itemValue)) {
582
+ item.selected = true;
583
+ item.setAttribute('selected', '');
584
+ } else {
585
+ item.selected = false;
586
+ item.removeAttribute('selected');
587
+ }
588
+ });
589
+
590
+ // Update display value after items are compliant
591
+ this._updateDisplayValueFromItems(items);
592
+ }
593
+
594
+ _syncValueFromItems(slotArg) {
595
+ const listZone = this.shadowRoot.querySelector('.list-zone');
596
+ const slot = slotArg || listZone?.querySelector('slot:not([name])');
597
+ if (!slot) return;
598
+
599
+ const items = slot.assignedElements({ flatten: true }).filter(el => el.localName === 'ds-list-item');
600
+
601
+
602
+ // console.log('DEBUG: _syncValueFromItems', { itemsCount: items.length, items });
603
+
604
+ if (this.multiple) {
605
+ const selectedItems = items.filter(el => {
606
+ // console.log('DEBUG: Item check:', el, el.hasAttribute('selected'), el.selected);
607
+ return el.hasAttribute('selected') || el.selected;
608
+ });
609
+ const values = selectedItems.map(el => el.getAttribute('value') || el.getAttribute('label') || el.innerText);
610
+ const newValue = values.join(',');
611
+
612
+ if (this.value !== newValue) {
613
+ this.value = newValue;
614
+ this._notifyChange();
615
+ }
616
+
617
+ } else {
618
+ const selectedItem = items.find(el => {
619
+ // console.log('DEBUG: Single Item check:', el, el.getAttribute('label'), el.hasAttribute('selected'), el.selected);
620
+ return el.hasAttribute('selected') || el.selected;
621
+ });
622
+ let newValue = '';
623
+ if (selectedItem) {
624
+ newValue = selectedItem.getAttribute('value') || selectedItem.getAttribute('label');
625
+ }
626
+
627
+ // console.log('DEBUG: New Value:', newValue);
628
+
629
+ if (this.value !== newValue) {
630
+ this.value = newValue;
631
+ this._notifyChange();
632
+ }
633
+ }
634
+
635
+ this._updateDisplayValueFromItems(items);
636
+ }
637
+
638
+ _updateDisplayValueFromItems(items) {
639
+ if (!items || !items.length) {
640
+ this._displayValue = '';
641
+ this._isAllSelected = false;
642
+ this._isIndeterminate = false;
643
+ return;
644
+ }
645
+
646
+ // CRITICAL: Filter out sections BEFORE counting
647
+ const selectableItems = Array.from(items).filter(item =>
648
+ item.getAttribute('variant') !== 'section'
649
+ );
650
+ const selectedItems = selectableItems.filter(el =>
651
+ el.hasAttribute('selected') || el.selected
652
+ );
653
+
654
+ const selectedCount = selectedItems.length;
655
+ const totalCount = selectableItems.length;
656
+
657
+
658
+
659
+ if (this.multiple) {
660
+ if (selectedCount === 0) {
661
+ this._displayValue = '';
662
+ this._isAllSelected = false;
663
+ this._isIndeterminate = false;
664
+ } else if (selectedCount === totalCount && totalCount > 0) {
665
+ // ALL selectable items are selected
666
+ this._displayValue = 'All selected';
667
+ this._isAllSelected = true;
668
+ this._isIndeterminate = false;
669
+ } else {
670
+ // SOME items selected (indeterminate)
671
+ const labels = selectedItems.map(el => el.getAttribute('label') || el.innerText.trim());
672
+ this._displayValue = labels.join(', ');
673
+ this._isAllSelected = false;
674
+ this._isIndeterminate = true;
675
+ }
676
+ } else {
677
+ // Single select mode
678
+ if (selectedItems.length > 0) {
679
+ this._displayValue = selectedItems[0].getAttribute('label') || selectedItems[0].innerText.trim();
680
+ } else {
681
+ this._displayValue = '';
682
+ }
683
+ this._isAllSelected = false;
684
+ this._isIndeterminate = false;
685
+ }
686
+ }
687
+
688
+
689
+
690
+ _handleSelectAll() {
691
+ // Get items from list-zone
692
+ const listZone = this.shadowRoot.querySelector('.list-zone');
693
+ const slot = listZone?.querySelector('slot:not([name])');
694
+ if (!slot) return;
695
+
696
+ const items = slot.assignedElements({ flatten: true }).filter(el => {
697
+ return el.localName === 'ds-list-item' && el.getAttribute('variant') !== 'section';
698
+ });
699
+
700
+ if (items.length === 0) return;
701
+
702
+ // Check current state: are ALL items selected?
703
+ const allSelected = items.every(el => el.selected === true || el.hasAttribute('selected'));
704
+
705
+ // Toggle: if ALL are selected, deselect all. Otherwise, select all.
706
+ const newState = !allSelected;
707
+
708
+
709
+
710
+ items.forEach(el => {
711
+ el.selected = newState;
712
+ if (newState) {
713
+ el.setAttribute('selected', '');
714
+ } else {
715
+ el.removeAttribute('selected');
716
+ }
717
+ });
718
+
719
+ // Update states IMMEDIATELY (not waiting for _syncValueFromItems)
720
+ this._isAllSelected = newState;
721
+ this._isIndeterminate = false;
722
+
723
+ // Then sync the value properly
724
+ this._syncValueFromItems();
725
+
726
+ // Force re-render
727
+ this.requestUpdate();
728
+ }
729
+
730
+ _handleClear(e) {
731
+ e.stopPropagation();
732
+ const listZone = this.shadowRoot.querySelector('.list-zone');
733
+ const slot = listZone?.querySelector('slot:not([name])');
734
+ if (!slot) return;
735
+
736
+ const items = slot.assignedElements({ flatten: true }).filter(el => el.localName === 'ds-list-item');
737
+
738
+ items.forEach(el => {
739
+ el.selected = false;
740
+ el.removeAttribute('selected');
741
+ });
742
+
743
+ this._syncValueFromItems();
744
+ }
745
+
746
+ _notifyChange() {
747
+ this.dispatchEvent(new CustomEvent('change', {
748
+ detail: { value: this.value },
749
+ bubbles: true,
750
+ composed: true
751
+ }));
752
+ }
753
+
754
+ _hasHeaderContent() {
755
+ // Has content if: selectAll is enabled OR slot="header" has children
756
+ if (this.multiple && this.selectAll) return true;
757
+
758
+ const headerSlot = this.renderRoot?.querySelector('slot[name="header"]');
759
+ return headerSlot?.assignedElements().length > 0;
760
+ }
761
+
762
+ _hasFooterContent() {
763
+ const footerSlot = this.renderRoot?.querySelector('slot[name="footer"]');
764
+ return footerSlot?.assignedElements().length > 0;
765
+ }
766
+
767
+ render() {
768
+ const hasValue = !!this._displayValue;
769
+ const showClear = this.clearable && hasValue && !this.disabled;
770
+ const activeDescendant = this._highlightedIndex >= 0 ? this._getEnabledItems()[this._highlightedIndex]?.id : undefined;
771
+ const isInline = this.labelPosition === 'inline-start';
772
+ const wrapperClass = isInline ? 'field-wrapper inline-label' : 'field-wrapper';
773
+ const labelStyle = isInline && this.labelWidth ? `width: ${this.labelWidth}` : '';
774
+
775
+ const dropdownContent = html`
776
+ <!-- Trigger -->
777
+ <div class="trigger"
778
+ id="${this._triggerId}"
779
+ @click=${this._toggleOpen}
780
+ @keydown=${this._handleTriggerKeydown}
781
+ tabindex="${this.disabled ? -1 : 0}"
782
+ role="combobox"
783
+ aria-labelledby="${this._labelId}"
784
+ aria-expanded="${this.open}"
785
+ aria-haspopup="listbox"
786
+ aria-controls="dropdown-menu"
787
+ aria-activedescendant="${activeDescendant || ''}"
788
+ >
789
+ <span class="value-text ${hasValue ? '' : 'placeholder'}">
790
+ ${hasValue ? this._displayValue : this.placeholder}
791
+ </span>
792
+
793
+ <div class="actions">
794
+ ${showClear ? html`
795
+ <ds-icon-button icon="close" variant="action" size="s" aria-label="Clear selection" @click=${this._handleClear}></ds-icon-button>
796
+ ` : nothing}
797
+ <ds-icon name="expand-more" size="sm" class="chevron"></ds-icon>
798
+ </div>
799
+ </div>
800
+
801
+ <!-- Popup Menu -->
802
+ <div class="popup" @mousedown=${(e) => e.preventDefault()}>
803
+ <ds-dropdown-panel
804
+ id="dropdown-list"
805
+ role="listbox"
806
+ @click=${this._handleItemSelect}
807
+ @mouseover=${this._handleMouseOver}
808
+ @mousemove=${this._handleMouseMove}
809
+ >
810
+ <!-- Header Zone (only if has content) -->
811
+ ${this._hasHeaderContent() ? html`
812
+ <div class="header-zone">
813
+ <slot name="header">
814
+ ${this.multiple && this.selectAll ? html`
815
+ <ds-list-item
816
+ variant="select-checkbox"
817
+ label="Select All"
818
+ ?selected=${this._isAllSelected}
819
+ ?indeterminate=${this._isIndeterminate}
820
+ ?highlighted=${this._focusZone === 'header'}
821
+ @click=${(e) => { e.stopPropagation(); e.preventDefault(); this._handleSelectAll(); }}
822
+ ></ds-list-item>
823
+ ` : nothing}
824
+ </slot>
825
+ </div>
826
+ ` : nothing}
827
+
828
+ <!-- List Zone (Scrollable items) -->
829
+ <div class="list-zone" role="listbox">
830
+ <slot @slotchange=${this._handleSlotChange}></slot>
831
+ </div>
832
+
833
+ <!-- Footer Zone (only if has content) -->
834
+ ${this._hasFooterContent() ? html`
835
+ <div class="footer-zone">
836
+ <slot name="footer"></slot>
837
+ </div>
838
+ ` : nothing}
839
+ </ds-dropdown-panel>
840
+ </div>
841
+
842
+ <!-- Helper/Error Mixin -->
843
+ ${this.renderFieldMessage(this.helper, this.validationMessage)}
844
+ `;
845
+
846
+ if (isInline) {
847
+ return html`
848
+ <div class="${wrapperClass}">
849
+ <div class="label-row" part="label-row" style="${labelStyle}">
850
+ ${this.label ? html`<label id="${this._labelId}">${this.label}</label>` : ''}
851
+ ${this.info ? html`
852
+ <ds-tooltip content="${this.info}" placement="top">
853
+ <ds-icon-button
854
+ icon="info"
855
+ variant="action"
856
+ size="s"
857
+ aria-label="${this.info}"
858
+ @click=${this._handleInfoClick}
859
+ ></ds-icon-button>
860
+ </ds-tooltip>
861
+ ` : ''}
862
+ </div>
863
+ <div class="field-content">
864
+ ${dropdownContent}
865
+ </div>
866
+ </div>
867
+ `;
868
+ }
869
+
870
+ return html`
871
+ <div class="field-wrapper">
872
+ <!-- Label Mixin -->
873
+ ${this.renderFieldLabel(this.label, this.info, this._triggerId, this._labelId)}
874
+ ${dropdownContent}
875
+ </div>
876
+ `;
877
+ }
878
+
879
+ _handleInfoClick(e) {
880
+ e.preventDefault();
881
+ e.stopPropagation();
882
+ this.dispatchEvent(new CustomEvent('info-click', {
883
+ bubbles: true,
884
+ composed: true,
885
+ detail: { info: this.info }
886
+ }));
887
+ }
888
+ }
889
+
890
+ customElements.define('ds-dropdown', DsDropdown);
891
+