Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 22, 2021 04:40 am GMT

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:

GIF showing example of React inline edit component

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 the screen 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

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To