[MM-62987] Include description on aria-label for last reply time, make link focused when using up/down arrows. (#30837)

* [MM-62987] Include description on aria-label for last reply time, make link focused when using up/down arrows.

* PR feedback

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2025-05-08 10:50:14 -04:00
коммит произвёл GitHub
родитель c33dbe8d37
Коммит 0058edb806
5 изменённых файлов: 566 добавлений и 464 удалений

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

@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/threading/global_threads/thread_item should report total number of replies 1`] = ` exports[`components/threading/global_threads/thread_item should report total number of replies 1`] = `
<div <Fragment>
<div
aria-describedby="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
aria-label="Thread by Someone" aria-label="Thread by Someone"
className="ThreadItem" className="ThreadItem"
id="" id=""
@@ -9,7 +11,7 @@ exports[`components/threading/global_threads/thread_item should report total num
onKeyDown={[Function]} onKeyDown={[Function]}
role="link" role="link"
tabIndex={0} tabIndex={0}
> >
<header> <header>
<div <div
className="ThreadItem__author" className="ThreadItem__author"
@@ -115,11 +117,37 @@ exports[`components/threading/global_threads/thread_item should report total num
} }
/> />
</div> </div>
</div> <span
className="sr-only"
id="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
>
<MemoizedFormattedMessage
defaultMessage="Last reply "
id="threading.threadItem.timestamp"
/>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</span>
</div>
</Fragment>
`; `;
exports[`components/threading/global_threads/thread_item should report unread mentions 1`] = ` exports[`components/threading/global_threads/thread_item should report unread mentions 1`] = `
<div <Fragment>
<div
aria-describedby="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
aria-label="Thread by Someone" aria-label="Thread by Someone"
className="ThreadItem has-unreads" className="ThreadItem has-unreads"
id="" id=""
@@ -127,7 +155,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
onKeyDown={[Function]} onKeyDown={[Function]}
role="link" role="link"
tabIndex={0} tabIndex={0}
> >
<header> <header>
<div <div
className="indicator" className="indicator"
@@ -242,11 +270,37 @@ exports[`components/threading/global_threads/thread_item should report unread me
} }
/> />
</div> </div>
</div> <span
className="sr-only"
id="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
>
<MemoizedFormattedMessage
defaultMessage="Last reply "
id="threading.threadItem.timestamp"
/>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</span>
</div>
</Fragment>
`; `;
exports[`components/threading/global_threads/thread_item should report unread messages 1`] = ` exports[`components/threading/global_threads/thread_item should report unread messages 1`] = `
<div <Fragment>
<div
aria-describedby="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
aria-label="Thread by Someone" aria-label="Thread by Someone"
className="ThreadItem has-unreads" className="ThreadItem has-unreads"
id="" id=""
@@ -254,7 +308,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
onKeyDown={[Function]} onKeyDown={[Function]}
role="link" role="link"
tabIndex={0} tabIndex={0}
> >
<header> <header>
<div <div
className="indicator" className="indicator"
@@ -367,5 +421,29 @@ exports[`components/threading/global_threads/thread_item should report unread me
} }
/> />
</div> </div>
</div> <span
className="sr-only"
id="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
>
<MemoizedFormattedMessage
defaultMessage="Last reply "
id="threading.threadItem.timestamp"
/>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</span>
</div>
</Fragment>
`; `;

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

@@ -266,7 +266,7 @@ describe('components/threading/global_threads/thread_item', () => {
test('should allow marking as unread on alt + click', () => { test('should allow marking as unread on alt + click', () => {
const wrapper = shallow(<ThreadItem {...props}/>); const wrapper = shallow(<ThreadItem {...props}/>);
wrapper.simulate('click', {altKey: true}); wrapper.find('div.ThreadItem').simulate('click', {altKey: true});
expect(updateThreadRead).not.toHaveBeenCalled(); expect(updateThreadRead).not.toHaveBeenCalled();
expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('user_id', 'tid', '1y8hpek81byspd4enyk9mp1ncw'); expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('user_id', 'tid', '1y8hpek81byspd4enyk9mp1ncw');
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1611786714912); expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1611786714912);

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

@@ -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, {memo, useCallback, useEffect, useMemo} from 'react'; import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react';
import type {MouseEvent, KeyboardEvent} from 'react'; import type {MouseEvent, KeyboardEvent} from 'react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
@@ -94,6 +94,7 @@ function ThreadItem({
const postAuthor = ensureString(post.props?.override_username) || displayName; const postAuthor = ensureString(post.props?.override_username) || displayName;
const getMentionKeysForPost = useMemo(() => makeGetMentionKeysForPost(), []); const getMentionKeysForPost = useMemo(() => makeGetMentionKeysForPost(), []);
const mentionsKeys = useSelector((state: GlobalState) => getMentionKeysForPost(state, post, channel)); const mentionsKeys = useSelector((state: GlobalState) => getMentionKeysForPost(state, post, channel));
const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (channel?.teammate_id) { if (channel?.teammate_id) {
@@ -107,6 +108,12 @@ function ThreadItem({
} }
}, [channel, thread?.post.channel_id]); }, [channel, thread?.post.channel_id]);
useEffect(() => {
if (isSelected) {
ref.current?.focus();
}
}, [isSelected, threadId]);
const participantIds = useMemo(() => { const participantIds = useMemo(() => {
const ids = (thread?.participants || []).flatMap(({id}) => { const ids = (thread?.participants || []).flatMap(({id}) => {
if (id === post.user_id) { if (id === post.user_id) {
@@ -188,6 +195,7 @@ function ThreadItem({
} }
return ( return (
<>
<div <div
style={style} style={style}
className={classNames('ThreadItem', { className={classNames('ThreadItem', {
@@ -200,9 +208,11 @@ function ThreadItem({
{id: 'threading.threadItem.ariaLabel', defaultMessage: 'Thread by {author}'}, {id: 'threading.threadItem.ariaLabel', defaultMessage: 'Thread by {author}'},
{author: postAuthor}, {author: postAuthor},
)} )}
aria-describedby={`ThreadItem__timestamp_${threadId}`}
id={isFirstThreadInList ? 'tutorial-threads-mobile-list' : ''} id={isFirstThreadInList ? 'tutorial-threads-mobile-list' : ''}
onClick={selectHandler} onClick={selectHandler}
onKeyDown={selectHandler} onKeyDown={selectHandler}
ref={ref}
> >
<header> <header>
{Boolean(newMentions || newReplies) && ( {Boolean(newMentions || newReplies) && (
@@ -315,7 +325,22 @@ function ThreadItem({
)} )}
</div> </div>
{showListTutorialTip && isFirstThreadInList && isMobileView && (<CRTListTutorialTip/>)} {showListTutorialTip && isFirstThreadInList && isMobileView && (<CRTListTutorialTip/>)}
<span
className='sr-only'
id={`ThreadItem__timestamp_${threadId}`}
>
<FormattedMessage
id='threading.threadItem.timestamp'
defaultMessage='Last reply '
/>
<Timestamp
{...THREADING_TIME}
className='alt-hidden'
value={lastReplyAt}
/>
</span>
</div> </div>
</>
); );
} }

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

@@ -120,9 +120,7 @@ const ThreadList = ({
} }
} }
select(data[threadIdToSelect]); select(data[threadIdToSelect]);
e.preventDefault();
// hacky way to ensure the thread item loses focus.
ref.current?.focus();
}, [selectedThreadId, data]); }, [selectedThreadId, data]);
useEffect(() => { useEffect(() => {

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

@@ -5497,6 +5497,7 @@
"threading.threadHeader.menu": "More Actions", "threading.threadHeader.menu": "More Actions",
"threading.threadItem.ariaLabel": "Thread by {author}", "threading.threadItem.ariaLabel": "Thread by {author}",
"threading.threadItem.menu": "Actions", "threading.threadItem.menu": "Actions",
"threading.threadItem.timestamp": "Last reply ",
"threading.threadList.markRead": "Mark all threads as read", "threading.threadList.markRead": "Mark all threads as read",
"threading.threadList.tabsLabel": "Filter visible threads", "threading.threadList.tabsLabel": "Filter visible threads",
"threading.threadMenu.copy": "Copy link", "threading.threadMenu.copy": "Copy link",