@ui-sdk/react 0.0.0-stage → 0.1.0-beta.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 (247) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +331 -0
  4. package/dist/accordion.js +25 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +8 -0
  7. package/dist/alert-dialog.d.ts +56 -0
  8. package/dist/alert-dialog.js +45 -0
  9. package/dist/async-options.d.ts +22 -0
  10. package/dist/async-options.js +56 -0
  11. package/dist/autocomplete.d.ts +102 -0
  12. package/dist/autocomplete.js +195 -0
  13. package/dist/avatar.d.ts +301 -0
  14. package/dist/avatar.js +17 -0
  15. package/dist/button.d.ts +21 -0
  16. package/dist/button.js +45 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +9 -0
  19. package/dist/calendar.d.ts +80 -0
  20. package/dist/calendar.js +257 -0
  21. package/dist/carousel.d.ts +52 -0
  22. package/dist/carousel.js +151 -0
  23. package/dist/checkbox.d.ts +24 -0
  24. package/dist/checkbox.js +98 -0
  25. package/dist/children.d.ts +38 -0
  26. package/dist/children.js +63 -0
  27. package/dist/collapsible.d.ts +302 -0
  28. package/dist/collapsible.js +19 -0
  29. package/dist/color-picker.d.ts +72 -0
  30. package/dist/color-picker.js +133 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +22 -0
  33. package/dist/combobox.d.ts +152 -0
  34. package/dist/combobox.js +338 -0
  35. package/dist/command.d.ts +88 -0
  36. package/dist/command.js +233 -0
  37. package/dist/controller-target.d.ts +3 -0
  38. package/dist/controller-target.js +59 -0
  39. package/dist/date-input.d.ts +19 -0
  40. package/dist/date-input.js +58 -0
  41. package/dist/date-picker.d.ts +47 -0
  42. package/dist/date-picker.js +310 -0
  43. package/dist/date.d.ts +2 -0
  44. package/dist/date.js +60 -0
  45. package/dist/dialog-parts.d.ts +23 -0
  46. package/dist/dialog-parts.js +142 -0
  47. package/dist/dialog.d.ts +57 -0
  48. package/dist/dialog.js +32 -0
  49. package/dist/drawer.d.ts +70 -0
  50. package/dist/drawer.js +158 -0
  51. package/dist/editor-context.d.ts +4 -0
  52. package/dist/editor-context.js +9 -0
  53. package/dist/field-context.d.ts +47 -0
  54. package/dist/field-context.js +87 -0
  55. package/dist/field.d.ts +49 -0
  56. package/dist/field.js +169 -0
  57. package/dist/file-input.d.ts +46 -0
  58. package/dist/file-input.js +170 -0
  59. package/dist/form-inputs.d.ts +5 -0
  60. package/dist/form-inputs.js +22 -0
  61. package/dist/gesture.d.ts +1 -0
  62. package/dist/gesture.js +16 -0
  63. package/dist/history.d.ts +2 -0
  64. package/dist/history.js +12 -0
  65. package/dist/hover-card.d.ts +33 -0
  66. package/dist/hover-card.js +84 -0
  67. package/dist/image-cropper.d.ts +333 -0
  68. package/dist/image-cropper.js +37 -0
  69. package/dist/image.d.ts +2 -0
  70. package/dist/image.js +6 -0
  71. package/dist/input.d.ts +22 -0
  72. package/dist/input.js +51 -0
  73. package/dist/label.d.ts +294 -0
  74. package/dist/label.js +9 -0
  75. package/dist/list-items.d.ts +15 -0
  76. package/dist/list-items.js +39 -0
  77. package/dist/list-text.d.ts +4 -0
  78. package/dist/list-text.js +15 -0
  79. package/dist/listbox.d.ts +56 -0
  80. package/dist/listbox.js +122 -0
  81. package/dist/menu.d.ts +116 -0
  82. package/dist/menu.js +426 -0
  83. package/dist/menubar-context.d.ts +4 -0
  84. package/dist/menubar-context.js +7 -0
  85. package/dist/menubar.d.ts +11 -0
  86. package/dist/menubar.js +32 -0
  87. package/dist/meter.d.ts +312 -0
  88. package/dist/meter.js +30 -0
  89. package/dist/navigation-menu.d.ts +61 -0
  90. package/dist/navigation-menu.js +169 -0
  91. package/dist/number-input.d.ts +44 -0
  92. package/dist/number-input.js +185 -0
  93. package/dist/number.d.ts +2 -0
  94. package/dist/number.js +40 -0
  95. package/dist/observe.d.ts +2 -0
  96. package/dist/observe.js +8 -0
  97. package/dist/otp-input.d.ts +29 -0
  98. package/dist/otp-input.js +94 -0
  99. package/dist/overlays.d.ts +26 -0
  100. package/dist/overlays.js +82 -0
  101. package/dist/pagination.d.ts +336 -0
  102. package/dist/pagination.js +71 -0
  103. package/dist/password-input.d.ts +23 -0
  104. package/dist/password-input.js +65 -0
  105. package/dist/popover.d.ts +54 -0
  106. package/dist/popover.js +151 -0
  107. package/dist/popup.d.ts +41 -0
  108. package/dist/popup.js +180 -0
  109. package/dist/preferences.d.ts +35 -0
  110. package/dist/preferences.js +93 -0
  111. package/dist/primitive.d.ts +2 -0
  112. package/dist/primitive.js +14 -0
  113. package/dist/progress.d.ts +312 -0
  114. package/dist/progress.js +30 -0
  115. package/dist/radio.d.ts +22 -0
  116. package/dist/radio.js +86 -0
  117. package/dist/rating.d.ts +20 -0
  118. package/dist/rating.js +94 -0
  119. package/dist/render-element.d.ts +35 -0
  120. package/dist/render-element.js +103 -0
  121. package/dist/resizable.d.ts +302 -0
  122. package/dist/resizable.js +23 -0
  123. package/dist/scroll-area.d.ts +324 -0
  124. package/dist/scroll-area.js +49 -0
  125. package/dist/segmented-control.d.ts +24 -0
  126. package/dist/segmented-control.js +78 -0
  127. package/dist/segmented-input.d.ts +15 -0
  128. package/dist/segmented-input.js +48 -0
  129. package/dist/select.d.ts +125 -0
  130. package/dist/select.js +330 -0
  131. package/dist/separator.d.ts +290 -0
  132. package/dist/separator.js +9 -0
  133. package/dist/shortcuts.d.ts +32 -0
  134. package/dist/shortcuts.js +61 -0
  135. package/dist/slider.d.ts +38 -0
  136. package/dist/slider.js +163 -0
  137. package/dist/switch.d.ts +15 -0
  138. package/dist/switch.js +39 -0
  139. package/dist/tabs.d.ts +331 -0
  140. package/dist/tabs.js +28 -0
  141. package/dist/text-control.d.ts +22 -0
  142. package/dist/text-control.js +24 -0
  143. package/dist/text-match.d.ts +2 -0
  144. package/dist/text-match.js +8 -0
  145. package/dist/textarea.d.ts +10 -0
  146. package/dist/textarea.js +48 -0
  147. package/dist/time-input.d.ts +19 -0
  148. package/dist/time-input.js +63 -0
  149. package/dist/timeline.d.ts +327 -0
  150. package/dist/timeline.js +31 -0
  151. package/dist/toast-parts.d.ts +63 -0
  152. package/dist/toast-parts.js +144 -0
  153. package/dist/toast.d.ts +2 -0
  154. package/dist/toast.js +22 -0
  155. package/dist/toggle.d.ts +23 -0
  156. package/dist/toggle.js +76 -0
  157. package/dist/token-input.d.ts +35 -0
  158. package/dist/token-input.js +100 -0
  159. package/dist/toolbar-context.d.ts +6 -0
  160. package/dist/toolbar-context.js +16 -0
  161. package/dist/toolbar.d.ts +38 -0
  162. package/dist/toolbar.js +63 -0
  163. package/dist/tooltip.d.ts +39 -0
  164. package/dist/tooltip.js +108 -0
  165. package/dist/tree.d.ts +51 -0
  166. package/dist/tree.js +150 -0
  167. package/dist/trigger-menu.d.ts +83 -0
  168. package/dist/trigger-menu.js +167 -0
  169. package/dist/use-primitive.d.ts +178 -0
  170. package/dist/use-primitive.js +674 -0
  171. package/dist/waterfall.d.ts +80 -0
  172. package/dist/waterfall.js +108 -0
  173. package/docs/README.md +89 -0
  174. package/docs/agents.md +1523 -0
  175. package/docs/components/accordion.md +1391 -0
  176. package/docs/components/alert-dialog.md +850 -0
  177. package/docs/components/autocomplete.md +1963 -0
  178. package/docs/components/avatar.md +252 -0
  179. package/docs/components/button.md +444 -0
  180. package/docs/components/calendar.md +1687 -0
  181. package/docs/components/carousel.md +1244 -0
  182. package/docs/components/checkbox.md +1069 -0
  183. package/docs/components/collapsible.md +567 -0
  184. package/docs/components/color-picker.md +736 -0
  185. package/docs/components/combobox.md +3647 -0
  186. package/docs/components/command.md +1573 -0
  187. package/docs/components/date-input.md +608 -0
  188. package/docs/components/date-picker.md +1266 -0
  189. package/docs/components/dialog.md +1469 -0
  190. package/docs/components/drawer.md +1197 -0
  191. package/docs/components/field.md +549 -0
  192. package/docs/components/file-input.md +609 -0
  193. package/docs/components/hover-card.md +514 -0
  194. package/docs/components/image-cropper.md +579 -0
  195. package/docs/components/input.md +301 -0
  196. package/docs/components/label.md +217 -0
  197. package/docs/components/listbox.md +1130 -0
  198. package/docs/components/menu.md +1286 -0
  199. package/docs/components/menubar.md +455 -0
  200. package/docs/components/meter.md +304 -0
  201. package/docs/components/navigation-menu.md +1581 -0
  202. package/docs/components/number-input.md +1086 -0
  203. package/docs/components/otp-input.md +565 -0
  204. package/docs/components/pagination.md +648 -0
  205. package/docs/components/password-input.md +737 -0
  206. package/docs/components/popover.md +1552 -0
  207. package/docs/components/preferences.md +220 -0
  208. package/docs/components/progress.md +443 -0
  209. package/docs/components/radio.md +846 -0
  210. package/docs/components/rating.md +458 -0
  211. package/docs/components/resizable.md +982 -0
  212. package/docs/components/scroll-area.md +980 -0
  213. package/docs/components/segmented-control.md +465 -0
  214. package/docs/components/select.md +2133 -0
  215. package/docs/components/separator.md +243 -0
  216. package/docs/components/slider.md +1169 -0
  217. package/docs/components/switch.md +613 -0
  218. package/docs/components/tabs.md +1014 -0
  219. package/docs/components/textarea.md +318 -0
  220. package/docs/components/time-input.md +231 -0
  221. package/docs/components/timeline.md +677 -0
  222. package/docs/components/toast.md +1395 -0
  223. package/docs/components/toggle.md +541 -0
  224. package/docs/components/token-input.md +1007 -0
  225. package/docs/components/toolbar.md +787 -0
  226. package/docs/components/tooltip.md +884 -0
  227. package/docs/components/tree.md +1346 -0
  228. package/docs/components/trigger-menu.md +1257 -0
  229. package/docs/components/waterfall.md +754 -0
  230. package/docs/controllers.md +89 -0
  231. package/docs/core.md +8027 -0
  232. package/docs/custom-parts.md +120 -0
  233. package/docs/first-component.md +83 -0
  234. package/docs/forms.md +148 -0
  235. package/docs/hooks/async-options.md +366 -0
  236. package/docs/hooks/shortcuts.md +221 -0
  237. package/docs/installation.md +45 -0
  238. package/docs/introduction.md +32 -0
  239. package/docs/lists-and-menus.md +170 -0
  240. package/docs/llms.txt +1844 -0
  241. package/docs/parts-and-composition.md +93 -0
  242. package/docs/popups.md +117 -0
  243. package/docs/rendering.md +121 -0
  244. package/docs/server-rendering.md +253 -0
  245. package/docs/state-and-events.md +105 -0
  246. package/docs/styling-and-animation.md +176 -0
  247. package/package.json +333 -3
