fluentui-svelte 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/components/app-surface/app-surface.svelte +96 -0
  2. package/dist/components/app-surface/app-surface.svelte.d.ts +22 -0
  3. package/dist/components/app-surface/docs/llms.md +73 -0
  4. package/dist/components/app-surface/types.d.ts +19 -0
  5. package/dist/components/app-surface/types.js +1 -0
  6. package/dist/components/app-window/app-window.d.ts +3 -0
  7. package/dist/components/app-window/app-window.js +3 -0
  8. package/dist/components/app-window/app-window.svelte +193 -0
  9. package/dist/components/app-window/app-window.svelte.d.ts +25 -0
  10. package/dist/components/app-window/docs/llms.md +119 -0
  11. package/dist/components/app-window/types.d.ts +66 -0
  12. package/dist/components/app-window/types.js +1 -0
  13. package/dist/components/auto-suggest-box/auto-suggest-box.svelte +47 -31
  14. package/dist/components/auto-suggest-box/auto-suggest-box.svelte.d.ts +2 -2
  15. package/dist/components/auto-suggest-box/docs/llms.md +34 -33
  16. package/dist/components/auto-suggest-box/option.svelte +6 -4
  17. package/dist/components/auto-suggest-box/types.d.ts +13 -16
  18. package/dist/components/avatar/avatar.svelte +4 -3
  19. package/dist/components/avatar/docs/llms.md +1 -1
  20. package/dist/components/avatar/type.d.ts +1 -1
  21. package/dist/components/badge/badge-icon.svelte +2 -2
  22. package/dist/components/badge/badge.svelte +2 -2
  23. package/dist/components/badge/docs/llms.md +2 -2
  24. package/dist/components/badge/types.d.ts +2 -2
  25. package/dist/components/breadcrumb/breadcrumb-button.svelte +2 -2
  26. package/dist/components/breadcrumb/breadcrumb-divider.svelte +2 -2
  27. package/dist/components/breadcrumb/breadcrumb-item.svelte +2 -2
  28. package/dist/components/breadcrumb/breadcrumb.svelte +10 -5
  29. package/dist/components/breadcrumb/docs/llms.md +11 -11
  30. package/dist/components/breadcrumb/types.d.ts +7 -7
  31. package/dist/components/button/button.svelte +2 -2
  32. package/dist/components/button/docs/llms.md +17 -17
  33. package/dist/components/button/split-button.svelte +7 -7
  34. package/dist/components/button/types.d.ts +5 -5
  35. package/dist/components/calendar-date-picker/calendar-date-picker.svelte +98 -10
  36. package/dist/components/calendar-date-picker/calendar-date-picker.svelte.d.ts +1 -1
  37. package/dist/components/calendar-date-picker/docs/llms.md +19 -14
  38. package/dist/components/calendar-date-picker/types.d.ts +22 -8
  39. package/dist/components/calendar-view/calendar-view-days.svelte +3 -2
  40. package/dist/components/calendar-view/calendar-view-grid.js +8 -3
  41. package/dist/components/calendar-view/calendar-view-item.svelte +10 -4
  42. package/dist/components/calendar-view/calendar-view-months.svelte +3 -2
  43. package/dist/components/calendar-view/calendar-view-years.svelte +3 -2
  44. package/dist/components/calendar-view/calendar-view.svelte +11 -4
  45. package/dist/components/calendar-view/calendar-view.svelte.js +5 -0
  46. package/dist/components/calendar-view/docs/llms.md +20 -19
  47. package/dist/components/calendar-view/types.d.ts +6 -1
  48. package/dist/components/card/card-footer.svelte +2 -2
  49. package/dist/components/card/card-header.svelte +5 -4
  50. package/dist/components/card/card-preview.svelte +13 -2
  51. package/dist/components/card/card.svelte +5 -5
  52. package/dist/components/card/docs/llms.md +22 -21
  53. package/dist/components/card/types.d.ts +8 -4
  54. package/dist/components/checkbox/checkbox.svelte +47 -26
  55. package/dist/components/checkbox/checkbox.svelte.d.ts +3 -24
  56. package/dist/components/checkbox/docs/llms.md +24 -23
  57. package/dist/components/checkbox/types.d.ts +10 -23
  58. package/dist/components/date-picker/date-picker.svelte +98 -27
  59. package/dist/components/date-picker/date-picker.svelte.d.ts +1 -1
  60. package/dist/components/date-picker/docs/llms.md +23 -20
  61. package/dist/components/date-picker/types.d.ts +24 -15
  62. package/dist/components/dialog/dialog-actions.svelte +17 -2
  63. package/dist/components/dialog/dialog-content.svelte +2 -2
  64. package/dist/components/dialog/dialog-content.svelte.d.ts +1 -1
  65. package/dist/components/dialog/dialog-surface.svelte +130 -8
  66. package/dist/components/dialog/dialog-title.svelte +23 -6
  67. package/dist/components/dialog/dialog-trigger.svelte +8 -2
  68. package/dist/components/dialog/dialog-trigger.svelte.d.ts +1 -1
  69. package/dist/components/dialog/dialog.svelte +22 -11
  70. package/dist/components/dialog/docs/llms.md +56 -19
  71. package/dist/components/dialog/types.d.ts +31 -9
  72. package/dist/components/dialog/utils.d.ts +2 -0
  73. package/dist/components/dialog/utils.js +1 -0
  74. package/dist/components/divider/divider.svelte +6 -4
  75. package/dist/components/divider/types.d.ts +1 -1
  76. package/dist/components/dropdown/docs/llms.md +30 -28
  77. package/dist/components/dropdown/dropdown-option.svelte +17 -5
  78. package/dist/components/dropdown/dropdown.svelte +182 -84
  79. package/dist/components/dropdown/dropdown.svelte.d.ts +1 -1
  80. package/dist/components/dropdown/types.d.ts +26 -20
  81. package/dist/components/dropdown-select/docs/llms.md +24 -22
  82. package/dist/components/dropdown-select/dropdown-select-option.svelte +2 -2
  83. package/dist/components/dropdown-select/dropdown-select.svelte +42 -7
  84. package/dist/components/dropdown-select/types.d.ts +4 -4
  85. package/dist/components/expander/docs/llms.md +3 -3
  86. package/dist/components/expander/expander.svelte +13 -8
  87. package/dist/components/expander/types.d.ts +5 -5
  88. package/dist/components/flyout/docs/llms.md +1 -1
  89. package/dist/components/flyout/flyout.svelte +17 -6
  90. package/dist/components/flyout/types.d.ts +1 -1
  91. package/dist/components/hyperlink/docs/llms.md +6 -6
  92. package/dist/components/hyperlink/hyperlink.svelte +13 -4
  93. package/dist/components/hyperlink/types.d.ts +1 -1
  94. package/dist/components/info-bar/docs/llms.md +11 -10
  95. package/dist/components/info-bar/info-bar.svelte +3 -2
  96. package/dist/components/info-bar/types.d.ts +4 -0
  97. package/dist/components/label/docs/llms.md +75 -22
  98. package/dist/components/label/label-context.d.ts +14 -0
  99. package/dist/components/label/label-context.js +10 -0
  100. package/dist/components/label/label.svelte +153 -30
  101. package/dist/components/label/label.svelte.d.ts +1 -1
  102. package/dist/components/label/types.d.ts +48 -4
  103. package/dist/components/list-view/docs/llms.md +103 -33
  104. package/dist/components/list-view/list-view-item-branch.svelte +257 -0
  105. package/dist/components/list-view/list-view-item-branch.svelte.d.ts +4 -0
  106. package/dist/components/list-view/list-view-item.svelte +164 -46
  107. package/dist/components/list-view/list-view-item.svelte.d.ts +5 -5
  108. package/dist/components/list-view/list-view.svelte +73 -25
  109. package/dist/components/list-view/types.d.ts +71 -8
  110. package/dist/components/list-view/utils.d.ts +10 -0
  111. package/dist/components/list-view/utils.js +17 -1
  112. package/dist/components/menu/docs/llms.md +98 -15
  113. package/dist/components/menu/menu-command-bar-item.svelte +61 -0
  114. package/dist/components/menu/menu-command-bar-item.svelte.d.ts +14 -0
  115. package/dist/components/menu/menu-command-bar.svelte +218 -0
  116. package/dist/components/menu/menu-command-bar.svelte.d.ts +21 -0
  117. package/dist/components/menu/menu-group.svelte +6 -3
  118. package/dist/components/menu/menu-item-checkbox.svelte +5 -4
  119. package/dist/components/menu/menu-item-radio.svelte +5 -4
  120. package/dist/components/menu/menu-item-switch.svelte +5 -4
  121. package/dist/components/menu/menu-item.svelte +5 -5
  122. package/dist/components/menu/menu-item.svelte.d.ts +2 -3
  123. package/dist/components/menu/menu-list.svelte +10 -1
  124. package/dist/components/menu/menu-popover.svelte +9 -5
  125. package/dist/components/menu/menu-trigger.svelte +2 -1
  126. package/dist/components/menu/menu.svelte.js +1 -0
  127. package/dist/components/menu/types.d.ts +62 -7
  128. package/dist/components/number-box/docs/llms.md +6 -4
  129. package/dist/components/number-box/number-box.svelte +35 -29
  130. package/dist/components/number-box/number-box.svelte.d.ts +1 -1
  131. package/dist/components/number-box/types.d.ts +1 -1
  132. package/dist/components/persona/docs/llms.md +29 -25
  133. package/dist/components/persona/persona.svelte +8 -6
  134. package/dist/components/persona/types.d.ts +4 -3
  135. package/dist/components/progress-bar/docs/llms.md +13 -13
  136. package/dist/components/progress-bar/progress-bar.svelte +6 -6
  137. package/dist/components/progress-bar/types.d.ts +4 -4
  138. package/dist/components/progress-ring/docs/llms.md +14 -14
  139. package/dist/components/progress-ring/progress-ring.svelte +5 -5
  140. package/dist/components/progress-ring/types.d.ts +3 -3
  141. package/dist/components/radio-button/docs/llms.md +11 -8
  142. package/dist/components/radio-button/radio-button.svelte +38 -12
  143. package/dist/components/radio-button/radio-button.svelte.d.ts +1 -1
  144. package/dist/components/radio-button/types.d.ts +11 -8
  145. package/dist/components/rating/docs/llms.md +111 -0
  146. package/dist/components/rating/rating.svelte +247 -0
  147. package/dist/components/rating/rating.svelte.d.ts +19 -0
  148. package/dist/components/rating/types.d.ts +44 -0
  149. package/dist/components/rating/types.js +1 -0
  150. package/dist/components/side-nav/docs/llms.md +110 -0
  151. package/dist/components/side-nav/side-nav-branch.svelte +43 -0
  152. package/dist/components/side-nav/side-nav-branch.svelte.d.ts +4 -0
  153. package/dist/components/side-nav/side-nav-item.svelte +75 -0
  154. package/dist/components/side-nav/side-nav-item.svelte.d.ts +25 -0
  155. package/dist/components/side-nav/side-nav-list.svelte +32 -0
  156. package/dist/components/side-nav/side-nav-list.svelte.d.ts +7 -0
  157. package/dist/components/side-nav/side-nav.d.ts +2 -0
  158. package/dist/components/side-nav/side-nav.js +2 -0
  159. package/dist/components/side-nav/side-nav.svelte +355 -0
  160. package/dist/components/side-nav/side-nav.svelte.d.ts +24 -0
  161. package/dist/components/side-nav/types.d.ts +149 -0
  162. package/dist/components/side-nav/types.js +1 -0
  163. package/dist/components/skeleton/docs/llms.md +1 -1
  164. package/dist/components/skeleton/skeleton.svelte +2 -2
  165. package/dist/components/skeleton/types.d.ts +1 -1
  166. package/dist/components/slider/docs/llms.md +31 -28
  167. package/dist/components/slider/slider.svelte +126 -61
  168. package/dist/components/slider/slider.svelte.d.ts +1 -3
  169. package/dist/components/slider/types.d.ts +21 -14
  170. package/dist/components/tab-view/docs/llms.md +7 -7
  171. package/dist/components/tab-view/tab-view-tab.svelte +16 -14
  172. package/dist/components/tab-view/tab-view.svelte +16 -9
  173. package/dist/components/tab-view/types.d.ts +6 -6
  174. package/dist/components/table/docs/llms.md +38 -33
  175. package/dist/components/table/table-body.svelte +2 -2
  176. package/dist/components/table/table-cell-actions.svelte +8 -2
  177. package/dist/components/table/table-cell-layout.svelte +2 -2
  178. package/dist/components/table/table-cell.svelte +2 -2
  179. package/dist/components/table/table-header-cell.svelte +2 -2
  180. package/dist/components/table/table-header.svelte +2 -2
  181. package/dist/components/table/table-row.svelte +2 -2
  182. package/dist/components/table/table-selection-cell.svelte +6 -6
  183. package/dist/components/table/table.svelte +2 -2
  184. package/dist/components/table/types.d.ts +11 -11
  185. package/dist/components/tag/docs/llms.md +383 -0
  186. package/dist/components/tag/tag-group.d.ts +2 -0
  187. package/dist/components/tag/tag-group.js +2 -0
  188. package/dist/components/tag/tag-group.svelte +117 -0
  189. package/dist/components/tag/tag-group.svelte.d.ts +20 -0
  190. package/dist/components/tag/tag.svelte +459 -0
  191. package/dist/components/tag/tag.svelte.d.ts +19 -0
  192. package/dist/components/tag/types.d.ts +137 -0
  193. package/dist/components/tag/types.js +1 -0
  194. package/dist/components/tag-picker/docs/llms.md +298 -0
  195. package/dist/components/tag-picker/option-group.svelte +51 -0
  196. package/dist/components/tag-picker/option-group.svelte.d.ts +4 -0
  197. package/dist/components/tag-picker/option.svelte +82 -0
  198. package/dist/components/tag-picker/option.svelte.d.ts +4 -0
  199. package/dist/components/tag-picker/tag-picker.d.ts +2 -0
  200. package/dist/components/tag-picker/tag-picker.js +2 -0
  201. package/dist/components/tag-picker/tag-picker.svelte +716 -0
  202. package/dist/components/tag-picker/tag-picker.svelte.d.ts +21 -0
  203. package/dist/components/tag-picker/types.d.ts +142 -0
  204. package/dist/components/tag-picker/types.js +1 -0
  205. package/dist/components/text-box/docs/llms.md +31 -4
  206. package/dist/components/text-box/text-box-button.svelte +3 -3
  207. package/dist/components/text-box/text-box-button.svelte.d.ts +1 -1
  208. package/dist/components/text-box/text-box.svelte +41 -14
  209. package/dist/components/text-box/types.d.ts +5 -5
  210. package/dist/components/textarea/docs/llms.md +4 -3
  211. package/dist/components/textarea/textarea.svelte +14 -6
  212. package/dist/components/textarea/types.d.ts +2 -4
  213. package/dist/components/time-picker/docs/llms.md +18 -15
  214. package/dist/components/time-picker/time-picker.svelte +133 -33
  215. package/dist/components/time-picker/time-picker.svelte.d.ts +1 -1
  216. package/dist/components/time-picker/types.d.ts +23 -13
  217. package/dist/components/title-bar/docs/llms.md +382 -0
  218. package/dist/components/title-bar/title-bar-left-controls.svelte +215 -0
  219. package/dist/components/title-bar/title-bar-left-controls.svelte.d.ts +22 -0
  220. package/dist/components/title-bar/title-bar-right-controls.svelte +190 -0
  221. package/dist/components/title-bar/title-bar-right-controls.svelte.d.ts +27 -0
  222. package/dist/components/title-bar/title-bar.d.ts +3 -0
  223. package/dist/components/title-bar/title-bar.js +4 -0
  224. package/dist/components/title-bar/title-bar.svelte +224 -0
  225. package/dist/components/title-bar/title-bar.svelte.d.ts +26 -0
  226. package/dist/components/title-bar/types.d.ts +165 -0
  227. package/dist/components/title-bar/types.js +1 -0
  228. package/dist/components/toggle-switch/docs/llms.md +11 -9
  229. package/dist/components/toggle-switch/toggle-switch.svelte +23 -14
  230. package/dist/components/toggle-switch/toggle-switch.svelte.d.ts +1 -1
  231. package/dist/components/toggle-switch/types.d.ts +12 -10
  232. package/dist/components/tooltip/docs/llms.md +1 -1
  233. package/dist/components/tooltip/tooltip.svelte +5 -4
  234. package/dist/components/tooltip/types.d.ts +1 -1
  235. package/dist/components/top-nav/docs/llms.md +1 -1
  236. package/dist/components/top-nav/top-nav-item.svelte +2 -2
  237. package/dist/components/top-nav/top-nav.svelte +2 -2
  238. package/dist/components/top-nav/types.d.ts +2 -2
  239. package/dist/components/tree-view/docs/llms.md +11 -11
  240. package/dist/components/tree-view/tree-view-item-content.svelte +3 -3
  241. package/dist/components/tree-view/tree-view-item.svelte +13 -5
  242. package/dist/components/tree-view/tree-view.svelte +20 -5
  243. package/dist/components/tree-view/tree-view.svelte.d.ts +1 -1
  244. package/dist/components/tree-view/tree-view.svelte.js +31 -3
  245. package/dist/components/tree-view/types.d.ts +7 -3
  246. package/dist/index.d.ts +25 -0
  247. package/dist/index.js +32 -0
  248. package/dist/internal/components/dynamic-carousel/dynamic-carousel.svelte +26 -5
  249. package/dist/internal/components/dynamic-carousel/dynamic-carousel.svelte.d.ts +2 -0
  250. package/dist/internal/constants.d.ts +0 -4
  251. package/dist/internal/constants.js +0 -4002
  252. package/dist/internal/index.d.ts +1 -0
  253. package/dist/internal/index.js +1 -0
  254. package/dist/internal/native-field.svelte.d.ts +44 -0
  255. package/dist/internal/native-field.svelte.js +85 -0
  256. package/dist/internal/utils.d.ts +16 -2
  257. package/dist/internal/utils.js +25 -3
  258. package/dist/types/index.d.ts +3 -3
  259. package/package.json +51 -37
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">
2
+ import { claimLabel } from '../label/label-context.js';
3
+ import { dispatchFieldEvents, invokeHandlers, NativeField } from '../../internal/index.js';
2
4
  import { onClickOutside } from 'runed';
