[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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
1efc3dacaf
Коммит
bd16f4f9bf
@@ -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 && (
|
||||
|
||||
Ссылка в новой задаче
Block a user