Back to Plate

Slate react text-block placeholder contract must omit native textarea placeholder

docs/solutions/developer-experience/2026-04-15-slate-react-text-block-placeholder-contract-must-omit-native-textarea-placeholder.md

53.0.63.1 KB
Original Source

Slate react text-block placeholder contract must omit native textarea placeholder

Problem

EditableTextBlocksProps exposes a current custom placeholder?: ReactNode contract so examples can render real JSX placeholder hosts.

The prop bag still inherited the native textarea placeholder?: string attribute, so TypeScript intersected the two contracts into garbage.

Symptoms

  • placeholder-surface.tsx and custom-placeholder.tsx started failing tsc
  • JSX placeholder values like <><p>Type something</p></> were rejected even though the runtime rendered them
  • the failure showed up as a bizarre string-heavy union instead of a clear prop contract error

What Didn't Work

  • treating the typecheck failure as unrelated repo dirt
  • staring only at the example files
  • trusting the runtime because the placeholder UI already worked in the browser

The problem was not the examples. The package type surface was lying.

Solution

Omit the native placeholder key from the inherited textarea attributes in editable-text-blocks.tsx so the current custom ReactNode placeholder contract owns the name cleanly.

ts
} & Omit<
  TextareaHTMLAttributes<HTMLDivElement>,
  | 'autoFocus'
  | 'children'
  | 'className'
  | 'id'
  | 'onKeyDown'
  | 'onPaste'
  | 'placeholder'
  | 'readOnly'
  | 'spellCheck'
  | 'style'
>;

Then pin the contract with a compile-time proof in primitives-contract.tsx using JSX placeholder hosts on both EditableBlocksProps and EditableTextBlocksProps.

Why This Works

Without the omit, the type system sees two different placeholder contracts:

  • native textarea: string
  • current text-block surface: ReactNode

That intersection is narrower than either real runtime contract, so callers get type errors for perfectly valid JSX.

Once the native key is removed, the current public surface becomes honest again: callers can pass strings or richer JSX because ReactNode owns the prop.

Prevention

  • When a public component defines a custom prop that reuses a native DOM prop name, omit the native key first.
  • Do not stop at runtime proof when a current example uses a public package prop; the exported type has to match the behavior.
  • Add a contract test the moment a public prop widens beyond the native DOM shape.
  • For projection-backed example recoveries, remember the sister trap from the same batch: if the example restores an outer Slate wrapper, thread projectionStore through that provider or the decoration slices never reach EditableBlocks.