3
5
  import ChevronDownFilled from 'fluentui-icons-svelte/ChevronDownFilled.svelte';
4
6
  import { Flyout, ListView, ListViewItem } from '../../index.js';
5
- import type { DropdownContext, DropdownProps } from './types.js';
7
+ import type { DropdownContext, DropdownOptionLabel, DropdownProps } from './types.js';
6
8
  import { tick } from 'svelte';
7
- import { SvelteMap } from 'svelte/reactivity';
8
- import { setTabspotAttributes, unsetTabspotSection } from 'tabspot';
9
+ import { SvelteSet } from 'svelte/reactivity';
10
+ import { clearTabspotActive, setTabspotActive, setTabspotAttributes, unsetTabspotSection } from 'tabspot';
9
11
  import { PREFIX } from '../../internal/constants.js';
10
12
  import { getGlobalFSContext } from '../../providers/fluentui-svelte/fluentui-svelte.js';
11
13
  import { setDropdownContext } from './dropdown-context.js';
@@ -13,26 +15,39 @@
13
15
  const FALLBACK_ID = $props.id();
14
16
  const ID = `${PREFIX}-dropdown-${FALLBACK_ID}`;
15
17
 
18
+ const labelContext = claimLabel(() => id);
19
+
16
20
  let {
17
- id = ID,
18
- name = id,
21
+ id = labelContext?.fieldId ?? ID,
22
+ name,
19
23
  placeholder = 'Select an option',
20
24
  multiple,
21
25
  value = $bindable(multiple ? [] : ''),
22
26
  disabled,
23
27
  flyoutMaxHeight,
24
- ref = $bindable(),
25
- wrapperProps,
26
- buttonRef = $bindable(),
27
- buttonProps,
28
- flyoutRef = $bindable(),
28
+ wrapperRef = $bindable(null),
29
+ wrapperAttributes,
30
+ ref = $bindable(null),
31
+ buttonRef = $bindable(null),
32
+ buttonAttributes,
33
+ flyoutRef = $bindable(null),
29
34
  flyoutProps,
30
- listboxRef = $bindable(),
35
+ listboxRef = $bindable(null),
31
36
  listboxProps,
32
- onclick,
33
- children
37
+ onfocus,
38
+ onblur,
39
+ children,
40
+ ...attributes
34
41
  }: DropdownProps = $props();
