The Textarea is inspired by GitHub's PR comment section. The impressive part is the @mention support including hover cards in the preview. The goal is to reproduce it without text editor library.
UI
Let's keep it short and use ui.shadcn components, which are mainly radix-ui primitives, styled with tailwindcss.
To style the preview, we use the @tailwindcss/typography plugin including the prose utility class.
A Combobox inside the Textarea
Getting the right keyboard, mouse and touch events and keeping the textarea focus while navigating through the mention Combobox required lots of work and testing. If you recognize any unexpected behavior, feel free to create a GitHub Issue.
For our implementation, three utility functions have been created:
getCaretCoordinates(): creates a duplicate textarea as div to return the current { top, left, height } properties of the caret (cheat seen in textarea-caret-position)
getCurrentWord(): returns the current word where the caret is at
replaceWord(value): replaces the word where the caret is at with the new word
Let's have a look at a simplified version of the Write Component:
Important implementation details are:
We hide the CommandInput and update the value via state updates. That way, it allows us to use the cmdk package with our custom use case under the hood.
We propagate keyboard events from the textarea to the input field when dropdown is visible with: inputRef.current?.dispatchEvent(new KeyboardEvent("keydown", e))
The Textarea listens to the caret position and the current word where the caret actually is and will be displayed whenever the currentWord.startsWith(“@“).
We didn't wrap our Command Component around a Popover or CommandDialog (like we did in Fancy Box). The Components would focus automatically and we would lose the ability to continue writing in the textarea.
The supported people are statically written inside of the code (see data.ts). A re-iteration of that Fancy Area Component could include dynamic data fetching via API and therefore using the Command.Loading Component.
To replace a word, we are using the deprecated, but still heavily used and yet supported function:
The reason: it easily supports undo. Please contact me if you know a simple non-deprecated way to do the same.
Transform Markdown into React
The transformation is mainly handled by rehype plugins.
Let's break all the plugins down!
The following steps transform the user input into valid react components:
rehype-react: transforms HTML into react components
We are using a regular expression (/@(\w+)/g) to wrap all the words, starting with "@", inside of a custom <mention> element. This also means that we need to tell rehype-sanitize that this specific html-tag and its corresponding attribute handle is safe as well as extending the rehype-react object that maps tag names to components. In our case { mention: Mention }.
The logic can be used for any custom Component!
We now can safely import the users content with:
Mention HoverCard
The Mention Component that is being used in the preview is a copy cat of shadcn example. This is also why the Avatar Component is being used. Period.