An Interest In:
Web News this Week
- March 21, 2024
- March 20, 2024
- March 19, 2024
- March 18, 2024
- March 17, 2024
- March 16, 2024
- March 15, 2024
How to build an inline edit component in React
Inline editing allows users to edit content without navigating to a separate edit screen. In this tutorial, well be building an accessible inline edit component in React. Heres the final product:
Well also learn how to write some unit tests with React Testing Library. Lets get started!
This tutorial assumes a basic understanding of React, including hooks.
If you want to jump straight to the full code, check out the React inline edit example on Codepen.
Inline editing and accessibility
When creating any React component, keep accessibility in mind. For example, your component should:
- Work with only a keyboard
- Use the correct HTML elements and other attributes to provide the most context to users
One way to approach writing an inline edit component is to have two separate components. One for a view mode and one for a edit mode:
// View mode<div onClick={startEditing}>Text value</div>// Edit mode<input value="Text value" />
When a user clicks on the view mode component, it will disappear and the edit mode will appear.
The second approach (and the one we will be implementing below) is to always use an input element. We can use CSS to make it look as though it has begun editing when a user focuses on it.
// View and edit mode<input value="Text value" />
By always using an input element, we get behaviours like tabbing and focusing for free. It also makes more explicit what the purpose of the component is.
Create your inline edit component with an input
Lets get started by creating a React component that uses the HTML input
tag:
const InlineEdit = ({ value, setValue }) => { const onChange = (event) => setValue(event.target.value); return ( <input type="text" aria-label="Field name" value={value} onChange={onChange} /> )}
The aria-label
tells screen reader users the purpose of the input. For instance, if it was the name of a list, you could use "List name".
Then, let's render our new InlineEdit
component, and pass in a value
and setValue
props:
const App = () => { const [value, setValue] = useState(); return <InlineEdit value={value} setValue={setValue} />;}
In a real-life app, the setValue
function would make an endpoint call to store the value in a database somewhere. For this tutorial though, we'll store the value in a useState
hook.
Add CSS to make it "click to edit"
Well then add some CSS to remove the input styling. This makes it look as though the user needs to click or focus on the input to start editing.
input { background-color: transparent; border: 0; padding: 8px;}
Well also add some styling to show that the component is editable when a user hovers over it:
input:hover { background-color: #d3d3d3; cursor: pointer;}
Allow users to save when they press Enter or Escape
If a user clicks away from the input, it will lose focus and return to view mode. To keep things keyboard-friendly, well want the escape and enter keys to achieve the same affect.
const InlineEdit = ({ value, setValue }) => { const onChange = (event) => setValue(event.target.value); const onKeyDown = (event) => { if (event.key === "Enter" || event.key === "Escape") { event.target.blur(); } } return ( <input type="text" aria-label="Field name" value={value} onChange={onChange} onKeyDown={onKeyDown} /> )}
Only save on exit
Currently we call the setValue
prop on each key press. In a real-life situation, where setValue
was making an endpoint call, it would be making an endpoint call per keypress.
We want to prevent this from happening until a user exits the input.
Lets create a local state variable called editingValue
. This is where well store the value of the input when it is in a editing phase.
const InlineEdit = ({ value, setValue }) => { const [editingValue, setEditingValue] = useState(value); const onChange = (event) => setEditingValue(event.target.value); const onKeyDown = (event) => { if (event.key === "Enter" || event.key === "Escape") { event.target.blur(); } } const onBlur = (event) => { setValue(event.target.value) } return ( <input type="text" aria-label="Field name" value={editingValue} onChange={onChange} onKeyDown={onKeyDown} onBlur={onBlur} /> )}
A user exiting the input will call the onBlur
handler. So we can use this to call setValue
.
Adding validation on empty strings
Finally, you dont want users to be able to save an empty string or spaces as a value. In that case, well cancel the edit and use the original value.
const onBlur = (event) => { if (event.target.value.trim() === "") { setValue(value); } else { setValue(event.target.value) }}
You'll now have a complete single-line inline edit component. Here's the full code:
import { useState } from 'react';const InlineEdit = ({ value, setValue }) => { const [editingValue, setEditingValue] = useState(value); const onChange = (event) => setEditingValue(event.target.value); const onKeyDown = (event) => { if (event.key === "Enter" || event.key === "Escape") { event.target.blur(); } } const onBlur = (event) => { if (event.target.value.trim() === "") { setEditingValue(value); } else { setValue(event.target.value) } } return ( <input type="text" aria-label="Field name" value={editingValue} onChange={onChange} onKeyDown={onKeyDown} onBlur={onBlur} /> );};const App = () => { const [value, setValue] = useState(); return <InlineEdit value={value} setValue={setValue} />;};
Creating a multiline inline edit
If you want your inline edit component to be multiline, we can use the textarea
element instead:
<textarea rows={1} aria-label="Field name" value={editingValue} onBlur={onBlur} onChange={onChange} onKeyDown={onKeyDown}/>
The one difference with textarea is that you pass in a rows
value. This specifies the height of your textarea.
By default, textareas aren't dynamic. Luckily, over on StackOverflow I found a solution to this problem.
If you add the following CSS to your text area:
textarea { resize: none; overflow: hidden; min-height: 14px; max-height: 100px;}
And then pass in an onInput
handler, youll be able to achieve a dynamic look.
import { useEffect } from 'react';const onInput = (event) => { if (event.target.scrollHeight > 33) { event.target.style.height = "5px"; event.target.style.height = (event.target.scrollHeight - 16) + "px"; }}return ( <textarea rows={1} aria-label="Field name" value={editingValue} onBlur={onBlur} onChange={onChange} onKeyDown={onKeyDown} onInput={onInput} />)
Note you may need to fiddle around with some of the values in the onInput
depending on the height and font size of your text area.
The one other thing youll need to add is a focus ring - the blue outline around a focused element. We can do this with some CSS:
textarea:focus { outline: 5px auto Highlight; /* Firefox */ outline: 5px auto -webkit-focus-ring-color; /* Chrome, Safari */}
And youre done! Here's the full code for a multiline inline edit:
import { useState, useRef } from 'react';const MultilineEdit = ({ value, setValue }) => { const [editingValue, setEditingValue] = useState(value); const onChange = (event) => setEditingValue(event.target.value); const onKeyDown = (event) => { if (event.key === "Enter" || event.key === "Escape") { event.target.blur(); } }; const onBlur = (event) => { if (event.target.value.trim() === "") { setEditingValue(value); } else { setValue(event.target.value); } }; const onInput = (target) => { if (target.scrollHeight > 33) { target.style.height = "5px"; target.style.height = target.scrollHeight - 16 + "px"; } }; const textareaRef = useRef(); useEffect(() => { onInput(textareaRef.current); }, [onInput, textareaRef]); return ( <textarea rows={1} aria-label="Field name" value={editingValue} onBlur={onBlur} onChange={onChange} onKeyDown={onKeyDown} onInput={(event) => onInput(event.target)} ref={textareaRef} /> );};
Ensure your component's functionality with unit tests
Before we finish, lets write a couple of unit tests to ensure the functionality of our component. Well be using React Testing Library:
npm install --save-dev @testing-library/react @testing-library/user-event# oryarn add -D @testing-library/react @testing-library/user-event
We can ensure that pressing enter causes the input to lose focus:
import { useState } from 'react';import { fireEvent, render, screen } from "@testing-library/react";import userEvent from "@testing-library/user-event";import InlineEdit from "./Inline-Edit";const apples = "apples"const oranges = "oranges"const TestComponent = () => { const [value, setValue] = useState(apples); return <InlineEdit value={value} setValue={setValue} />;}describe("Inline Edit component", () => { test("should save input and lose focus when user presses enter", () => { render(<TestComponent />) const input = screen.getByRole("textbox"); userEvent.type(input, `{selectall}${oranges}{enter}`); // RTL doesn't properly trigger component's onBlur() fireEvent.blur(input); expect(input).not.toHaveFocus(); expect(input).toHaveValue(oranges); });});
If you havent used React Testing Library before, lets break this test down:
- The
render
function will render your component into a container. You can access it using thescreen
variable - We search for the input component via its aria role,
"textbox"
- We can use the
userEvent.type()
function to simulate a user typing. If you want to type special keys like space or enter, you can do it with curly braces around it (e.g{space}
and{enter}
)
Similarly, we can write two more unit tests:
test("should focus when tabbed to", () => { render(<TestComponent />); const input = screen.getByRole("textbox"); expect(document.body).toHaveFocus(); userEvent.tab(); expect(input).toHaveFocus();});test("should reset to last-saved value if input is empty", () => { render(<TestComponent />); const input = screen.getByRole("textbox"); userEvent.type(input, "{selectall}{space}{enter}"); fireEvent.blur(input); expect(input).toHaveValue(originalName)});
And finally, we can use a cool library called jest-axe. You can use it to assert that your component doesnt have any accessibility violations:
import { axe, toHaveNoViolations } from "jest-axe"expect.extend(toHaveNoViolations)test("should not have any accessibility violations", async () => { const { container } = render(<TestComponent />); const results = await axe(container); expect(results).toHaveNoViolations();});
If we had forgotten to include an aria-label
, for instance, then this test would have failed.
And that's it! Now you should be able to create inline-editable components for your React app, complete with unit tests.
Original Link: https://dev.to/emma/how-to-build-an-inline-edit-component-in-react-358p
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To