35
42
 
43
+ const BUTTON_ID = $derived(buttonAttributes?.id ?? `${id}-button`);
44
+
45
+ const native = new NativeField(
46
+ () => ref,
47
+ () => buttonRef,
48
+ labelContext
49
+ );
50
+
36
51
  let open = $state(false);
37
52
 
38
53
  let roundClass = $state('bottom');
@@ -41,16 +56,19 @@
41
56
 
42
57
  const OPEN_KEYS = ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Enter', ' '];
43
58
 
44
- // Options only mount while the flyout is open, so we cache the last-known text
45
- // for every value here to keep rendering the selection once the list is closed.
46
- const optionLabels = new SvelteMap<string, string>();
59
+ // The list stays in the page while closed, so every option is here from the start, and is read live.
60
+ const options = new SvelteSet<DropdownOptionLabel>();
61
+ const optionLabels = $derived(new Map([...options].map((option) => [option.value, option.text])));
47
62
 
48
63
  const context: DropdownContext = $state({
49
64
  config: null,
50
65
  state: null,
51
66
  events: null,
52
67
  methods: {
53
- registerOption: (optionValue: string, text: string) => optionLabels.set(optionValue, text)
68
+ registerOption: (option) => {
69
+ options.add(option);
70
+ return () => options.delete(option);
71
+ }
54
72
  }
55
73
  });
