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)
|
<Memo(Button)
|
||||||
|
aria-label="Actions"
|
||||||
className="Button___icon"
|
className="Button___icon"
|
||||||
marginTop={true}
|
marginTop={true}
|
||||||
>
|
>
|
||||||
@@ -183,6 +184,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Memo(Button)
|
<Memo(Button)
|
||||||
|
aria-label="Actions"
|
||||||
className="Button___icon"
|
className="Button___icon"
|
||||||
marginTop={true}
|
marginTop={true}
|
||||||
>
|
>
|
||||||
@@ -307,6 +309,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Memo(Button)
|
<Memo(Button)
|
||||||
|
aria-label="Actions"
|
||||||
className="Button___icon"
|
className="Button___icon"
|
||||||
marginTop={true}
|
marginTop={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ function ThreadItem({
|
|||||||
<Button
|
<Button
|
||||||
marginTop={true}
|
marginTop={true}
|
||||||
className='Button___icon'
|
className='Button___icon'
|
||||||
|
aria-label={formatMessage({
|
||||||
|
id: 'threading.threadItem.menu',
|
||||||
|
defaultMessage: 'Actions',
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<DotsVerticalIcon size={18}/>
|
<DotsVerticalIcon size={18}/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,14 +8,25 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
|
|||||||
>
|
>
|
||||||
<Header
|
<Header
|
||||||
heading={
|
heading={
|
||||||
<React.Fragment>
|
<div
|
||||||
|
aria-label="Filter visible threads"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
className="tab-buttons-list"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="tab-button-wrapper"
|
className="tab-button-wrapper"
|
||||||
>
|
>
|
||||||
<Memo(Button)
|
<Memo(Button)
|
||||||
|
aria-controls="threads-list"
|
||||||
|
aria-selected={true}
|
||||||
className="Button___large Margined"
|
className="Button___large Margined"
|
||||||
|
id="threads-list-filter-none"
|
||||||
isActive={true}
|
isActive={true}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
defaultMessage="Followed threads"
|
defaultMessage="Followed threads"
|
||||||
@@ -28,10 +39,16 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
|
|||||||
id="threads-list-unread-button"
|
id="threads-list-unread-button"
|
||||||
>
|
>
|
||||||
<Memo(Button)
|
<Memo(Button)
|
||||||
|
aria-controls="threads-list"
|
||||||
|
aria-selected={false}
|
||||||
className="Button___large Margined"
|
className="Button___large Margined"
|
||||||
hasDot={true}
|
hasDot={true}
|
||||||
|
id="threads-list-filter-unread"
|
||||||
isActive={false}
|
isActive={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
defaultMessage="Unreads"
|
defaultMessage="Unreads"
|
||||||
@@ -39,7 +56,7 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
|
|||||||
/>
|
/>
|
||||||
</Memo(Button)>
|
</Memo(Button)>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</div>
|
||||||
}
|
}
|
||||||
id="tutorial-threads-mobile-header"
|
id="tutorial-threads-mobile-header"
|
||||||
right={
|
right={
|
||||||
@@ -47,9 +64,10 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
|
|||||||
className="right-anchor"
|
className="right-anchor"
|
||||||
>
|
>
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
title="Mark all as read"
|
title="Mark all threads as read"
|
||||||
>
|
>
|
||||||
<Memo(Button)
|
<Memo(Button)
|
||||||
|
aria-label="Mark all threads as read"
|
||||||
className="Button___large Button___icon"
|
className="Button___large Button___icon"
|
||||||
id="threads-list__mark-all-as-read"
|
id="threads-list__mark-all-as-read"
|
||||||
marginTop={true}
|
marginTop={true}
|
||||||
@@ -70,6 +88,8 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
|
|||||||
<div
|
<div
|
||||||
className="threads"
|
className="threads"
|
||||||
data-testid="threads_list"
|
data-testid="threads_list"
|
||||||
|
id="threads-list"
|
||||||
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
<Memo(VirtualizedThreadList)
|
<Memo(VirtualizedThreadList)
|
||||||
addNoMoreResultsItem={false}
|
addNoMoreResultsItem={false}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
|
|
||||||
|
.tab-buttons-list {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-button-wrapper {
|
.tab-button-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import WithTooltip from 'components/with_tooltip';
|
|||||||
|
|
||||||
import {A11yClassNames, Constants, CrtTutorialSteps, ModalIdentifiers, Preferences} from 'utils/constants';
|
import {A11yClassNames, Constants, CrtTutorialSteps, ModalIdentifiers, Preferences} from 'utils/constants';
|
||||||
import * as Keyboard from 'utils/keyboard';
|
import * as Keyboard from 'utils/keyboard';
|
||||||
|
import {a11yFocus, mod} from 'utils/utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
@@ -131,13 +132,12 @@ const ThreadList = ({
|
|||||||
};
|
};
|
||||||
}, [handleKeyDown]);
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
const handleRead = useCallback(() => {
|
const handleSetFilter = useCallback((filter: ThreadFilter) => {
|
||||||
setFilter(ThreadFilter.none);
|
if (filter === ThreadFilter.unread) {
|
||||||
}, [setFilter]);
|
trackEvent('crt', 'filter_threads_by_unread');
|
||||||
|
}
|
||||||
|
|
||||||
const handleUnread = useCallback(() => {
|
setFilter(filter);
|
||||||
trackEvent('crt', 'filter_threads_by_unread');
|
|
||||||
setFilter(ThreadFilter.unread);
|
|
||||||
}, [setFilter]);
|
}, [setFilter]);
|
||||||
|
|
||||||
const handleLoadMoreItems = useCallback(async (startIndex) => {
|
const handleLoadMoreItems = useCallback(async (startIndex) => {
|
||||||
@@ -186,6 +186,23 @@ const ThreadList = ({
|
|||||||
}));
|
}));
|
||||||
}, [handleAllMarkedRead]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -196,12 +213,19 @@ const ThreadList = ({
|
|||||||
<Header
|
<Header
|
||||||
id={'tutorial-threads-mobile-header'}
|
id={'tutorial-threads-mobile-header'}
|
||||||
heading={(
|
heading={(
|
||||||
<>
|
<div
|
||||||
|
className='tab-buttons-list'
|
||||||
|
aria-label={formatMessage({
|
||||||
|
id: 'threading.threadList.tabsLabel',
|
||||||
|
defaultMessage: 'Filter visible threads',
|
||||||
|
})}
|
||||||
|
{...tabListProps}
|
||||||
|
>
|
||||||
<div className={'tab-button-wrapper'}>
|
<div className={'tab-button-wrapper'}>
|
||||||
<Button
|
<Button
|
||||||
className={'Button___large Margined'}
|
className={'Button___large Margined'}
|
||||||
isActive={currentFilter === ThreadFilter.none}
|
isActive={currentFilter === ThreadFilter.none}
|
||||||
onClick={handleRead}
|
{...tabProps[0]}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='globalThreads.heading'
|
id='globalThreads.heading'
|
||||||
@@ -217,7 +241,7 @@ const ThreadList = ({
|
|||||||
className={'Button___large Margined'}
|
className={'Button___large Margined'}
|
||||||
isActive={currentFilter === ThreadFilter.unread}
|
isActive={currentFilter === ThreadFilter.unread}
|
||||||
hasDot={someUnread}
|
hasDot={someUnread}
|
||||||
onClick={handleUnread}
|
{...tabProps[1]}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='threading.filters.unreads'
|
id='threading.filters.unreads'
|
||||||
@@ -226,18 +250,22 @@ const ThreadList = ({
|
|||||||
</Button>
|
</Button>
|
||||||
{showUnreadTutorialTip && <CRTUnreadTutorialTip/>}
|
{showUnreadTutorialTip && <CRTUnreadTutorialTip/>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
right={(
|
right={(
|
||||||
<div className='right-anchor'>
|
<div className='right-anchor'>
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
title={formatMessage({
|
title={formatMessage({
|
||||||
id: 'threading.threadList.markRead',
|
id: 'threading.threadList.markRead',
|
||||||
defaultMessage: 'Mark all as read',
|
defaultMessage: 'Mark all threads as read',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
id={'threads-list__mark-all-as-read'}
|
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'}
|
className={'Button___large Button___icon'}
|
||||||
onClick={handleOpenMarkAllAsReadModal}
|
onClick={handleOpenMarkAllAsReadModal}
|
||||||
marginTop={true}
|
marginTop={true}
|
||||||
@@ -251,6 +279,8 @@ const ThreadList = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
id='threads-list'
|
||||||
|
role='tabpanel'
|
||||||
className='threads'
|
className='threads'
|
||||||
data-testid={'threads_list'}
|
data-testid={'threads_list'}
|
||||||
>
|
>
|
||||||
@@ -281,4 +311,58 @@ const ThreadList = ({
|
|||||||
</div>
|
</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);
|
export default memo(ThreadList);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ exports[`components/threading/common/thread_menu should match snapshot 1`] = `
|
|||||||
test
|
test
|
||||||
</button>
|
</button>
|
||||||
<Menu
|
<Menu
|
||||||
ariaLabel=""
|
ariaLabel="Actions"
|
||||||
openLeft={true}
|
openLeft={true}
|
||||||
>
|
>
|
||||||
<MenuItemAction
|
<MenuItemAction
|
||||||
@@ -53,7 +53,7 @@ exports[`components/threading/common/thread_menu should match snapshot after ope
|
|||||||
test
|
test
|
||||||
</button>
|
</button>
|
||||||
<Menu
|
<Menu
|
||||||
ariaLabel=""
|
ariaLabel="Actions"
|
||||||
openLeft={true}
|
openLeft={true}
|
||||||
>
|
>
|
||||||
<MenuItemAction
|
<MenuItemAction
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ function ThreadMenu({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<Menu
|
<Menu
|
||||||
ariaLabel={''}
|
ariaLabel={formatMessage({
|
||||||
|
id: 'threading.threadItem.menu',
|
||||||
|
defaultMessage: 'Actions',
|
||||||
|
})}
|
||||||
openLeft={true}
|
openLeft={true}
|
||||||
>
|
>
|
||||||
<Menu.ItemAction
|
<Menu.ItemAction
|
||||||
|
|||||||
@@ -5487,7 +5487,8 @@
|
|||||||
"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.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.copy": "Copy link",
|
||||||
"threading.threadMenu.follow": "Follow thread",
|
"threading.threadMenu.follow": "Follow thread",
|
||||||
"threading.threadMenu.followExtra": "You will be notified about replies",
|
"threading.threadMenu.followExtra": "You will be notified about replies",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user