[MM-62989][MM-63039][MM-63033][MM-64174][MM-63040][MM-63032][MM-63001][MM-63006][MM-63037] Various accessibility fixes for the Create Channel modal (#30888)

* [MM-62989] Replace channel purpose input with Input component

* [MM-63039] Fix styles so that a11y--focused box shadow is applied to the private/public channel buttons

* [MM-63033] Add aria-describedby and role=alert to the URL input on create channel modal

* Fix lint

* [MM-63035] Change legend to label, fieldset to div in the Input component

* Fix i18n

* [MM-64174] Stop propagation of enter event while editing the new channel

* [MM-63037] Add role=img and an aria label to the error img icon

* Fix snap

* Revert "[MM-63035] Change legend to label, fieldset to div in the Input component"

This reverts commit e8516f3e6a266c77db2b1695a036db717041a9ef.

* [MM-63040] Remove tabindex=0 from GenericModal wrapper, fix issue with URLInput that I caused D:

* Fix snap

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2025-06-18 16:29:04 -04:00
коммит произвёл GitHub
родитель 1efc3dacaf
Коммит bd16f4f9bf
12 изменённых файлов: 43 добавлений и 56 удалений

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

@@ -35,7 +35,6 @@ Object {
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"
@@ -519,7 +518,6 @@ Object {
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"
@@ -1163,7 +1161,6 @@ Object {
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"
@@ -1526,7 +1523,6 @@ Object {
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"
@@ -2108,7 +2104,6 @@ Object {
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"
@@ -2690,7 +2685,6 @@ Object {
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"

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

@@ -9,15 +9,17 @@
.new-channel-modal-purpose-textarea {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
border-radius: 4px;
background: var(--center-channel-bg);
color: var(--center-channel-color);
font-size: 14px;
line-height: 20px;
resize: none;
.Input_wrapper textarea.form-control {
height: auto;
border: none;
}
&:hover {
border-color: rgba(var(--center-channel-color-rgb), 0.48);
}
@@ -43,26 +45,6 @@
line-height: 16px;
text-align: left;
}
.new-channel-modal-purpose-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;
}
}
}
}
}

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

@@ -145,9 +145,9 @@ describe('components/new_channel_modal', () => {
expect(privateChannelHeading).toBeInTheDocument();
expect(privateChannelHeading.nextSibling).toHaveTextContent('Only invited members');
const purposeTextArea = screen.getByPlaceholderText('Enter a purpose for this channel (optional)');
const purposeTextArea = screen.getByLabelText('Channel Purpose');
expect(purposeTextArea).toBeInTheDocument();
expect(purposeTextArea).toHaveClass('new-channel-modal-purpose-textarea');
expect(purposeTextArea).toHaveClass('Input form-control medium');
const purposeDesc = screen.getByText('This will be displayed when browsing for channels.');
expect(purposeDesc).toBeInTheDocument();
@@ -263,7 +263,7 @@ describe('components/new_channel_modal', () => {
);
// Change purpose
const ChannelPurposeTextArea = screen.getByPlaceholderText('Enter a purpose for this channel (optional)');
const ChannelPurposeTextArea = screen.getByLabelText('Channel Purpose');
expect(ChannelPurposeTextArea).toBeInTheDocument();
userEvent.click(ChannelPurposeTextArea);

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

@@ -23,6 +23,7 @@ import {switchToChannel} from 'actions/views/channel';
import {closeModal} from 'actions/views/modals';
import ChannelNameFormField from 'components/channel_name_form_field/channel_name_form_field';
import Input from 'components/widgets/inputs/input/input';
import PublicPrivateSelector from 'components/widgets/public-private-selector/public-private-selector';
import WithTooltip from 'components/with_tooltip';
@@ -283,16 +284,18 @@ const NewChannelModal = () => {
onChange={handleOnTypeChange}
/>
<div className='new-channel-modal-purpose-container'>
<textarea
<Input
id='new-channel-modal-purpose'
className={classNames('new-channel-modal-purpose-textarea', {'with-error': purposeError})}
placeholder={formatMessage({id: 'channel_modal.purpose.placeholder', defaultMessage: 'Enter a purpose for this channel (optional)'})}
rows={4}
maxLength={Constants.MAX_CHANNELPURPOSE_LENGTH}
autoComplete='off'
type='textarea'
value={purpose}
onChange={handleOnPurposeChange}
onKeyDown={handleOnPurposeKeyDown}
label={formatMessage({id: 'channel_modal.purpose.label', defaultMessage: 'Channel Purpose'})}
placeholder={formatMessage({id: 'channel_modal.purpose.placeholder', defaultMessage: 'Enter a purpose for this channel (optional)'})}
maxLength={Constants.MAX_CHANNELPURPOSE_LENGTH}
autoComplete='off'
className={classNames('new-channel-modal-purpose-textarea', {'with-error': purposeError})}
rows={4}
/>
{purposeError ? (
<div className='new-channel-modal-purpose-error'>

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

@@ -36,7 +36,6 @@ exports[`components/marketplace/ doesn't show web marketplace banner in FeatureF
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"
@@ -148,7 +147,6 @@ exports[`components/marketplace/ hides search, shows web marketplace banner in F
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"
@@ -261,7 +259,6 @@ exports[`components/marketplace/ should render default 1`] = `
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"
@@ -425,7 +422,6 @@ exports[`components/marketplace/ should render with error banner 1`] = `
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"
@@ -606,7 +602,6 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"
@@ -778,7 +773,6 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"
@@ -968,7 +962,6 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
<div
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
onKeyDown={[Function]}
tabIndex={0}
>
<ModalHeader
bsClass="modal-header"

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

@@ -125,6 +125,7 @@ Object {
aria-hidden="true"
aria-label=""
class="icon error icon-alert-circle-outline"
role="img"
/>
<span>
Please enter a valid business email address.

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

@@ -42,6 +42,7 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
clearable?: boolean;
clearableTooltipText?: string;
onClear?: () => void;
rows?: number;
validate?: (value: React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>['value']) => CustomMessageInputType | undefined;
}
@@ -74,6 +75,7 @@ const Input = React.forwardRef((
onBlur,
onChange,
onClear,
rows,
validate,
...otherProps
}: InputProps,
@@ -223,7 +225,7 @@ const Input = React.forwardRef((
aria-label={ariaLabel}
aria-describedby={customInputLabel ? errorId : undefined}
aria-invalid={error || hasError}
rows={3}
rows={rows || 3}
name={name}
disabled={disabled}
{...otherProps}
@@ -291,8 +293,9 @@ const Input = React.forwardRef((
'icon-information-outline': (customInputLabel?.type || 'error') === ItemStatus.INFO,
'icon-check': (customInputLabel?.type || 'error') === ItemStatus.SUCCESS,
})}
aria-hidden={Boolean(customInputLabel.value)}
role='img'
aria-label={customInputLabel.value ? '' : customInputLabel.type || 'error'}
aria-hidden={Boolean(customInputLabel.value)}
/>
<span>{customInputLabel?.value}</span>
</div>

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

@@ -70,13 +70,17 @@ function UrlInput({
onBlur(event);
}
};
const handleOnButtonClick = () => {
if (!hasError) {
setEditing(!editing);
}
};
const handlePropagateKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
if (event.key === 'Enter') {
event.stopPropagation();
}
};
const urlInputLabel = (
<span
className='url-input-label'
@@ -116,12 +120,14 @@ function UrlInput({
hasError={hasError}
onChange={handleOnInputChange}
onBlur={handleOnInputBlur}
aria-describedby='url-input-error'
/>
)}
<button
className={classNames('url-input-button', {disabled: hasError})}
disabled={hasError}
onClick={handleOnButtonClick}
onKeyDown={handlePropagateKeyDown}
>
<span className='url-input-button-label'>
{editing ? formatMessage({id: 'url_input.buttonLabel.done', defaultMessage: 'Done'}) : formatMessage({id: 'url_input.buttonLabel.edit', defaultMessage: 'Edit'})}
@@ -131,7 +137,12 @@ function UrlInput({
{error && (
<div className='url-input-error'>
<i className='icon icon-alert-outline'/>
<span>{error}</span>
<span
id='url-input-error'
role='alert'
>
{error}
</span>
</div>
)}
</div>

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

@@ -12,14 +12,16 @@
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
border-radius: 4px;
background: var(--center-channel-bg);
box-shadow: var(--elevation-1);
&:not(.a11y--focused) {
box-shadow: var(--elevation-1);
}
&:not(:first-child) {
margin-left: 8px;
}
&.a11y--focused,
&:hover {
&:not(.a11y--focused):hover {
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: var(--elevation-2);
}
@@ -32,7 +34,7 @@
fill: var(--button-bg);
}
&:hover {
&:not(.a11y--focused):hover {
border: 1px solid rgba(var(--sidebar-text-active-border-rgb), 0.4);
box-shadow: var(--elevation-2);
}

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

@@ -3609,6 +3609,7 @@
"channel_modal.name.placeholder": "Enter a name for your new channel",
"channel_modal.name.shorter": "Channel names must have maximum 64 characters.",
"channel_modal.purpose.info": "This will be displayed when browsing for channels.",
"channel_modal.purpose.label": "Channel Purpose",
"channel_modal.purpose.placeholder": "Enter a purpose for this channel (optional)",
"channel_modal.purposeTooLong": "The purpose exceeds the maximum of 250 characters",
"channel_modal.type.private.description": "Only invited members",

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

@@ -34,7 +34,6 @@ exports[`GenericModal should match snapshot for base case 1`] = `
>
<div
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
tabindex="0"
>
<div
class="modal-header"

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

@@ -115,7 +115,6 @@ export const GenericModal: React.FC<Props> = ({
compassDesign,
backdrop,
backdropClassName,
tabIndex,
children,
autoFocusConfirmButton,
headerInput,
@@ -288,7 +287,6 @@ export const GenericModal: React.FC<Props> = ({
<div
ref={containerRef}
onKeyDown={onEnterKeyDown}
tabIndex={tabIndex || 0}
className='GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher'
>
{showHeader && (