56
74
 
@@ -65,8 +83,11 @@
65
83
  );
66
84
 
67
85
  onClickOutside(
68
- () => ref,
69
- () => closeDropdown()
86
+ () => wrapperRef,
87
+ (event) => {
88
+ if (event.target instanceof Element && event.target.closest('label')?.control === ref) return;
89
+ closeDropdown();
90
+ }
70
91
  );
71
92
 
72
93
  function handlePlacementChange(newPlacement: string) {
@@ -77,7 +98,12 @@
77
98
  if (disabled || open) return;
78
99
  open = true;
79
100
  await tick();
80
- globalFSContext?.state?.tabspotInstance?.rebuild(ref);
101
+ globalFSContext?.state?.tabspotInstance?.rebuild(wrapperRef ?? undefined);
102
+ // The cursor starts on the selection, or on the first option, so it is announced on open.
103
+ const start =
104
+ listboxRef?.querySelector<HTMLElement>('.fs-list-view-item[aria-selected="true"]:not([aria-disabled="true"])') ??
105
+ listboxRef?.querySelector<HTMLElement>('.fs-list-view-item:not([aria-disabled="true"])');
106
+ if (start) setTabspotActive(start, { nearest: true });
81
107
  }
82
108
 
83
109
  function closeDropdown() {
@@ -91,6 +117,13 @@
91
117
 
92
118
  const { key } = e;
93
119
 
120
+ if (open && (key === 'Enter' || key === ' ')) {
121
+ e.preventDefault();
122
+ // Focus stays on the button, so the option under the cursor never sees the key: click it here.
123
+ listboxRef?.querySelector<HTMLElement>('[data-active="true"]')?.click();
124
+ return;
125
+ }
126
+
94
127
  if (OPEN_KEYS.includes(key)) {
95
128
  e.preventDefault();
96
129
  await openDropdown();
@@ -103,18 +136,32 @@
103
136
  }
104
137
  }
105
138
 
