[MM-50821] Notify about successful login only once via screen reader (#24979)
* [MM-50821] Notify about successful login only once via screen reader * clear timeout via useEffect instead of inside setTimeout in AdvanceTextEditor * declare timeout as ref in AdvancedTextEditor --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bfd6f5aa01
Коммит
c6a6ad9b4b
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import type {ServerError} from '@mattermost/types/errors';
|
|||||||
import type {FileInfo} from '@mattermost/types/files';
|
import type {FileInfo} from '@mattermost/types/files';
|
||||||
|
|
||||||
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
||||||
|
import LocalStorageStore from 'stores/local_storage_store';
|
||||||
|
|
||||||
import AutoHeightSwitcher from 'components/common/auto_height_switcher';
|
import AutoHeightSwitcher from 'components/common/auto_height_switcher';
|
||||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
||||||
@@ -191,6 +192,7 @@ const AdvanceTextEditor = ({
|
|||||||
const emojiPickerRef = useRef<HTMLButtonElement>(null);
|
const emojiPickerRef = useRef<HTMLButtonElement>(null);
|
||||||
const editorActionsRef = useRef<HTMLDivElement>(null);
|
const editorActionsRef = useRef<HTMLDivElement>(null);
|
||||||
const editorBodyRef = useRef<HTMLDivElement>(null);
|
const editorBodyRef = useRef<HTMLDivElement>(null);
|
||||||
|
const timeout = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
const [renderScrollbar, setRenderScrollbar] = useState(false);
|
const [renderScrollbar, setRenderScrollbar] = useState(false);
|
||||||
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
||||||
@@ -605,6 +607,29 @@ const AdvanceTextEditor = ({
|
|||||||
}
|
}
|
||||||
}, [handleWidthChange, message]);
|
}, [handleWidthChange, message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => timeout.current && clearTimeout(timeout.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const wasNotifiedOfLogIn = LocalStorageStore.getWasNotifiedOfLogIn();
|
||||||
|
|
||||||
|
const ariaLabel = useMemo(() => {
|
||||||
|
let label;
|
||||||
|
if (!wasNotifiedOfLogIn) {
|
||||||
|
label = Utils.localizeMessage(
|
||||||
|
'channelView.login.successfull',
|
||||||
|
'Login Successful',
|
||||||
|
);
|
||||||
|
|
||||||
|
// set timeout to make sure aria-label is read by a screen reader,
|
||||||
|
// and then set the flag to "true" to make sure it's not read again until a user logs back in
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
LocalStorageStore.setWasNotifiedOfLogIn(true);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
return label ? `${label} ${ariaLabelMessageInput}` : ariaLabelMessageInput;
|
||||||
|
}, [ariaLabelMessageInput, wasNotifiedOfLogIn]);
|
||||||
|
|
||||||
const formattingBar = (
|
const formattingBar = (
|
||||||
<AutoHeightSwitcher
|
<AutoHeightSwitcher
|
||||||
showSlot={showFormattingBar ? 1 : 2}
|
showSlot={showFormattingBar ? 1 : 2}
|
||||||
@@ -632,16 +657,17 @@ const AdvanceTextEditor = ({
|
|||||||
'formatting-bar': showFormattingBar,
|
'formatting-bar': showFormattingBar,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
{!wasNotifiedOfLogIn && (
|
||||||
id={'speak-'}
|
<div
|
||||||
aria-live='assertive'
|
aria-live='assertive'
|
||||||
className='sr-only'
|
className='sr-only'
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channelView.login.successfull'
|
id='channelView.login.successfull'
|
||||||
defaultMessage='Login Successfull'
|
defaultMessage='Login Successful'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={'AdvancedTextEditor__body'}
|
className={'AdvancedTextEditor__body'}
|
||||||
disabled={readOnlyChannel}
|
disabled={readOnlyChannel}
|
||||||
@@ -651,10 +677,7 @@ const AdvanceTextEditor = ({
|
|||||||
role='application'
|
role='application'
|
||||||
id='advancedTextEditorCell'
|
id='advancedTextEditorCell'
|
||||||
data-a11y-sort-order='2'
|
data-a11y-sort-order='2'
|
||||||
aria-label={Utils.localizeMessage(
|
aria-label={ariaLabel}
|
||||||
'channelView.login.successfull',
|
|
||||||
'Login Successfull',
|
|
||||||
) + ' ' + ariaLabelMessageInput}
|
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className='AdvancedTextEditor__cell a11y__region'
|
className='AdvancedTextEditor__cell a11y__region'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -659,6 +659,11 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
// Record a successful login to local storage. If an unintentional logout occurs, e.g.
|
// Record a successful login to local storage. If an unintentional logout occurs, e.g.
|
||||||
// via session expiration, this bit won't get reset and we can notify the user as such.
|
// via session expiration, this bit won't get reset and we can notify the user as such.
|
||||||
LocalStorageStore.setWasLoggedIn(true);
|
LocalStorageStore.setWasLoggedIn(true);
|
||||||
|
|
||||||
|
// After a user has just logged in, we set the following flag to "false" so that after
|
||||||
|
// a user is notified of successful login, we can set it back to "true"
|
||||||
|
LocalStorageStore.setWasNotifiedOfLogIn(false);
|
||||||
|
|
||||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
||||||
history.push(redirectTo);
|
history.push(redirectTo);
|
||||||
} else if (team) {
|
} else if (team) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const getPenultimateChannelNameKey = (userId: string, teamId: string) =>
|
|||||||
const getRecentEmojisKey = (userId: string) => ['recent_emojis', userId].join(':');
|
const getRecentEmojisKey = (userId: string) => ['recent_emojis', userId].join(':');
|
||||||
const getWasLoggedInKey = () => 'was_logged_in';
|
const getWasLoggedInKey = () => 'was_logged_in';
|
||||||
const teamIdJoinedOnLoadKey = 'teamIdJoinedOnLoad';
|
const teamIdJoinedOnLoadKey = 'teamIdJoinedOnLoad';
|
||||||
|
const wasNotifiedOfLogInKey = 'was_notified_of_login';
|
||||||
|
|
||||||
const getPathScopedKey = (path: string, key: string) => {
|
const getPathScopedKey = (path: string, key: string) => {
|
||||||
if (path === '' || path === '/') {
|
if (path === '' || path === '/') {
|
||||||
@@ -156,6 +157,16 @@ class LocalStorageStoreClass {
|
|||||||
getWasLoggedIn() {
|
getWasLoggedIn() {
|
||||||
return this.getItem(getWasLoggedInKey()) === 'true';
|
return this.getItem(getWasLoggedInKey()) === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the following flag's setter and getter are used to make sure a user is notified (via aria-label)
|
||||||
|
// about a successful login only once (MM-50821)
|
||||||
|
setWasNotifiedOfLogIn(wasNotified: boolean) {
|
||||||
|
this.setItem(wasNotifiedOfLogInKey, String(wasNotified));
|
||||||
|
}
|
||||||
|
|
||||||
|
getWasNotifiedOfLogIn() {
|
||||||
|
return this.getItem(wasNotifiedOfLogInKey) === 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocalStorageStore = new LocalStorageStoreClass();
|
const LocalStorageStore = new LocalStorageStoreClass();
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user