[MM-62974][MM-62984][MM-63016][MM-63025] Various fixes for the Create Team screen (#30905)

* [MM-62974] Focus team name inputs when error occurs

* [MM-62984] Change h5 to label on create team screens

* [MM-63016] Add a page title for the create team page

* [MM-63025] Add role=alert and aria-describedby for error messages on create team page

* Fix i18n
Этот коммит содержится в:
Devin Binnie
2025-05-07 15:20:22 -04:00
коммит произвёл GitHub
родитель ee61301b67
Коммит 7efb83396b
9 изменённых файлов: 95 добавлений и 14 удалений

Просмотреть файл

@@ -8,13 +8,15 @@ exports[`/components/create_team/components/display_name should match snapshot 1
className="signup-team-logo" className="signup-team-logo"
src="logo.png" src="logo.png"
/> />
<h5> <label
htmlFor="teamNameInput"
>
<MemoizedFormattedMessage <MemoizedFormattedMessage
defaultMessage="Team Name" defaultMessage="Team Name"
id="create_team.display_name.teamName" id="create_team.display_name.teamName"
tagName="strong" tagName="strong"
/> />
</h5> </label>
<div <div
className="form-group" className="form-group"
> >
@@ -25,6 +27,7 @@ exports[`/components/create_team/components/display_name should match snapshot 1
className="col-sm-9" className="col-sm-9"
> >
<input <input
aria-describedby="teamNameInputError"
autoFocus={true} autoFocus={true}
className="form-control" className="form-control"
id="teamNameInput" id="teamNameInput"

Просмотреть файл

@@ -169,4 +169,19 @@ describe('/components/create_team/components/display_name', () => {
/>, />,
); );
}); });
test('should focus input when validation error occurs', () => {
const wrapper = mountWithIntl(<DisplayName {...defaultProps}/>);
const input = wrapper.find('.form-control').getDOMNode() as HTMLInputElement;
const focusSpy = jest.spyOn(input, 'focus');
// Trigger validation error
input.value = '';
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {
preventDefault: () => jest.fn(),
});
expect(focusSpy).toHaveBeenCalled();
});
}); });

Просмотреть файл

@@ -36,8 +36,11 @@ type State = {
} }
export default class TeamSignupDisplayNamePage extends React.PureComponent<Props, State> { export default class TeamSignupDisplayNamePage extends React.PureComponent<Props, State> {
teamNameInput: React.RefObject<HTMLInputElement>;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.teamNameInput = React.createRef();
this.state = { this.state = {
teamDisplayName: this.props.state.team?.display_name || '', teamDisplayName: this.props.state.team?.display_name || '',
@@ -59,6 +62,7 @@ export default class TeamSignupDisplayNamePage extends React.PureComponent<Props
defaultMessage='This field is required' defaultMessage='This field is required'
/>), />),
}); });
this.teamNameInput.current?.focus();
return; return;
} else if (displayName.length < Constants.MIN_TEAMNAME_LENGTH || displayName.length > Constants.MAX_TEAMNAME_LENGTH) { } else if (displayName.length < Constants.MIN_TEAMNAME_LENGTH || displayName.length > Constants.MAX_TEAMNAME_LENGTH) {
this.setState({nameError: ( this.setState({nameError: (
@@ -71,6 +75,7 @@ export default class TeamSignupDisplayNamePage extends React.PureComponent<Props
}} }}
/>), />),
}); });
this.teamNameInput.current?.focus();
return; return;
} }
@@ -94,7 +99,15 @@ export default class TeamSignupDisplayNamePage extends React.PureComponent<Props
let nameError = null; let nameError = null;
let nameDivClass = 'form-group'; let nameDivClass = 'form-group';
if (this.state.nameError) { if (this.state.nameError) {
nameError = <label className='control-label'>{this.state.nameError}</label>; nameError = (
<label
role='alert'
className='control-label'
id='teamNameInputError'
>
{this.state.nameError}
</label>
);
nameDivClass += ' has-error'; nameDivClass += ' has-error';
} }
@@ -106,19 +119,20 @@ export default class TeamSignupDisplayNamePage extends React.PureComponent<Props
className='signup-team-logo' className='signup-team-logo'
src={logoImage} src={logoImage}
/> />
<h5> <label htmlFor='teamNameInput'>
<FormattedMessage <FormattedMessage
id='create_team.display_name.teamName' id='create_team.display_name.teamName'
tagName='strong' tagName='strong'
defaultMessage='Team Name' defaultMessage='Team Name'
/> />
</h5> </label>
<div className={nameDivClass}> <div className={nameDivClass}>
<div className='row'> <div className='row'>
<div className='col-sm-9'> <div className='col-sm-9'>
<input <input
id='teamNameInput' id='teamNameInput'
type='text' type='text'
ref={this.teamNameInput}
className='form-control' className='form-control'
placeholder='' placeholder=''
maxLength={128} maxLength={128}
@@ -127,6 +141,7 @@ export default class TeamSignupDisplayNamePage extends React.PureComponent<Props
onFocus={this.handleFocus} onFocus={this.handleFocus}
onChange={this.handleDisplayNameChange} onChange={this.handleDisplayNameChange}
spellCheck='false' spellCheck='false'
aria-describedby='teamNameInputError'
/> />
</div> </div>
</div> </div>

