bhargavshukla

Custom clipboard behaviour in React

Jun 10, 2024

Recently I worked on an interesting bug in a React app. We had a table that would highlight rows when you select them. The problem was when you tried to copy some text from a column in the table, it would copy the highlight styling as part of the text. This was not ideal, as you’d paste colourful text when you just want plaintext. All major OSes and word processors allow you to paste text without any formatting, but that’s an extra step for the end-user that we wanted to avoid.

In my research for a solution, I learned about the Clipboard API in JavaScript. My first attempt was straightforward. I needed to apply this behaviour across the entire app, so I intercepted the browser’s “copy” event in my app’s entry point with a useEffect hook (the actual component was class based, so I needed to use the class lifecycle methods componentDidMount and componentWillUnmount, but the idea remains the same in functional components).

function App() {
    // existing code
    useEffect(() => {
        document.addEventListener("copy", (event) => {
            const selection = document.getSelection();
            event.clipboardData.setData(
                "text/plain",
                selection.toString().trim()
            );
            event.preventDefault();
        });

        // cleanup function
        return () => document.removeEventListener("copy", () => {
        });
    }, [])
}

I ran some initial tests, and the text seemed to get copied and pasted without any fancy formatting or highlighting. Success! However, there was another problem. If the column contained a link, the hyperlink behind the text was also getting stripped out, which isn’t what I wanted. I needed a way to detect if the copied text contained a link, and skip the stripping logic in that case.

The document.getSelection() function returns a Selection object containing information about the selected text and the corresponding HTML node. Using that, I check if the parent element of the selected text is an a tag, and only override the clipboard if it isn’t. Here’s that implementation.

function App() {
    // existing code
    useEffect(() => {
        document.addEventListener("copy", (event) => {
            const selection = document.getSelection();
            if (selection.focusNode.parentElement.localName !== "a") {
                event.clipboardData.setData(
                    "text/plain",
                    selection.toString().trim()
                );
                event.preventDefault();
            }
        });

        // cleanup function
        return () => document.removeEventListener("copy", () => {
        });
    }, [])
}

There might be a better way to check for specific tags, but this approach worked for my use case. No more quirky highlighting issues! This was my first time interacting with the clipboard API, and it was a fun experience learning about its ins and outs!

If you have any questions or feedback about this article, feel free to email me at feedback (@) thesilverhand (.) blog.

Previous Article →

Intro to XState