What Text Area Popovers Taught Me About Browser APIs

What Text Area Popovers Taught Me About Browser APIs


I recently went down several rabbit holes about building WYSIWYG editors and popovers. While on maternity leave, I finally have time to deeply explore random technical problems without deadlines – a rare opportunity.

Thus far, I’ve focused on contributing to an open source AI agent called codename goose built with a Rust backend and Electron-based chat interface. I submitted a pull request to add a WYSIWYG editor to its chat interface, opting to build a custom solution instead of using existing packages. The maintainers appreciated this approach since editor dependencies bloat bundle size, but raised concerns about the toolbar consuming too much visual space. They suggested implementing a popover toolbar instead.

I wrongly assumed creating a floating toolbar would be simple. I aimed to:

  • Show a popover toolbar when text is selected
  • Position it precisely above the selection
  • Handle word-wrapped text spanning multiple lines
  • Maintain accurate positioning during scroll

Handling popovers in a text area element turned out to be more complex than expected. In this blog post, I’ll share what I learned.



Text areas aren’t your average DOM elements.

Unlike typical HTML elements where we can manipulate contents, measure positions, and add new elements, text areas only expose raw text content and basic selection APIs. Browsers control their rendering behind the scenes.

I asked Claude to generate an analogy for further illustration:

  • Regular HTML elements are like having a house where you can move the furniture around, add new items, measure distances between things, etc.
  • Text areas are more like looking through a window into a room you can’t enter. You can see what’s inside and make some basic changes (like adding/removing text), but you can’t reach in and manipulate things directly. The browser handles all the internal workings using native OS text editing capabilities.



Popovers outside of text areas



The Popover API

All modern browsers include a built-in Popover API for creating popup elements. Here’s an example:



Limitations

While this API is cross-browser compatible and straightforward to implement, it comes with several limitations:

  • It only works with button elements since the required popovertarget attribute is only available on buttons
  • You have to use CSS to position the popover relative to its target element
  • And, my biggest limitation is that it doesn’t work within text areas.

Shout out to Mark Techson for introducing me to the Popover API via Una Kravets’ conference talk called Less Cruft, More Power: Leverage the Power of the Web Platform.



The Selection API

I wanted my popover to appear wherever the user selected text. This required me to:

  • Know the position of the selected text
  • Listen for events that happened when text was selected and deselected

I came across Colby Fayock’s blog post called How to Share Selected Text in React with the Selection API. While Colby’s focus was on text sharing functionality, his post introduced me to the Selection API – which could help me position my popover relative to selected text.

The Selection API lives in window.getSelection(). When you call this method, it returns a Selection object that tells you about the text a user has selected on the page.



getRangeAt(0)

From this Selection object, you can call getRangeAt(0) to find out exactly where the selection begins and ends. It gives you two numbers:

  • startOffset – where the selection begins
  • endOffset – where the selection ends

In a selection, each character has an index. For the text “Hello, World! Welcome.”, the indexes look like this:

If you select the word “World”, then startOffset is 7 (where “W” begins) and endOffset is 12 (right after “d”).

Side note: I learned that the parameter 0 in getRangeAt(0) tells the browser which selection you want information about. When you select text, each selection gets stored at different indexes in an array. Most browsers only let you select one piece of text at a time, so you’ll only have one item at index 0. But browsers like Firefox let you hold Ctrl to select multiple pieces of text. If you try to access indexes greater than 0 in browsers that don’t support multiple selections, you’ll get an error.



getBoundingClientRect()

getRangeAt(0) gives you access to getBoundingClientRect().

getBoundingClientRect() returns a box of measurements around your selected text. It tells you the top, right, bottom, and left positions of this box, plus its width and height.

With these measurements, I could place my popover right above any text the user selects, like this:

While this approach worked for most HTML elements, text area elements provide limited access to the Selection API, so I needed an alternative approach.



The Mirrored Div

Through rubber ducking with Claude, I learned about the mirrored div approach, a workaround for determining selection coordinates in a text area.

Here’s how it works: create an invisible div that overlays the text area, containing the exact same content and styling. When a user selects text, they’re actually interacting with this invisible div rather than the text area underneath it. This gives you access to the full Selection API while maintaining what looks and feels like a standard text area to the user.

I found validation for this technique in Jhey Thompkins’ blog post “HOW TO: Where’s the text cursor?“, which introduced me to the getComputedStyle() method. This API returns the computed CSS styles of HTML elements, letting developers match the text area’s appearance in the overlay div with precision.

But just like with a real mirror, things aren’t always what they seem. Similar to the warning on car mirrors that “objects may be closer than they appear,” our mirrored div can distort reality in subtle ways:

  • Text can wrap at different points between the div and text area
  • Browsers handle spacing and font rendering differently, causing text positions to shift unexpectedly



Why not use an NPM package?

I’ve tried a few packages, and I found that most of these packages work well with regular DOM elements. However, they struggle with text areas due to the same fundamental limitations I stated earlier – limited access to the text area’s internal rendering and positioning.



Conclusion

While browsers have come a long way in supporting rich text interactions, working with text areas remains surprisingly complex. I had fun learning about these browser APIs. Maybe future APIs will make tasks like selection-based popovers more straightforward.

If you’ve tackled text area customization in a way I haven’t explored, I’d love to hear about your approaches.



Source link
lol

By stp2y

Leave a Reply

Your email address will not be published. Required fields are marked *

No widgets found. Go to Widget page and add the widget in Offcanvas Sidebar Widget Area.