Просмотреть файл

@@ -8,13 +8,15 @@ exports[`/components/create_team/components/display_name should match snapshot 1
className="signup-team-logo" className="signup-team-logo"
src="logo.png" src="logo.png"
/> />
<h5> <label
htmlFor="teamURLInput"
>
<MemoizedFormattedMessage <MemoizedFormattedMessage
defaultMessage="Team URL" defaultMessage="Team URL"
id="create_team.team_url.teamUrl" id="create_team.team_url.teamUrl"
tagName="strong" tagName="strong"
/> />
</h5> </label>
<div <div
className="form-group" className="form-group"
> >
@@ -37,6 +39,7 @@ exports[`/components/create_team/components/display_name should match snapshot 1
</span> </span>
</WithTooltip> </WithTooltip>
<input <input
aria-describedby="teamURLInputError"
autoFocus={true} autoFocus={true}
className="form-control" className="form-control"
id="teamURLInput" id="teamURLInput"

Просмотреть файл

@@ -126,4 +126,19 @@ describe('/components/create_team/components/display_name', () => {
expect(screen.getByText('Please try another.', {exact: false})).toBeInTheDocument(); expect(screen.getByText('Please try another.', {exact: false})).toBeInTheDocument();
}); });
test('should focus input when validation error occurs', () => {
renderWithContext(
<TeamUrl {...defaultProps}/>,
);
const input = screen.getByRole('textbox');
userEvent.clear(input);
const focusSpy = jest.spyOn(input, 'focus');
// Trigger validation error by submitting empty input
screen.getByText('Finish').click();
expect(focusSpy).toHaveBeenCalled();
});
}); });

Просмотреть файл

@@ -57,9 +57,11 @@ type Props = {
} }
export default class TeamUrl extends React.PureComponent<Props, State> { export default class TeamUrl extends React.PureComponent<Props, State> {
teamURLInput: React.RefObject<HTMLInputElement>;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.teamURLInput = React.createRef();
this.state = { this.state = {
nameError: '', nameError: '',
isLoading: false, isLoading: false,
@@ -95,6 +97,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
defaultMessage='This field is required' defaultMessage='This field is required'
/>), />),
}); });
this.teamURLInput.current?.focus();
return; return;
} }
@@ -109,6 +112,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
}} }}
/>), />),
}); });
this.teamURLInput.current?.focus();
return; return;
} }
@@ -119,6 +123,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
defaultMessage="Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash." defaultMessage="Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."
/>), />),
}); });
this.teamURLInput.current?.focus();
return; return;
} }
@@ -191,7 +196,15 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
let nameError = null; let nameError = null;
let nameDivClass = 'form-group'; let nameDivClass = 'form-group';
if (this.state.nameError) { if (this.state.nameError) {
nameError = <label className='control-label'>{this.state.nameError}</label>; nameError = (
<label
role='alert'
className='control-label'
id='teamURLInputError'
>
{this.state.nameError}
</label>
);
nameDivClass += ' has-error'; nameDivClass += ' has-error';
} }
@@ -221,13 +234,13 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
className='signup-team-logo' className='signup-team-logo'
src={logoImage} src={logoImage}
/> />
<h5> <label htmlFor='teamURLInput'>
<FormattedMessage <FormattedMessage
id='create_team.team_url.teamUrl' id='create_team.team_url.teamUrl'
tagName='strong' tagName='strong'
defaultMessage='Team URL' defaultMessage='Team URL'
/> />
</h5> </label>
<div className={nameDivClass}> <div className={nameDivClass}>
<div className='row'> <div className='row'>
<div className='col-sm-11'> <div className='col-sm-11'>
@@ -242,6 +255,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
<input <input
id='teamURLInput' id='teamURLInput'
type='text' type='text'
ref={this.teamURLInput}
className='form-control' className='form-control'
placeholder='' placeholder=''
maxLength={128} maxLength={128}
@@ -250,6 +264,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
onFocus={this.handleFocus} onFocus={this.handleFocus}
onChange={this.handleTeamURLInputChange} onChange={this.handleTeamURLInputChange}
spellCheck='false' spellCheck='false'
aria-describedby='teamURLInputError'
/> />
</div> </div>
</div> </div>