106
- async function handleClick(e: MouseEvent) {
139
+ async function handleClick() {
107
140
  if (open) closeDropdown();
108
141
  else await openDropdown();
109
- onclick?.(e);
142
+ }
143
+
144
+ /** A label activates the native select it belongs to, which hands the activation to the button. */
145
+ async function openFromField() {
146
+ buttonRef?.focus();
147
+ await openDropdown();
148
+ }
149
+
150
+ function select(items: string[] | undefined) {
151
+ const next = multiple ? (items ?? []) : (items?.[0] ?? '');
152
+ if (!multiple && next === value) return;
153
+ value = next;
154
+ dispatchFieldEvents(ref);
110
155
  }
111
156
 
112
157
  // Force rebuild of tabspot handling tree
113
158
  $effect(() => {
114
- if (!ref) return;
159
+ if (!wrapperRef) return;
115
160
 
116
161
  if (!open) {
117
- unsetTabspotSection(ref, 'root');
162
+ // The list stays in the page once closed: nothing in it reads as chosen.
163
+ clearTabspotActive(wrapperRef);
164
+ unsetTabspotSection(wrapperRef, 'root');
118
165
  return;
119
166
  }
120
167
 
@@ -127,7 +174,7 @@
127
174
  axis: 'vertical',
128
175
  items: '.fs-list-view-item',
129
176
  activation: {
130
- controller: `#${id}-button`,
177
+ controller: `#${CSS.escape(BUTTON_ID)}`,
131
178
  mode: 'activedescendant',
132
179
  mark: {
133
180
  attribute: 'data-active'
@@ -135,83 +182,119 @@
135
182
  }
136
183
  }
137
184
  },
138
- element: ref,
185
+ element: wrapperRef,
139
186
  merge: true
140
187
  });
141
188
  });
142
189
  </script>
143
190
 
