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
@@ -0,0 +1,716 @@
1
+ <script lang="ts">
2
+ import { claimLabel } from '../label/label-context.js';
3
+ import { tick } from 'svelte';
4
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
5
+ import { onClickOutside } from 'runed';
6
+ import ChevronDownRegular from 'fluentui-icons-svelte/ChevronDownRegular.svelte';
7
+ import { PREFIX } from '../../internal/constants.js';
8
+ import { dispatchFieldEvents, invokeHandlers, NativeField, RenderSoC } from '../../internal/index.js';
9
+ import { setTagPickerContext } from './tag-picker.js';
10
+ import { Flyout, ListView, ListViewItem, Tag, TagGroup } from '../../index.js';
11
+ import {
12
+ clearTabspotActive,
13
+ setTabspotActive,
14
+ setTabspotAttributes,
15
+ type TabspotNavigationEvent,
16
+ type TabspotNodeOptions
17
+ } from 'tabspot';
18
+ import { getGlobalFSContext } from '../../providers/fluentui-svelte/fluentui-svelte.js';
19
+ import type { TagSize } from '../tag/types.js';
20
+ import type { OptionLabel, OptionType, TagPickerContext, TagPickerProps, TagPickerSizes } from './types.js';
21
+
22
+ const FALLBACK_ID = $props.id();
23
+ const ID = `${PREFIX}-tagpicker-${FALLBACK_ID}`;
24
+ const LIST_ID = `${ID}-list`;
25
+
26
+ /** The tag is one step smaller than the control around it. */
27
+ const TAG_SIZES: Record<TagPickerSizes, TagSize> = { medium: 'extra-small', large: 'small', 'extra-large': 'medium' };
28
+
29
+ const labelContext = claimLabel(() => id);
30
+
31
+ let {
32
+ wrapperRef = $bindable(null),
33
+ id = labelContext?.fieldId,
34
+ name = id,
35
+ class: classes,
36
+ ref = $bindable(null),
37
+ inputRef = $bindable(null),
38
+ inputAttributes,
39
+ selectedOptions = $bindable([]),
40
+ value = $bindable(''),
41
+ open = $bindable(false),
42
+ single,
43
+ disabled,
44
+ size = 'medium',
45
+ placeholder,
46
+ notFoundText = 'No options available',
47
+ expandIcon = ChevronDownRegular,
48
+ secondaryAction,
49
+ tag,
50
+ tagGroupProps,
51
+ wrapperAttributes,
52
+ onfocus,
53
+ onblur,
54
+ onSelectionChange,
55
+ querySubmitted,
56
+ children,
57
+ ...attributes
58
+ }: TagPickerProps = $props();
59
+
60
+ const INPUT_ID = $derived(inputAttributes?.id ?? `${ID}-input`);
61
+ const INPUT_NAME = $derived(inputAttributes?.name ?? INPUT_ID);
62
+
63
+ const native = new NativeField(
64
+ () => ref,
65
+ () => inputRef,
66
+ labelContext
67
+ );
68
+
69
+ const aria = $derived(native.aria(attributes, inputAttributes));
70
+
71
+ /** A `fieldset` disables the picker without its `disabled` prop, and only the native field knows. */
72
+ const blocked = () => _picker.disabled || !!ref?.matches(':disabled');
73
+
74
+ /**
75
+ * The props the invariants rule over. The markup and the context read them through here, so
76
+ * reading any of them validates the lot during SSR and on every prop update. A picker with no
77
+ * list, or a disabled one, can never be open.
78
+ */
79
+ const _picker = $derived.by(() => {
80
+ if (single && selectedOptions.length > 1) {
81
+ throw new Error('A single TagPicker cannot hold more than one selected option');
82
+ }
83
+ const hasList = !!children;
84
+ return { single, disabled, hasList, open: open && hasList && !disabled };
85
+ });
86
+
87
+ /** The options on screen, by id. */
88
+ const options = new SvelteMap<string, OptionType>();
89
+
90
+ /** What the tag of each value says. The list stays in the page while closed, so every option is here, read live. */
91
+ const optionLabels = new SvelteSet<OptionLabel>();
92
+ const labels = $derived(new Map([...optionLabels].map((option) => [option.value, option.text])));
93
+
94
+ let listViewRef = $state<HTMLUListElement | null>(null);
95
+
96
+ let roundClass = $state('bottom');
97
+
98
+ let activeOption: OptionType | null = $state(null);
99
+
100
+ const tagSize = $derived(TAG_SIZES[size]);
101
+
102
+ const NAVIGATION: TabspotNodeOptions = $derived({
103
+ root: {},
104
+ mover: {
105
+ axis: 'vertical',
106
+ items: '.fs-tagpicker-option',
107
+ skip: "[aria-disabled='true']",
108
+ activation: {
109
+ mode: 'activedescendant',
110
+ controller: `#${CSS.escape(INPUT_ID)}`,
111
+ mark: { attribute: 'data-active' }
112
+ }
113
+ }
114
+ });
115
+
116
+ const context: TagPickerContext = $state({
117
+ config: null,
118
+ state: {
119
+ get query() {
120
+ return value;
121
+ },
122
+ get selectedOptions() {
123
+ return selectedOptions;
124
+ },
125
+ get open() {
126
+ return _picker.open;
127
+ }
128
+ },
129
+ events: null,
130
+ methods: {
131
+ registerLabel: (option) => {
132
+ optionLabels.add(option);
133
+ return () => optionLabels.delete(option);
134
+ },
135
+ setOption: (option) => {
136
+ options.set(option.id, option);
137
+ },
138
+ deleteOption: (id) => {
139
+ options.delete(id);
140
+ },
141
+ chooseOption: async (e, id) => {
142
+ const option = options.get(id);
143
+
144
+ if (!option || option.disabled || blocked() || selectedOptions.includes(option.value)) return;
145
+
146
+ selectedOptions = _picker.single ? [option.value] : [...selectedOptions, option.value];
147
+ value = '';
148
+ activeOption = null;
149
+ dispatchFieldEvents(ref);
150
+ onSelectionChange?.(e, selectedOptions);
151
+
152
+ if (_picker.single) open = false;
153
+
154
+ // The option under the cursor just left the screen.
155
+ await tick();
156
+
157
+ if (listViewRef) clearTabspotActive(listViewRef);
158
+ }
159
+ }
160
+ });
161
+
162
+ setTagPickerContext(context);
163
+
164
+ /** Put the cursor on `element`, or send it home when there is none. */
165
+ function pointCursorAt(element: HTMLElement | null) {
166
+ if (!listViewRef) return;
167
+
168
+ // `nearest` walks past an option the cursor cannot land on.
169
+ if (element && setTabspotActive(element, { nearest: true }).ok) return;
170
+
171
+ clearTabspotActive(listViewRef);
172
+ readCursor(null);
173
+ }
174
+
175
+ function readCursor(element: HTMLElement | null) {
176
+ const option = element ? (options.get(element.id) ?? null) : null;
177
+
178
+ if (option?.id === activeOption?.id) return;
179
+
180
+ activeOption = option;
181
+
182
+ if (option) element?.scrollIntoView({ block: 'nearest' });
183
+ }
184
+
185
+ /** `atEdge` means the list ran out of options: send the cursor back to the input. */
186
+ function navigate(event: TabspotNavigationEvent) {
187
+ if (!event.atEdge) {
188
+ readCursor(event.to);
189
+ return;
190
+ }
191
+
192
+ event.preventDefault();
193
+
194
+ if (listViewRef) clearTabspotActive(listViewRef);
195
+
196
+ readCursor(null);
197
+ }
198
+
199
+ /** Open, then let Tabspot enter the list. */
200
+ async function enterList(key: 'ArrowUp' | 'ArrowDown') {
201
+ open = true;
202
+ await tick();
203
+
204
+ // The key is replayed rather than turned into a `setTabspotActive` call: a second arrow
205
+ // pressed while this one is still in flight is a second move, and two cursor sets onto
206
+ // the same option would be one. The extra hop lets Tabspot see the list it just got.
207
+ await undefined;
208
+ await tick();
209
+
210
+ inputRef?.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
211
+ }
212
+
213
+ function handleKeyDown(e: KeyboardEvent) {
214
+ if (e.isComposing) return;
215
+
216
+ switch (e.key) {
217
+ case 'ArrowDown':
218
+ case 'ArrowUp':
219
+ // An arrow inside an open list is Tabspot's. The replay of this one is not trusted.
220
+ if (!e.isTrusted || _picker.open || !_picker.hasList || _picker.disabled) return;
221
+
222
+ e.preventDefault();
223
+ enterList(e.key);
224
+ break;
225
+ case 'Enter':
226
+ e.preventDefault();
227
+
228
+ if (_picker.open && activeOption) context.methods.chooseOption(e, activeOption.id);
229
+ else if (value) querySubmitted?.(e, value);
230
+ else if (_picker.hasList && !_picker.disabled) open = true;
231
+ break;
232
+ case 'Escape':
233
+ if (!_picker.open) return;
234
+
235
+ e.preventDefault();
236
+ open = false;
237
+ break;
238
+ case 'Tab':
239
+ open = false;
240
+ break;
241
+ case 'Backspace':
242
+ if (_picker.disabled || value !== '' || selectedOptions.length === 0) return;
243
+
244
+ selectedOptions = selectedOptions.slice(0, -1);
245
+ dispatchFieldEvents(ref);
246
+ onSelectionChange?.(e, selectedOptions);
247
+ break;
248
+ }
249
+ }
250
+
251
+ async function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
252
+ if (!_picker.hasList || _picker.disabled) return;
253
+
254
+ if (e.currentTarget.value && !open) open = true;
255
+
256
+ await tick();
257
+
258
+ // The first match is the one Enter would choose.
259
+ pointCursorAt(value ? (listViewRef?.querySelector<HTMLElement>('.fs-tagpicker-option') ?? null) : null);
260
+ }
261
+
262
+ function handleDismiss(e: MouseEvent | KeyboardEvent, data: { value: string }) {
263
+ selectedOptions = selectedOptions.filter((v) => v !== data.value);
264
+ dispatchFieldEvents(ref);
265
+ onSelectionChange?.(e, selectedOptions);
266
+ inputRef?.focus();
267
+ }
268
+
269
+ function handleControlClick(e: MouseEvent) {
270
+ // Tags and the aside have a job of their own.
271
+ if (blocked() || (e.target as Element).closest('.fs-tag, .fs-tagpicker-aside')) return;
272
+
273
+ inputRef?.focus();
274
+
275
+ if (_picker.hasList) open = true;
276
+ }
277
+
278
+ function handleChevronClick() {
279
+ if (blocked()) return;
280
+
281
+ inputRef?.focus();
282
+
283
+ if (_picker.hasList) open = !_picker.open;
284
+ }
285
+
286
+ /** A click on the secondary action must not strand focus on it: the list is still open, and Escape lives on the input. */
287
+ function handleAsideClick(e: MouseEvent) {
288
+ if (_picker.open && !(e.target as Element).closest('.chevron')) inputRef?.focus();
289
+ }
290
+
291
+ /** Focus stays in the input: the chrome around it only forwards the click. */
292
+ function keepFocus(e: MouseEvent) {
293
+ if (!(e.target as Element).closest('input, button')) e.preventDefault();
294
+ }
295
+
296
+ function handlePlacementChange(newPlacement: string) {
297
+ roundClass = newPlacement.includes('-') ? newPlacement.split('-')[0] : newPlacement;
298
+ }
299
+
300
+ onClickOutside(
301
+ () => wrapperRef,
302
+ () => (open = false)
303
+ );
304
+
305
+ const globalContext = getGlobalFSContext();
306
+
307
+ // Closing takes the list down, and leaving `activeOption` behind would point at nothing.
308
+ $effect(() => {
309
+ if (!_picker.open) activeOption = null;
310
+ });
311
+
312
+ $effect(() => {
313
+ if (!_picker.open || !listViewRef) return;
314
+
315
+ const list = listViewRef;
316
+
317
+ setTabspotAttributes({ element: list, config: NAVIGATION });
318
+
319
+ const unsubscribe = globalContext?.state.tabspotInstance?.subscribe(list, navigate);
320
+
321
+ // The list stays in the page once closed: nothing in it reads as chosen.
322
+ return () => {
323
+ clearTabspotActive(list);
324
+ unsubscribe?.();
325
+ };
326
+ });
327
+
328
+ // Tabspot does not notice an option that left the list, so it learns the options that are left.
329
+ $effect(() => {
330
+ void [...options.keys()];
331
+
332
+ if (_picker.open && listViewRef) globalContext?.state.tabspotInstance?.rebuild(listViewRef);
333
+ });
334
+ </script>
335
+
336
+ <!--
337
+ @component
338
+ A text field and a dropdown that give people a way to choose several options from a list, or to enter their own choice. Every choice is shown as a dismissible Tag in the control.
339
+
340
+ - Usage:
341
+ ```tsx
342
+ <script>
343
+ import { TagPicker, TagPickerOption } from 'fluentui-svelte';
344
+
345
+ let selectedOptions = $state([]);
346
+ </script>
347
+
348
+ <TagPicker bind:selectedOptions name="fruits" inputAttributes={{ 'aria-label': 'Fruits' }}>
349
+ <TagPickerOption value="Apple">Apple</TagPickerOption>
350
+ <TagPickerOption value="Banana">Banana</TagPickerOption>
351
+ </TagPicker>
352
+ ```
353
+ -->
354
+ <div
355
+ {...wrapperAttributes}
356
+ class={['fs-tagpicker', `size-${size}`, { disabled: _picker.disabled, open: _picker.open }, wrapperAttributes?.class]}
357
+ bind:this={wrapperRef}
358
+ >
359
+ {#if _picker.hasList}
360
+ <!-- Hidden while closed, not gone: each option still tells its tag what to say, and comes before
361
+ the tags, so the server writes them too. It shows once it has the wrapper to be placed against. -->
362
+ <Flyout
363
+ floating
364
+ hidden={!_picker.open || !wrapperRef}
365
+ offset={0}
366
+ reference={wrapperRef ?? undefined}
367
+ roundCorners="bottom"
368
+ placementConfig={{ allowedPlacements: ['bottom', 'top'] }}
369
+ onPlacementChange={(place) => handlePlacementChange(place)}
370
+ class={['fs-tagpicker-flyout', roundClass === 'top' && 'top-shadow']}
371
+ >
372
+ <ListView
373
+ role="listbox"
374
+ selectionMode={_picker.single ? 'single' : 'multiselect'}
375
+ navigationMode="items"
376
+ disableTabspot
377
+ id={LIST_ID}
378
+ aria-multiselectable={_picker.single ? undefined : 'true'}
379
+ aria-label={aria['aria-label']}
380
+ aria-labelledby={aria['aria-labelledby']}
381
+ bind:ref={listViewRef}
382
+ onmousedown={(e: MouseEvent) => e.preventDefault()}
383
+ >
384
+ {#if options.size === 0}
385
+ <ListViewItem role="option" disabled>{notFoundText}</ListViewItem>
386
+ {/if}
387
+ {@render children?.()}
388
+ </ListView>
389
+ </Flyout>
390
+ {/if}
391
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
392
+ <div class={['control', _picker.open && `square-${roundClass}`]} onclick={handleControlClick} onmousedown={keepFocus}>
393
+ <div class="content">
394
+ {#if selectedOptions.length > 0}
395
+ <TagGroup
396
+ aria-label="Selected options"
397
+ {...tagGroupProps}
398
+ size={tagSize}
399
+ disabled={_picker.disabled}
400
+ dismissible
401
+ onDismiss={handleDismiss}
402
+ >
403
+ {#each selectedOptions as v (v)}
404
+ {@const text = labels.get(v) ?? v}
405
+ {#if tag}
406
+ {@render tag({ value: v, text })}
407
+ {:else}
408
+ <Tag value={v} title={text}>{text}</Tag>
409
+ {/if}
410
+ {/each}
411
+ </TagGroup>
412
+ {/if}
413
+ <input
414
+ {...inputAttributes}
415
+ {...aria}
416
+ autocomplete="off"
417
+ bind:this={inputRef}
418
+ type="text"
419
+ {placeholder}
420
+ id={INPUT_ID}
421
+ name={INPUT_NAME}
422
+ disabled={_picker.disabled}
423
+ role={_picker.hasList ? 'combobox' : undefined}
424
+ aria-expanded={_picker.hasList ? _picker.open : undefined}
425
+ aria-haspopup={_picker.hasList ? 'listbox' : undefined}
426
+ aria-autocomplete={_picker.hasList ? 'list' : undefined}
427
+ aria-controls={_picker.open ? LIST_ID : undefined}
428
+ bind:value
429
+ oninput={(e) => invokeHandlers(e, [disabled], [handleInput, inputAttributes?.oninput])}
430
+ onkeydown={(e) => invokeHandlers(e, [disabled], [handleKeyDown, inputAttributes?.onkeydown])}
431
+ onfocus={(e) => invokeHandlers(e, [], [onfocus, inputAttributes?.onfocus])}
432
+ onblur={(e) => invokeHandlers(e, [], [onblur, inputAttributes?.onblur])}
433
+ />
434
+ </div>
435
+ {#if secondaryAction || (expandIcon && _picker.hasList)}
436
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
437
+ <span class="fs-tagpicker-aside" onclick={handleAsideClick}>
438
+ {@render secondaryAction?.()}
439
+ <!-- Without a list there is nothing to expand. -->
440
+ {#if expandIcon && _picker.hasList}
441
+ <span class="chevron" aria-hidden="true" onclick={handleChevronClick}>
442
+ <RenderSoC SoC={expandIcon} />
443
+ </span>
444
+ {/if}
445
+ </span>
446
+ {/if}
447
+ </div>
448
+ <!-- The field of the form: it holds the chosen options, never the typed text, so it is
449
+ submitted, validated and reset like any multiple select, behind the control and out of
450
+ the tab order. See `NativeField`. -->
451
+ <select
452
+ {...attributes}
453
+ {id}
454
+ {name}
455
+ class={['native-field', classes]}
456
+ aria-hidden={native.reported ? undefined : 'true'}
457
+ multiple
458
+ tabindex={-1}
459
+ disabled={_picker.disabled}
460
+ bind:value={selectedOptions}
461
+ bind:this={ref}
462
+ onclick={() => inputRef?.focus()}
463
+ >
464
+ {#each selectedOptions as option (option)}
465
+ <option value={option}>{labels.get(option) ?? option}</option>
466
+ {/each}
467
+ </select>
468
+ </div>
469
+
470
+ <style>
471
+ .fs-tagpicker {
472
+ --tp-min-height: 2rem;
473
+ --tp-group-pad: 0.375rem;
474
+ --tp-gap: 0.25rem;
475
+ --tp-input-pad: 0.375rem;
476
+ --tp-icon: 1rem;
477
+ --tp-icon-gap: 0.125rem;
478
+
479
+ position: relative;
480
+ box-sizing: border-box;
481
+ inline-size: 100%;
482
+ min-inline-size: 15.625rem;
483
+ &.size-large {
484
+ --tp-min-height: 2.5rem;
485
+ --tp-group-pad: 0.5rem;
486
+ --tp-gap: 0.375rem;
487
+ --tp-input-pad: 0.625rem;
488
+ --tp-icon: 1.25rem;
489
+ }
490
+ &.size-extra-large {
491
+ --tp-min-height: 2.75rem;
492
+ --tp-group-pad: 0.5rem;
493
+ --tp-gap: 0.375rem;
494
+ --tp-input-pad: 0.75rem;
495
+ --tp-icon: 1.5rem;
496
+ --tp-icon-gap: 0.375rem;
497
+ }
498
+ }
499
+
500
+ .control {
501
+ position: relative;
502
+ display: grid;
503
+ grid-template-columns: minmax(0, 1fr) auto;
504
+ box-sizing: border-box;
505
+ min-block-size: var(--tp-min-height);
506
+ padding-inline: 0.75rem;
507
+ border-radius: var(--fs-control-border-radius);
508
+ background: var(--fs-control-fill-default);
509
+ cursor: text;
510
+ &:hover {
511
+ background: var(--fs-control-fill-secondary);
512
+ }
513
+ &::before {
514
+ content: '';
515
+ pointer-events: none;
516
+ inline-size: calc(100% + 0.125rem);
517
+ block-size: calc(100% + 0.125rem);
518
+ position: absolute;
519
+ inset-block-start: -0.063rem;
520
+ inset-inline-start: -0.063rem;
521
+ padding: 0.063rem;
522
+ box-sizing: border-box;
523
+ border-radius: 0.313rem;
524
+ mask:
525
+ linear-gradient(#fff 0 0) content-box,
526
+ linear-gradient(#fff 0 0);
527
+ mask-composite: exclude;
528
+ background: var(--fs-elevation-text-border);
529
+ z-index: 1;
530
+ }
531
+ /* The bar grows from the middle on focus. Its clip-path is static: animating it would freeze the page. */
532
+ &::after {
533
+ content: '';
534
+ pointer-events: none;
535
+ inline-size: 0;
536
+ block-size: 10px;
537
+ border-radius: 0 0 5px 5px;
538
+ position: absolute;
539
+ inset-block-end: -0.063rem;
540
+ inset-inline-start: calc(50% - 0.063rem);
541
+ clip-path: polygon(0 75%, 100% 75%, 100% 100%, 0% 100%);
542
+ background: transparent;
543
+ transition:
544
+ inline-size var(--fs-normal-duration) var(--fs-point-to-point),
545
+ inset-inline-start var(--fs-normal-duration) var(--fs-point-to-point);
546
+ z-index: 2;
547
+ }
548
+ &.square-top {
549
+ border-start-start-radius: 0;
550
+ border-start-end-radius: 0;
551
+ &::before,
552
+ &::after {
553
+ border-start-start-radius: 0;
554
+ border-start-end-radius: 0;
555
+ }
556
+ }
557
+ &.square-bottom {
558
+ border-end-start-radius: 0;
559
+ border-end-end-radius: 0;
560
+ &::before,
561
+ &::after {
562
+ border-end-start-radius: 0;
563
+ border-end-end-radius: 0;
564
+ }
565
+ }
566
+ }
567
+
568
+ .fs-tagpicker:focus-within .control,
569
+ .open .control {
570
+ &::after {
571
+ inline-size: calc(100% + 0.125rem);
572
+ inset-inline-start: -0.063rem;
573
+ background-color: var(--fs-accent-fill-default);
574
+ }
575
+ }
576
+ .fs-tagpicker:focus-within .control {
577
+ background: var(--fs-control-fill-input-active);
578
+ & input::placeholder {
579
+ color: var(--fs-text-tertiary);
580
+ }
581
+ }
582
+ .fs-tagpicker > .native-field {
583
+ position: absolute;
584
+ inset: 0;
585
+ inline-size: 100%;
586
+ block-size: 100%;
587
+ margin: 0;
588
+ box-sizing: border-box;
589
+ opacity: 0;
590
+ pointer-events: none;
591
+ }
592
+ .fs-tagpicker:has(> .native-field:is(:user-invalid, [aria-invalid='true'])) .control::after {
593
+ inline-size: calc(100% + 0.125rem);
594
+ inset-inline-start: -0.063rem;
595
+ background-color: var(--fs-system-critical);
596
+ }
597
+
598
+ .content {
599
+ display: flex;
600
+ flex-wrap: wrap;
601
+ align-items: center;
602
+ column-gap: var(--tp-gap);
603
+ min-inline-size: 0;
604
+ /* The tags are flex items of the control, so the input follows the last one instead of starting a row. */
605
+ & :global(.fs-tag-group) {
606
+ display: contents;
607
+ }
608
+ /* A long tag shortens itself instead of overflowing the control. */
609
+ & :global(.fs-tag) {
610
+ max-inline-size: 100%;
611
+ margin-block: var(--tp-group-pad);
612
+ }
613
+ & :global(.fs-tag-primary-text) {
614
+ overflow: hidden;
615
+ text-overflow: ellipsis;
616
+ }
617
+ }
618
+
619
+ input {
620
+ flex: 1 1 0;
621
+ min-inline-size: 1.5rem;
622
+ max-inline-size: 100%;
623
+ box-sizing: border-box;
624
+ margin: 0;
625
+ padding: var(--tp-input-pad) 0;
626
+ appearance: none;
627
+ background: transparent;
628
+ border: none;
629
+ outline: none;
630
+ color: var(--fs-text-primary);
631
+ font-family: inherit;
632
+ font-size: var(--fs-body2-font-size);
633
+ &::placeholder {
634
+ color: var(--fs-text-secondary);
635
+ }
636
+ &[readonly] {
637
+ caret-color: transparent;
638
+ cursor: pointer;
639
+ }
640
+ }
641
+
642
+ .fs-tagpicker-aside {
643
+ display: flex;
644
+ align-items: center;
645
+ align-self: start;
646
+ block-size: var(--tp-min-height);
647
+ color: var(--fs-text-secondary);
648
+ /* The control sits above the border mask, and the action must stay clickable. */
649
+ z-index: 2;
650
+ /* The action sits inside the control, whatever size its own button has. */
651
+ & :global(.fs-button) {
652
+ block-size: calc(var(--tp-min-height) - 0.25rem);
653
+ padding-block: 0;
654
+ font-size: var(--fs-body2-font-size);
655
+ line-height: var(--fs-body2-line-height);
656
+ }
657
+ }
658
+ /* The arrow behaves like a subtle button. Its negative margins keep the icon where it was. */
659
+ .chevron {
660
+ display: flex;
661
+ margin-inline: calc(var(--tp-icon-gap) - 0.25rem) -0.25rem;
662
+ padding: 0.25rem;
663
+ border-radius: var(--fs-control-border-radius);
664
+ cursor: pointer;
665
+ &:hover {
666
+ background: var(--fs-subtle-fill-secondary);
667
+ }
668
+ &:active {
669
+ color: var(--fs-text-tertiary);
670
+ background: var(--fs-subtle-fill-tertiary);
671
+ }
672
+ & :global(svg) {
673
+ display: block;
674
+ inline-size: var(--tp-icon);
675
+ block-size: var(--tp-icon);
676
+ fill: currentColor;
677
+ }
678
+ }
679
+
680
+ .fs-tagpicker:is(.disabled, :has(> .native-field:disabled)) .control {
681
+ cursor: not-allowed;
682
+ background: var(--fs-control-fill-disabled);
683
+ &::before {
684
+ background: var(--fs-control-stroke-default);
685
+ }
686
+ & input,
687
+ & input::placeholder,
688
+ & .fs-tagpicker-aside {
689
+ color: var(--fs-text-disabled);
690
+ }
691
+ & .chevron {
692
+ cursor: not-allowed;
693
+ &:hover,
694
+ &:active {
695
+ color: inherit;
696
+ background: none;
697
+ }
698
+ }
699
+ }
700
+
701
+ .fs-tagpicker :global(.fs-flyout) {
702
+ padding: 0;
703
+ left: -1px;
704
+ width: calc(100% + 2px);
705
+ z-index: 10;
706
+ overflow: hidden;
707
+ max-block-size: min(80vh, 20rem);
708
+ &:global(.top-shadow) {
709
+ box-shadow: 0 -0.5rem 0.8rem #00000024;
710
+ }
711
+ }
712
+ .fs-tagpicker :global(.fs-list-view) {
713
+ overflow-y: auto;
714
+ padding: 4px;
715
+ }
716
+ </style>