Skip to content

Replace native textarea with contentEditable for inline autocomplete#394

Draft
vimzh wants to merge 3 commits intoaymericzip:mainfrom
vimzh:feat/contenteditable-textarea
Draft

Replace native textarea with contentEditable for inline autocomplete#394
vimzh wants to merge 3 commits intoaymericzip:mainfrom
vimzh:feat/contenteditable-textarea

Conversation

@vimzh
Copy link

@vimzh vimzh commented Mar 14, 2026

Summary

Replaces the native <textarea> in AutoCompleteTextarea with a contentEditable div (ContentEditableTextArea) to enable inline ghost text rendering for autocomplete suggestions.

Review feedback addressed

  • Removed unused _cursorToLinePos function and all dead autocomplete state (ghostPos, cursorAtFetch, isTyped, debouncedText)
  • Replaced .at(-1) with bracket access for ES2020 compatibility
  • Fixed broken input logic — complete rewrite of contentEditable input handling

What changed

ContentEditableTextArea (new component):

  • All input routed through React state — no browser DOM mutations
  • Cursor position preserved across re-renders via pendingCaretRef
  • Full selection support (select-all + Backspace/Delete/type)
  • Modifier key support: Option+Backspace/Delete (word), Cmd+Backspace/Delete (line)
  • Cmd+Z/Redo blocked (would desync React state)
  • IME composition guards for CJK input
  • Grapheme-safe deletion for emoji via Intl.Segmenter
  • Cut handler (clipboard write + state delete)
  • Drag-and-drop blocked
  • Spell-check replacements handled (insertReplacementText)
  • {...rest} spread before explicit handlers to prevent override
  • Placeholder alignment matched to inputVariants padding
  • Zero-width space stripped from text extraction

AutocompleteTextArea (simplified):

  • Removed dead imports (useAutocomplete, useConfiguration)
  • isActive prop properly gates ghost text display
  • aria-invalid and aria-describedby now forwarded
  • Cached text.split('\n') (was called 3x per render)

Tests:

  • 24 unit tests covering hook behavior, rendering, accessibility, controlled values, and edge cases

Test plan

  • TypeScript: zero errors (npx tsc --noEmit)
  • Unit tests: 24/24 passing
  • Full design-system suite: 33/33 passing
  • Prettier: all files formatted
  • No unused imports, no dead code, no any types
  • Consumer compatibility verified (ContentEditorTextArea, AutocompletionSection)
  • Barrel exports intact (all 4 components)
  • Package build passes
  • Security review: no XSS vectors, paste uses text/plain only
  • Manual test: type, Enter, Backspace, select-all+delete, Option+Backspace, Cmd+X, paste

… autocomplete

Swap the native <textarea> in AutocompleteTextArea with a contentEditable
<div> that renders each line as a <span>. This enables pixel-accurate
inline ghost text for autocomplete on any line, without the positioning
hacks required by absolute-positioned overlays.

New ContentEditableTextArea component with:
- Per-line span rendering with editable + ghost segments
- useContentEditable hook for DOM <-> state sync and caret management
- Memoized Line component to skip unchanged line re-renders
- Auto-sizing, LTR/RTL, disabled state, placeholder
- Full ARIA: role=textbox, aria-multiline, aria-autocomplete=inline

Closes aymericzip#197
Copy link
Owner

@aymericzip aymericzip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @vimzh

Thanks for your contribution,

Here a quick review

);
} catch (err) {
console.error('Autocomplete error:', err);
const _cursorToLinePos = useCallback((cursor: number, src: string) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont seems to be used

? text.split('\n').length - 1
: ghostPos?.line;
const activeOffset = suggestionProp
? (text.split('\n').at(-1)?.length ?? 0)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property 'at' does not exist on type 'string[]'

@gentle-gecko-okogwkww0gkkk4040
Copy link

gentle-gecko-okogwkww0gkkk4040 bot commented Mar 14, 2026

The preview deployment for website failed. 🔴

Open Build Logs

Last updated at: 2026-03-14 21:57:12 CET

@aymericzip
Copy link
Owner

aymericzip commented Mar 14, 2026

the logic is broken

Screen.Recording.2026-03-14.at.4.09.37.PM.mov

@aymericzip aymericzip marked this pull request as draft March 14, 2026 22:11
@gentle-gecko-okogwkww0gkkk4040
Copy link

gentle-gecko-okogwkww0gkkk4040 bot commented Mar 14, 2026

The preview deployment for design-system is ready. 🟢

Open Preview | Open Build Logs

Last updated at: 2026-03-15 21:33:55 CET

…dback

Address all maintainer review comments on PR aymericzip#394:

- Remove unused _cursorToLinePos function and dead autocomplete state
- Replace .at(-1) with bracket access for ES2020 compatibility
- Fix broken input logic by intercepting all keyboard/clipboard events
  via React state instead of allowing browser DOM mutations
- Add cursor preservation across re-renders via pendingCaretRef
- Handle text selections for Backspace/Delete/Enter/Cut/Paste
- Support modifier keys: Option+Backspace (word), Cmd+Backspace (line)
- Block Cmd+Z/Redo to prevent React state desync
- Add IME composition guards for CJK input
- Add grapheme-safe deletion for emoji and surrogate pairs
- Block drag-and-drop to prevent uncontrolled DOM mutations
- Handle spell-check replacements via insertReplacementText
- Fix {..rest} spread ordering to prevent handler override
- Fix placeholder alignment to match inputVariants padding
- Forward aria-invalid and aria-describedby to contentEditable div
- Strip zero-width spaces from text extraction
- Add 24 unit tests for hook and component behavior
@vimzh vimzh requested a review from aymericzip March 15, 2026 13:05
@vimzh
Copy link
Author

vimzh commented Mar 15, 2026

Hey @aymericzip, thanks for the review and for recording the video, I've pushed a fix addressing all the feedback
Let me know if there's anything else you'd like me to adjust!

Copy link
Owner

@aymericzip aymericzip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to me. just before merging, few point regarding the codebase convention

  • remove the useMemo / useCallback. We use the react compiler
  • prefer import of import {ChangeEvent} from 'react' over import * as react from 'react'
  • Remove react 18 related syntax (e.g. Line.displayName = 'Line';)

…review

React compiler handles memoization, so useCallback and memo wrappers
are unnecessary. Also removes Line.displayName (React 18 pattern).
@vimzh
Copy link
Author

vimzh commented Mar 16, 2026

Hey @aymericzip, thanks for the review! I've pushed the changes:

  • Removed all useCallback/memo wrappers (React compiler handles memoization)
  • Removed Line.displayName (React 18 pattern)
  • Imports were already using named imports (import { FC } from 'react')

Let me know if there's anything else!

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.

2 participants