@portabletext/plugin-typeahead-picker 2.0.6 → 3.0.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.
package/README.md CHANGED
@@ -37,9 +37,9 @@ const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
37
37
  // Return matches for the keyword. Can be sync or async (with mode: 'async').
38
38
  getMatches: ({keyword}) => searchEmojis(keyword),
39
39
 
40
- // Actions to execute when a match is selected (Enter/Tab or click).
40
+ // Action to execute when a match is selected (Enter/Tab or click).
41
41
  // Receives the event containing the selected match and pattern selection.
42
- actions: [
42
+ onSelect: [
43
43
  ({event}) => [
44
44
  raise({type: 'delete', at: event.patternSelection}), // Delete `:joy`
45
45
  raise({type: 'insert.text', text: event.match.emoji}), // Insert 😂
@@ -114,7 +114,7 @@ const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
114
114
  keyword: /\S*/,
115
115
  delimiter: ':',
116
116
  getMatches: ({keyword}) => searchEmojis(keyword),
117
- actions: [
117
+ onSelect: [
118
118
  ({event}) => [
119
119
  raise({type: 'delete', at: event.patternSelection}),
120
120
  raise({type: 'insert.text', text: event.match.emoji}),
@@ -136,7 +136,7 @@ const mentionPicker = defineTypeaheadPicker<MentionMatch>({
136
136
  keyword: /\w*/,
137
137
  debounceMs: 200,
138
138
  getMatches: async ({keyword}) => api.searchUsers(keyword),
139
- actions: [
139
+ onSelect: [
140
140
  ({event}) => [
141
141
  raise({type: 'delete', at: event.patternSelection}),
142
142
  raise({
@@ -158,7 +158,7 @@ const commandPicker = defineTypeaheadPicker<CommandMatch>({
158
158
  trigger: /^\//, // ^ anchors to start of block
159
159
  keyword: /\w*/,
160
160
  getMatches: ({keyword}) => searchCommands(keyword),
161
- actions: [
161
+ onSelect: [
162
162
  ({event}) => {
163
163
  switch (event.match.command) {
164
164
  case 'h1':
@@ -183,6 +183,41 @@ const commandPicker = defineTypeaheadPicker<CommandMatch>({
183
183
 
184
184
  `/heading` shows matching commands, but only when `/` is at the start of a block. Text like `hello /heading` will NOT trigger the picker.
185
185
 
186
+ ### Picker with guard
187
+
188
+ Use `guard` to conditionally prevent the picker from activating. The guard runs at trigger time (when the trigger character is typed) and has the same signature as a behavior guard, receiving `snapshot`, `event`, and `dom`.
189
+
190
+ ```ts
191
+ const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
192
+ trigger: /:/,
193
+ keyword: /\S*/,
194
+ delimiter: ':',
195
+ getMatches: ({keyword}) => searchEmojis(keyword),
196
+
197
+ // Guard runs when `:` is typed - return false to block activation
198
+ guard: ({snapshot, event, dom}) => {
199
+ // Don't activate if another UI element is open
200
+ if (isDialogOpen()) {
201
+ return false
202
+ }
203
+
204
+ return true
205
+ },
206
+
207
+ onSelect: [
208
+ ({event}) => [
209
+ raise({type: 'delete', at: event.patternSelection}),
210
+ raise({type: 'insert.text', text: event.match.emoji}),
211
+ ],
212
+ ],
213
+ })
214
+ ```
215
+
216
+ The guard is useful for:
217
+
218
+ - Avoiding conflicts when another picker or dialog is already open
219
+ - Checking editor state or mode before allowing the picker
220
+
186
221
  ## API Reference
187
222
 
188
223
  ### `defineTypeaheadPicker(config)`
@@ -196,10 +231,12 @@ Creates a picker definition to pass to `useTypeaheadPicker`.
196
231
  | `trigger` | `RegExp` | Pattern that activates the picker. Can include `^` for start-of-block triggers. Must be single-character (e.g., `/:/`, `/@/`, `/^\//`). |
197
232
  | `keyword` | `RegExp` | Pattern matching characters after the trigger (e.g., `/\S*/`, `/\w*/`). |
198
233
  | `delimiter` | `string?` | Optional delimiter that triggers auto-completion (e.g., `':'` for `:joy:`) |
234
+ | `guard` | `TypeaheadTriggerGuard?` | Optional guard that runs at trigger time to conditionally prevent activation |
199
235
  | `mode` | `'sync' \| 'async'` | Whether `getMatches` returns synchronously or a Promise (default: `'sync'`) |
200
236
  | `debounceMs` | `number?` | Delay in ms before calling `getMatches`. Useful for both async (API calls) and sync (expensive local search) modes. (default: `0`) |
201
237
  | `getMatches` | `(ctx: {keyword: string}) => TMatch[]` | Function that returns matches for the keyword |
202
- | `actions` | `Array<TypeaheadSelectActionSet>` | Actions to execute when a match is selected |
238
+ | `onSelect` | `TypeaheadSelectActionSet[]` | Action sets to execute when a match is selected |
239
+ | `onDismiss` | `TypeaheadDismissActionSet[]?` | Optional action sets to execute when the picker is dismissed |
203
240
 
204
241
  **Trigger pattern rules:**
205
242
 
@@ -316,16 +353,59 @@ function EmojiPickerPlugin() {
316
353
 
317
354
  The error is cleared when the picker returns to idle (e.g., via Escape or cursor movement).
318
355
 
319
- ## Advanced Actions
356
+ ## onDismiss
320
357
 
321
- Action functions receive more than just the event. The full payload includes access to the editor snapshot, which is useful for generating keys, accessing the schema, or reading the current editor state.
358
+ The optional `onDismiss` callback runs when the picker is dismissed via Escape. This is useful for cleaning up the typed trigger and keyword text.
359
+
360
+ ```ts
361
+ const mentionPicker = defineTypeaheadPicker<MentionMatch>({
362
+ trigger: /@/,
363
+ keyword: /\w*/,
364
+ getMatches: ({keyword}) => searchUsers(keyword),
365
+ onSelect: [
366
+ ({event}) => [
367
+ raise({type: 'delete', at: event.patternSelection}),
368
+ raise({type: 'insert.text', text: `@${event.match.name}`}),
369
+ ],
370
+ ],
371
+ // Delete the typed text when user presses Escape
372
+ onDismiss: [
373
+ ({event}) => [raise({type: 'delete', at: event.patternSelection})],
374
+ ],
375
+ })
376
+ ```
377
+
378
+ Without `onDismiss`, pressing Escape leaves the typed text in place (e.g., `@john` remains in the editor). With `onDismiss` configured to delete the pattern, the text is removed.
379
+
380
+ **onDismiss payload:**
381
+
382
+ | Property | Description |
383
+ | ------------------------ | -------------------------------------------------------------- |
384
+ | `event.patternSelection` | Selection range covering the trigger + keyword (e.g., `@john`) |
385
+ | `snapshot` | Current editor snapshot |
386
+
387
+ Note: `onDismiss` is called when the user actively dismisses the picker:
388
+
389
+ - Pressing Escape
390
+ - Pressing Enter/Tab when there are no matches
391
+ - Programmatically via `picker.send({type: 'dismiss'})`
392
+
393
+ It is NOT called when:
394
+
395
+ - The user selects a match (Enter/Tab/click with a match selected)
396
+ - The picker is dismissed due to cursor movement
397
+ - The picker is dismissed due to invalid pattern (e.g., typing a space)
398
+
399
+ ## Advanced onSelect
400
+
401
+ The `onSelect` callback receives more than just the event. The full payload includes access to the editor snapshot, which is useful for generating keys, accessing the schema, or reading the current editor state.
322
402
 
323
403
  ```tsx
324
404
  const commandPicker = defineTypeaheadPicker<CommandMatch>({
325
405
  trigger: /^\//,
326
406
  keyword: /\w*/,
327
407
  getMatches: ({keyword}) => searchCommands(keyword),
328
- actions: [
408
+ onSelect: [
329
409
  ({event, snapshot}) => {
330
410
  // Access schema to check for block object fields
331
411
  const blockObjectSchema = snapshot.context.schema.blockObjects.find(
@@ -347,7 +427,7 @@ const commandPicker = defineTypeaheadPicker<CommandMatch>({
347
427
  })
348
428
  ```
349
429
 
350
- **Action payload:**
430
+ **onSelect payload:**
351
431
 
352
432
  | Property | Description |
353
433
  | ---------- | ----------------------------------------------------------------------------- |
@@ -463,6 +543,7 @@ The following keyboard shortcuts are handled automatically by the picker:
463
543
  - **Check position anchors**: `^` means start of block, not start of line. `hello /command` won't match `/^\//`
464
544
  - **Check for conflicts**: Only one picker can be active at a time
465
545
  - **Avoid multi-character triggers**: Triggers like `/##/` don't work because the picker only activates on newly typed single characters
546
+ - **Check guard**: If you have a `guard` configured, ensure it's returning `true` when activation should be allowed
466
547
 
467
548
  ### Auto-completion doesn't work
468
549
 
@@ -478,14 +559,12 @@ The following keyboard shortcuts are handled automatically by the picker:
478
559
 
479
560
  ### Focus issues after selection
480
561
 
481
- - Ensure your actions include focus restoration if needed:
562
+ - Ensure your onSelect includes focus restoration if needed:
482
563
  ```tsx
483
- actions: [
564
+ onSelect: [
484
565
  ({event}) => [
485
566
  raise({type: 'delete', at: event.patternSelection}),
486
567
  raise({type: 'insert.text', text: event.match.emoji}),
487
- ],
488
- () => [
489
568
  effect(({send}) => {
490
569
  send({type: 'focus'})
491
570
  }),
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type {EditorSelection} from '@portabletext/editor'
2
- import type {BehaviorActionSet} from '@portabletext/editor/behaviors'
2
+ import type {
3
+ BehaviorActionSet,
4
+ BehaviorGuard,
5
+ } from '@portabletext/editor/behaviors'
3
6
 
4
7
  declare type AsyncConfigWithDelimiter<TMatch extends AutoCompleteMatch> =
5
8
  BaseConfigWithDelimiter<TMatch> & {
@@ -90,10 +93,20 @@ declare type BaseConfigWithDelimiter<TMatch extends AutoCompleteMatch> = {
90
93
  */
91
94
  delimiter: string
92
95
  /**
93
- * Actions to execute when a match is selected.
94
- * Typically deletes the trigger text and inserts the selected content.
96
+ * Guard function that runs at trigger time to conditionally prevent activation.
97
+ * Return `false` to block activation, or `true` to allow it.
95
98
  */
96
- actions: Array<TypeaheadSelectActionSet<TMatch>>
99
+ guard?: TypeaheadTriggerGuard
100
+ /**
101
+ * Called when a match is selected.
102
+ * Returns behavior actions to execute (e.g., delete trigger text, insert content).
103
+ */
104
+ onSelect: TypeaheadSelectActionSet<TMatch>[]
105
+ /**
106
+ * Called when the picker is dismissed.
107
+ * Returns behavior actions to execute (optional cleanup).
108
+ */
109
+ onDismiss?: TypeaheadDismissActionSet[]
97
110
  }
98
111
 
99
112
  declare type BaseConfigWithoutDelimiter<TMatch extends object> = {
@@ -114,10 +127,20 @@ declare type BaseConfigWithoutDelimiter<TMatch extends object> = {
114
127
  keyword: RegExp
115
128
  delimiter?: undefined
116
129
  /**
117
- * Actions to execute when a match is selected.
118
- * Typically deletes the trigger text and inserts the selected content.
130
+ * Guard function that runs at trigger time to conditionally prevent activation.
131
+ * Return `false` to block activation, or `true` to allow it.
132
+ */
133
+ guard?: TypeaheadTriggerGuard
134
+ /**
135
+ * Called when a match is selected.
136
+ * Returns behavior actions to execute (e.g., delete trigger text, insert content).
137
+ */
138
+ onSelect: TypeaheadSelectActionSet<TMatch>[]
139
+ /**
140
+ * Called when the picker is dismissed.
141
+ * Returns behavior actions to execute (optional cleanup).
119
142
  */
120
- actions: Array<TypeaheadSelectActionSet<TMatch>>
143
+ onDismiss?: TypeaheadDismissActionSet[]
121
144
  }
122
145
 
123
146
  /**
@@ -130,7 +153,12 @@ declare type BaseConfigWithoutDelimiter<TMatch extends object> = {
130
153
  * keyword: /[\S]+/,
131
154
  * delimiter: ':',
132
155
  * getMatches: ({keyword}) => searchEmojis(keyword),
133
- * actions: [insertEmojiAction],
156
+ * onSelect: [
157
+ * ({event}) => [
158
+ * raise({type: 'delete', at: event.patternSelection}),
159
+ * raise({type: 'insert.text', text: event.match.emoji}),
160
+ * ],
161
+ * ],
134
162
  * })
135
163
  * ```
136
164
  *
@@ -142,17 +170,31 @@ declare type BaseConfigWithoutDelimiter<TMatch extends object> = {
142
170
  * keyword: /[\w]+/,
143
171
  * debounceMs: 200,
144
172
  * getMatches: async ({keyword}) => api.searchUsers(keyword),
145
- * actions: [insertMentionAction],
173
+ * onSelect: [
174
+ * ({event}) => [
175
+ * raise({type: 'delete', at: event.patternSelection}),
176
+ * raise({type: 'insert.inline object', inlineObject: {_type: 'mention', userId: event.match.id}}),
177
+ * ],
178
+ * ],
146
179
  * })
147
180
  * ```
148
181
  *
149
- * @example Slash commands at start of block
182
+ * @example Picker with guard (runs at trigger time)
150
183
  * ```ts
151
- * const slashCommandPicker = defineTypeaheadPicker({
152
- * trigger: /^\//,
153
- * keyword: /[\w]+/,
154
- * getMatches: ({keyword}) => filterCommands(keyword),
155
- * actions: [executeCommandAction],
184
+ * const emojiPicker = defineTypeaheadPicker({
185
+ * trigger: /:/,
186
+ * keyword: /[\S]+/,
187
+ * getMatches: ({keyword}) => searchEmojis(keyword),
188
+ * guard: ({snapshot, event, dom}) => {
189
+ * if (anotherPickerIsOpen()) return false
190
+ * return true
191
+ * },
192
+ * onSelect: [
193
+ * ({event}) => [
194
+ * raise({type: 'delete', at: event.patternSelection}),
195
+ * raise({type: 'insert.text', text: event.match.emoji}),
196
+ * ],
197
+ * ],
156
198
  * })
157
199
  * ```
158
200
  *
@@ -244,6 +286,28 @@ declare type SyncConfigWithoutDelimiter<TMatch extends object> =
244
286
  getMatches: (context: {keyword: string}) => ReadonlyArray<TMatch>
245
287
  }
246
288
 
289
+ /**
290
+ * Action set that runs when the picker is dismissed.
291
+ * Returns an array of behavior actions to execute (optional cleanup).
292
+ *
293
+ * @public
294
+ */
295
+ export declare type TypeaheadDismissActionSet = BehaviorActionSet<
296
+ TypeaheadDismissEvent,
297
+ true
298
+ >
299
+
300
+ /**
301
+ * Event passed to `onDismiss` when the picker is dismissed.
302
+ *
303
+ * @public
304
+ */
305
+ export declare type TypeaheadDismissEvent = {
306
+ type: 'custom.typeahead dismiss'
307
+ /** Selection range covering the full pattern match (e.g., `@john`) for cleanup */
308
+ patternSelection: NonNullable<EditorSelection>
309
+ }
310
+
247
311
  /**
248
312
  * The picker instance returned by {@link useTypeaheadPicker}.
249
313
  *
@@ -329,7 +393,12 @@ export declare type TypeaheadPickerContext<TMatch> = {
329
393
  * keyword: /[\S]+/,
330
394
  * delimiter: ':',
331
395
  * getMatches: ({keyword}) => searchEmojis(keyword),
332
- * actions: [insertEmojiAction],
396
+ * onSelect: [
397
+ * ({event}) => [
398
+ * raise({type: 'delete', at: event.patternSelection}),
399
+ * raise({type: 'insert.text', text: event.match.emoji}),
400
+ * ],
401
+ * ],
333
402
  * })
334
403
  * ```
335
404
  *
@@ -384,12 +453,33 @@ declare type TypeaheadPickerDefinitionBase<TMatch extends object> = {
384
453
  */
385
454
  delimiter?: string
386
455
  /**
387
- * Actions to execute when a match is selected.
388
- * Typically deletes the trigger text and inserts the selected content.
456
+ * Guard function that runs at trigger time to conditionally prevent the picker
457
+ * from activating.
458
+ * Return `false` to block activation, or `true` to allow it.
459
+ *
460
+ * @see {@link TypeaheadTriggerGuard}
461
+ */
462
+ guard?: TypeaheadTriggerGuard
463
+ /**
464
+ * Called when a match is selected.
465
+ * Returns behavior actions to execute (e.g., delete trigger text, insert content).
389
466
  *
390
- * @see {@link TypeaheadSelectActionSet}
467
+ * @example
468
+ * ```ts
469
+ * onSelect: [
470
+ * ({event}) => [
471
+ * raise({type: 'delete', at: event.patternSelection}),
472
+ * raise({type: 'insert.text', text: event.match.emoji}),
473
+ * ],
474
+ * ]
475
+ * ```
476
+ */
477
+ onSelect: TypeaheadSelectActionSet<TMatch>[]
478
+ /**
479
+ * Called when the picker is dismissed (Escape, cursor movement, etc.).
480
+ * Returns behavior actions to execute (optional cleanup).
391
481
  */
392
- actions: Array<TypeaheadSelectActionSet<TMatch>>
482
+ onDismiss?: TypeaheadDismissActionSet[]
393
483
  }
394
484
 
395
485
  /**
@@ -465,19 +555,9 @@ export declare type TypeaheadPickerState =
465
555
  }
466
556
 
467
557
  /**
468
- * Action function that runs when a match is selected.
558
+ * Action set that runs when a match is selected.
469
559
  * Returns an array of behavior actions to execute (e.g., delete trigger text, insert content).
470
560
  *
471
- * @example
472
- * ```ts
473
- * const insertEmoji: TypeaheadSelectActionSet<EmojiMatch> = (
474
- * {event},
475
- * ) => [
476
- * raise({type: 'delete', at: event.patternSelection}),
477
- * raise({type: 'insert.text', text: event.match.emoji}),
478
- * ]
479
- * ```
480
- *
481
561
  * @public
482
562
  */
483
563
  export declare type TypeaheadSelectActionSet<TMatch> = BehaviorActionSet<
@@ -486,12 +566,12 @@ export declare type TypeaheadSelectActionSet<TMatch> = BehaviorActionSet<
486
566
  >
487
567
 
488
568
  /**
489
- * Event passed to typeahead select action sets.
569
+ * Event passed to `onSelect` when a match is selected.
490
570
  *
491
571
  * @public
492
572
  */
493
573
  export declare type TypeaheadSelectEvent<TMatch> = {
494
- type: 'typeahead.select'
574
+ type: 'custom.typeahead select'
495
575
  /** The match that was selected */
496
576
  match: TMatch
497
577
  /** The extracted keyword (e.g., `joy` from `:joy`) */
@@ -500,6 +580,39 @@ export declare type TypeaheadSelectEvent<TMatch> = {
500
580
  patternSelection: NonNullable<EditorSelection>
501
581
  }
502
582
 
583
+ /**
584
+ * Event passed to the trigger guard when the picker is about to activate.
585
+ *
586
+ * @public
587
+ */
588
+ export declare type TypeaheadTriggerEvent = {
589
+ type: 'custom.typeahead trigger found'
590
+ }
591
+
592
+ /**
593
+ * Guard function that runs at trigger time to conditionally prevent the picker
594
+ * from activating. Has the same signature as a behavior guard.
595
+ *
596
+ * Return `false` to block activation, or `true` to allow it.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * guard: ({snapshot, event, dom}) => {
601
+ * // Block activation if another picker is open
602
+ * if (anotherPickerIsOpen()) return false
603
+ *
604
+ * // Allow activation
605
+ * return true
606
+ * }
607
+ * ```
608
+ *
609
+ * @public
610
+ */
611
+ export declare type TypeaheadTriggerGuard = BehaviorGuard<
612
+ TypeaheadTriggerEvent,
613
+ true
614
+ >
615
+
503
616
  /**
504
617
  * React hook that activates a typeahead picker and returns its current state.
505
618
  *