@@ -0,0 +1,1257 @@
1
+ # Trigger Menu
2
+
3
+ "Opens suggestions at the caret when you type a trigger such as @, /, or #."
4
+
5
+ A trigger menu suggests what to insert when someone types a trigger character, like `@` for people or `/` for commands. It opens at the caret, narrows as they type, and inserts the choice into the text.
6
+
7
+ ```tsx title="demo.tsx"
8
+ "use client"
9
+
10
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
11
+ import {
12
+ type TriggerMenuDataItem,
13
+ TriggerMenu,
14
+ TriggerMenuContent,
15
+ TriggerMenuEmpty,
16
+ TriggerMenuInput,
17
+ TriggerMenuItem,
18
+ TriggerMenuList,
19
+ TriggerMenuStatus,
20
+ } from "@ui-sdk/react/trigger-menu"
21
+
22
+ const people = [
23
+ { value: "hollis", label: "Hollis Grant" },
24
+ { value: "ines", label: "Inès Moreau" },
25
+ { value: "kofi", label: "Kofi Mensah" },
26
+ { value: "wren", label: "Wren Takahashi" },
27
+ { value: "tomasz", label: "Tomasz Wójcik" },
28
+ ]
29
+
30
+ function Example() {
31
+ return (
32
+ <TriggerMenu triggers={[{ char: "@", type: "mention", items: people }]}>
33
+ <Field>
34
+ <FieldLabel>Message</FieldLabel>
35
+ <FieldControl
36
+ render={<TriggerMenuInput placeholder="Type @ to mention someone…" />}
37
+ />
38
+ </Field>
39
+ <div data-slot="visually-hidden">
40
+ <TriggerMenuStatus />
41
+ </div>
42
+ <TriggerMenuContent>
43
+ <TriggerMenuEmpty>No one found</TriggerMenuEmpty>
44
+ <TriggerMenuList>
45
+ {(state) =>
46
+ (state.items as TriggerMenuDataItem[]).map((item) => (
47
+ <TriggerMenuItem
48
+ key={item.value}
49
+ value={item.value}
50
+ label={item.label}
51
+ >
52
+ {item.label}
53
+ </TriggerMenuItem>
54
+ ))
55
+ }
56
+ </TriggerMenuList>
57
+ </TriggerMenuContent>
58
+ </TriggerMenu>
59
+ )
60
+ }
61
+
62
+ export { Example }
63
+ ```
64
+
65
+ Type @, narrow the list with a name, and press Enter to insert the highlighted person.
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ npm install @ui-sdk/react @ui-sdk/core
71
+ ```
72
+
73
+ ## Composition
74
+
75
+ Use the following composition to build a `TriggerMenu`:
76
+
77
+ ```text
78
+ TriggerMenu
79
+ ├── TriggerMenuInput
80
+ ├── TriggerMenuStatus
81
+ └── TriggerMenuContent
82
+ ├── TriggerMenuEmpty
83
+ └── TriggerMenuList
84
+ ├── TriggerMenuItem
85
+ └── TriggerMenuItem
86
+ ```
87
+
88
+ `TriggerMenu` holds the triggers and what each one lists. `TriggerMenuInput` is the field people type in, and `TriggerMenuContent` is the popup with the suggestions. Without a `TriggerMenuContent` among its direct children, the menu renders the current trigger's items for you. Inside a [Token Input](./token-input.md), leave out `TriggerMenuInput`: the menu suggests into the token input.
89
+
90
+ ## Usage
91
+
92
+ Import the parts and put them together:
93
+
94
+ ```tsx
95
+ import {
96
+ TriggerMenu,
97
+ TriggerMenuContent,
98
+ TriggerMenuInput,
99
+ TriggerMenuItem,
100
+ TriggerMenuList,
101
+ } from "@ui-sdk/react/trigger-menu"
102
+
103
+ function Example() {
104
+ return (
105
+ <TriggerMenu triggers={[{ char: "@" }]}>
106
+ <TriggerMenuInput
107
+ aria-label="Message"
108
+ placeholder="Type @ to mention someone…"
109
+ />
110
+ <TriggerMenuContent>
111
+ <TriggerMenuList>
112
+ <TriggerMenuItem value="hollis" label="Hollis Grant">
113
+ Hollis Grant
114
+ </TriggerMenuItem>
115
+ <TriggerMenuItem value="ines" label="Inès Moreau">
116
+ Inès Moreau
117
+ </TriggerMenuItem>
118
+ </TriggerMenuList>
119
+ </TriggerMenuContent>
120
+ </TriggerMenu>
121
+ )
122
+ }
123
+
124
+ export { Example }
125
+ ```
126
+
127
+ `label` is the text an item is matched by and inserts. Choosing Hollis Grant replaces `@` and what was typed after it with `@Hollis Grant `.
128
+
129
+ ## Trigger Menu or Token Input
130
+
131
+ Trigger Menu suggests what to insert: it matches the trigger, filters, moves the highlight, loads items and asks for a command's arguments. [Token Input](./token-input.md) holds a value that mixes text and tokens.
132
+
133
+ Use Trigger Menu with a textarea or an input to insert text. Put it inside a Token Input to insert tokens instead. Another editor connects through an [adapter](#any-editor).
134
+
135
+ ## Triggers as Data
136
+
137
+ Give each trigger a `char` and its `items`. Items can be strings, objects, groups or a loader, and they render themselves when you leave out `TriggerMenuContent`. The same field can offer people after `@`, commands after `/` and channels after `#`.
138
+
139
+ ```tsx title="data.tsx"
140
+ "use client"
141
+
142
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
143
+ import {
144
+ type TriggerDefinition,
145
+ TriggerMenu,
146
+ TriggerMenuInput,
147
+ } from "@ui-sdk/react/trigger-menu"
148
+
149
+ const people = [
150
+ { value: "hollis", label: "Hollis Grant" },
151
+ { value: "ines", label: "Inès Moreau" },
152
+ { value: "kofi", label: "Kofi Mensah" },
153
+ { value: "wren", label: "Wren Takahashi" },
154
+ { value: "tomasz", label: "Tomasz Wójcik" },
155
+ ]
156
+
157
+ const commands = ["away", "remind", "search", "status"]
158
+
159
+ const channels = [
160
+ "announcements",
161
+ "design",
162
+ "design-reviews",
163
+ "engineering",
164
+ "general",
165
+ "launches",
166
+ "support",
167
+ ]
168
+
169
+ function searchChannels(query: string, { signal }: { signal: AbortSignal }) {
170
+ return new Promise<string[]>((resolve, reject) => {
171
+ const timer = setTimeout(
172
+ () =>
173
+ resolve(
174
+ channels.filter((channel) => channel.includes(query.toLowerCase()))
175
+ ),
176
+ 400
177
+ )
178
+ signal.addEventListener("abort", () => {
179
+ clearTimeout(timer)
180
+ reject(signal.reason)
181
+ })
182
+ })
183
+ }
184
+
185
+ const triggers: TriggerDefinition[] = [
186
+ { char: "@", items: people },
187
+ { char: "/", boundary: "start", items: commands },
188
+ { char: "#", items: searchChannels },
189
+ ]
190
+
191
+ function Example() {
192
+ return (
193
+ <TriggerMenu triggers={triggers}>
194
+ <Field>
195
+ <FieldLabel>Message</FieldLabel>
196
+ <FieldControl
197
+ render={<TriggerMenuInput placeholder="Type @, / or #…" />}
198
+ />
199
+ </Field>
200
+ </TriggerMenu>
201
+ )
202
+ }
203
+
204
+ export { Example }
205
+ ```
206
+
207
+ Type @ for people, / at the start for commands, or # to load channels.
208
+
209
+ ## Commands with Arguments
210
+
211
+ A trigger with `boundary: "start"` matches only at the start of the text, so a leading space escapes it. An item with `arguments` asks for each one in order: `TriggerMenuHint` shows what's left at the caret, Tab accepts the highlighted suggestion and moves to the next argument, Enter finishes once only optional arguments remain, and Backspace past an argument's start goes back. An argument without `items` is free text. `after: "submit"` sends the form once the item is inserted.
212
+
213
+ ```tsx title="commands.tsx"
214
+ "use client"
215
+
216
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
217
+ import {
218
+ TriggerMenu,
219
+ TriggerMenuHint,
220
+ TriggerMenuInput,
221
+ } from "@ui-sdk/react/trigger-menu"
222
+ import { useLog } from "./log"
223
+
224
+ const commands = [
225
+ {
226
+ value: "status",
227
+ label: "status",
228
+ arguments: [
229
+ {
230
+ name: "status",
231
+ hint: "<status>",
232
+ required: true,
233
+ items: ["available", "busy", "away"],
234
+ },
235
+ {
236
+ name: "until",
237
+ hint: "[until]",
238
+ items: ["30m", "1h", "4h", "today"],
239
+ allowCustomValue: false,
240
+ },
241
+ ],
242
+ },
243
+ {
244
+ value: "search",
245
+ label: "search",
246
+ arguments: [{ name: "query", hint: "<query>", required: true }],
247
+ },
248
+ { value: "away", label: "away", after: "submit" as const },
249
+ { value: "clear", label: "clear" },
250
+ ]
251
+
252
+ function Example() {
253
+ const log = useLog()
254
+ return (
255
+ <form
256
+ data-slot="stack"
257
+ onSubmit={(event) => {
258
+ event.preventDefault()
259
+ log(`Sent ${new FormData(event.currentTarget).get("message")}`)
260
+ }}
261
+ >
262
+ <TriggerMenu
263
+ triggers={[
264
+ { char: "/", boundary: "start", type: "command", items: commands },
265
+ ]}
266
+ onSelect={(value, details) =>
267
+ log(
268
+ ["Chose", `/${value}`, ...Object.values(details.arguments)].join(
269
+ " "
270
+ )
271
+ )
272
+ }
273
+ >
274
+ <Field name="message">
275
+ <FieldLabel>Message</FieldLabel>
276
+ <FieldControl
277
+ render={<TriggerMenuInput placeholder="Type / for commands…" />}
278
+ />
279
+ </Field>
280
+ <TriggerMenuHint />
281
+ </TriggerMenu>
282
+ </form>
283
+ )
284
+ }
285
+
286
+ export { Example }
287
+ ```
288
+
289
+ Type /status, then press Tab to choose a status and an optional time, and Enter to finish. Choose /away to send it at once. The console logs each command.
290
+
291
+ ## Icons
292
+
293
+ Put an item's icon in a `TriggerMenuItemIcon`, before its text. Screen readers skip it, and filtering and the text a choice inserts ignore it, so an SVG's `<title>`, an icon font's glyph or an emoji never joins the item's name or the field. Items as data have no icon, so compose the items that show one.
294
+
295
+ - **Icons that point a way**, like arrows and reply, flip in right-to-left text with `mirror`.
296
+ - **Items with only an icon** need an `aria-label`, which filtering matches too. In development, one with no name warns.
297
+ - **Lining up items.** Items with an icon get `data-has-icon`, and the list gets `data-has-icons` while any item has one, so items without an icon can keep its space with padding.
298
+
299
+ ```tsx title="icons.tsx"
300
+ "use client"
301
+
302
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
303
+ import {
304
+ TriggerMenu,
305
+ TriggerMenuContent,
306
+ TriggerMenuInput,
307
+ TriggerMenuItem,
308
+ TriggerMenuItemIcon,
309
+ TriggerMenuList,
310
+ } from "@ui-sdk/react/trigger-menu"
311
+ import {
312
+ ClockIcon,
313
+ ReplyIcon,
314
+ SearchIcon,
315
+ ShareIcon,
316
+ TrashIcon,
317
+ } from "lucide-react"
318
+
319
+ const commands = [
320
+ { value: "reply", icon: ReplyIcon, mirror: true },
321
+ { value: "remind", icon: ClockIcon },
322
+ { value: "search", icon: SearchIcon },
323
+ { value: "share", icon: ShareIcon },
324
+ { value: "clear", icon: TrashIcon },
325
+ ]
326
+
327
+ function Example() {
328
+ return (
329
+ <TriggerMenu triggers={[{ char: "/", boundary: "start", type: "command" }]}>
330
+ <Field>
331
+ <FieldLabel>Message</FieldLabel>
332
+ <FieldControl
333
+ render={<TriggerMenuInput placeholder="Type / for commands…" />}
334
+ />
335
+ </Field>
336
+ <TriggerMenuContent>
337
+ <TriggerMenuList>
338
+ {commands.map((command) => (
339
+ <TriggerMenuItem key={command.value} value={command.value}>
340
+ <TriggerMenuItemIcon mirror={command.mirror}>
341
+ <command.icon />
342
+ </TriggerMenuItemIcon>
343
+ {command.value}
344
+ </TriggerMenuItem>
345
+ ))}
346
+ </TriggerMenuList>
347
+ </TriggerMenuContent>
348
+ </TriggerMenu>
349
+ )
350
+ }
351
+
352
+ export { Example }
353
+ ```
354
+
355
+ Type / to open the commands, then type re to narrow them. Each icon sits before its name, and filtering ignores it.
356
+
357
+ ## Loading Items
358
+
359
+ A trigger's `items` can be a loader, called with the query and an `AbortSignal` once typing pauses. A newer query aborts the older load, so only its results land, and the last results stay while the next ones load. `TriggerMenuStatus` and `TriggerMenuEmpty` get `loading`, and while a load runs, `aria-busy` marks the list, so you can fade the results that are about to change.
360
+
361
+ ```tsx title="async.tsx"
362
+ "use client"
363
+
364
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
365
+ import {
366
+ type TriggerMenuDataItem,
367
+ TriggerMenu,
368
+ TriggerMenuContent,
369
+ TriggerMenuEmpty,
370
+ TriggerMenuInput,
371
+ TriggerMenuItem,
372
+ TriggerMenuList,
373
+ TriggerMenuStatus,
374
+ } from "@ui-sdk/react/trigger-menu"
375
+
376
+ const channels = [
377
+ "announcements",
378
+ "design",
379
+ "design-reviews",
380
+ "engineering",
381
+ "general",
382
+ "launches",
383
+ "support",
384
+ ]
385
+
386
+ function searchChannels(query: string, { signal }: { signal: AbortSignal }) {
387
+ return new Promise<{ value: string; label: string }[]>((resolve, reject) => {
388
+ const timer = setTimeout(
389
+ () =>
390
+ resolve(
391
+ channels
392
+ .filter((channel) => channel.includes(query.toLowerCase()))
393
+ .map((channel) => ({ value: channel, label: channel }))
394
+ ),
395
+ 600
396
+ )
397
+ signal.addEventListener("abort", () => {
398
+ clearTimeout(timer)
399
+ reject(signal.reason)
400
+ })
401
+ })
402
+ }
403
+
404
+ function Example() {
405
+ return (
406
+ <TriggerMenu triggers={[{ char: "#", type: "channel", items: searchChannels }]}>
407
+ <Field>
408
+ <FieldLabel>Message</FieldLabel>
409
+ <FieldControl
410
+ render={<TriggerMenuInput placeholder="Type # to link a channel…" />}
411
+ />
412
+ </Field>
413
+ <TriggerMenuContent>
414
+ <TriggerMenuStatus>
415
+ {(state) =>
416
+ state.loading
417
+ ? "Loading channels…"
418
+ : state.count
419
+ ? `${state.count} ${state.count === 1 ? "channel" : "channels"}`
420
+ : ""
421
+ }
422
+ </TriggerMenuStatus>
423
+ <TriggerMenuEmpty>
424
+ {(state) =>
425
+ state.empty && !state.loading
426
+ ? `No channels match “${state.query}”`
427
+ : null
428
+ }
429
+ </TriggerMenuEmpty>
430
+ <TriggerMenuList>
431
+ {(state) =>
432
+ (state.items as TriggerMenuDataItem[]).map((item) => (
433
+ <TriggerMenuItem key={item.value} value={item.value} label={item.label}>
434
+ {item.label}
435
+ </TriggerMenuItem>
436
+ ))
437
+ }
438
+ </TriggerMenuList>
439
+ </TriggerMenuContent>
440
+ </TriggerMenu>
441
+ )
442
+ }
443
+
444
+ export { Example }
445
+ ```
446
+
447
+ Type # and a channel name. The list says it's loading, and older results fade while newer ones load.
448
+
449
+ ## Custom Items
450
+
451
+ Compose `TriggerMenuItem`s with any content, in groups. `label` is what's matched and inserted, `keywords` adds words to match, and `disabled` keeps an item in the list without letting it be chosen. A trigger's `spaces`, `pattern` and `maxLength` decide what its query may hold, `filter` how it narrows the items, and `insert` what replaces the trigger and query. An item's own `onSelect` runs first and can insert something else with `details.insert()`.
452
+
453
+ ```tsx title="custom-items.tsx"
454
+ "use client"
455
+
456
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
457
+ import {
458
+ TriggerMenu,
459
+ TriggerMenuContent,
460
+ TriggerMenuGroup,
461
+ TriggerMenuGroupLabel,
462
+ TriggerMenuInput,
463
+ TriggerMenuItem,
464
+ TriggerMenuItemIcon,
465
+ TriggerMenuList,
466
+ TriggerMenuSeparator,
467
+ } from "@ui-sdk/react/trigger-menu"
468
+ import { UsersIcon } from "lucide-react"
469
+
470
+ const people = [
471
+ { value: "hollis", label: "Hollis Grant" },
472
+ { value: "ines", label: "Inès Moreau" },
473
+ { value: "kofi", label: "Kofi Mensah" },
474
+ { value: "wren", label: "Wren Takahashi" },
475
+ { value: "tomasz", label: "Tomasz Wójcik", disabled: true },
476
+ ]
477
+
478
+ function Example() {
479
+ return (
480
+ <TriggerMenu
481
+ triggers={[
482
+ {
483
+ char: "@",
484
+ spaces: 2,
485
+ maxLength: 30,
486
+ pattern: /[\p{L}.'-]/u,
487
+ filter: "startsWith",
488
+ insert: (item) => `@${item.label.split(" ")[0]} `,
489
+ },
490
+ ]}
491
+ >
492
+ <Field>
493
+ <FieldLabel>Message</FieldLabel>
494
+ <FieldControl
495
+ render={<TriggerMenuInput placeholder="Type @ and a first name…" />}
496
+ />
497
+ </Field>
498
+ <TriggerMenuContent>
499
+ <TriggerMenuList>
500
+ <TriggerMenuGroup>
501
+ <TriggerMenuGroupLabel>People</TriggerMenuGroupLabel>
502
+ {people.map((person) => (
503
+ <TriggerMenuItem
504
+ key={person.value}
505
+ value={person.value}
506
+ label={person.label}
507
+ disabled={person.disabled}
508
+ >
509
+ {person.label}
510
+ <span data-slot="hint">
511
+ {person.disabled ? "Deactivated" : `@${person.value}`}
512
+ </span>
513
+ </TriggerMenuItem>
514
+ ))}
515
+ </TriggerMenuGroup>
516
+ <TriggerMenuSeparator />
517
+ <TriggerMenuItem
518
+ value="here"
519
+ label="here"
520
+ keywords={["everyone", "channel"]}
521
+ onSelect={(value, details) => details.insert("@here ")}
522
+ >
523
+ <TriggerMenuItemIcon>
524
+ <UsersIcon />
525
+ </TriggerMenuItemIcon>
526
+ Notify everyone here
527
+ </TriggerMenuItem>
528
+ </TriggerMenuList>
529
+ </TriggerMenuContent>
530
+ </TriggerMenu>
531
+ )
532
+ }
533
+
534
+ export { Example }
535
+ ```
536
+
537
+ Type @ and the start of a first name to insert it, or choose Notify everyone here to insert @here. Deactivated people can't be chosen.
538
+
539
+ ## Input
540
+
541
+ Render the input part as an `<input>` for a single line. It's a combobox with `aria-expanded`. In a single line, Enter often submits a form, so this example sets `autoHighlight={false}`: Enter chooses only once an arrow key has highlighted a person, and `loop={false}` stops the arrows at either end of the list.
542
+
543
+ ```tsx title="input.tsx"
544
+ "use client"
545
+
546
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
547
+ import {
548
+ type TriggerMenuDataItem,
549
+ TriggerMenu,
550
+ TriggerMenuContent,
551
+ TriggerMenuInput,
552
+ TriggerMenuItem,
553
+ TriggerMenuList,
554
+ } from "@ui-sdk/react/trigger-menu"
555
+
556
+ const people = [
557
+ { value: "hollis", label: "Hollis Grant" },
558
+ { value: "ines", label: "Inès Moreau" },
559
+ { value: "kofi", label: "Kofi Mensah" },
560
+ { value: "wren", label: "Wren Takahashi" },
561
+ { value: "tomasz", label: "Tomasz Wójcik" },
562
+ ]
563
+
564
+ function Example() {
565
+ return (
566
+ <TriggerMenu triggers={[{ char: "@", items: people }]} autoHighlight={false} loop={false}>
567
+ <Field>
568
+ <FieldLabel>Task</FieldLabel>
569
+ <FieldControl
570
+ render={
571
+ <TriggerMenuInput
572
+ render={<input />}
573
+ placeholder="Type @ to assign someone…"
574
+ />
575
+ }
576
+ />
577
+ </Field>
578
+ <TriggerMenuContent>
579
+ <TriggerMenuList>
580
+ {(state) =>
581
+ (state.items as TriggerMenuDataItem[]).map((item) => (
582
+ <TriggerMenuItem key={item.value} value={item.value} label={item.label}>
583
+ {item.label}
584
+ </TriggerMenuItem>
585
+ ))
586
+ }
587
+ </TriggerMenuList>
588
+ </TriggerMenuContent>
589
+ </TriggerMenu>
590
+ )
591
+ }
592
+
593
+ export { Example }
594
+ ```
595
+
596
+ Type @ in the empty task. Nothing is highlighted until you press ArrowDown, then Enter assigns the person.
597
+
598
+ ## Positioning
599
+
600
+ By default, the list opens below the trigger at the caret and flips to stay on screen. `side`, `align` and the offsets place it, and `anchor` positions it against another element. This example opens the list above the whole field, lines its items up with the label, and renders it inside the field with `container`.
601
+
602
+ ```tsx title="positioning.tsx"
603
+ "use client"
604
+
605
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
606
+ import {
607
+ type TriggerMenuDataItem,
608
+ TriggerMenu,
609
+ TriggerMenuContent,
610
+ TriggerMenuInput,
611
+ TriggerMenuItem,
612
+ TriggerMenuList,
613
+ } from "@ui-sdk/react/trigger-menu"
614
+ import * as React from "react"
615
+
616
+ const people = [
617
+ { value: "hollis", label: "Hollis Grant" },
618
+ { value: "ines", label: "Inès Moreau" },
619
+ { value: "kofi", label: "Kofi Mensah" },
620
+ { value: "wren", label: "Wren Takahashi" },
621
+ { value: "tomasz", label: "Tomasz Wójcik" },
622
+ ]
623
+
624
+ function Example() {
625
+ const [field, setField] = React.useState<HTMLDivElement | null>(null)
626
+ return (
627
+ <TriggerMenu triggers={[{ char: "@", items: people }]}>
628
+ <Field ref={setField}>
629
+ <FieldLabel>Comment</FieldLabel>
630
+ <FieldControl
631
+ render={<TriggerMenuInput placeholder="Type @ to mention someone…" />}
632
+ />
633
+ </Field>
634
+ <TriggerMenuContent
635
+ side="top"
636
+ align="start"
637
+ sideOffset={8}
638
+ alignOffset={-5}
639
+ collisionPadding={16}
640
+ anchor={field}
641
+ container={field}
642
+ keepMounted
643
+ >
644
+ <TriggerMenuList>
645
+ {(state) =>
646
+ (state.items as TriggerMenuDataItem[]).map((item) => (
647
+ <TriggerMenuItem key={item.value} value={item.value} label={item.label}>
648
+ {item.label}
649
+ </TriggerMenuItem>
650
+ ))
651
+ }
652
+ </TriggerMenuList>
653
+ </TriggerMenuContent>
654
+ </TriggerMenu>
655
+ )
656
+ }
657
+
658
+ export { Example }
659
+ ```
660
+
661
+ Type @ in the comment. The list opens above the field, lined up with its label.
662
+
663
+ ## Controlled
664
+
665
+ Pass `open` and update it in `onOpenChange` to own the popup state. The details say why it changed. `onQueryChange` reports the text after the trigger and the argument being typed, and `onOpenChangeComplete` runs once the menu has finished opening or closing. Escape dismisses a match, and `shortcut` reopens it, Control+Space by default.
666
+
667
+ ```tsx title="controlled.tsx"
668
+ "use client"
669
+
670
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
671
+ import { TriggerMenu, TriggerMenuInput } from "@ui-sdk/react/trigger-menu"
672
+ import * as React from "react"
673
+ import { useLog } from "./log"
674
+
675
+ const people = [
676
+ { value: "hollis", label: "Hollis Grant" },
677
+ { value: "ines", label: "Inès Moreau" },
678
+ { value: "kofi", label: "Kofi Mensah" },
679
+ { value: "wren", label: "Wren Takahashi" },
680
+ { value: "tomasz", label: "Tomasz Wójcik" },
681
+ ]
682
+
683
+ const commands = [
684
+ {
685
+ value: "status",
686
+ label: "status",
687
+ arguments: [
688
+ {
689
+ name: "status",
690
+ hint: "<status>",
691
+ required: true,
692
+ items: ["available", "busy", "away"],
693
+ },
694
+ ],
695
+ },
696
+ ]
697
+
698
+ function Example() {
699
+ const log = useLog()
700
+ const [open, setOpen] = React.useState(false)
701
+ return (
702
+ <TriggerMenu
703
+ triggers={[
704
+ { char: "@", items: people },
705
+ { char: "/", boundary: "start", items: commands },
706
+ ]}
707
+ open={open}
708
+ onOpenChange={(next, details) => {
709
+ setOpen(next)
710
+ log(`${next ? "Opening" : "Closing"} (${details.reason})`)
711
+ }}
712
+ onOpenChangeComplete={(next) => log(next ? "Open" : "Closed")}
713
+ onQueryChange={(query, details) =>
714
+ log(
715
+ `Query “${query}”${details.argument ? ` for ${details.argument}` : ""}`
716
+ )
717
+ }
718
+ shortcut="ctrl+j"
719
+ >
720
+ <Field>
721
+ <FieldLabel>Message</FieldLabel>
722
+ <FieldControl
723
+ render={<TriggerMenuInput placeholder="Type @ or /…" />}
724
+ />
725
+ </Field>
726
+ </TriggerMenu>
727
+ )
728
+ }
729
+
730
+ export { Example }
731
+ ```
732
+
733
+ Type @ and a name, press Escape, then Control+J to reopen the list. The console logs each callback with its reason.
734
+
735
+ ## Controller
736
+
737
+ Create a controller with `createTriggerMenuController()` and pass it as `controller` to open suggestions, insert text at the caret, or read the current match. `open()` types the trigger at the caret and returns a Promise that resolves when the menu closes.
738
+
739
+ ```tsx title="controller.tsx"
740
+ "use client"
741
+
742
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
743
+ import {
744
+ type TriggerMenuDataItem,
745
+ TriggerMenu,
746
+ TriggerMenuContent,
747
+ TriggerMenuInput,
748
+ TriggerMenuItem,
749
+ TriggerMenuList,
750
+ createTriggerMenuController,
751
+ } from "@ui-sdk/react/trigger-menu"
752
+ import * as React from "react"
753
+ import { useLog } from "./log"
754
+
755
+ const people = [
756
+ { value: "hollis", label: "Hollis Grant" },
757
+ { value: "ines", label: "Inès Moreau" },
758
+ { value: "kofi", label: "Kofi Mensah" },
759
+ { value: "wren", label: "Wren Takahashi" },
760
+ { value: "tomasz", label: "Tomasz Wójcik" },
761
+ ]
762
+
763
+ function Example() {
764
+ const log = useLog()
765
+ const [menu] = React.useState(createTriggerMenuController)
766
+ return (
767
+ <div data-slot="stack">
768
+ <div data-slot="controls">
769
+ <button
770
+ type="button"
771
+ onMouseDown={(event) => event.preventDefault()}
772
+ onClick={async () => {
773
+ const closed = await menu.open({ trigger: "@" })
774
+ log(`Closed (${closed.reason})`)
775
+ }}
776
+ >
777
+ Mention
778
+ </button>
779
+ <button
780
+ type="button"
781
+ onMouseDown={(event) => event.preventDefault()}
782
+ onClick={() =>
783
+ menu.insert(
784
+ new Date().toLocaleDateString("en-US", {
785
+ month: "long",
786
+ day: "numeric",
787
+ })
788
+ )
789
+ }
790
+ >
791
+ Insert Date
792
+ </button>
793
+ </div>
794
+ <TriggerMenu controller={menu} triggers={[{ char: "@", items: people }]}>
795
+ <Field>
796
+ <FieldLabel>Message</FieldLabel>
797
+ <FieldControl
798
+ render={<TriggerMenuInput placeholder="Write a message…" />}
799
+ />
800
+ </Field>
801
+ <TriggerMenuContent>
802
+ <TriggerMenuList>
803
+ {(state) =>
804
+ (state.items as TriggerMenuDataItem[]).map((item) => (
805
+ <TriggerMenuItem key={item.value} value={item.value} label={item.label}>
806
+ {item.label}
807
+ </TriggerMenuItem>
808
+ ))
809
+ }
810
+ </TriggerMenuList>
811
+ </TriggerMenuContent>
812
+ </TriggerMenu>
813
+ </div>
814
+ )
815
+ }
816
+
817
+ export { Example }
818
+ ```
819
+
820
+ Press Mention and choose a person. The console logs why the list closed. Insert Date adds today's date at the caret.
821
+
822
+ ## Any Editor
823
+
824
+ Pass `editor` to connect the menu to a field you render yourself, or to another editor. `textFieldAdapter()` makes one for a textarea or an input.
825
+
826
+ ```tsx title="editor.tsx"
827
+ "use client"
828
+
829
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/react/field"
830
+ import {
831
+ type TextEditorAdapter,
832
+ type TriggerMenuDataItem,
833
+ TriggerMenu,
834
+ TriggerMenuContent,
835
+ TriggerMenuItem,
836
+ TriggerMenuList,
837
+ textFieldAdapter,
838
+ } from "@ui-sdk/react/trigger-menu"
839
+ import * as React from "react"
840
+
841
+ const people = [
842
+ { value: "hollis", label: "Hollis Grant" },
843
+ { value: "ines", label: "Inès Moreau" },
844
+ { value: "kofi", label: "Kofi Mensah" },
845
+ { value: "wren", label: "Wren Takahashi" },
846
+ { value: "tomasz", label: "Tomasz Wójcik" },
847
+ ]
848
+
849
+ function Example() {
850
+ const [editor, setEditor] = React.useState<TextEditorAdapter>()
851
+ return (
852
+ <TriggerMenu editor={editor} triggers={[{ char: "@", items: people }]}>
853
+ <Field>
854
+ <FieldLabel>Notes</FieldLabel>
855
+ <FieldControl
856
+ render={
857
+ <textarea
858
+ ref={(field) => {
859
+ if (field && editor?.element !== field) {
860
+ setEditor(textFieldAdapter(field))
861
+ }
862
+ }}
863
+ placeholder="Type @ to mention someone…"
864
+ />
865
+ }
866
+ />
867
+ </Field>
868
+ <TriggerMenuContent>
869
+ <TriggerMenuList>
870
+ {(state) =>
871
+ (state.items as TriggerMenuDataItem[]).map((item) => (
872
+ <TriggerMenuItem key={item.value} value={item.value} label={item.label}>
873
+ {item.label}
874
+ </TriggerMenuItem>
875
+ ))
876
+ }
877
+ </TriggerMenuList>
878
+ </TriggerMenuContent>
879
+ </TriggerMenu>
880
+ )
881
+ }
882
+
883
+ export { Example }
884
+ ```
885
+
886
+ Type @ in the notes, a textarea connected through an editor adapter, and choose a person.
887
+
888
+ An adapter for another editor reads its text and selection, measures a rectangle at an offset, replaces a range as one undo step, and reports changes. Report changes that weren't the user's with `user: false`, like a collaborator's, and the menu never opens for them.
889
+
890
+ - `element` (`HTMLElement`): The focused element that gets keys and the menu's ARIA. Undefined until it's in the page.
891
+ - `textBefore` (`(length: number) => { text: string; offset: number; } | null`): Up to `length` characters before the caret, and the offset they start at. A token reads as U+FFFC.
892
+ - `selection` (`() => TextRange | null`): The selection as text offsets, or null while unfocused.
893
+ - `rectAt` (`(offset: number) => TextRect | null`): The rectangle at a text offset, for anchoring.
894
+ - `replace` (`(range: TextRange, content: string | TokenInsert, options?: EditorReplaceOptions) => void`): Replaces a range with text or a token as one undoable step, and puts the caret after it.
895
+ - `subscribe` (`(listener: (change: EditorChange) => void) => () => void`): Calls `listener` for text edits, caret moves and composition. Returns a cleanup.
896
+ - `composing` (`() => boolean`): Whether an input method is composing.
897
+ - `tokens?` (`boolean`): Whether `replace` accepts tokens.
898
+ - `submit?` (`((event?: Event) => void)`): Sends what's typed, like a form submit, for items that end with `after: "submit"`.
899
+
900
+ ## Undo
901
+
902
+ Choosing an item keeps the native text field's undo history. Press Command+Z or Control+Z to bring back the query. Safari may combine the insertion with the typing before it in one undo step.
903
+
904
+ A custom editor adapter must make each replacement one undo step. [Token Input](./token-input.md) handles its own token history.
905
+
906
+ ## Custom Rendering
907
+
908
+ Every part takes `render`. Render `TriggerMenuInput` as an `<input>`, as the input example does, or through another component, like a `FieldControl`, as every example here does to join a [Field](./field.md). [Parts and Composition](../parts-and-composition.md#choose-the-element) explains how props merge, and each part's state is listed under [State](#state).
909
+
910
+ ## Styling
911
+
912
+ Style the field, the popup and its items. The content shows `data-trigger`, so each trigger's list can look different, like commands in a monospace font. Items show `data-highlighted` and `data-disabled`, and `data-has-icon` when they hold a `TriggerMenuItemIcon`; the list shows `data-has-icons` while any item has one. Fade a loading list with `aria-busy`, and style the hint as ghost text: it's positioned at the caret for you.
913
+
914
+ Each part has a `data-slot`, such as `trigger-menu-item`, and shows its state through the attributes listed under [Data Attributes](#data-attributes).
915
+
916
+ ### TriggerMenuContent
917
+ - `--anchor-width`: Width of the anchor, usually the trigger. Use it to match widths.
918
+ - `--anchor-height`: Height of the anchor.
919
+ - `--available-width`: Space beside the popup, up to the viewport edge. Not inherited either.
920
+ - `--available-height`: Space between the anchor and the viewport edge. Use it for max-height. Not inherited, so a child that needs it copies it on the popup, like `[--list-height:var(--available-height)]`, and reads `max-h-(--list-height)`. The first popup registers both names for the whole page with `CSS.registerProperty`, so they don't inherit anywhere. A page that needs them to inherit calls it first, with `inherits: true`. An `@property` rule can't, since a registration in script wins.
921
+ - `--transform-origin`: The point on the anchor the popup opens from. Use it to scale from the right place.
922
+ - `--arrow-offset`: Where the anchor's center falls along the popup's edge, kept `arrowPadding` from the corners. Position a ::before arrow with it.
923
+
924
+ ## Data Attributes
925
+
926
+ ### TriggerMenuInput
927
+ `[data-slot="trigger-menu-input"]`
928
+
929
+ - `data-closed`: Present while the list is closed, including while it animates out.
930
+ - `data-open`: Present while the list is open.
931
+ - `data-trigger`: The trigger of the current match, like `@`, while there is one.
932
+
933
+ ### TriggerMenuContent
934
+ `[data-slot="trigger-menu-content"]`
935
+
936
+ - `data-closed`: Present while the list is closed, including while it animates out.
937
+ - `data-starting-style`: Present for the first frame. Style the state you animate from.
938
+ - `data-ending-style`: Present while the list animates out. Style the state you animate to.
939
+ - `data-open`: Present while the list is open.
940
+ - `data-trigger`: The trigger of the current match, like `@`, while there is one.
941
+ - `data-has-icons`: Present while any item inside holds an icon part, so items without one can line up.
942
+
943
+ ### TriggerMenuItem
944
+ `[data-slot="trigger-menu-item"]`
945
+
946
+ - `data-disabled`: Present when the item can't be chosen.
947
+ - `data-highlighted`: Present on the highlighted item. Focus stays in the field.
948
+ - `data-has-icon`: Present on an item while it holds an icon part.
949
+
950
+ ### TriggerMenuEmpty
951
+ `[data-slot="trigger-menu-empty"]`
952
+
953
+ - `data-empty`: Present while no item matches.
954
+ - `data-loading`: Present while items load.
955
+
956
+ ### TriggerMenuItemIcon
957
+ `[data-slot="trigger-menu-item-icon"]`
958
+
959
+ - `data-mirror`: Present on an icon that flips in right-to-left text, with `mirror`.
960
+
961
+ ## API Reference
962
+
963
+ Props and callbacks are the same as below, as React props.
964
+
965
+ ### TriggerMenu
966
+
967
+ - `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
968
+ - `triggers` (`TriggerDefinition<T>[]`): The texts that open the menu, and what each one lists.
969
+ - `editor?` (`TextEditorAdapter`): The editor the menu reads and writes, for editors without a part of their own, like Tiptap. TriggerMenuInput and a surrounding TokenInput set it for you.
970
+ - `open?` (`boolean`): Whether the menu is open. Pass it to control it; it shows only while there's a match.
971
+ - `onOpenChange?` (`((open: boolean, details: ChangeDetails<TriggerMenuOpenChangeReason> & { trigger: string | null; }) => void)`): Runs when the menu wants to open or close. `details.reason` says why, `details.trigger` which trigger, and `details.cancel()` stops it.
972
+ - `onOpenChangeComplete?` (`((open: boolean) => void)`): Runs once the menu has finished opening or closing, after any animation.
973
+ - `onQueryChange?` (`((query: string, details: ChangeDetails<TriggerMenuQueryChangeReason> & { trigger: string; argument?: string; }) => void)`): Runs when the text after the trigger changes. Read-only: it's the editor's text. `details.argument` names the argument being typed.
974
+ - `onSelect?` (`((value: T, details: TriggerMenuSelectDetails<T>) => void | Promise<void | Partial<TokenInsert>>)`): Runs when an item, with every required argument, is chosen. The default insertion follows unless `details.preventDefault()`. Return a Promise and a token is inserted at once as pending, updated with what it resolves to and removed if it rejects; the menu closes either way.
975
+ - `pendingAnnouncement?` (`string`): What screen readers hear once an `onSelect` Promise has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
976
+ - `status?` (`AsyncOptionsStatus`): Whether items that come from outside are loading or failed, for the status and empty parts. Loaders report their own.
977
+ - `autoHighlight?` (`boolean`): Whether the first match is highlighted so Enter and Tab choose it. Visual only: screen readers hear about it once an arrow key moves the highlight, so typed characters keep being echoed.
978
+ - `loop?` (`boolean`): Whether arrow keys wrap from the last item to the first.
979
+ - `shortcut?` (`string`): Reopens a match dismissed with Escape, with the caret in it.
980
+ - `controller?` (`object`): Opens, closes and inserts from anywhere. Create one with `createTriggerMenuController()`.
981
+ - `dismissible?` (`boolean`): Whether Escape, an outside press, focus or the window leaving, Android's back gesture and a sheet's swipe can close it. Triggers, Close parts and the controller always can. Pass `false` while it holds unsaved work, and ask in `onDismissAttempt`.
982
+ - `onDismissAttempt?` (`((details: { reason: "escape-key"; event?: KeyboardEvent | undefined; } | { reason: "outside-press"; event?: MouseEvent | undefined; }) => void)`): Runs when `dismissible` refused a dismissal made on purpose: Escape, an outside press, back or a swipe. Focus or the window leaving is refused without it. Ask "Discard changes?" here.
983
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
984
+
985
+ ### Triggers
986
+
987
+ - `char` (`string`): The text that starts a match: `"@"`, `"/"`, `"#"`, or several characters like `"[["`.
988
+ - `boundary?` (`TriggerBoundary`): Where a match may start.
989
+ - `spaces?` (`number | boolean`): Whether the query may contain spaces, or up to how many words, like `@next Wednesday`.
990
+ - `pattern?` (`RegExp`): Characters a query may contain, tested one at a time.
991
+ - `maxLength?` (`number`): The longest query before the match ends.
992
+ - `type?` (`string`): The kind of token choosing an item inserts, where the editor takes tokens.
993
+ - `items?` (`TriggerItemsLoader<T> | TriggerMenuData<T>`): Suggestions as data, or a loader given the query and an AbortSignal. Without them, the composed items show.
994
+ - `filter?` (`ListFilter<T>`): How typing narrows data items. Loaded items aren't filtered.
995
+ - `insert?` (`((item: TriggerMenuDataItem<T>, details: { match: TriggerMatch; arguments: Record<string, string>; }) => string | TokenInsert)`): What replaces the trigger and query. Defaults to a token where the editor takes tokens, else the trigger, the label and a space.
996
+
997
+ ### Items as Data
998
+
999
+ - `value` (`T`): Identifies the item, and is what choosing it reports. A string, or any value with `itemToValue`.
1000
+ - `label` (`string`): The text shown and matched by typeahead and filtering. A select's trigger shows it once chosen.
1001
+ - `keywords?` (`string[]`): More words filtering matches, like `["team"]` for a plan called Pro.
1002
+ - `disabled?` (`boolean`): Keeps the item from being chosen. The keyboard skips it, like in a native select.
1003
+ - `after?` (`TriggerAfter`): After choosing it: `"compose"` closes the menu, `"continue"` keeps it open and lists the same trigger's items for the text typed after it, and `"submit"` asks the editor to send. Tab never submits.
1004
+ - `arguments?` (`TriggerArgument[]`): Typed arguments asked for in order once it's chosen, like `/model <name>`.
1005
+
1006
+ ### Arguments
1007
+
1008
+ - `name` (`string`): The name it's reported under in `details.arguments`.
1009
+ - `hint?` (`string`): Ghost text shown at the caret until it's typed. Defaults to `<name>`.
1010
+ - `required?` (`boolean`): Whether Enter can finish the command without it.
1011
+ - `items?` (`TriggerItemsLoader<string> | TriggerMenuData<string>`): Suggestions for it, as data or a loader. Without them, it's free text and no list shows.
1012
+ - `allowCustomValue?` (`boolean`): Whether text that matches no suggestion is accepted, like a combobox's custom value.
1013
+
1014
+ ### TriggerMenuInput
1015
+
1016
+ The field the menu suggests into. Renders a `textarea`, or an `input` through `render`.
1017
+
1018
+ ### TriggerMenuStatus
1019
+
1020
+ A polite status that says the count and the keys once typing pauses. Pass a function as children to say something else. Renders a `div`.
1021
+
1022
+ ### TriggerMenuContent
1023
+
1024
+ - `side?` (`Side`): Which side of the anchor the popup opens on. Flips to the other side when there isn't room. Which side of the trigger the list opens on.
1025
+ - `align?` (`Align`): How the popup lines up with the anchor along that side. `start` and `end` follow the reading direction. How the list lines up with the trigger along that side.
1026
+ - `sideOffset?` (`number`): Gap between the anchor and the popup, in pixels. Gap between the line of text and the list, in pixels.
1027
+ - `alignOffset?` (`number`): Shift along the anchor, in pixels. Shift along the text, in pixels.
1028
+ - `collisionPadding?` (`number`): Space to keep from the viewport edges, in pixels.
1029
+ - `arrowPadding?` (`number`): Space to keep between the arrow and the popup's corners, in pixels.
1030
+ - `anchor?` (`string | Anchor | null`): Positions the popup against this instead of its trigger: an element, a CSS selector, a point like `{ x, y }`, or anything with `getBoundingClientRect()`. It's read on every check, so it can move. A controller's `open({ anchor })` wins for that open.
1031
+ - `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
1032
+ - `keepMounted?` (`boolean`): Whether the list stays in the page, hidden, while closed.
1033
+
1034
+ ### TriggerMenuEmpty
1035
+
1036
+ Shown while no item matches. Rendering it keeps the menu open with no matches; without it, the menu closes until something matches. Renders a `div`.
1037
+
1038
+ ### TriggerMenuList
1039
+
1040
+ The listbox. Without children, it renders the current trigger's items. Renders a `div`.
1041
+
1042
+ ### TriggerMenuItem
1043
+
1044
+ - `value` (`T`): What `onSelect` receives when the item is chosen.
1045
+ - `label?` (`string`): The text inserted and matched, when the content isn't plain text.
1046
+ - `keywords?` (`string[]`): More words filtering matches.
1047
+ - `disabled?` (`boolean`): Keeps the item from being chosen. It can still be highlighted, as the listbox pattern asks.
1048
+ - `onSelect?` (`((value: T, details: TriggerMenuSelectDetails<T>) => void | Promise<void | Partial<TokenInsert>>)`): Runs before the menu's `onSelect`. `details.preventDefault()` skips the default insertion.
1049
+
1050
+ ### TriggerMenuItemIcon
1051
+
1052
+ - `mirror?` (`boolean`): Flips the icon horizontally in right-to-left text, for icons that point a way, like arrows and chevrons. Icons that picture a thing, like a clock or a check, stay as they are.
1053
+
1054
+ ### TriggerMenuGroup and TriggerMenuGroupLabel
1055
+
1056
+ A group of items, named by its label. Each renders a `div`.
1057
+
1058
+ ### TriggerMenuSeparator
1059
+
1060
+ A line between items or groups, hidden from screen readers. Renders a `div`.
1061
+
1062
+ ### TriggerMenuHint
1063
+
1064
+ Ghost text at the caret for the arguments still to type, like `<status>`. Renders a `span`, hidden from screen readers.
1065
+
1066
+ ### State
1067
+
1068
+ What render functions and state-based class names receive, per part.
1069
+
1070
+ `TriggerMenuInput`:
1071
+
1072
+ - `open` (`boolean`): Whether the menu is open. It opens on a typed trigger, and closes when nothing matches unless it's loading or renders an empty part.
1073
+ - `trigger` (`string | null`): The trigger of the current match, like `"@"`, or null.
1074
+
1075
+ `TriggerMenuContent` and `TriggerMenuList`:
1076
+
1077
+ - `open` (`boolean`): Whether the menu is open. False while it animates out.
1078
+ - `trigger` (`string | null`): The trigger of the current match, like `"@"`, or null.
1079
+ - `query` (`string`): The text after the trigger, or after the current argument's start.
1080
+ - `argument` (`string | null`): The argument being typed, by name, or null at the first level.
1081
+ - `status` (`AsyncOptionsStatus`): Whether items are loading or failed.
1082
+ - `items` (`TriggerMenuData<unknown>`): The visible data items, for rendering them yourself.
1083
+
1084
+ `TriggerMenuItem`:
1085
+
1086
+ - `highlighted` (`boolean`): Whether the item is highlighted, by the keyboard or the pointer.
1087
+ - `disabled` (`boolean`): Whether the item can't be chosen.
1088
+
1089
+ `TriggerMenuItemIcon`:
1090
+
1091
+ - `mirror` (`boolean`): Whether it flips in right-to-left text.
1092
+
1093
+ `TriggerMenuEmpty`:
1094
+
1095
+ - `loading` (`boolean`): Whether items are loading.
1096
+ - `empty` (`boolean`): Whether no item matches.
1097
+ - `query` (`string`): The text after the trigger.
1098
+
1099
+ `TriggerMenuStatus`:
1100
+
1101
+ - `open` (`boolean`): Whether the menu is open. It opens on a typed trigger, and closes when nothing matches unless it's loading or renders an empty part.
1102
+ - `status` (`AsyncOptionsStatus`): Whether items are loading or failed.
1103
+ - `loading` (`boolean`): Whether items are loading.
1104
+ - `count` (`number`): How many items match.
1105
+ - `argument` (`string | null`): The argument being typed, by name, or null.
1106
+
1107
+ `TriggerMenuHint`:
1108
+
1109
+ - `argument` (`string | null`): The argument being typed, by name, or null.
1110
+ - `hint` (`string`): The ghost text: the hints of the arguments still to type.
1111
+
1112
+ ### Reasons
1113
+
1114
+ `details.reason` says why the menu opened or closed.
1115
+
1116
+ - `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
1117
+ - `caret-move` (`Event | KeyboardEvent | PointerEvent`): The caret left a trigger menu's match: an arrow key, Home or End, a click, or a selection change.
1118
+ - `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
1119
+ - `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
1120
+ - `close-press` (`MouseEvent | KeyboardEvent | SubmitEvent`): A Close part was pressed, or a form with `method="dialog"` was submitted.
1121
+ - `outside-press` (`MouseEvent`): A press outside the layer.
1122
+ - `focus-out` (`KeyboardEvent | FocusEvent`): Focus left the layer, or Tab moved it out.
1123
+ - `window-blur` (`FocusEvent`): The window lost focus.
1124
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
1125
+ - `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
1126
+
1127
+ With `dismissible` false, the refused dismissals `onDismissAttempt` reports:
1128
+
1129
+ - `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
1130
+ - `outside-press` (`MouseEvent`): A press outside the layer.
1131
+
1132
+ Why the query changed:
1133
+
1134
+ - `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
1135
+ - `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
1136
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
1137
+
1138
+ ### Controller
1139
+
1140
+ Create one with `createTriggerMenuController()` and pass it as `controller`.
1141
+
1142
+ - `isOpen()` (`() => boolean`): Whether the menu is open.
1143
+ - `setOpen(open, options?)` (`(open: boolean, options?: TriggerMenuChangeOptions<TriggerMenuOpenChangeReason> | undefined) => boolean`): Opens or closes the menu. Opening needs a match; see `open()` for one that types the trigger. Returns false when the change was canceled or it was already open or closed.
1144
+ - `open(options?)` (`(options?: (TriggerMenuChangeOptions<TriggerMenuOpenChangeReason> & { trigger?: string | undefined; signal?: AbortSignal | undefined; }) | undefined) => Promise<...>`): Opens the menu for `trigger`, typing it at the caret first unless a match for it is already there, and resolves once it closes. Resolves exactly once and never rejects.
1145
+ - `close(options?)` (`(options?: TriggerMenuChangeOptions<TriggerMenuOpenChangeReason> | undefined) => boolean`): Closes the menu. The match isn't dismissed, so typing in it opens it again. Returns false when the close was canceled or it was already closed.
1146
+ - `getQuery()` (`() => string`): The text after the trigger, or after the current argument's start. Empty while closed.
1147
+ - `getMatch()` (`() => TriggerMatch | null`): The current match, or null.
1148
+ - `insert(content)` (`(content: string | Token) => void`): Replaces the current match with text or a token, or inserts at the caret without one.
1149
+ - `focus()` (`() => void`): Moves focus to the editor.
1150
+
1151
+ ### Events
1152
+
1153
+ Callbacks get the new state first, then the details.
1154
+
1155
+ | Prop | On | Receives |
1156
+ | ---------------------- | -------------------------------- | -------------------------------------------------------------------------------- |
1157
+ | `onOpenChange` | `TriggerMenu` | `(open, details)`: the new state, the [reason](#reasons) and the `trigger` |
1158
+ | `onOpenChangeComplete` | `TriggerMenu` | `(open)`, after the animation |
1159
+ | `onQueryChange` | `TriggerMenu` | `(query, details)`: the text after the trigger, and the `argument` being typed |
1160
+ | `onDismissAttempt` | `TriggerMenu` | `({ reason, event })`, when a dismissal was refused |
1161
+ | `onSelect` | `TriggerMenu`, `TriggerMenuItem` | `(value, details)`: the item, its `arguments`, `insert()` and `preventDefault()` |
1162
+
1163
+ When `onSelect` returns a Promise and the editor takes tokens, the token is inserted at once with `data-pending`, updated with what the Promise resolves to, and removed if it rejects. The menu closes either way, so people keep typing.
1164
+
1165
+ ## Accessibility
1166
+
1167
+ Focus stays in the field while the arrow keys move the highlight. A `<textarea>` stays a textbox, the only role ARIA allows it, and points at the list with `aria-controls` and, after an arrow key, `aria-activedescendant`. An `<input>` is a combobox with `aria-expanded`. Counts and loading are said through `TriggerMenuStatus`, a polite status, never through the field, so render one: visually hidden, as in the demo, or at the top of the list, as in [Loading Items](#loading-items). Give the field a visible label through a [Field](./field.md), or an `aria-label`.
1168
+
1169
+ ### Keyboard
1170
+
1171
+ | Key | Does |
1172
+ | -------------------- | ------------------------------------------------------------------------------------------------- |
1173
+ | ArrowDown, ArrowUp | Moves the highlight while the list is open; the caret stays in the field |
1174
+ | Enter | Chooses the highlighted item. In a command's arguments, finishes once only optional ones are left |
1175
+ | Tab | Chooses the highlighted item, or accepts an argument and moves to the next |
1176
+ | Escape | Closes the list and dismisses the match, so typing on doesn't reopen it |
1177
+ | Control+Space | Reopens a dismissed match with the caret in it; `shortcut` changes the key |
1178
+ | Backspace | Past an argument's start, goes back to the previous argument |
1179
+ | Command+Z, Control+Z | Undoes the insertion and brings back the query |
1180
+
1181
+ ### Tested Behavior
1182
+
1183
+ Every line below is an automated test.
1184
+
1185
+ **Opening**
1186
+
1187
+ - Typing a trigger opens the list with every item.
1188
+ - Typing after the trigger filters the list.
1189
+ - Queries match Unicode letters and ignore accents.
1190
+ - A trigger mid-word doesn't open.
1191
+ - A trigger followed by a space doesn't open.
1192
+ - A command trigger opens only at the start, so a leading space escapes it.
1193
+ - The list opens at the trigger and stays there as the query grows.
1194
+ - Pasting a trigger opens the list.
1195
+ - Focusing a field that already has a match doesn't open the list.
1196
+ - Moving the caret into a match doesn't open the list.
1197
+ - A query that matches nothing closes the list, and Backspace that matches again opens it.
1198
+ - With an empty part, a query that matches nothing keeps the list open with the message.
1199
+
1200
+ **Choosing**
1201
+
1202
+ - The first match is highlighted, and Enter inserts it with a space.
1203
+ - Tab accepts the highlighted suggestion and focus stays in the field.
1204
+ - Arrow keys move the highlight, and wrap.
1205
+ - A disabled item can be highlighted but not chosen.
1206
+ - Clicking an item inserts it and keeps focus and the caret in the field.
1207
+ - Text after the caret stays where it was.
1208
+
1209
+ **Dismissing**
1210
+
1211
+ - Escape closes the list, keeps the text and focus.
1212
+ - A dismissed match stays closed through caret moves, typing in it and clicks.
1213
+ - A new trigger after a dismissed one opens.
1214
+ - Deleting and retyping the trigger opens it again.
1215
+ - Ctrl+Space reopens a dismissed match.
1216
+ - Moving the caret out of the match closes the list.
1217
+ - Pressing outside closes the list.
1218
+
1219
+ **Arguments**
1220
+
1221
+ - A command with arguments asks for each one, and Enter finishes with them.
1222
+ - Enter finishes once only optional arguments are left.
1223
+ - A free-text argument shows no list, and Enter takes the typed text.
1224
+ - Backspace past an argument's start goes back a level.
1225
+ - Escape in an argument keeps the inserted text.
1226
+ - An item that submits sends the form once inserted.
1227
+
1228
+ **Loading**
1229
+
1230
+ - Items load for the query.
1231
+ - A newer query aborts the older load, and only its results land.
1232
+
1233
+ **Undo**
1234
+
1235
+ - Undo after choosing brings the query back, never a dead undo.
1236
+
1237
+ **Input**
1238
+
1239
+ - The SDK's Textarea works as the field, and undo still works.
1240
+ - An <input> field works the same.
1241
+
1242
+ Runs in React, Vue and web components.
1243
+
1244
+ - No axe violations while closed.
1245
+ - No axe violations while open with an item highlighted.
1246
+ - No axe violations while open on an input.
1247
+ - No axe violations with nothing found.
1248
+ - A textarea stays a textbox, without aria-expanded, and controls the list while open.
1249
+ - An input is a combobox that says whether it's expanded.
1250
+ - Aria-activedescendant waits for an arrow key and clears on the next edit.
1251
+ - Two menus on a page have their own ids, and the role never changes.
1252
+ - Options are named by their text.
1253
+ - The status says the count and the keys once typing pauses, then that nothing matches.
1254
+ - The hint is hidden from assistive technology, and the status names the argument.
1255
+ - Accessibility tree.
1256
+
1257
+ Runs in React, Vue and web components.