Add axe compliance for new room list (#30700)

* Add tests for axe violations for the new room list

* axe doesn't like a ul/li with roles listbox/option. Changing to div/button as we have elsewhere like RoomListitemView.

* Fix RoomListPrimaryFilters test

* Justify the items in the primary filter container

to get the dropdown button on the right again

* Update snapshot

* Make the room list itself focusable

As the comment said, there was no real reason it needed to be, except
that there was because of axe. Probably having the children focusable
would be better, but Virtuoso wraps them in more divs which doesn't
satisfy axe's requirements since those inner divs are not the scrollable
ones. I can't see a better option than this right now.

* Update snapshot

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
David Langley 2025-09-09 17:54:13 +01:00 committed by GitHub
parent a73f4f5803
commit dba4ca26e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 31 additions and 42 deletions

View File

@ -41,7 +41,7 @@ test.describe("Room list", () => {
} }
}); });
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => { test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
const roomListView = getRoomList(page); const roomListView = getRoomList(page);
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible(); await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png"); await expect(roomListView).toMatchScreenshot("room-list.png");
@ -54,6 +54,7 @@ test.describe("Room list", () => {
// scrollListToBottom seems to leave the mouse hovered over the list, move it away. // scrollListToBottom seems to leave the mouse hovered over the list, move it away.
await page.getByRole("button", { name: "User menu" }).hover(); await page.getByRole("button", { name: "User menu" }).hover();
await expect(axe).toHaveNoViolations();
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png"); await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
}); });

View File

@ -285,7 +285,10 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
return ( return (
<Virtuoso <Virtuoso
tabIndex={props.tabIndex || undefined} // We don't need to focus the container, so leave it undefined by default // note that either the container of direct children must be focusable to be axe
// compliant, so we leave tabIndex as the default so the container can be focused
// (virtuoso wraps the children inside another couple of elements so setting it
// on those doesn't seem to work, unfortunately)
ref={virtuosoHandleRef} ref={virtuosoHandleRef}
scrollerRef={scrollerRef} scrollerRef={scrollerRef}
onKeyDown={keyDownCallback} onKeyDown={keyDownCallback}

View File

@ -36,6 +36,7 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
data-testid="primary-filters" data-testid="primary-filters"
gap="var(--cpd-space-3x)" gap="var(--cpd-space-3x)"
direction="row-reverse" direction="row-reverse"
justify="space-between"
> >
{displayChevron && ( {displayChevron && (
<IconButton <IconButton
@ -52,7 +53,7 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
)} )}
<Flex <Flex
id={id} id={id}
as="ul" as="div"
role="listbox" role="listbox"
aria-label={_t("room_list|primary_filters")} aria-label={_t("room_list|primary_filters")}
align="center" align="center"
@ -61,11 +62,9 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
ref={ref} ref={ref}
> >
{filters.map((filter, i) => ( {filters.map((filter, i) => (
<li role="option" aria-selected={filter.active} key={i}> <ChatFilter key={i} role="option" selected={filter.active} onClick={() => filter.toggle()}>
<ChatFilter selected={filter.active} onClick={() => filter.toggle()}> {filter.name}
{filter.name} </ChatFilter>
</ChatFilter>
</li>
))} ))}
</Flex> </Flex>
</Flex> </Flex>

View File

@ -63,7 +63,7 @@ describe("<RoomListPrimaryFilters />", () => {
render(<RoomListPrimaryFilters vm={vm} />); render(<RoomListPrimaryFilters vm={vm} />);
// Click on an inactive filter // Click on an inactive filter
await user.click(screen.getByRole("button", { name: "People" })); await user.click(screen.getByRole("option", { name: "People" }));
// Check that the toggle function was called // Check that the toggle function was called
expect(filterToggleMocks[0]).toHaveBeenCalledTimes(1); expect(filterToggleMocks[0]).toHaveBeenCalledTimes(1);

View File

@ -9,6 +9,7 @@ exports[`<RoomList /> should render a room list 1`] = `
data-virtuoso-scroller="true" data-virtuoso-scroller="true"
role="listbox" role="listbox"
style="height: 100%; outline: none; overflow-y: auto; position: relative;" style="height: 100%; outline: none; overflow-y: auto; position: relative;"
tabindex="0"
> >
<div <div
data-viewport-type="element" data-viewport-type="element"

View File

@ -5,58 +5,43 @@ exports[`<RoomListPrimaryFilters /> should renders all filters correctly 1`] = `
<div <div
class="flex mx_RoomListPrimaryFilters" class="flex mx_RoomListPrimaryFilters"
data-testid="primary-filters" data-testid="primary-filters"
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
> >
<ul <div
aria-label="Room list filters" aria-label="Room list filters"
class="flex" class="flex"
id="«r0»" id="«r0»"
role="listbox" role="listbox"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: wrap;" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: wrap;"
> >
<li <button
aria-hidden="false" aria-hidden="false"
aria-selected="false" aria-selected="false"
class="_chat-filter_5qdp0_8"
role="option" role="option"
tabindex="0"
> >
<button People
aria-selected="false" </button>
class="_chat-filter_5qdp0_8" <button
role="button"
tabindex="0"
>
People
</button>
</li>
<li
aria-hidden="false" aria-hidden="false"
aria-selected="true" aria-selected="true"
class="_chat-filter_5qdp0_8"
role="option" role="option"
tabindex="0"
> >
<button Rooms
aria-selected="true" </button>
class="_chat-filter_5qdp0_8" <button
role="button"
tabindex="0"
>
Rooms
</button>
</li>
<li
aria-hidden="false" aria-hidden="false"
aria-selected="false" aria-selected="false"
class="_chat-filter_5qdp0_8"
role="option" role="option"
tabindex="0"
> >
<button Unreads
aria-selected="false" </button>
class="_chat-filter_5qdp0_8" </div>
role="button"
tabindex="0"
>
Unreads
</button>
</li>
</ul>
</div> </div>
</DocumentFragment> </DocumentFragment>
`; `;