1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

Autofocus dialog form fields, allow form submissions via pressing enter inside the form (#524)

* chore: add prettier as a dev dependency

The project has a .prettierrc, so seems to depend on that for its
formatting, but there was no prettier installed with the node modules.

* chore: add autofocus to all clearly defined first inputs on dialogs.

* fix: wrap the disable env input in a form and give it autofocus.

* fix: submit form when pressing enter

* fix: only autofocus the submit button if there is no other content.

When multiple (enabled) elements have the autofocus attribute, the
browser picks the last element in the tree. This means that if there
is a form with a text input with autofocus and a submit button with
autofocus, the button will win, causing the user to have to tab back up.

Only doing this if there are no children will cause some changes,
however:

Dialogs with textual children will no longer focus the accept-button
when appearing.

However, dialogs such as the create new api token dialog will give the
focus to the first input field instead of to the create button.

* fix: add formId prop to dialog element; adapt behavior

If the component receives a form id, it will treat the primary button
as the submit button for that form. To stop a full page reload, we
call the `preventDefault` on the submit event before calling the handler.

* chore: remove redundant spacing in component.

* fix: hook environment disable form up with the new form id system.

* chore: Update existing modal forms to pass in formId

* fix: Type the dialog event wrapper

* fix: change 'allows' => 'allow' because the noun is pluralized.

* fix: add autofocus to js add-tag-dialog-component.

I've got a feeling this component isn't in use anymore, though, as the
exact same text appears in a TS-version of this component.

* fix: add autofocus to add user form.

This seems to only be used as the main piece of a modal, so adding
autofocus seems pretty safe here, but I could be wrong.

* fix: Update snapshot test after changing wording.

* fix: add autofocus to update user form

* fix: add autofocus to the create toggle form.

This is a little besides the task's actual point. However! This form
is only ever used on the page where it's the only bit of content. I'd
argue that when the user navigates to this form, it's because they
want to create a feature. Thus, adding autofocus to the first field
makes a lot of sense to me.

* refactor: set button type to 'undefined' when it isn't 'submit'

This allows Material to use their default type based on whatever
heuristics they use. It's most likely going to be 'button' for the
foreseeable future, but in the event that they change it, passing
undefined instead should future-proof this a bit.

* fix: set type to button when formId is not present

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Thomas Heartman 2021-11-29 15:18:12 +01:00 committed by GitHub
parent 165170cd5c
commit dd1ab1ca72
17 changed files with 99 additions and 29 deletions

View File

@ -73,6 +73,7 @@
"lodash.clonedeep": "4.5.0",
"lodash.flow": "3.5.0",
"node-fetch": "2.6.6",
"prettier": "^2.4.1",
"react": "17.0.2",
"react-dnd": "14.0.4",
"react-dnd-html5-backend": "14.0.2",

View File

@ -144,6 +144,8 @@ const ApiTokenCreate = ({
{ key: 'ADMIN', label: 'Admin', title: 'Admin API token' },
];
const formId = 'create-api-token-form';
return (
<Dialogue
onClick={() => submit()}
@ -152,8 +154,10 @@ const ApiTokenCreate = ({
primaryButtonText="Create"
secondaryButtonText="Cancel"
title="New API token"
formId={formId}
>
<form
id={formId}
onSubmit={submit}
className={classNames(
styles.addApiKeyForm,
@ -161,6 +165,7 @@ const ApiTokenCreate = ({
)}
>
<TextField
autoFocus
value={data.username}
name="username"
onChange={setUsername}

View File

@ -22,6 +22,7 @@ interface IDialogue {
fullWidth?: boolean;
maxWidth?: 'lg' | 'sm' | 'xs' | 'md' | 'xl';
disabledPrimaryButton?: boolean;
formId?: string;
}
const Dialogue: React.FC<IDialogue> = ({
@ -35,8 +36,15 @@ const Dialogue: React.FC<IDialogue> = ({
secondaryButtonText,
maxWidth = 'sm',
fullWidth = false,
formId,
}) => {
const styles = useStyles();
const handleClick = formId
? (e: React.SyntheticEvent) => {
e.preventDefault();
onClick(e);
}
: onClick;
return (
<Dialog
open={open}
@ -61,12 +69,14 @@ const Dialogue: React.FC<IDialogue> = ({
condition={Boolean(onClick)}
show={
<Button
form={formId}
color="primary"
variant="contained"
onClick={onClick}
autoFocus
onClick={handleClick}
autoFocus={!formId}
disabled={disabledPrimaryButton}
data-test={DIALOGUE_CONFIRM_ID}
type={formId ? 'submit' : 'button'}
>
{primaryButtonText || "Yes, I'm sure"}
</Button>
@ -77,7 +87,7 @@ const Dialogue: React.FC<IDialogue> = ({
condition={Boolean(onClose)}
show={
<Button onClick={onClose}>
{secondaryButtonText || 'No take me back'}{' '}
{secondaryButtonText || 'No, take me back'}
</Button>
}
/>

View File

@ -75,6 +75,8 @@ const EditEnvironment = ({
setType(env.type);
};
const formId = 'edit-environment-form';
return (
<Dialogue
open={editEnvironment}
@ -84,6 +86,7 @@ const EditEnvironment = ({
primaryButtonText="Save"
secondaryButtonText="Cancel"
disabledPrimaryButton={isDisabled()}
formId={formId}
>
<div className={styles.body} ref={ref}>
<h3 className={styles.formHeader} data-loading>
@ -92,7 +95,7 @@ const EditEnvironment = ({
<h3 className={styles.subheader} data-loading>
<CloudCircle className={styles.icon} /> {env.name}
</h3>
<form>
<form id={formId}>
<EnvironmentTypeSelector
onChange={handleTypeChange}
value={type}

View File

@ -34,6 +34,8 @@ const EnvironmentDeleteConfirm = ({
setConfirmName('');
};
const formId = 'delete-environment-confirmation-form';
return (
<Dialogue
title="Are you sure you want to delete this environment?"
@ -43,6 +45,7 @@ const EnvironmentDeleteConfirm = ({
onClick={handleDeleteEnvironment}
disabledPrimaryButton={env?.name !== confirmName}
onClose={handleCancel}
formId={formId}
>
<Alert severity="error">
Danger. Deleting this environment will result in removing all
@ -59,12 +62,15 @@ const EnvironmentDeleteConfirm = ({
environment in the textfield below: <strong>{env?.name}</strong>
</p>
<Input
onChange={handleChange}
value={confirmName}
label="Environment name"
className={styles.environmentDeleteInput}
/>
<form id={formId}>
<Input
autoFocus
onChange={handleChange}
value={confirmName}
label="Environment name"
className={styles.environmentDeleteInput}
/>
</form>
</Dialogue>
);
};

View File

@ -110,6 +110,7 @@ const FeatureCreate = () => {
<input type="hidden" name="project" value={projectId} />
<div className={styles.formContainer}>
<Input
autoFocus
label="Name"
placeholder="Unique-name"
className={styles.nameInput}

View File

@ -66,6 +66,8 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
}
};
const formId = 'add-tag-form';
return (
<>
<Dialogue
@ -76,14 +78,16 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
onClick={onSubmit}
disabledPrimaryButton={loading}
onClose={onCancel}
formId={formId}
>
<>
<DialogContentText>
Tags allows you to group features together
Tags allow you to group features together
</DialogContentText>
<form onSubmit={onSubmit}>
<form id={formId} onSubmit={onSubmit}>
<section className={styles.dialogFormContent}>
<TagSelect
autoFocus
name="type"
value={tag.type}
onChange={e => setValue('type', e.target.value)}

View File

@ -212,6 +212,8 @@ const AddVariant = ({
const isFixWeight = data.weightType === weightTypes.FIX;
const formId = 'add-feature-variant-form';
return (
<Dialogue
open={showDialog}
@ -224,11 +226,17 @@ const AddVariant = ({
title={title}
fullWidth
maxWidth="md"
formId={formId}
>
<form onSubmit={submit} className={commonStyles.contentSpacingY}>
<form
id={formId}
onSubmit={submit}
className={commonStyles.contentSpacingY}
>
<p style={{ color: 'red' }}>{error.general}</p>
<TextField
label="Variant name"
autoFocus
name="name"
placeholder=""
className={commonStyles.fullWidth}

View File

@ -62,6 +62,7 @@ class AddTagDialogComponent extends Component {
};
render() {
const { tag, errors, openDialog } = this.state;
const formId = 'add-tag-dialog-form';
return (
<React.Fragment>
<Button onClick={this.handleOpenDialog.bind(this)}>
@ -75,14 +76,16 @@ class AddTagDialogComponent extends Component {
title="Add tags to feature toggle"
onClick={this.onSubmit}
onClose={this.onCancel}
formId={formId}
>
<>
<DialogContentText>
Tags allows you to group features together
Tags allow you to group features together
</DialogContentText>
<form onSubmit={this.onSubmit}>
<form id={formId} onSubmit={this.onSubmit}>
<section className={styles.dialogueFormContent}>
<TagSelect
autoFocus
name="type"
value={tag.type}
onChange={v =>

View File

@ -178,6 +178,8 @@ const AddVariant = ({
const isFixWeight = data.weightType === weightTypes.FIX;
const formId = 'add-variant-form';
return (
<Dialog
open={showDialog}
@ -190,10 +192,16 @@ const AddVariant = ({
title={title}
fullWidth
maxWidth="md"
formId={formId}
>
<form onSubmit={submit} className={commonStyles.contentSpacingY}>
<form
id={formId}
onSubmit={submit}
className={commonStyles.contentSpacingY}
>
<p style={{ color: 'red' }}>{error.general}</p>
<TextField
autoFocus
label="Variant name"
name="name"
placeholder=""

View File

@ -27,6 +27,8 @@ const EnvironmentDisableConfirm = ({
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setConfirmName(e.currentTarget.value);
const formId = 'disable-environment-confirmation-form';
return (
<Dialogue
title="Are you sure you want to disable this environment?"
@ -36,10 +38,11 @@ const EnvironmentDisableConfirm = ({
onClick={() => handleDisableEnvironment()}
disabledPrimaryButton={env?.name !== confirmName}
onClose={handleCancelDisableEnvironment}
formId={formId}
>
<Alert severity="error">
Danger. Disabling an environment can impact client applications
connected to the environment and result in feature toggles being
Danger. Disabling an environment can impact client applications
connected to the environment and result in feature toggles being
disabled.
</Alert>
@ -48,12 +51,15 @@ const EnvironmentDisableConfirm = ({
environment in the textfield below: <strong>{env?.name}</strong>
</p>
<Input
onChange={handleChange}
value={confirmName}
label="Environment name"
className={styles.environmentDeleteInput}
/>
<form id={formId}>
<Input
autoFocus
onChange={handleChange}
value={confirmName}
label="Environment name"
className={styles.environmentDeleteInput}
/>
</form>
</Dialogue>
);
};

View File

@ -37,7 +37,7 @@ exports[`it supports editMode 1`] = `
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
Tag types allows you to group tags together in the management UI
Tag types allow you to group tags together in the management UI
</h6>
<form
className="addTagTypeForm contentSpacing"
@ -140,7 +140,7 @@ exports[`renders correctly for creating 1`] = `
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
Tag types allows you to group tags together in the management UI
Tag types allow you to group tags together in the management UI
</h6>
<form
className="addTagTypeForm contentSpacing"
@ -243,7 +243,7 @@ exports[`renders correctly for creating without permissions 1`] = `
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
Tag types allows you to group tags together in the management UI
Tag types allow you to group tags together in the management UI
</h6>
<form
className="addTagTypeForm contentSpacing"

View File

@ -71,7 +71,7 @@ const AddTagTypeComponent = ({
)}
>
<Typography variant="subtitle1">
Tag types allows you to group tags together in the
Tag types allow you to group tags together in the
management UI
</Typography>
<form

View File

@ -61,11 +61,14 @@ const AddUser = ({
closeDialog();
};
const formId = 'add-user-dialog-form';
return (
<Dialogue
onClick={e => {
submit(e);
}}
formId={formId}
open={showDialog}
onClose={onCancel}
primaryButtonText="Add user"
@ -74,6 +77,7 @@ const AddUser = ({
fullWidth
>
<AddUserForm
formId={formId}
userApiErrors={userApiErrors}
data={data}
setData={setData}

View File

@ -30,6 +30,7 @@ function AddUserForm({
roles,
userLoading,
userApiErrors,
formId,
}) {
const ref = useLoading(userLoading);
const commonStyles = useCommonStyles();
@ -76,7 +77,7 @@ function AddUserForm({
userApiErrors[ADD_USER_ERROR] || userApiErrors[UPDATE_USER_ERROR];
return (
<div ref={ref}>
<form onSubmit={submit}>
<form id={formId} onSubmit={submit}>
<DialogContent>
<ConditionallyRender
condition={apiError}
@ -110,6 +111,7 @@ function AddUserForm({
/>
<TextField
autoFocus
label="Full name"
data-loading
name="name"

View File

@ -44,11 +44,14 @@ function AddUser({
closeDialog();
};
const formId = 'update-user-dialog-form'
return (
<Dialogue
onClick={e => {
submit(e);
}}
formId={formId}
open={showDialog}
onClose={onCancel}
primaryButtonText="Update user"
@ -64,6 +67,7 @@ function AddUser({
error={error}
userApiErrors={userApiErrors}
userLoading={userLoading}
formId={formId}
/>
</Dialogue>
);

View File

@ -10155,6 +10155,11 @@ prepend-http@^1.0.0:
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
pretty-bytes@^5.3.0, pretty-bytes@^5.6.0:
version "5.6.0"
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz"