fix: Forms - Add 'escape' button / key handler in forms (#240)

In v1, we had a quick and easy way to leave creation forms - an escape button (and key handler) in the top right corner.

This was especially helpful in cases where the form was long enough that the 'Cancel' button was off-screen.

This ports over that component into v2 and hooks up into our two existing forms:
<img width="977" alt="Screen Shot 2022-02-09 at 5 48 03 PM" src="https://user-images.githubusercontent.com/88213859/153321503-af957f2b-d674-4ebf-9ac5-97939cb9153f.png">

In addition, this adds test cases + a storybook story for it.
This commit is contained in:
Bryan
2022-02-15 16:43:27 -08:00
committed by GitHub
parent a86f2ee11d
commit 35291d358a
7 changed files with 162 additions and 2 deletions
@@ -0,0 +1,16 @@
import { Story } from "@storybook/react"
import React from "react"
import { FormCloseButton, FormCloseButtonProps } from "./FormCloseButton"
export default {
title: "Form/FormCloseButton",
component: FormCloseButton,
argTypes: {
onClose: { action: "onClose" },
},
}
const Template: Story<FormCloseButtonProps> = (args) => <FormCloseButton {...args} />
export const Example = Template.bind({})
Example.args = {}
@@ -0,0 +1,70 @@
import { fireEvent, render, screen } from "@testing-library/react"
import React from "react"
import { FormCloseButton } from "./FormCloseButton"
describe("FormCloseButton", () => {
it("renders", async () => {
// When
render(
<FormCloseButton
onClose={() => {
return
}}
/>,
)
// Then
await screen.findByText("ESC")
})
it("calls onClose when clicked", async () => {
// Given
const onClose = jest.fn()
// When
render(<FormCloseButton onClose={onClose} />)
// Then
const element = await screen.findByText("ESC")
// When
fireEvent.click(element)
// Then
expect(onClose).toBeCalledTimes(1)
})
it("calls onClose when escape is pressed", async () => {
// Given
const onClose = jest.fn()
// When
render(<FormCloseButton onClose={onClose} />)
// Then
const element = await screen.findByText("ESC")
// When
fireEvent.keyDown(element, { key: "Escape", code: "Esc", charCode: 27 })
// Then
expect(onClose).toBeCalledTimes(1)
})
it("doesn't call onClose if another key is pressed", async () => {
// Given
const onClose = jest.fn()
// When
render(<FormCloseButton onClose={onClose} />)
// Then
const element = await screen.findByText("ESC")
// When
fireEvent.keyDown(element, { key: "Enter", code: "Enter", charCode: 13 })
// Then
expect(onClose).toBeCalledTimes(0)
})
})
+55
View File
@@ -0,0 +1,55 @@
import IconButton from "@material-ui/core/IconButton"
import { makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import React, { useEffect } from "react"
import { CloseIcon } from "../Icons/Close"
export interface FormCloseButtonProps {
onClose: () => void
}
export const FormCloseButton: React.FC<FormCloseButtonProps> = ({ onClose }) => {
const styles = useStyles()
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose()
}
}
document.body.addEventListener("keydown", handleKeyPress, false)
return () => {
document.body.removeEventListener("keydown", handleKeyPress, false)
}
}, [])
return (
<IconButton className={styles.closeButton} onClick={onClose} size="medium">
<CloseIcon />
<Typography variant="caption" className={styles.label}>
ESC
</Typography>
</IconButton>
)
}
const useStyles = makeStyles((theme) => ({
closeButton: {
position: "fixed",
top: theme.spacing(3),
right: theme.spacing(6),
opacity: 0.5,
color: theme.palette.text.primary,
"&:hover": {
opacity: 1,
},
},
label: {
position: "absolute",
left: "50%",
top: "100%",
transform: "translate(-50%, 50%)",
},
}))
+1
View File
@@ -1,3 +1,4 @@
export * from "./FormCloseButton"
export * from "./FormSection"
export * from "./FormDropdownField"
export * from "./FormTextField"
+8
View File
@@ -0,0 +1,8 @@
import SvgIcon from "@material-ui/core/SvgIcon"
import React from "react"
export const CloseIcon: typeof SvgIcon = (props) => (
<SvgIcon {...props} viewBox="0 0 31 31">
<path d="M29.5 1.5l-28 28M29.5 29.5l-28-28" stroke="currentcolor" strokeMiterlimit="10" strokeLinecap="square" />
</SvgIcon>
)
+9 -1
View File
@@ -4,7 +4,14 @@ import { FormikContextType, useFormik } from "formik"
import React from "react"
import * as Yup from "yup"
import { DropdownItem, FormDropdownField, FormTextField, FormTitle, FormSection } from "../components/Form"
import {
DropdownItem,
FormDropdownField,
FormTextField,
FormTitle,
FormSection,
FormCloseButton,
} from "../components/Form"
import { LoadingButton } from "../components/Button"
import { Organization, Project, Provisioner, CreateProjectRequest } from "./../api"
@@ -59,6 +66,7 @@ export const CreateProjectForm: React.FC<CreateProjectFormProps> = ({
return (
<div className={styles.root}>
<FormTitle title="Create Project" />
<FormCloseButton onClose={onCancel} />
<FormSection title="Name">
<FormTextField
+3 -1
View File
@@ -4,7 +4,7 @@ import { FormikContextType, useFormik } from "formik"
import React from "react"
import * as Yup from "yup"
import { FormTextField, FormTitle, FormSection } from "../components/Form"
import { FormCloseButton, FormTextField, FormTitle, FormSection } from "../components/Form"
import { LoadingButton } from "../components/Button"
import { Project, Workspace, CreateWorkspaceRequest } from "../api"
@@ -45,6 +45,8 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({ project, on
</span>
}
/>
<FormCloseButton onClose={onCancel} />
<FormSection title="Name">
<FormTextField
form={form}