[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>
Этот коммит содержится в:
Devin Binnie
2025-06-18 09:25:13 -04:00
коммит произвёл GitHub
родитель 3d2aa70b7b
Коммит 26613fb4c6
29 изменённых файлов: 319 добавлений и 222 удалений

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

@@ -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;