mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
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:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,3 +1,4 @@
|
||||
export * from "./FormCloseButton"
|
||||
export * from "./FormSection"
|
||||
export * from "./FormDropdownField"
|
||||
export * from "./FormTextField"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user