UNPKG

@aircall/blocks

Version:

Aircall Blocks — higher-level UI compositions built on @aircall/ds

127 lines (105 loc) 6.39 kB
--- name: aircall-blocks/migrate-dashboard/rich-textarea description: > Migrate @dashboard/library TagHighlightTextarea to the @aircall/ds RichTextarea. Covers the #tag colored-pill mapping, the controlled string value -> uncontrolled defaultValue + onValueChange({ plainText }) shift, and the props RichTextarea does not yet cover (maxLength counter, onBlur/onFocus). Load when a file imports TagHighlightTextarea from @dashboard/library. type: sub-skill library: aircall-blocks requires: - aircall-blocks/setup - aircall-blocks/migrate-dashboard sources: - "aircall/hydra:packages/ds/src/components/rich-textarea.tsx" - "aircall/hydra:packages/ds/src/index.ts" --- This skill builds on aircall-blocks/migrate-dashboard. `TagHighlightTextarea` is a TipTap multi-line textarea that highlights `#tag` mentions as colored pills and reports its content as a plain-text string. The @aircall/ds `RichTextarea` (Plate.js) covers the same `#tag` pill behavior natively via its `tags` prop, so the swap is mostly a prop remap plus one behavioral shift: RichTextarea is **uncontrolled** and reports `{ plainText, json }` rather than a controlled string. ## 1. Component mapping | @dashboard/library `TagHighlightTextarea` | @aircall/ds `RichTextarea` | | --- | --- | | `TagHighlightTextarea` (root) | `RichTextarea` (root, all-in-one) | | `tags: CompanyTag[]` (`{ id, name, color }`, `color` = hex) | `tags: TagOption[]` (`{ id, label, color? }`) — `name` -> `label`; hex `color` -> DS `Badge` semantic color (see gap 2) | | `value: string` (controlled) | `defaultValue: string` (uncontrolled — set once at mount; see gap 1) | | `onChange: (value: string) => void` | `onValueChange: ({ plainText }) => void` — call your handler with `plainText` | | `placeholder?: string` | `placeholder?: string` (1:1) | | Multi-line (Enter inserts a newline) | `submitOnEnter={false}` — RichTextarea defaults to Enter-submits; a textarea must opt out so Enter is a newline (Shift+Enter also inserts one) | | `maxLength?` + the `n/max` counter | **No built-in equivalent** — enforce and render at the consumer level (see gap 3) | | `onBlur?` / `onFocus?` | **Not supported** by RichTextarea props (see gap 4) | | The `#` tag suggestion menu (colored rows) | Built in — typing `#` opens the tag menu filtered over `tags`; selection inserts a colored `Badge` pill | | free-typed unknown `#tag` | RichTextarea offers a "Create" row for free text; the created tag has no `id`. If you must restrict to known tags only, filter it out in your `onValueChange` handler | ## 2. Imports ```tsx import { RichTextarea, type TagOption } from '@aircall/ds'; ``` Drop the TipTap stack entirely — `@tiptap/*`, `@xstyled/styled-components`, the local `createSuggestionRenderer` / `createTagMentionExtension` / `parseInitialContent` / `SuggestionList` helpers, and the `EditorWrapper` styled component are all replaced by the single `RichTextarea` import. `getPlainText` is replaced by the `plainText` field of `onValueChange`. ## 3. Before / after Before (`@dashboard/library`): ```tsx <TagHighlightTextarea value={note} onChange={setNote} tags={companyTags} // { id, name, color: '#0662B5' }[] placeholder="Add a note..." maxLength={280} onBlur={handleBlur} /> ``` After (`@aircall/ds`): ```tsx const tags: TagOption[] = companyTags.map(t => ({ id: t.id, label: t.name, color: toBadgeColor(t.color), // hex -> DS Badge semantic color (gap 2) })); <RichTextarea tags={tags} defaultValue={note} // uncontrolled: initial value only (gap 1) submitOnEnter={false} // textarea: Enter = newline placeholder="Add a note..." aria-label="Note" // placeholder is not an accessible name onValueChange={({ plainText }) => setNote(plainText)} className="min-h-[150px]" // match the old 150px min-height if needed /> ``` ## 4. Gaps and caveats (read before migrating) 1. **Controlled -> uncontrolled.** RichTextarea seeds from `defaultValue` only at mount and never re-reads it. `TagHighlightTextarea` synced its editor when `value` changed externally (form reset/undo); RichTextarea does not. Drive your state from `onValueChange`. To force a reset (form reset, switching records), remount by changing `key`: `<RichTextarea key={recordId} defaultValue={initialNote} … />`. Do NOT feed `plainText` back in as `defaultValue` every render expecting it to update — it won't. 2. **Tag color fidelity.** `CompanyTag.color` is an arbitrary hex, mapped in the legacy component to the nearest palette swatch. `TagOption.color` is a DS `Badge` **semantic** color (not a hex), so exact hex colors are not preserved. Map each hex to the nearest DS `Badge` color (reuse your existing nearest-swatch logic, or a small lookup). DS `Badge` supports `legacyColor` for raw hex, but `RichTextarea`'s `TagOption` does not expose it yet — if exact hex pills are a hard requirement, file a DS ticket to add `legacyColor` passthrough to `TagOption` rather than blocking the migration. 3. **`maxLength` + counter.** RichTextarea has no `maxLength` or character counter. Track it in the consumer: count `plainText.length` in `onValueChange`, render your own counter, and enforce the limit (e.g. ignore updates once over the limit, or trim). Do not assume the component will cap input. 4. **`onBlur` / `onFocus`.** RichTextarea does not expose these props. If you need them, attach handlers to the wrapping element (`onBlurCapture` / `onFocusCapture` on a parent `div`), or file a DS ticket to add them. Do not invent props that are not in the type. 5. **Plain text keeps tag labels.** `onValueChange`'s `plainText` includes the `#tag` labels (e.g. `"Call @ 9am about #billing"`), matching `getPlainText`. Use `plainText`, not `json`, when the backend expects the legacy string. ## 5. Verify - `tsc --noEmit` — the callback changed from `onChange(string)` to `onValueChange({ plainText })`; fix every call site. - Confirm no `@tiptap/*` / `@xstyled/*` imports remain in the migrated file. - Manually: type text, `#` opens the tag menu, a picked tag renders as a colored pill, and your `onValueChange` receives the plain-text string. Enter inserts a newline (not submit). - Tests: DS popups need the jsdom shims from `@aircall/ds#aircall-ds/setup`.