Просмотреть файл

@@ -5,7 +5,7 @@ import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import CreateTeam from './create_team'; import {CreateTeam} from './create_team';
describe('component/create_team', () => { describe('component/create_team', () => {
const baseProps = { const baseProps = {
@@ -25,6 +25,7 @@ describe('component/create_team', () => {
}, },
history: jest.fn(), history: jest.fn(),
location: jest.fn(), location: jest.fn(),
intl: {formatMessage: jest.fn()},
} as any; } as any;
test('should match snapshot default', () => { test('should match snapshot default', () => {

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage, injectIntl, type IntlShape} from 'react-intl';
import {Route, Switch, Redirect} from 'react-router-dom'; import {Route, Switch, Redirect} from 'react-router-dom';
import type {RouteComponentProps} from 'react-router-dom'; import type {RouteComponentProps} from 'react-router-dom';
@@ -48,6 +48,7 @@ export type Props = {
isCloud: boolean; isCloud: boolean;
isFreeTrial: boolean; isFreeTrial: boolean;
usageDeltas: CloudUsage; usageDeltas: CloudUsage;
intl: IntlShape;
}; };
type State = { type State = {
@@ -55,7 +56,7 @@ type State = {
wizard: string; wizard: string;
}; };
export default class CreateTeam extends React.PureComponent<Props & RouteComponentProps, State> { export class CreateTeam extends React.PureComponent<Props & RouteComponentProps, State> {
public constructor(props: Props & RouteComponentProps) { public constructor(props: Props & RouteComponentProps) {
super(props); super(props);
@@ -65,6 +66,16 @@ export default class CreateTeam extends React.PureComponent<Props & RouteCompone
}; };
} }
componentDidMount() {
const {formatMessage} = this.props.intl;
document.title = formatMessage({
id: 'create_team.pageTitle',
defaultMessage: 'Create a team - {siteName}',
}, {
siteName: this.props.siteName || 'Mattermost',
});
}
public updateParent = (state: State) => { public updateParent = (state: State) => {
this.setState(state); this.setState(state);
this.props.history.push('/create_team/' + state.wizard); this.props.history.push('/create_team/' + state.wizard);
@@ -156,3 +167,5 @@ export default class CreateTeam extends React.PureComponent<Props & RouteCompone
); );
} }
} }
export default injectIntl(CreateTeam);

Просмотреть файл

@@ -3647,6 +3647,7 @@
"create_team.display_name.next": "Next", "create_team.display_name.next": "Next",
"create_team.display_name.required": "This field is required", "create_team.display_name.required": "This field is required",
"create_team.display_name.teamName": "Team Name", "create_team.display_name.teamName": "Team Name",
"create_team.pageTitle": "Create a team - {siteName}",
"create_team.team_url.back": "Back to previous step", "create_team.team_url.back": "Back to previous step",
"create_team.team_url.charLength": "Name must be {min} or more characters up to a maximum of {max}", "create_team.team_url.charLength": "Name must be {min} or more characters up to a maximum of {max}",
"create_team.team_url.creatingTeam": "Creating team...", "create_team.team_url.creatingTeam": "Creating team...",