Skip to content

fix(desktop): disable spellcheck/autocorrect/autocapitalize on emoji picker search#1438

Merged
wpfleger96 merged 6 commits into
mainfrom
duncan/emoji-picker-no-autocorrect
Jul 1, 2026
Merged

fix(desktop): disable spellcheck/autocorrect/autocapitalize on emoji picker search#1438
wpfleger96 merged 6 commits into
mainfrom
duncan/emoji-picker-no-autocorrect

Conversation

@wpfleger96

Copy link
Copy Markdown
Collaborator

Problem

The emoji picker's search input (rendered inside em-emoji-picker's open shadow DOM by emoji-mart) had spellcheck, autocorrect, and autocapitalize enabled. Emoji names aren't grammatical — autocorrect and auto-capitalization just add wasted correction hops when searching.

Why it's not a prop

emoji-mart renders the search <input type="search"> inside a shadow root. Shadow DOM blocks outside CSS and attribute props from reaching the input, and emoji-mart exposes no API to configure these attributes.

Fix

EmojiPicker.tsx now wraps <Picker> in a host <div ref>. A useEffect calls disableSearchInputCorrections after mount, which:

  1. Finds the em-emoji-picker custom element in the host.
  2. Fast path: if the shadow input is already present (e.g. re-mount from hot cache), sets the three attributes immediately and returns.
  3. Slow path: emoji-mart mounts its shadow content asynchronously, so if the input isn't there yet, a MutationObserver on the shadow root watches for it, applies the attributes once it appears, then disconnects.
  4. Returns a cleanup function so the observer is disconnected on unmount — no leaks.

Scoped to EmojiPicker.tsx — the single centralized picker every call site renders (reactions, composer, status).

Test

New E2E assertion in custom-emoji.spec.ts: opens the reaction picker on the seeded reactable message, waits for the search input to be visible, then evaluates the shadow root directly to assert spellcheck === false, autocorrect === "off", autocapitalize === "off".

Checks

just desktop-check   # biome: 0 errors — EXIT 0
just desktop-test    # 1447 pass, 0 fail — EXIT 0

(E2E runs in CI via Playwright; unit suite confirmed green above.)

npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 6 commits July 1, 2026 16:22
…picker search

The emoji picker's search input is rendered inside an open shadow DOM
by emoji-mart — outside CSS and props cannot reach it, and emoji-mart
exposes no prop to set these attributes.

After mount, traverse the shadow root of em-emoji-picker and set:
  - input.spellcheck = false
  - autocorrect="off"
  - autocapitalize="off"

emoji-mart mounts its custom element asynchronously, so the input is not
present on first render. A MutationObserver watches the shadow subtree,
applies the attributes once the input appears, then disconnects. The
fast path handles re-mounts where the input is already present. Cleanup
disconnects the observer on unmount so nothing leaks.

Scoped to EmojiPicker.tsx — the single centralized picker. All call
sites (reactions, composer, status) get the fix automatically.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…nputCorrections

tsc TS18047/TS18049: the closure inside disableSearchInputCorrections
captured picker.shadowRoot through the narrowed-but-re-widened path —
tsc cannot narrow a variable across a nested function boundary.

Fix: capture picker?.shadowRoot as a typed const (root: ShadowRoot)
after the null guard, then reference root inside the closure. The
variable is a new binding with a definite non-null type, so tsc is
satisfied and behavior is identical.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The spellcheck/autocorrect fix added a shadow-DOM traversal via a host div
ref. This shifted the relative ordering of emoji-mart's async connectedCallback
focus call and Radix's focus-scope onOpenAutoFocus handler, causing Radix to
win the race and steal focus from the search input.

Fix: thread autoFocus into disableSearchInputCorrections. When autoFocus is
true, applyAttributes() calls input.focus() after setting the three attributes.
This fires on both the fast path (input already present) and the slow
MutationObserver path, making our code the deterministic focus owner rather
than racing emoji-mart's own attempt.

Call sites that pass autoFocus=false (ComposerEmojiPicker and others that
never requested autofocus) are unaffected.

Regression guard added to custom-emoji.spec.ts: asserts the shadow input is
shadowRoot.activeElement immediately after the picker opens — fails if Radix
or any other mechanism steals focus.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Per Will's request (Option 2), the composer emoji picker now autofocuses
its search input on open — the same deterministic shadow-DOM path that
the reaction popover already uses. One-line change: pass autoFocus to
EmojiPicker in ComposerEmojiPicker.

Focus-restore on select is safe: insertEmoji calls
editor.chain().focus() before setIsEmojiPickerOpen(false), so the
Tiptap editor owns focus before Radix's onCloseAutoFocus fires. No
focus-stranding risk.

New E2E test asserts shadowRoot.activeElement === the search input on
open, and that picking an emoji returns focus to the composer input.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Without onOpenAutoFocus suppression, Radix's FocusScope fires after our
MutationObserver-owned input.focus() and moves focus to the first tabbable
element in the PopoverContent — which is not the shadow-DOM search input.
Result: composer autofocus E2E test fails (isFocused === false).

Add onOpenAutoFocus={(e) => e.preventDefault()} to let our
disableSearchInputCorrections traversal own focus on open. Add
onCloseAutoFocus={(e) => e.preventDefault()} to let insertEmoji's
editor.chain().focus() stand on close rather than Radix returning focus
to the trigger button.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…smiss

The unconditional onCloseAutoFocus suppression left keyboard focus on a
detached shadow input (or document.body) when the user dismissed the
picker via Escape or click-outside without selecting an emoji.

Fix: thread an onClose prop through ComposerEmojiPicker. MessageComposer
Toolbar passes () => editor?.commands.focus(), which runs in onCloseAutoFocus
after preventDefault(). On the emoji-select path insertEmoji already called
editor.chain().focus() before the popover closes, making the onClose call
a focus no-op. On the Escape/click-outside path, onClose explicitly returns
focus to the editor so the user can keep typing without an extra click.

E2E: add Escape-dismiss test asserting message-input is focused.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/emoji-picker-no-autocorrect branch from d34b6ce to 5226fc3 Compare July 1, 2026 20:22
@wpfleger96 wpfleger96 merged commit bdeab23 into main Jul 1, 2026
18 checks passed
@wpfleger96 wpfleger96 deleted the duncan/emoji-picker-no-autocorrect branch July 1, 2026 20:23
wpfleger96 pushed a commit that referenced this pull request Jul 2, 2026
…etrics

Consolidate PR #1445 (kind 44200 relay/core implementation) into PR #1441
(NIP-AM doc + full relay/core). The relay/core changes are reviewed and
Thufir-cleared (pass 2, all axes 9/10). No commits were rewritten.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

* origin/duncan/nip-am-kind-relay:
  chore(fmt): run rustfmt on NIP-AM kind 44200 relay changes
  fix(relay/core): plug COUNT existence-leak and StopReason forward-compat for NIP-AM
  fix(relay/core): close result-level read gate for kind:44200 (NIP-AM)
  feat(core/relay): add NIP-AM kind 44200 (agent turn metrics) with relay plumbing
  feat(desktop): restore observer-feed regressions from #1381 and classify 4 new session/update types (#1412)
  fix(desktop): disable spellcheck/autocorrect/autocapitalize on emoji picker search (#1438)
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