Skip to content

feat: open the typeahead picker on multi-character triggers - #3414

Draft
christianhg wants to merge 1 commit into
mainfrom
feat/typeahead-multi-char-trigger
Draft

christianhg wants to merge 1 commit into
mainfrom
feat/typeahead-multi-char-trigger

Conversation

@christianhg

@christianhg christianhg commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Triggers of more than one character, such as /\[\[/ or /:::/, now open the picker when the character that completes them is typed: [ followed by [ opens it with an empty keyword, and typing on updates the keyword. Inserting [foo in one go after an existing [, as Android keyboards do when committing a word, opens the picker with the keyword foo. Selecting a match replaces every trigger character along with the keyword, and with a delimiter such as ], typing [[foo] auto-completes an exact match.

import {raise} from '@portabletext/editor/behaviors'
import {defineTypeaheadPicker} from '@portabletext/plugin-typeahead-picker'

const pages = ['Getting started', 'Glossary', 'Roadmap']

function searchPages(keyword: string) {
  return pages
    .filter((title) => title.toLowerCase().startsWith(keyword.toLowerCase()))
    .map((title) => ({
      title,
      type:
        title.toLowerCase() === keyword.toLowerCase()
          ? ('exact' as const)
          : ('partial' as const),
    }))
}

const wikiLinkPicker = defineTypeaheadPicker({
  trigger: /\[\[/,
  keyword: /[^\]]*/,
  delimiter: ']',
  getMatches: ({keyword}) => searchPages(keyword),
  onSelect: [
    ({event}) => [
      raise({type: 'delete', at: event.patternSelection}),
      raise({type: 'insert.text', text: event.match.title}),
    ],
  ],
})

Until now the picker opened only when the inserted text equalled the trigger match, so a multi-character trigger worked when pasted in one go and never when typed. The trigger rule now also accepts a match that ends at the caret whose text ends with what was just inserted. The partial rule had its own refusal for matches that start before the insertion, which blocked the Android case where a keyboard commits [foo after an existing [. It now lets that through when the insertion lands inside the trigger part of the match. The trigger length comes from matching the existing trigger pattern against the start of the match text, the same way the keyword is already extracted, rather than a named group that could clash with groups in the user's own patterns.

Single-character triggers are unchanged by construction: a non-empty suffix of a one-character match is the match itself, and a one-character trigger that starts before the insertion cannot end after it. Every existing test passes untouched. The new scenarios in pattern-edge-cases.test.tsx replace the test that pinned multi-character triggers as unsupported. Neither guard depends on the trigger length, and a /:::/ scenario pins a three-character trigger. A /\w+/ keyword variant pins the trigger rule change on its own, and the bulk [foo and [foo] inserts pin the partial rule change.

One limit stays and is now documented in the README: typing [[, Escape, then [ does not reopen the picker, because the input rule finds only the first non-overlapping [[ in [[[. A stale or dismissed first [ does count when the second one is typed, which follows the rule that the character just typed completes the trigger.

@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
portable-text-editor-documentation Ready Ready Preview Oct 21, 2026 4:45pm UTC
portable-text-example-basic Ready Ready Preview Oct 21, 2026 4:45pm UTC
portable-text-playground Ready Ready Preview Oct 21, 2026 4:45pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7b5bf1f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@portabletext/plugin-typeahead-picker Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Stats

✅ No significant changes.

All scenario measurements (7)

🗺️ @portabletext/editor / @portabletext/editor · @portabletext/editor / @portabletext/editor/behaviors · @portabletext/editor / @portabletext/editor/plugins · @portabletext/editor / @portabletext/editor/selectors · @portabletext/editor / @portabletext/editor/traversal · @portabletext/editor / @portabletext/editor/utils · @portabletext/markdown / @portabletext/markdown · Artifacts

Scenario Kind Bundle (raw / gzip) Gzip change Import time Import change
⚪ @portabletext/editor / @portabletext/editor export 1.07 MB / 136.5 KB None 67 ms +2 ms, +2.8%
⚪ @portabletext/editor / @portabletext/editor/behaviors export 4.0 KB / 147 B None 2 ms +0 ms, +1.0%
⚪ @portabletext/editor / @portabletext/editor/plugins export 5.1 KB / 645 B None 7 ms +0 ms, +1.1%
⚪ @portabletext/editor / @portabletext/editor/selectors export 94.9 KB / 9.3 KB None 8 ms +0 ms, +0.9%
⚪ @portabletext/editor / @portabletext/editor/traversal export 43.8 KB / 4.0 KB None 5 ms -0 ms, -3.0%
⚪ @portabletext/editor / @portabletext/editor/utils export 33.7 KB / 4.1 KB None 6 ms -0 ms, -0.0%
⚪ @portabletext/markdown / @portabletext/markdown export 407.2 KB / 64.2 KB None 27 ms -0 ms, -0.9%

Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time.

The picker opened only when the inserted text equalled the trigger
match (`event.textInserted !== lastMatch.text` in the trigger rule
guard), which a multi-character trigger like `/\[\[/` can only satisfy
when all its characters arrive in one insert. Typing `[` then `[` left
the picker idle. The partial rule refused any match starting before the
insertion unless the inserted text matched the partial pattern on its
own, so an Android-style commit of `[foo` after an existing `[` was
refused too.

The trigger rule now also accepts a match that ends at the caret when
the match text ends with the inserted text. The partial rule skips its
starts-before-insertion refusal when the insertion lands inside the
trigger part of the match, measured by matching the trigger pattern
against the start of the match text. Neither change depends on the
trigger length.

Single-character triggers behave as before: a non-empty suffix of a
one-character match is the match itself, and a one-character trigger
that starts before the insertion cannot end after it. Typing `[[`,
Escape, then `[` still does not reopen the picker, because the input
rule finds only the first non-overlapping `[[`.

This branch was successfully deployed

3 active deployments
Preview – portable-text-playground — 7b5bf1f5 Deployed Oct 21, 2026 by vercel[bot]
Preview – portable-text-example-basic — 7b5bf1f5 Deployed Oct 21, 2026 by vercel[bot]
Preview – portable-text-editor-documentation — 7b5bf1f5 Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant