[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>
Этот коммит содержится в:
Akbar Abdrakhmanov
2023-10-30 23:48:29 +02:00
коммит произвёл GitHub
родитель bfd6f5aa01
Коммит c6a6ad9b4b
3 изменённых файлов: 54 добавлений и 15 удалений

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

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