MM-63056/MM-63058/MM-63049 Improve accessibility of Threads list (#30816)

* MM-63056 Add accessible name to Threads item menus

* MM-63058 Add accessible name to Threads mark as unread button

* MM-63049 Change Threads list to use tab pattern for filtering

* Revert accidentally added i18n string
Этот коммит содержится в:
Harrison Healey
2025-04-28 15:54:50 -04:00
коммит произвёл GitHub
родитель 22ce9b606f
Коммит 210bdfcb72
8 изменённых файлов: 137 добавлений и 18 удалений

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

@@ -57,6 +57,7 @@ exports[`components/threading/global_threads/thread_item should report total num
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
@@ -183,6 +184,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
@@ -307,6 +309,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>

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

@@ -258,6 +258,10 @@ function ThreadItem({
<Button
marginTop={true}
className='Button___icon'
aria-label={formatMessage({
id: 'threading.threadItem.menu',
defaultMessage: 'Actions',
})}
>
<DotsVerticalIcon size={18}/>
</Button>

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

@@ -8,14 +8,25 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
>
<Header
heading={
<React.Fragment>
<div
aria-label="Filter visible threads"
aria-orientation="horizontal"
className="tab-buttons-list"
role="tablist"
>
<div
className="tab-button-wrapper"
>
<Memo(Button)
aria-controls="threads-list"
aria-selected={true}
className="Button___large Margined"
id="threads-list-filter-none"
isActive={true}
onClick={[Function]}
onKeyDown={[Function]}
role="tab"
tabIndex={0}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Followed threads"
@@ -28,10 +39,16 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
id="threads-list-unread-button"
>
<Memo(Button)
aria-controls="threads-list"
aria-selected={false}
className="Button___large Margined"
hasDot={true}
id="threads-list-filter-unread"
isActive={false}
onClick={[Function]}
onKeyDown={[Function]}
role="tab"
tabIndex={-1}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Unreads"
@@ -39,7 +56,7 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
/>
</Memo(Button)>
</div>
</React.Fragment>
</div>
}
id="tutorial-threads-mobile-header"
right={
@@ -47,9 +64,10 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
className="right-anchor"
>
<WithTooltip
title="Mark all as read"
title="Mark all threads as read"
>
<Memo(Button)
aria-label="Mark all threads as read"
className="Button___large Button___icon"
id="threads-list__mark-all-as-read"
marginTop={true}
@@ -70,6 +88,8 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
<div
className="threads"
data-testid="threads_list"
id="threads-list"
role="tabpanel"
>
<Memo(VirtualizedThreadList)
addNoMoreResultsItem={false}

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

@@ -13,6 +13,10 @@
color: rgba(var(--center-channel-color-rgb), 0.75);
grid-area: header;
.tab-buttons-list {
display: flex;
}
.tab-button-wrapper {
position: relative;
display: flex;

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

@@ -26,6 +26,7 @@ import WithTooltip from 'components/with_tooltip';
import {A11yClassNames, Constants, CrtTutorialSteps, ModalIdentifiers, Preferences} from 'utils/constants';
import * as Keyboard from 'utils/keyboard';
import {a11yFocus, mod} from 'utils/utils';
import type {GlobalState} from 'types/store';
@@ -131,13 +132,12 @@ const ThreadList = ({
};
}, [handleKeyDown]);
const handleRead = useCallback(() => {
setFilter(ThreadFilter.none);
}, [setFilter]);
const handleSetFilter = useCallback((filter: ThreadFilter) => {
if (filter === ThreadFilter.unread) {
trackEvent('crt', 'filter_threads_by_unread');
}
const handleUnread = useCallback(() => {
trackEvent('crt', 'filter_threads_by_unread');
setFilter(ThreadFilter.unread);
setFilter(filter);
}, [setFilter]);
const handleLoadMoreItems = useCallback(async (startIndex) => {
@@ -186,6 +186,23 @@ const ThreadList = ({
}));
}, [handleAllMarkedRead]);
const {tabListProps, tabProps} = useTabs<ThreadFilter>({
activeTab: currentFilter,
setActiveTab: handleSetFilter,
tabs: [
{
id: 'threads-list-filter-none',
name: ThreadFilter.none,
panelId: 'threads-list',
},
{
id: 'threads-list-filter-unread',
name: ThreadFilter.unread,
panelId: 'threads-list',
},
],
});
return (
<div
tabIndex={0}
@@ -196,12 +213,19 @@ const ThreadList = ({
<Header
id={'tutorial-threads-mobile-header'}
heading={(
<>
<div
className='tab-buttons-list'
aria-label={formatMessage({
id: 'threading.threadList.tabsLabel',
defaultMessage: 'Filter visible threads',
})}
{...tabListProps}
>
<div className={'tab-button-wrapper'}>
<Button
className={'Button___large Margined'}
isActive={currentFilter === ThreadFilter.none}
onClick={handleRead}
{...tabProps[0]}
>
<FormattedMessage
id='globalThreads.heading'
@@ -217,7 +241,7 @@ const ThreadList = ({
className={'Button___large Margined'}
isActive={currentFilter === ThreadFilter.unread}
hasDot={someUnread}
onClick={handleUnread}
{...tabProps[1]}
>
<FormattedMessage
id='threading.filters.unreads'
@@ -226,18 +250,22 @@ const ThreadList = ({
</Button>
{showUnreadTutorialTip && <CRTUnreadTutorialTip/>}
</div>
</>
</div>
)}
right={(
<div className='right-anchor'>
<WithTooltip
title={formatMessage({
id: 'threading.threadList.markRead',
defaultMessage: 'Mark all as read',
defaultMessage: 'Mark all threads as read',
})}
>
<Button
id={'threads-list__mark-all-as-read'}
aria-label={formatMessage({
id: 'threading.threadList.markRead',
defaultMessage: 'Mark all threads as read',
})}
className={'Button___large Button___icon'}
onClick={handleOpenMarkAllAsReadModal}
marginTop={true}
@@ -251,6 +279,8 @@ const ThreadList = ({
)}
/>
<div
id='threads-list'
role='tabpanel'
className='threads'
data-testid={'threads_list'}
>
@@ -281,4 +311,58 @@ const ThreadList = ({
</div>
);
};
function useTabs<TabName extends string>({
activeTab,
setActiveTab,
tabs,
}: {
activeTab: TabName;
setActiveTab: (tab: TabName) => void;
tabs: Array<{
id: string;
name: TabName;
panelId: string;
}>;
}): {
tabListProps: React.HTMLAttributes<HTMLElement>;
tabProps: Array<React.HTMLAttributes<HTMLElement>>;
} {
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
let delta = 0;
if (Keyboard.isKeyPressed(e, Constants.KeyCodes.RIGHT)) {
delta = 1;
} else if (Keyboard.isKeyPressed(e, Constants.KeyCodes.LEFT)) {
delta = -1;
}
if (delta === 0) {
return;
}
let index = tabs.findIndex((tab) => tab.name === activeTab);
index += delta;
index = mod(index, tabs.length);
setActiveTab(tabs[index].name);
a11yFocus(document.getElementById(tabs[index].id));
}, [activeTab, setActiveTab, tabs]);
return {
tabListProps: {
role: 'tablist',
'aria-orientation': 'horizontal',
},
tabProps: tabs.map((tab) => ({
id: tab.id,
role: 'tab',
onClick: () => setActiveTab(tab.name),
onKeyDown: handleKeyDown,
tabIndex: tab.name === activeTab ? 0 : -1,
'aria-controls': tab.panelId,
'aria-selected': activeTab === tab.name,
})),
};
}
export default memo(ThreadList);

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

@@ -10,7 +10,7 @@ exports[`components/threading/common/thread_menu should match snapshot 1`] = `
test
</button>
<Menu
ariaLabel=""
ariaLabel="Actions"
openLeft={true}
>
<MenuItemAction
@@ -53,7 +53,7 @@ exports[`components/threading/common/thread_menu should match snapshot after ope
test
</button>
<Menu
ariaLabel=""
ariaLabel="Actions"
openLeft={true}
>
<MenuItemAction

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

@@ -81,7 +81,10 @@ function ThreadMenu({
>
{children}
<Menu
ariaLabel={''}
ariaLabel={formatMessage({
id: 'threading.threadItem.menu',
defaultMessage: 'Actions',
})}
openLeft={true}
>
<Menu.ItemAction

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

@@ -5487,7 +5487,8 @@
"threading.threadHeader.menu": "More Actions",
"threading.threadItem.ariaLabel": "Thread by {author}",
"threading.threadItem.menu": "Actions",
"threading.threadList.markRead": "Mark all as read",
"threading.threadList.markRead": "Mark all threads as read",
"threading.threadList.tabsLabel": "Filter visible threads",
"threading.threadMenu.copy": "Copy link",
"threading.threadMenu.follow": "Follow thread",
"threading.threadMenu.followExtra": "You will be notified about replies",