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) <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",