144
- <div class={['fs-dropdown', `square-${roundClass}`, { open, disabled }]} bind:this={ref} {...wrapperProps}>
191
+ <div
192
+ {...wrapperAttributes}
193
+ class={['fs-dropdown', `square-${roundClass}`, { open, disabled }, wrapperAttributes?.class]}
194
+ bind:this={wrapperRef}
195
+ >
196
+ <!-- Hidden while closed, not gone: each option reports its text as it is created, and comes before
197
+ the button, so the button shows it before the list ever opens, on the server too. -->
198
+ <Flyout
199
+ {...flyoutProps}
200
+ hidden={!open}
201
+ style="{flyoutProps?.style ?? ''}; {flyoutMaxHeight ? `max-height: ${flyoutMaxHeight};` : ''}"
202
+ offset={0}
203
+ bind:ref={flyoutRef}
204
+ floating
205
+ roundCorners="bottom"
206
+ reference={wrapperRef ?? undefined}
207
+ id={`${id}-flyout`}
208
+ onPlacementChange={(place) => {
209
+ handlePlacementChange(place);
210
+ flyoutProps?.onPlacementChange?.(place);
211
+ }}
212
+ placementConfig={{ allowedPlacements: ['bottom', 'top'] }}
213
+ class="fs-dropdown-flyout {roundClass === 'top' ? 'top-shadow' : ''} {flyoutProps?.class}"
214
+ >
215
+ <ListView
216
+ {...listboxProps}
217
+ bind:ref={listboxRef}
218
+ bind:selectedItems={() => selectedValues, select}
219
+ selectionMode={multiple ? 'multiselect' : 'single'}
220
+ toggleOnClick={multiple}
221
+ onSelectionChange={(event, selected) => {
222
+ if (!multiple) closeDropdown();
223
+ listboxProps?.onSelectionChange?.(event, selected);
224
+ }}
225
+ navigationMode="items"
226
+ id={`${id}-listbox`}
227
+ role="listbox"
228
+ disableTabspot
229
+ >
230
+ {#if !children}
231
+ <ListViewItem disabled>No options available</ListViewItem>
232
+ {:else}
233
+ {@render children?.()}
234
+ {/if}
235
+ </ListView>
236
+ </Flyout>
145
237
  <ChevronDownFilled class="indicator" />
146
238
  <button
239
+ {...buttonAttributes}
240
+ {...native.aria(attributes, buttonAttributes)}
147
241
  bind:this={buttonRef}
148
242
  type="button"
149
243
  role="combobox"
150
- onclick={handleClick}
151
- onkeydown={handleKeyDown}
152
- aria-controls={`${id}-listbox`}
244
+ onclick={(e) => invokeHandlers(e, [disabled], [handleClick, buttonAttributes?.onclick])}
245
+ onkeydown={(e) => invokeHandlers(e, [disabled], [handleKeyDown, buttonAttributes?.onkeydown])}
246
+ onfocus={(e) => invokeHandlers(e, [], [onfocus, buttonAttributes?.onfocus])}
247
+ onblur={(e) => invokeHandlers(e, [], [onblur, buttonAttributes?.onblur])}
248
+ aria-controls={open ? `${id}-listbox` : undefined}
153
249
  aria-expanded={open}
154
- id={`${id}-button`}
250
+ id={BUTTON_ID}
155
251
  aria-haspopup="listbox"
156
252
  {disabled}
157
- class={['fs-dropdown-button', hasValue && 'has-value']}
158
- {...buttonProps}
253
+ class={['fs-dropdown-button', hasValue && 'has-value', buttonAttributes?.class]}
159
254
  >
160
255
  {displayLabel}
161
256
  </button>
162
257
 
163
- <input type="hidden" {name} {value} {id} {disabled} />
164
-
165
- {#if open}
166
- <Flyout
167
- style={flyoutMaxHeight ? `max-height: ${flyoutMaxHeight};` : ''}
168
- offset={0}
169
- bind:ref={flyoutRef}
170
- {...flyoutProps}
171
- floating
172
- roundCorners="bottom"
173
- reference={ref}
174
- id={`${id}-flyout`}
175
- onPlacementChange={(place) => handlePlacementChange(place)}
176
- placementConfig={{ allowedPlacements: ['bottom', 'top'] }}
177
- class="fs-dropdown-flyout {roundClass === 'top' ? 'top-shadow' : ''} {flyoutProps?.class}"
258
+ <!-- The field of the form: submitted, validated and reset like any select input,
259
+ behind the button and out of the tab order, since the button is what people use. See `NativeField`. -->
260
+ <!-- Svelte needs a static `multiple` to bind a select, hence the two branches. This is the native form -->
261
+ {#if multiple}
262
+ <select
263
+ {...attributes}
264
+ class="native-field"
265
+ aria-hidden={native.reported ? undefined : 'true'}
266
+ multiple
267
+ tabindex={-1}
268
+ {id}
269
+ {name}
270
+ {disabled}
271
+ bind:value
272
+ bind:this={ref}
273
+ onclick={openFromField}
178
274
  >
179
- <ListView
180
- bind:ref={listboxRef}
181
- bind:selectedItems={
182
- () => {
183
- if (multiple) {
184
- return (value as string[]) || [];
185
- } else {
186
- return value ? [value as string] : [];
187
- }
188
- },
189
- (v) => {
190
- if (multiple) {
191
- value = v || [];
192
- } else {
193
- value = v?.[0] || '';
194
- }
195
- }
196
- }
197
- selectionMode={multiple ? 'multiselect' : 'single'}
198
- toggleOnClick={multiple}
199
- onSelectionChange={() => {
200
- if (!multiple) closeDropdown();
201
- }}
202
- navigationMode="items"
203
- id={`${id}-listbox`}
204
- role="listbox"
205
- disableTabspot
206
- {...listboxProps}
207
- >
208
- {#if !children}
209
- <ListViewItem disabled>No options available</ListViewItem>
210
- {:else}
211
- {@render children?.()}
212
- {/if}
213
- </ListView>
214
- </Flyout>
275
+ {#each selectedValues as option (option)}
276
+ <option value={option}>{optionLabels.get(option) ?? option}</option>
277
+ {/each}
278
+ </select>
279
+ {:else}
280
+ <select
281
+ {...attributes}
282
+ class="native-field"
283
+ aria-hidden={native.reported ? undefined : 'true'}
284
+ tabindex={-1}
285
+ {id}
286
+ {name}
287
+ {disabled}
288
+ bind:value
289
+ bind:this={ref}
290
+ onclick={openFromField}
291
+ >
292
+ <!-- The placeholder makes an empty value missing for `required`, and is what a reset returns to. -->
293
+ <option value="" hidden></option>
294
+ {#if hasValue}
295
+ <option {value}>{displayLabel}</option>
296
+ {/if}
297
+ </select>
215
298
  {/if}
216
299
  </div>
217
300
 
@@ -300,6 +383,21 @@
300
383
  left: -0.063rem;
301
384
  background-color: var(--fs-accent-fill-default);
302
385
  }
386
+ &:has(> .native-field:is(:user-invalid, [aria-invalid='true'])):after {
387
+ width: calc(100% + 0.125rem);
388
+ left: -0.063rem;
389
+ background-color: var(--fs-system-critical);
390
+ }
391
+ & .native-field {
392
+ position: absolute;
393
+ inset: 0;
394
+ inline-size: 100%;
395
+ block-size: 100%;
396
+ margin: 0;
397
+ box-sizing: border-box;
398
+ opacity: 0;
399
+ pointer-events: none;
400
+ }
303
401
  & :global(> .indicator) {
304
402
  width: 1rem;
305
403
  position: absolute;
@@ -1,4 +1,4 @@
1
1
  import type { DropdownProps } from './types.ts';
2
- declare const Dropdown: import("svelte").Component<DropdownProps, {}, "ref" | "value" | "flyoutRef" | "buttonRef" | "listboxRef">;
2
+ declare const Dropdown: import("svelte").Component<DropdownProps, {}, "ref" | "value" | "wrapperRef" | "flyoutRef" | "buttonRef" | "listboxRef">;
3
3
  type Dropdown = ReturnType<typeof Dropdown>;
4
4
  export default Dropdown;
@@ -1,7 +1,8 @@
1
- import type { HTMLAttributes, HTMLButtonAttributes, HTMLInputAttributes } from 'svelte/elements';
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes, HTMLButtonAttributes, HTMLSelectAttributes } from 'svelte/elements';
2
3
  import type { ListViewProps, ListViewItemProps } from '../list-view/types.ts';
3
4
  import type { FlyoutProps } from '../flyout/types.ts';
4
- import type { FSContext } from '../../internal/index.js';
5
+ import type { FSContext, NativeFieldAttributes } from '../../internal/index.js';
5
6
  /** The value shape of a dropdown that allows more than one selection. */
6
7
  export type DropdownMultipleValue = {
7
8
  multiple: true;
@@ -28,34 +29,32 @@ export type DropdownProps = {
28
29
  placeholder?: string;
29
30
  /** Disables the user interaction. */
30
31
  disabled?: boolean;
31
- /** The name submitted with the form. Falls back to the id of the dropdown.
32
- * @default id
33
- */
32
+ /** The name the value is submitted under. Without one, the form leaves the value out, as with any field. */
34
33
  name?: string | null;
35
- /** The DOM reference of the dropdown element.
34
+ /** The DOM reference of the wrapper element.
36
35
  * @bindable
37
36
  */
38
- ref?: HTMLDivElement;
39
- /** Called when the dropdown is clicked. */
40
- onclick?: (e: MouseEvent) => void;
37
+ wrapperRef?: HTMLDivElement | null;
41
38
  /** The attributes to spread on the wrapper element.
42
39
  * @type HTMLAttributes
43
40
  */
44
- wrapperProps?: HTMLAttributes<HTMLDivElement>;
41
+ wrapperAttributes?: HTMLAttributes<HTMLDivElement>;
42
+ /** The DOM reference of the native select that holds the value. The other attributes of the dropdown are applied to it.
43
+ * @bindable
44
+ */
45
+ ref?: HTMLSelectElement | null;
45
46
  /** The DOM reference of the button that opens the dropdown.
46
47
  * @bindable
47
48
  */
48
- buttonRef?: HTMLButtonElement;
49
+ buttonRef?: HTMLButtonElement | null;
49
50
  /** The attributes to spread on the button that opens the dropdown.
50
51
  * @type HTMLButtonAttributes
51
52
  */
52
- buttonProps?: HTMLButtonAttributes;
53
- /** The DOM reference of the hidden input holding the value. */
54
- inputRef?: HTMLInputElement;
53
+ buttonAttributes?: Omit<HTMLButtonAttributes, 'type' | 'role' | 'value'>;
55
54
  /** The DOM reference of the flyout element.
56
55
  * @bindable
57
56
  */
58
- flyoutRef?: HTMLDivElement;
57
+ flyoutRef?: HTMLDivElement | null;
59
58
  /** The props to spread on the flyout.
60
59
  * @type FlyoutProps
61
60
  */
@@ -65,18 +64,25 @@ export type DropdownProps = {
65
64
  /** The DOM reference of the listbox element.
66
65
  * @bindable
67
66
  */
68
- listboxRef?: HTMLUListElement;
67
+ listboxRef?: HTMLUListElement | null;
69
68
  /** The props to spread on the listbox rendered inside the flyout.
70
69
  * @type ListViewProps
71
70
  */
72
71
  listboxProps?: Omit<ListViewProps<'ul'>, 'ref' | 'children' | 'selectedItems' | 'selectionMode'>;
73
- } & (DropdownMultipleValue | DropdownSingleValue) & Omit<HTMLInputAttributes, 'value' | 'onclick'>;
72
+ /** The `DropdownOption` elements of the list. */
73
+ children?: Snippet;
74
+ } & (DropdownMultipleValue | DropdownSingleValue) & NativeFieldAttributes<HTMLSelectAttributes>;
74
75
  /** @propsmith DropdownOptionProps */
75
76
  export type DropdownOptionProps = {
76
- /** The text shown by the dropdown once the option is selected. Falls back to the rendered children. */
77
+ /** The text shown by the dropdown once the option is selected. Falls back to the rendered children, which the server cannot read: pass it when rendering on the server. */
77
78
  text?: string;
78
79
  } & ListViewItemProps<'li'>;
80
+ /** What an option tells the trigger, read live. */
81
+ export type DropdownOptionLabel = {
82
+ readonly value?: string;
83
+ readonly text?: string;
84
+ };
79
85
  export type DropdownContext = FSContext<null, null, null, {
80
- /** Options report their display text so the trigger can show a label instead of the raw value. */
81
- registerOption: (value: string, text: string) => void;
86
+ /** Options report their display text so the trigger can show a label instead of the raw value. Returns the call that withdraws the option. */
87
+ registerOption: (option: DropdownOptionLabel) => () => void;
82
88
  }>;
@@ -11,7 +11,7 @@ The Dropdown Select is a dropdown built on the native `<select>` element. It let
11
11
  import { DropdownSelect, DropdownSelectOption } from 'fluentui-svelte';
12
12
  </script>
13
13
 
14
- <DropdownSelect>
14
+ <DropdownSelect aria-label="Choose an option">
15
15
  <DropdownSelectOption value="A">Option A</DropdownSelectOption>
16
16
  <DropdownSelectOption value="B">Option B</DropdownSelectOption>
17
17
  <DropdownSelectOption value="C">Option C</DropdownSelectOption>
@@ -25,13 +25,13 @@ The Dropdown Select is a dropdown built on the native `<select>` element. It let
25
25
  Options can render rich content such as a `Persona`. Provide a `text` prop for the plain-text selection label.
26
26
 
27
27
  ```svelte
28
- <DropdownSelect>
28
+ <DropdownSelect aria-label="Choose an option">
29
29
  <DropdownSelectOption value="John Doe" text="John Doe">
30
30
  <Persona
31
31
  name="John Doe"
32
32
  primaryText="Software Engineer"
33
33
  presence={{ status: 'available' }}
34
- avatar={{ color: 'colorful', idForColor: '' }}
34
+ avatarProps={{ color: 'colorful', idForColor: '' }}
35
35
  />
36
36
  </DropdownSelectOption>
37
37
  <DropdownSelectOption value="B">Option B</DropdownSelectOption>
@@ -44,28 +44,30 @@ Options can render rich content such as a `Persona`. Provide a `text` prop for t
44
44
  Set the `multiple` prop to allow selecting more than one option.
45
45
 
46
46
  ```svelte
47
- <DropdownSelect multiple>
47
+ <DropdownSelect multiple aria-label="Choose options">
48
48
  <DropdownSelectOption value="A">Option A</DropdownSelectOption>
49
49
  <DropdownSelectOption value="B">Option B</DropdownSelectOption>
50
50
  <DropdownSelectOption value="C">Option C</DropdownSelectOption>
51
51
  </DropdownSelect>
52
52
  ```
53
53
 
54
+ HTML attributes, including class, id and ARIA attributes, apply to the control element. Bind ref to access that control. Use wrapperAttributes and wrapperRef for the outer container.
55
+
54
56
  ## Component Props
55
57
 
56
58
  <!-- DO NOT EDIT THIS SECTION (propsmith generated) -->
57
59
  <!-- props:DropdownSelectProps -->
58
60
 
59
- | Name | Type | Default | Description |
60
- | ----------------------------- | -------------------------- | -------------------------- | ---------------------------------------------------------------------- |
61
- | `value` _bindable_ | `string` &#124; `string[]` | `multiple ? [] : ''` | The selected value. An array of values when `multiple` is set. |
62
- | `multiple` | `boolean` | | Allows more than one option to be selected at a time. |
63
- | `placeholder` | `string` | `'Native Select Dropdown'` | The text shown while no option is selected. |
64
- | `hidePlaceholder` | `boolean` | | Keeps the placeholder out of the option list once a value is selected. |
65
- | `ref` _bindable_ | `HTMLSelectElement` | | The DOM reference of the select element. |
66
- | `wrapperRef` _bindable_ | `HTMLDivElement` | | The DOM reference of the wrapper element. |
67
- | `wrapperProps` | `HTMLAttributes` | | The attributes to spread on the wrapper element. |
68
- | Element Attributes (`select`) | | | |
61
+ | Name | Type | Default | Description |
62
+ | ----------------------------- | --------------------------------- | -------------------------- | ---------------------------------------------------------------------- |
63
+ | `value` _bindable_ | `string` &#124; `string[]` | `multiple ? [] : ''` | The selected value. An array of values when `multiple` is set. |
64
+ | `multiple` | `boolean` | | Allows more than one option to be selected at a time. |
65
+ | `placeholder` | `string` | `'Native Select Dropdown'` | The text shown while no option is selected. |
66
+ | `hidePlaceholder` | `boolean` | | Keeps the placeholder out of the option list once a value is selected. |
67
+ | `ref` _bindable_ | `HTMLSelectElement` &#124; `null` | | The DOM reference of the select element. |
68
+ | `wrapperRef` _bindable_ | `HTMLDivElement` &#124; `null` | | The DOM reference of the wrapper element. |
69
+ | `wrapperAttributes` | `HTMLAttributes` | | The attributes to spread on the wrapper element. |
70
+ | Element Attributes (`select`) | | | |
69
71
 
70
72
  <!-- /props:DropdownSelectProps -->
71
73
 
@@ -74,13 +76,13 @@ Set the `multiple` prop to allow selecting more than one option.
74
76
  <!-- DO NOT EDIT THIS SECTION (propsmith generated) -->
75
77
  <!-- props:DropdownSelectOptionProps -->
76
78
 
77
- | Name | Type | Default | Description |
78
- | ----------------------------- | ------------- | ------- | ------------------------------------------------------------- |
79
- | `value` | `string` | | The value reported to the select when the option is picked. |
80
- | `text` | `string` | | The label of the option. Falls back to the rendered children. |
81
- | `disabled` | `boolean` | | Disables the user interaction. |
82
- | `ref` _bindable_ | `HTMLElement` | | The DOM reference of the option element. |
83
- | `children` | `Snippet` | | The content of the option, used when no `text` is given. |
84
- | Element Attributes (`option`) | | | |
79
+ | Name | Type | Default | Description |
80
+ | ----------------------------- | --------------------------- | ------- | ------------------------------------------------------------- |
81
+ | `value` | `string` | | The value reported to the select when the option is picked. |
82
+ | `text` | `string` | | The label of the option. Falls back to the rendered children. |
83
+ | `disabled` | `boolean` | | Disables the user interaction. |
84
+ | `ref` _bindable_ | `HTMLElement` &#124; `null` | | The DOM reference of the option element. |
85
+ | `children` | `Snippet` | | The content of the option, used when no `text` is given. |
86
+ | Element Attributes (`option`) | | | |
85
87
 
86
88
  <!-- /props:DropdownSelectOptionProps -->
@@ -6,14 +6,14 @@
6
6
  value,
7
7
  text,
8
8
  disabled,
9
- ref = $bindable(),
9
+ ref = $bindable(null),
10
10
  class: classes,
11
11
  children,
12
12
  ...attributes
13
13
  }: DropdownSelectOptionProps = $props();
14
14
  </script>
15
15
 
16
- <option class="fs-dropdown-select-option {classes}" {value} {disabled} bind:this={ref} {...attributes}>
16
+ <option {...attributes} class="fs-dropdown-select-option {classes}" {value} {disabled} bind:this={ref}>
17
17
  <span class="option-content">
18
18
  <CheckmarkFilled class="checkmark" width="16" height="16" />
19
19
  {#if text}
@@ -1,22 +1,29 @@
1
1
  <script lang="ts">
2
+ import { claimLabel } from '../label/label-context.js';
3
+ import { flushSync } from 'svelte';
2
4
  import ChevronDownFilled from 'fluentui-icons-svelte/ChevronDownFilled.svelte';
3
5
  import { selectDirection } from './dropdown-select.svelte.js';
4
6
  import type { DropdownSelectProps } from './types.js';
5
7
 
8
+ const labelContext = claimLabel(() => id);
9
+
6
10
  let {
7
11
  hidePlaceholder,
8
12
  placeholder = 'Native Select Dropdown',
9
- ref = $bindable(),
13
+ ref = $bindable(null),
10
14
  children,
11
- wrapperRef = $bindable(),
12
- wrapperProps,
15
+ wrapperRef = $bindable(null),
16
+ wrapperAttributes,
13
17
  multiple,
18
+ class: classes,
14
19
  value = $bindable(multiple ? [] : ''),
20
+ id = labelContext?.fieldId,
21
+ name = id,
15
22
  ...attributes
16
23
  }: DropdownSelectProps = $props();
17
24
  </script>
18
25
 
19
- <div class="fs-dropdown" bind:this={wrapperRef} {...wrapperProps}>
26
+ <div {...wrapperAttributes} class={['fs-dropdown', wrapperAttributes?.class]} bind:this={wrapperRef}>
20
27
  {#if !multiple}
21
28
  <ChevronDownFilled class="indicator" />
22
29
  {/if}
@@ -25,17 +32,40 @@
25
32
  We need to use a if block with the same structure
26
33
  -->
27
34
  {#if multiple}
28
- <select class="fs-dropdown-select" {@attach selectDirection()} multiple bind:value bind:this={ref} {...attributes}>
35
+ <select
36
+ {...attributes}
37
+ {id}
38
+ {name}
39
+ class={['fs-dropdown-select', classes]}
40
+ {@attach selectDirection()}
41
+ multiple
42
+ bind:value
43
+ bind:this={ref}
44
+ >
29
45
  <button>
30
46
  <selectedcontent></selectedcontent>
31
47
  </button>
32
48
  {#if !hidePlaceholder}
33
- <option class="fs-dropdown-placeholder" disabled selected hidden value="">{placeholder}</option>
49
+ <option class="fs-dropdown-placeholder" disabled selected={!value?.length} hidden value="">{placeholder}</option
50
+ >
34
51
  {/if}
35
52
  {@render children?.()}
36
53
  </select>
37
54
  {:else}
38
- <select class="fs-dropdown-select" {@attach selectDirection()} bind:value bind:this={ref} {...attributes}>
55
+ <select
56
+ {...attributes}
57
+ {id}
58
+ {name}
59
+ class={['fs-dropdown-select', classes]}
60
+ {@attach selectDirection()}
61
+ bind:value
62
+ bind:this={ref}
63
+ oninputcapture={(event) => {
64
+ // selectedcontent mutates before change; sync the binding before its observer restores the old value.
65
+ flushSync(() => (value = event.currentTarget.value));
66
+ attributes.oninputcapture?.(event);
67
+ }}
68
+ >
39
69
  <button>
40
70
  <selectedcontent></selectedcontent>
41
71
  </button>
@@ -94,6 +124,11 @@
94
124
  left: 0;
95
125
  background-color: var(--fs-accent-fill-default);
96
126
  }
127
+ &:has(select:is(:user-invalid, [aria-invalid='true'])):after {
128
+ width: calc(100%);
129
+ left: 0;
130
+ background-color: var(--fs-system-critical);
131
+ }
97
132
  & .fs-dropdown-select {
98
133
  border-radius: 0;
99
134
  background: unset;