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

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

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

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

@@ -126,4 +126,19 @@ describe('/components/create_team/components/display_name', () => {
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> {
teamURLInput: React.RefObject<HTMLInputElement>;
constructor(props: Props) {
super(props);
this.teamURLInput = React.createRef();
this.state = {
nameError: '',
isLoading: false,
@@ -95,6 +97,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
defaultMessage='This field is required'
/>),
});
this.teamURLInput.current?.focus();
return;
}
@@ -109,6 +112,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
}}
/>),
});
this.teamURLInput.current?.focus();
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."
/>),
});
this.teamURLInput.current?.focus();
return;
}
@@ -191,7 +196,15 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
let nameError = null;
let nameDivClass = 'form-group';
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';
}
@@ -221,13 +234,13 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
className='signup-team-logo'
src={logoImage}
/>
<h5>
<label htmlFor='teamURLInput'>
<FormattedMessage
id='create_team.team_url.teamUrl'
tagName='strong'
defaultMessage='Team URL'
/>
</h5>
</label>
<div className={nameDivClass}>
<div className='row'>
<div className='col-sm-11'>
@@ -242,6 +255,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
<input
id='teamURLInput'
type='text'
ref={this.teamURLInput}
className='form-control'
placeholder=''
maxLength={128}
@@ -250,6 +264,7 @@ export default class TeamUrl extends React.PureComponent<Props, State> {
onFocus={this.handleFocus}
onChange={this.handleTeamURLInputChange}
spellCheck='false'
aria-describedby='teamURLInputError'
/>
</div>
</div>

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

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

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
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 type {RouteComponentProps} from 'react-router-dom';
@@ -48,6 +48,7 @@ export type Props = {
isCloud: boolean;
isFreeTrial: boolean;
usageDeltas: CloudUsage;
intl: IntlShape;
};
type State = {
@@ -55,7 +56,7 @@ type State = {
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) {
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) => {
this.setState(state);
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.required": "This field is required",
"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.charLength": "Name must be {min} or more characters up to a maximum of {max}",
"create_team.team_url.creatingTeam": "Creating team...",