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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
22ce9b606f
Коммит
210bdfcb72
@@ -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",
|
||||
|
||||
Ссылка в новой задаче
Block a user