[MM-63035][MM-63970][MM-63978][MM-64028][MM-64019][MM-64022][MM-64026][MM-64023][MM-61625] Create User Group modal accessibility fixes (#31047)
* [MM-63970] Add aria-live for creating a group, always use modal animation * [MM-63978] Move my fix for multiselect remove button focus to the multiselect component * [MM-63035][MM-64028] Change legend to label, fieldset to div in the Input component * [MM-64019] Add fieldset and legend to Add people * [MM-64026] Add required to Name and Mention boxes * [MM-64023] Add results available aria-live area to multiselect * [MM-64022] Restore original multivalueremove for users_email_input, use ariaLabelRenderer for Multiselect * Use noteText for required for now * Fix * Fix e2e * Only show required prompt when there are no users picked * Fix cancel button submitting form * PR feedback * Do the required field for multiselect properly * Other PR feedback * Fix checks --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3d2aa70b7b
Коммит
26613fb4c6
@@ -71,7 +71,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
|
||||
});
|
||||
|
||||
// * Verify if the reader is able to read out the selected row
|
||||
cy.get('.filtered-user-list .sr-only').
|
||||
cy.get('.filtered-user-list div.sr-only:not([role="status"])').
|
||||
should('have.attr', 'aria-live', 'polite').
|
||||
and('have.attr', 'aria-atomic', 'true').
|
||||
invoke('text').then((text) => {
|
||||
@@ -181,7 +181,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
|
||||
});
|
||||
|
||||
// * Verify if the reader is able to read out the selected row
|
||||
cy.get('.filtered-user-list .sr-only').
|
||||
cy.get('.filtered-user-list div.sr-only:not([role="status"])').
|
||||
should('have.attr', 'aria-live', 'polite').
|
||||
and('have.attr', 'aria-atomic', 'true').
|
||||
invoke('text').then((text) => {
|
||||
|
||||
@@ -103,6 +103,7 @@ exports[`components/AddGroupsToChannelModal should match snapshot 1`] = `
|
||||
"id": "multiselect.addGroupsPlaceholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
|
||||
@@ -140,6 +140,7 @@ exports[`components/AddGroupsToTeamModal should match snapshot 1`] = `
|
||||
"id": "multiselect.addGroupsPlaceholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
|
||||
@@ -76,6 +76,7 @@ exports[`component/add_user_to_group_multiselect should match snapshot with diff
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
@@ -175,6 +176,7 @@ exports[`component/add_user_to_group_multiselect should match snapshot with prof
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
@@ -259,6 +261,7 @@ exports[`component/add_user_to_group_multiselect should match snapshot without a
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
|
||||
@@ -245,6 +245,7 @@ export class AddUserToGroupMultiSelect extends React.PureComponent<Props, State>
|
||||
backButtonText={this.props.backButtonText}
|
||||
maxValues={maxValues}
|
||||
numRemainingText={numRemainingText}
|
||||
required={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -225,6 +225,7 @@ exports[`components/admin_console/add_users_to_team_modal/AddUsersToTeamModal sh
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -466,6 +467,7 @@ exports[`components/admin_console/add_users_to_team_modal/AddUsersToTeamModal sh
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
|
||||
&.Input_fieldset___legend {
|
||||
>legend {
|
||||
>label {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ exports[`admin_console/add_users_to_role_modal search should not include bot use
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -335,6 +336,7 @@ exports[`admin_console/add_users_to_role_modal should exclude user 1`] = `
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -530,6 +532,7 @@ exports[`admin_console/add_users_to_role_modal should have single passed value 1
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -766,6 +769,7 @@ exports[`admin_console/add_users_to_role_modal should include additional user 1`
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -1002,6 +1006,7 @@ exports[`admin_console/add_users_to_role_modal should include additional user 2`
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -1197,6 +1202,7 @@ exports[`admin_console/add_users_to_role_modal should not include bot user 1`] =
|
||||
"id": "multiselect.placeholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
|
||||
@@ -106,6 +106,7 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
||||
"id": "multiselect.placeholder.peopleOrGroups",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -233,6 +234,7 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
||||
"id": "multiselect.placeholder.peopleOrGroups",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -360,6 +362,7 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
||||
"id": "multiselect.placeholder.peopleOrGroups",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -487,6 +490,7 @@ exports[`components/channel_invite_modal should match snapshot with exclude and
|
||||
"id": "multiselect.placeholder.peopleOrGroups",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="bottom"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
&__content {
|
||||
padding: 0;
|
||||
|
||||
.multi-select__container {
|
||||
.multi-select__container, .multi-select__error {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ exports[`components/ChannelSelectorModal exclude already selected 1`] = `
|
||||
"id": "multiselect.addChannelsPlaceholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
@@ -336,6 +337,7 @@ exports[`components/ChannelSelectorModal should match snapshot 1`] = `
|
||||
"id": "multiselect.addChannelsPlaceholder",
|
||||
}
|
||||
}
|
||||
required={false}
|
||||
saveButtonPosition="top"
|
||||
saving={false}
|
||||
savingEnabled={true}
|
||||
|
||||
@@ -24,13 +24,18 @@ exports[`component/create_user_groups_modal should match snapshot with back butt
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="sr-only"
|
||||
role="alert"
|
||||
/>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
@@ -88,6 +93,7 @@ exports[`component/create_user_groups_modal should match snapshot with back butt
|
||||
"id": "user_groups_modal.name",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
@@ -111,37 +117,42 @@ exports[`component/create_user_groups_modal should match snapshot with back butt
|
||||
"id": "user_groups_modal.mention",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Add People"
|
||||
id="user_groups_modal.addPeople"
|
||||
/>
|
||||
</h2>
|
||||
<div
|
||||
className="group-add-user"
|
||||
<fieldset
|
||||
className="group-add-people-fieldset"
|
||||
>
|
||||
<Connect(injectIntl(AddUserToGroupMultiSelect))
|
||||
addUserCallback={[Function]}
|
||||
backButtonClass="multiselect-back"
|
||||
backButtonClick={[Function]}
|
||||
backButtonText={
|
||||
Object {
|
||||
"defaultMessage": "Cancel",
|
||||
"id": "multiselect.cancelButton",
|
||||
<legend>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Add People"
|
||||
id="user_groups_modal.addPeople"
|
||||
/>
|
||||
</legend>
|
||||
<div
|
||||
className="group-add-user"
|
||||
>
|
||||
<Connect(injectIntl(AddUserToGroupMultiSelect))
|
||||
addUserCallback={[Function]}
|
||||
backButtonClass="multiselect-back"
|
||||
backButtonClick={[Function]}
|
||||
backButtonText={
|
||||
Object {
|
||||
"defaultMessage": "Cancel",
|
||||
"id": "multiselect.cancelButton",
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteUserCallback={[Function]}
|
||||
focusOnLoad={false}
|
||||
multilSelectKey="addUsersToGroupKey"
|
||||
onSubmitCallback={[Function]}
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
/>
|
||||
</div>
|
||||
deleteUserCallback={[Function]}
|
||||
focusOnLoad={false}
|
||||
multilSelectKey="addUsersToGroupKey"
|
||||
onSubmitCallback={[Function]}
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
@@ -171,13 +182,18 @@ exports[`component/create_user_groups_modal should match snapshot without back b
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="sr-only"
|
||||
role="alert"
|
||||
/>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
@@ -221,6 +237,7 @@ exports[`component/create_user_groups_modal should match snapshot without back b
|
||||
"id": "user_groups_modal.name",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
@@ -244,37 +261,42 @@ exports[`component/create_user_groups_modal should match snapshot without back b
|
||||
"id": "user_groups_modal.mention",
|
||||
}
|
||||
}
|
||||
required={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Add People"
|
||||
id="user_groups_modal.addPeople"
|
||||
/>
|
||||
</h2>
|
||||
<div
|
||||
className="group-add-user"
|
||||
<fieldset
|
||||
className="group-add-people-fieldset"
|
||||
>
|
||||
<Connect(injectIntl(AddUserToGroupMultiSelect))
|
||||
addUserCallback={[Function]}
|
||||
backButtonClass="multiselect-back"
|
||||
backButtonClick={[Function]}
|
||||
backButtonText={
|
||||
Object {
|
||||
"defaultMessage": "Cancel",
|
||||
"id": "multiselect.cancelButton",
|
||||
<legend>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Add People"
|
||||
id="user_groups_modal.addPeople"
|
||||
/>
|
||||
</legend>
|
||||
<div
|
||||
className="group-add-user"
|
||||
>
|
||||
<Connect(injectIntl(AddUserToGroupMultiSelect))
|
||||
addUserCallback={[Function]}
|
||||
backButtonClass="multiselect-back"
|
||||
backButtonClick={[Function]}
|
||||
backButtonText={
|
||||
Object {
|
||||
"defaultMessage": "Cancel",
|
||||
"id": "multiselect.cancelButton",
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteUserCallback={[Function]}
|
||||
focusOnLoad={false}
|
||||
multilSelectKey="addUsersToGroupKey"
|
||||
onSubmitCallback={[Function]}
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
/>
|
||||
</div>
|
||||
deleteUserCallback={[Function]}
|
||||
focusOnLoad={false}
|
||||
multilSelectKey="addUsersToGroupKey"
|
||||
onSubmitCallback={[Function]}
|
||||
saving={false}
|
||||
savingEnabled={false}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
||||
@@ -41,6 +41,7 @@ type State = {
|
||||
nameInputErrorText: React.ReactNode;
|
||||
showUnknownError: boolean;
|
||||
saving: boolean;
|
||||
saved: boolean;
|
||||
}
|
||||
|
||||
export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
@@ -58,6 +59,7 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
nameInputErrorText: '',
|
||||
showUnknownError: false,
|
||||
saving: false,
|
||||
saved: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,15 +94,22 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
this.setState({usersToAdd});
|
||||
};
|
||||
|
||||
goBack = () => {
|
||||
onExited = () => {
|
||||
if (typeof this.props.backButtonCallback === 'function') {
|
||||
this.props.backButtonCallback();
|
||||
this.props.onExited();
|
||||
}
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
createGroup = async (users?: UserProfile[]) => {
|
||||
this.setState({showUnknownError: false, mentionInputErrorText: '', nameInputErrorText: '', saving: true});
|
||||
this.setState({
|
||||
showUnknownError: false,
|
||||
mentionInputErrorText: '',
|
||||
nameInputErrorText: '',
|
||||
saving: true,
|
||||
saved: false,
|
||||
});
|
||||
|
||||
let mention = this.state.mention;
|
||||
const displayName = this.state.name;
|
||||
|
||||
@@ -200,9 +209,8 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
this.setState({showUnknownError: true});
|
||||
}
|
||||
this.setState({saving: false});
|
||||
} else if (typeof this.props.backButtonCallback === 'function') {
|
||||
this.goBack();
|
||||
} else {
|
||||
this.setState({saved: true});
|
||||
this.doHide();
|
||||
}
|
||||
};
|
||||
@@ -213,11 +221,23 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
dialogClassName='a11y__modal user-groups-modal-create'
|
||||
show={this.state.show}
|
||||
onHide={this.doHide}
|
||||
onExited={this.props.onExited}
|
||||
onExited={this.onExited}
|
||||
role='none'
|
||||
aria-labelledby='createUserGroupsModalLabel'
|
||||
id='createUserGroupsModal'
|
||||
>
|
||||
<div
|
||||
role='alert'
|
||||
aria-live='polite'
|
||||
className='sr-only'
|
||||
>
|
||||
{this.state.saved &&
|
||||
<FormattedMessage
|
||||
id='user_groups_modal.groupCreatedSuccess'
|
||||
defaultMessage='Group created successfully'
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<Modal.Header closeButton={true}>
|
||||
{
|
||||
typeof this.props.backButtonCallback === 'function' ? (
|
||||
@@ -226,9 +246,7 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
type='button'
|
||||
className='modal-header-back-button btn btn-icon'
|
||||
aria-label={this.props.intl.formatMessage({id: 'user_groups_modal.goBackLabel', defaultMessage: 'Back'})}
|
||||
onClick={() => {
|
||||
this.goBack();
|
||||
}}
|
||||
onClick={this.doHide}
|
||||
>
|
||||
<i className='icon icon-arrow-left'/>
|
||||
</button>
|
||||
@@ -267,6 +285,7 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
data-testid='nameInput'
|
||||
maxLength={64}
|
||||
autoFocus={true}
|
||||
required={true}
|
||||
customMessage={{type: ItemStatus.ERROR, value: this.state.nameInputErrorText}}
|
||||
/>
|
||||
</div>
|
||||
@@ -278,31 +297,32 @@ export class CreateUserGroupsModal extends React.PureComponent<Props, State> {
|
||||
value={this.state.mention}
|
||||
maxLength={64}
|
||||
data-testid='mentionInput'
|
||||
required={true}
|
||||
customMessage={{type: ItemStatus.ERROR, value: this.state.mentionInputErrorText}}
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='user_groups_modal.addPeople'
|
||||
defaultMessage='Add People'
|
||||
/>
|
||||
</h2>
|
||||
<div className='group-add-user'>
|
||||
<AddUserToGroupMultiSelect
|
||||
multilSelectKey={'addUsersToGroupKey'}
|
||||
onSubmitCallback={this.createGroup}
|
||||
focusOnLoad={false}
|
||||
savingEnabled={this.isSaveEnabled()}
|
||||
addUserCallback={this.addUserCallback}
|
||||
deleteUserCallback={this.deleteUserCallback}
|
||||
backButtonText={defineMessage({id: 'multiselect.cancelButton', defaultMessage: 'Cancel'})}
|
||||
backButtonClick={
|
||||
typeof this.props.backButtonCallback === 'function' ? this.goBack : this.doHide
|
||||
}
|
||||
backButtonClass={'multiselect-back'}
|
||||
saving={this.state.saving}
|
||||
/>
|
||||
</div>
|
||||
<fieldset className='group-add-people-fieldset'>
|
||||
<legend>
|
||||
<FormattedMessage
|
||||
id='user_groups_modal.addPeople'
|
||||
defaultMessage='Add People'
|
||||
/>
|
||||
</legend>
|
||||
<div className='group-add-user'>
|
||||
<AddUserToGroupMultiSelect
|
||||
multilSelectKey={'addUsersToGroupKey'}
|
||||
onSubmitCallback={this.createGroup}
|
||||
focusOnLoad={false}
|
||||
savingEnabled={this.isSaveEnabled()}
|
||||
addUserCallback={this.addUserCallback}
|
||||
deleteUserCallback={this.deleteUserCallback}
|
||||
backButtonText={defineMessage({id: 'multiselect.cancelButton', defaultMessage: 'Cancel'})}
|
||||
backButtonClick={this.doHide}
|
||||
backButtonClass={'multiselect-back'}
|
||||
saving={this.state.saving}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
{
|
||||
this.state.showUnknownError &&
|
||||
<div className='Input___error group-error'>
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`components/multiselect/multiselect MultiSelectList should match snapsho
|
||||
className="multi-select__container react-select"
|
||||
>
|
||||
<ForwardRef
|
||||
aria-invalid={false}
|
||||
className=""
|
||||
classNamePrefix="react-select-auto react-select"
|
||||
components={
|
||||
@@ -29,6 +30,7 @@ exports[`components/multiselect/multiselect MultiSelectList should match snapsho
|
||||
isClearable={false}
|
||||
isMulti={true}
|
||||
menuIsOpen={false}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onInputChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -196,6 +198,7 @@ exports[`components/multiselect/multiselect should match snapshot 1`] = `
|
||||
className="multi-select__container react-select"
|
||||
>
|
||||
<ForwardRef
|
||||
aria-invalid={false}
|
||||
className=""
|
||||
classNamePrefix="react-select-auto react-select"
|
||||
components={
|
||||
@@ -213,6 +216,7 @@ exports[`components/multiselect/multiselect should match snapshot 1`] = `
|
||||
isClearable={false}
|
||||
isMulti={true}
|
||||
menuIsOpen={false}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onInputChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -371,6 +375,7 @@ exports[`components/multiselect/multiselect should match snapshot for page 2 1`]
|
||||
className="multi-select__container react-select"
|
||||
>
|
||||
<ForwardRef
|
||||
aria-invalid={false}
|
||||
className=""
|
||||
classNamePrefix="react-select-auto react-select"
|
||||
components={
|
||||
@@ -388,6 +393,7 @@ exports[`components/multiselect/multiselect should match snapshot for page 2 1`]
|
||||
isClearable={false}
|
||||
isMulti={true}
|
||||
menuIsOpen={false}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onInputChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {ComponentProps, ReactNode} from 'react';
|
||||
import type {IntlShape, MessageDescriptor} from 'react-intl';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import ReactSelect, {components} from 'react-select';
|
||||
import type {GetOptionValue, InputActionMeta, SelectInstance} from 'react-select';
|
||||
import type {GetOptionValue, InputActionMeta, MultiValueRemoveProps, SelectInstance} from 'react-select';
|
||||
|
||||
import SaveButton from 'components/save_button';
|
||||
import CloseCircleSolidIcon from 'components/widgets/icons/close_circle_solid_icon';
|
||||
@@ -67,12 +67,14 @@ export type Props<T extends Value> = {
|
||||
savingEnabled?: boolean;
|
||||
handleCancel?: () => void;
|
||||
customNoOptionsMessage?: React.ReactNode;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
a11yActive: boolean;
|
||||
input: string;
|
||||
page: number;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
@@ -88,6 +90,7 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
valueWithImage: false,
|
||||
focusOnLoad: true,
|
||||
savingEnabled: true,
|
||||
required: false,
|
||||
};
|
||||
|
||||
public constructor(props: Props<T>) {
|
||||
@@ -97,9 +100,16 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
a11yActive: false,
|
||||
page: 0,
|
||||
input: '',
|
||||
hasError: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props<T>) {
|
||||
if (prevProps.values !== this.props.values) {
|
||||
this.validateInput();
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const inputRef: unknown = this.reactSelectRef.current && this.reactSelectRef.current.inputRef;
|
||||
|
||||
@@ -265,12 +275,6 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
this.props.handleDelete(values);
|
||||
};
|
||||
|
||||
MultiValueRemove = ({children, innerProps}: any) => (
|
||||
<div {...innerProps}>
|
||||
{children || <CloseCircleSolidIcon/>}
|
||||
</div>
|
||||
);
|
||||
|
||||
formatOptionLabel = (user: any) => {
|
||||
const profileImg = imageURLForUser(user.id, user.last_picture_update);
|
||||
|
||||
@@ -292,6 +296,33 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
return this.props.valueWithImage ? <components.MultiValueLabel {...props}/> : this.props.valueRenderer;
|
||||
};
|
||||
|
||||
private MultiValueRemove = ({children, innerProps, data}: MultiValueRemoveProps<T>) => (
|
||||
<div
|
||||
{...innerProps}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={this.props.intl.formatMessage({
|
||||
id: 'multiselect.remove',
|
||||
defaultMessage: 'Remove {label}',
|
||||
}, {
|
||||
label: this.props.ariaLabelRenderer(data),
|
||||
})}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
innerProps.onClick?.(e as unknown as React.MouseEvent<HTMLDivElement, MouseEvent>);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children || <CloseCircleSolidIcon/>}
|
||||
</div>
|
||||
);
|
||||
|
||||
private validateInput = () => {
|
||||
this.setState({hasError: Boolean(this.props.required && this.props.values.length === 0)});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const options = Object.assign([...this.props.options]);
|
||||
const {totalCount, users, values} = this.props;
|
||||
@@ -463,7 +494,11 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
<>
|
||||
<div className='filtered-user-list'>
|
||||
<div className='filter-row filter-row--full'>
|
||||
<div className='multi-select__container react-select'>
|
||||
<div
|
||||
className={classNames('multi-select__container react-select', {
|
||||
'has-error': this.state.hasError,
|
||||
})}
|
||||
>
|
||||
<ReactSelect
|
||||
id='selectItems'
|
||||
ref={this.reactSelectRef as React.RefObject<any>} // type of ref on @types/react-select is outdated
|
||||
@@ -491,6 +526,9 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
aria-label={formatAsString(this.props.intl.formatMessage, this.props.placeholderText)}
|
||||
className={this.state.a11yActive ? 'multi-select__focused' : ''}
|
||||
classNamePrefix='react-select-auto react-select'
|
||||
aria-invalid={this.state.hasError}
|
||||
aria-describedby={this.state.hasError ? 'multiSelectMessageError' : undefined}
|
||||
onBlur={this.validateInput}
|
||||
/>
|
||||
{this.props.saveButtonPosition === 'top' &&
|
||||
<SaveButton
|
||||
@@ -518,6 +556,18 @@ export class MultiSelect<T extends Value> extends React.PureComponent<Props<T>,
|
||||
>
|
||||
{noteTextContainer}
|
||||
</div>
|
||||
{this.state.hasError && (
|
||||
<div
|
||||
className='multi-select__error'
|
||||
role='alert'
|
||||
id='multiSelectMessageError'
|
||||
>
|
||||
<i className='icon icon-alert-circle-outline'/>
|
||||
<span>
|
||||
{this.props.intl.formatMessage({id: 'multiselect.required', defaultMessage: 'This field is required'})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{this.props.saveButtonPosition === 'top' &&
|
||||
<div className='filter-controls'>
|
||||
{previousButton}
|
||||
|
||||
@@ -213,6 +213,17 @@ export default class MultiSelectList<T extends Value> extends React.PureComponen
|
||||
|
||||
renderOutput = (
|
||||
<div className='more-modal__list'>
|
||||
<div
|
||||
className='sr-only'
|
||||
aria-live='polite'
|
||||
role='status'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='multiselect.list.resultsAvailable'
|
||||
defaultMessage='{count, plural, one {# result found} other {# results found}} for your search.'
|
||||
values={{count: options.length}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='sr-only'
|
||||
aria-live='polite'
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
padding-bottom: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
fieldset {
|
||||
.Input_fieldset {
|
||||
max-width: 110px;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,10 +66,11 @@ Object {
|
||||
<div
|
||||
class="Input_container"
|
||||
>
|
||||
<fieldset
|
||||
<div
|
||||
class="Input_fieldset name_input"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<legend
|
||||
<label
|
||||
class="Input_legend"
|
||||
/>
|
||||
<div
|
||||
@@ -85,19 +86,20 @@ Object {
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Input_container"
|
||||
>
|
||||
<fieldset
|
||||
<div
|
||||
class="Input_fieldset email_input Input_fieldset___error Input_fieldset___legend"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<legend
|
||||
<label
|
||||
class="Input_legend Input_legend___focus"
|
||||
>
|
||||
Business Email
|
||||
</legend>
|
||||
</label>
|
||||
<div
|
||||
class="Input_wrapper"
|
||||
>
|
||||
@@ -113,7 +115,7 @@ Object {
|
||||
value="test@mattermost.com"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div
|
||||
class="Input___customMessage Input___error"
|
||||
id="error_email"
|
||||
@@ -132,10 +134,11 @@ Object {
|
||||
<div
|
||||
class="Input_container"
|
||||
>
|
||||
<fieldset
|
||||
<div
|
||||
class="Input_fieldset company_name_input"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<legend
|
||||
<label
|
||||
class="Input_legend"
|
||||
/>
|
||||
<div
|
||||
@@ -151,7 +154,7 @@ Object {
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DropdownInput Input_container"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.description-section-input {
|
||||
fieldset {
|
||||
.Input_fieldset {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,12 +389,19 @@
|
||||
padding: 12px 32px 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 0 32px;
|
||||
color: inherit;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
.group-add-people-fieldset {
|
||||
margin-top: 20px;
|
||||
|
||||
legend {
|
||||
padding: 0 32px;
|
||||
border: none;
|
||||
margin-bottom: 10px;
|
||||
color: inherit;
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-add-user {
|
||||
|
||||
@@ -5,10 +5,11 @@ exports[`components/widgets/inputs/Input should match snapshot 1`] = `
|
||||
<div
|
||||
class="Input_container"
|
||||
>
|
||||
<fieldset
|
||||
<div
|
||||
class="Input_fieldset"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<legend
|
||||
<label
|
||||
class="Input_legend"
|
||||
/>
|
||||
<div
|
||||
@@ -20,7 +21,7 @@ exports[`components/widgets/inputs/Input should match snapshot 1`] = `
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('components/widgets/inputs/Input', () => {
|
||||
});
|
||||
|
||||
// Check for error styling
|
||||
const fieldset = screen.getByRole('group');
|
||||
const fieldset = screen.getByTestId('input-wrapper');
|
||||
expect(fieldset).toHaveClass('Input_fieldset___error');
|
||||
|
||||
// Check for error message
|
||||
@@ -108,7 +108,7 @@ describe('components/widgets/inputs/Input', () => {
|
||||
});
|
||||
|
||||
// Check for error styling
|
||||
const fieldset = screen.getByRole('group');
|
||||
const fieldset = screen.getByTestId('input-wrapper');
|
||||
expect(fieldset).toHaveClass('Input_fieldset___error');
|
||||
|
||||
// Check for error message
|
||||
@@ -159,7 +159,7 @@ describe('components/widgets/inputs/Input', () => {
|
||||
});
|
||||
|
||||
// Check for error styling
|
||||
const fieldset = screen.getByRole('group');
|
||||
const fieldset = screen.getByTestId('input-wrapper');
|
||||
expect(fieldset).toHaveClass('Input_fieldset___error');
|
||||
});
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('components/widgets/inputs/Input', () => {
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
|
||||
// Check for error styling
|
||||
const fieldset = screen.getByRole('group');
|
||||
const fieldset = screen.getByTestId('input-wrapper');
|
||||
expect(fieldset).toHaveClass('Input_fieldset___error');
|
||||
});
|
||||
|
||||
|
||||
@@ -256,16 +256,17 @@ const Input = React.forwardRef((
|
||||
|
||||
return (
|
||||
<div className={classNames('Input_container', containerClassName, {disabled})}>
|
||||
<fieldset
|
||||
<div
|
||||
className={classNames('Input_fieldset', className, {
|
||||
Input_fieldset___error: hasError || customInputLabel?.type === 'error',
|
||||
Input_fieldset___legend: showLegend,
|
||||
})}
|
||||
data-testid='input-wrapper'
|
||||
>
|
||||
{useLegend && (
|
||||
<legend className={classNames('Input_legend', {Input_legend___focus: showLegend})}>
|
||||
<label className={classNames('Input_legend', {Input_legend___focus: showLegend})}>
|
||||
{showLegend ? formatAsString(formatMessage, label || placeholder) : null}
|
||||
</legend>
|
||||
</label>
|
||||
)}
|
||||
<div className={classNames('Input_wrapper', wrapperClassName)}>
|
||||
{inputPrefix}
|
||||
@@ -275,7 +276,7 @@ const Input = React.forwardRef((
|
||||
{clearButton}
|
||||
</div>
|
||||
{addon}
|
||||
</fieldset>
|
||||
</div>
|
||||
{/* Display custom or derived error messages */}
|
||||
{customInputLabel && (
|
||||
<div
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import UsersEmailsInput from './users_emails_input';
|
||||
|
||||
describe('components/widgets/inputs/UsersEmailsInput', () => {
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<UsersEmailsInput
|
||||
placeholder='test'
|
||||
ariaLabel='test'
|
||||
usersLoader={jest.fn()}
|
||||
onChange={jest.fn()}
|
||||
value={[
|
||||
'test@email.com',
|
||||
{
|
||||
id: 'test-user-id',
|
||||
username: 'test-username',
|
||||
first_name: 'test',
|
||||
last_name: 'user',
|
||||
} as UserProfile,
|
||||
]}
|
||||
errorMessage={{
|
||||
id: 'errorMessageId',
|
||||
defaultMessage: 'errorMessageDefault',
|
||||
}}
|
||||
onInputChange={jest.fn()}
|
||||
inputValue=''
|
||||
emailInvitationsEnabled={false}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchInlineSnapshot(`
|
||||
<Fragment>
|
||||
<ForwardRef
|
||||
aria-label="test"
|
||||
className="UsersEmailsInput empty"
|
||||
classNamePrefix="users-emails-input"
|
||||
components={
|
||||
Object {
|
||||
"IndicatorsContainer": [Function],
|
||||
"Input": [Function],
|
||||
"MultiValueRemove": [Function],
|
||||
"NoOptionsMessage": [Function],
|
||||
}
|
||||
}
|
||||
defaultMenuIsOpen={false}
|
||||
defaultOptions={false}
|
||||
formatOptionLabel={[Function]}
|
||||
getOptionValue={[Function]}
|
||||
inputValue=""
|
||||
isClearable={false}
|
||||
isMulti={true}
|
||||
isValidNewOption={[Function]}
|
||||
loadOptions={[Function]}
|
||||
loadingMessage={[Function]}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInputChange={[Function]}
|
||||
openMenuOnClick={false}
|
||||
openMenuOnFocus={true}
|
||||
placeholder="test"
|
||||
styles={
|
||||
Object {
|
||||
"input": [Function],
|
||||
"placeholder": [Function],
|
||||
}
|
||||
}
|
||||
tabSelectsValue={true}
|
||||
value={
|
||||
Array [
|
||||
Object {
|
||||
"label": "test@email.com",
|
||||
"value": "test@email.com",
|
||||
},
|
||||
Object {
|
||||
"first_name": "test",
|
||||
"id": "test-user-id",
|
||||
"last_name": "user",
|
||||
"username": "test-username",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,8 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import type {RefObject} from 'react';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
import type {MessageDescriptor, IntlShape} from 'react-intl';
|
||||
import {FormattedMessage, defineMessages, injectIntl} from 'react-intl';
|
||||
import {components} from 'react-select';
|
||||
import type {FormatOptionLabelMeta, InputActionMeta, InputProps, Options, StylesConfig, SelectInstance, MultiValue, SingleValue, OptionsOrGroups, GroupBase, MultiValueRemoveProps} from 'react-select';
|
||||
import AsyncCreatable from 'react-select/async-creatable';
|
||||
@@ -50,6 +50,7 @@ type Props = {
|
||||
autoFocus?: boolean;
|
||||
suppressNoOptionsMessage?: boolean;
|
||||
onPaste?: (e: ClipboardEvent) => void;
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
export type EmailInvite = {
|
||||
@@ -79,7 +80,7 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
export default class UsersEmailsInput extends React.PureComponent<Props, State> {
|
||||
export class UsersEmailsInput extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
noMatchMessage: messages.noMatchDefault,
|
||||
validAddress: messages.validAddressDefault,
|
||||
@@ -259,6 +260,18 @@ export default class UsersEmailsInput extends React.PureComponent<Props, State>
|
||||
);
|
||||
};
|
||||
|
||||
private renderAriaLabel = (option: UserProfile | EmailInvite): string => {
|
||||
if (!option) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.isUserProfile(option)) {
|
||||
return (option as UserProfile).username;
|
||||
}
|
||||
|
||||
return (option as EmailInvite).value;
|
||||
};
|
||||
|
||||
MultiValueRemove = (props: MultiValueRemoveProps<EmailInvite | UserProfile, true>) => {
|
||||
const {children, innerProps} = props;
|
||||
|
||||
@@ -267,6 +280,12 @@ export default class UsersEmailsInput extends React.PureComponent<Props, State>
|
||||
{...innerProps}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={this.props.intl.formatMessage({
|
||||
id: 'multiselect.remove',
|
||||
defaultMessage: 'Remove {label}',
|
||||
}, {
|
||||
label: this.renderAriaLabel(props.data),
|
||||
})}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
@@ -564,3 +583,5 @@ export default class UsersEmailsInput extends React.PureComponent<Props, State>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(UsersEmailsInput);
|
||||
|
||||
@@ -4736,6 +4736,7 @@
|
||||
"multiselect.creating": "Creating...",
|
||||
"multiselect.go": "Go",
|
||||
"multiselect.list.notFound": "No results found matching <b>{searchQuery}</b>",
|
||||
"multiselect.list.resultsAvailable": "{count, plural, one {# result found} other {# results found}} for your search.",
|
||||
"multiselect.loading": "Loading...",
|
||||
"multiselect.maxGroupMembers": "No more than 256 members can be added to a group at once.",
|
||||
"multiselect.maxPeople": "Use ↑↓ to browse, ↵ to select. You can't add more than 7 people. Please <a>create a channel</a> to include more people. ",
|
||||
@@ -4745,6 +4746,8 @@
|
||||
"multiselect.numRemaining": "Up to {max, number} can be added at a time. You have {num, number} remaining.",
|
||||
"multiselect.placeholder": "Search for people",
|
||||
"multiselect.placeholder.peopleOrGroups": "Search for people or groups",
|
||||
"multiselect.remove": "Remove {label}",
|
||||
"multiselect.required": "This field is required",
|
||||
"multiselect.saveDetailsButton": "Save Details",
|
||||
"multiselect.savingDetailsButton": "Saving...",
|
||||
"multiselect.selectChannels": "Use ↑↓ to browse, ↵ to select.",
|
||||
@@ -5764,6 +5767,7 @@
|
||||
"user_groups_modal.editGroupTitle": "Edit Group Details",
|
||||
"user_groups_modal.filterAriaLabel": "Groups Filter",
|
||||
"user_groups_modal.goBackLabel": "Back",
|
||||
"user_groups_modal.groupCreatedSuccess": "Group created successfully",
|
||||
"user_groups_modal.joinGroup": "Join Group",
|
||||
"user_groups_modal.leaveGroup": "Leave Group",
|
||||
"user_groups_modal.memberCount": "{member_count} {member_count, plural, one {member} other {members}}",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
.modal .channel-invite__content,
|
||||
.modal .user-groups-modal__content {
|
||||
.multi-select__container {
|
||||
.multi-select__container, .multi-select__error {
|
||||
height: auto;
|
||||
padding: 0 3.2rem;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
height: 40px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&.has-error.react-select .react-select__control.react-select-auto {
|
||||
border-color: var(--error-text);
|
||||
}
|
||||
}
|
||||
|
||||
.multi-select__focused {
|
||||
@@ -52,3 +56,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-select__error {
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
color: var(--error-text);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
|
||||
i {
|
||||
height: 14px;
|
||||
align-self: baseline;
|
||||
margin-right: 7px;
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,7 +525,7 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.multi-select__container {
|
||||
.multi-select__container, .multi-select__error {
|
||||
.btn {
|
||||
display: block;
|
||||
min-width: 50px;
|
||||
|
||||
Ссылка в новой задаче
Block a user