Make the placeholder chips insert themselves at the cursor instead of only naming themselves #143

Closed
opened 2026-08-23 09:10:13 -05:00 by bermudalamb · 0 comments
Owner

The row of placeholder chips above each email editor is display-only. In frontend/src/admin/EmailTemplateEditor.tsx they are plain antd Tags:

{template.available.map((name) => (
  <Tag key={name} style={{ fontFamily: 'monospace' }}>{`{{${name}}}`}</Tag>
))}

They tell an admin which placeholders a template accepts and then leave them to retype {{holdDuration}} by hand into the subject or the body, getting the braces and the exact spelling right unaided. A typo does not announce itself either — a misspelled placeholder is not a required one, so the save succeeds and the email ships with a literal {{holdDuraton}} in it.

Clicking a chip should insert its tag at the current cursor position in whichever field was last focused, the subject line or the body.

This matters more since #136 than it did before. verification and passwordReset went from two placeholders to five, cartReminder from three to six, and favoriteSold and favoriteWithdrawn from two to five. The list is now long enough to be worth clicking rather than reading.

What makes this more than a click handler

There are two targets with different mechanics. The subject is a plain antd Input; the body is MDEditor from @uiw/react-md-editor, which owns its own textarea. Inserting means tracking which of the two was focused most recently, because a click on a chip moves focus away from both and the component then has to know where to put the text.

Cursor position has to survive the insert. Both fields are controlled — subject and body are React state — so the insert is a string splice plus a setState, after which React re-renders and the caret goes to the end of the field unless it is explicitly restored. It should land immediately after the inserted tag so an admin can keep typing. That means holding a ref to each underlying textarea/input and setting selectionStart/selectionEnd after the state has committed.

Mutating the DOM directly will appear to work and will not. Writing into the textarea's value does not update body, so the next keystroke re-renders from state and the inserted placeholder vanishes. The insert has to go through the same onChange path typing does.

A selection should be replaced, not straddled. If text is selected when the chip is clicked, the tag should replace it, which is what every editor does.

The chips need to become real controls. An antd Tag is a span, so as it stands a keyboard user cannot reach or activate one. Clicking is only half the feature; these should be buttons, reachable by tab and activated by enter and space, and still look like the compact monospace chips they are today.

There has to be a defined behaviour before either field is focused. Landing on the tab and clicking a chip straight away should do something predictable rather than nothing — appending to the body is the obvious default, but it should be a decision rather than an accident of which ref happens to be set.

Acceptance criteria

  • Clicking a placeholder chip inserts its {{tag}} at the caret in whichever of the subject or body was last focused
  • A selection in the target field is replaced by the tag
  • The caret ends up directly after the inserted tag, and the field keeps focus so typing continues naturally
  • The inserted text goes through the normal change path, so the preview updates and the value persists on save
  • Chips are keyboard reachable and activate on enter and space
  • Behaviour before either field has been focused is defined and predictable
  • Covered by an e2e test that inserts into both the subject and the body, mid-text rather than at the end
The row of placeholder chips above each email editor is display-only. In `frontend/src/admin/EmailTemplateEditor.tsx` they are plain antd `Tag`s: ```tsx {template.available.map((name) => ( <Tag key={name} style={{ fontFamily: 'monospace' }}>{`{{${name}}}`}</Tag> ))} ``` They tell an admin which placeholders a template accepts and then leave them to retype `{{holdDuration}}` by hand into the subject or the body, getting the braces and the exact spelling right unaided. A typo does not announce itself either — a misspelled placeholder is not a required one, so the save succeeds and the email ships with a literal `{{holdDuraton}}` in it. Clicking a chip should insert its tag at the current cursor position in whichever field was last focused, the subject line or the body. This matters more since #136 than it did before. `verification` and `passwordReset` went from two placeholders to five, `cartReminder` from three to six, and `favoriteSold` and `favoriteWithdrawn` from two to five. The list is now long enough to be worth clicking rather than reading. ### What makes this more than a click handler **There are two targets with different mechanics.** The subject is a plain antd `Input`; the body is `MDEditor` from `@uiw/react-md-editor`, which owns its own textarea. Inserting means tracking which of the two was focused most recently, because a click on a chip moves focus away from both and the component then has to know where to put the text. **Cursor position has to survive the insert.** Both fields are controlled — `subject` and `body` are React state — so the insert is a string splice plus a `setState`, after which React re-renders and the caret goes to the end of the field unless it is explicitly restored. It should land immediately after the inserted tag so an admin can keep typing. That means holding a ref to each underlying textarea/input and setting `selectionStart`/`selectionEnd` after the state has committed. **Mutating the DOM directly will appear to work and will not.** Writing into the textarea's `value` does not update `body`, so the next keystroke re-renders from state and the inserted placeholder vanishes. The insert has to go through the same `onChange` path typing does. **A selection should be replaced, not straddled.** If text is selected when the chip is clicked, the tag should replace it, which is what every editor does. **The chips need to become real controls.** An antd `Tag` is a `span`, so as it stands a keyboard user cannot reach or activate one. Clicking is only half the feature; these should be buttons, reachable by tab and activated by enter and space, and still look like the compact monospace chips they are today. **There has to be a defined behaviour before either field is focused.** Landing on the tab and clicking a chip straight away should do something predictable rather than nothing — appending to the body is the obvious default, but it should be a decision rather than an accident of which ref happens to be set. ### Acceptance criteria - Clicking a placeholder chip inserts its `{{tag}}` at the caret in whichever of the subject or body was last focused - A selection in the target field is replaced by the tag - The caret ends up directly after the inserted tag, and the field keeps focus so typing continues naturally - The inserted text goes through the normal change path, so the preview updates and the value persists on save - Chips are keyboard reachable and activate on enter and space - Behaviour before either field has been focused is defined and predictable - Covered by an e2e test that inserts into both the subject and the body, mid-text rather than at the end
bermudalamb added this to the Customer and Admin UI review findings project 2026-08-23 09:11:24 -05:00
bermudalamb self-assigned this 2026-08-23 09:11:30 -05:00
bermudalamb added reference feature/143-clickable-placeholders 2026-08-24 15:48:12 -05:00
bermudalamb moved this to Review in Customer and Admin UI review findings on 2026-08-24 15:48:19 -05:00
bermudalamb moved this to Ready for Release in Customer and Admin UI review findings on 2026-09-09 13:33:40 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#143