New room list: filter list can be collapsed (#29992)

* feat: add new hook to check if a node is visible

* feat: filters in new room list can be collapsed

* feat: add animation to filter list

* feat: hide chevron when list fit on one line

* fix: use correct label for expand button

* test: update room list panel snapshots

* test: add tests for useIsNodeVisible

* chore: update i18n

* test: add tests for primary filters

* test(e2e): update existing screenshots

* test(e2e): update primary filter tests

* chore: typo in css file

* refactor: replace ternary by if in filter condition

* feat: compute filter height instead of hardcoded value

* fix: floor floating computation on filter

* refactor: move hooks to dedicated files

* test: update tests

* feat: rework collapse feature

* test: remove room list panel snapshot

* test: update room list primary filter tests

* test(e2e): update screenshots

* test(e2e): update screenshots

* test(e2e): fix favourite filter in scroll behaviour test

* fix: accessibility order when tabbing
This commit is contained in:
Florian Duros
2025-06-11 15:49:20 +02:00
committed by GitHub
parent 451a99d49e
commit 389a0e689e
39 changed files with 341 additions and 531 deletions

View File

@@ -22,13 +22,21 @@ test.describe("Room list filters and sort", () => {
});
function getPrimaryFilters(page: Page): Locator {
return page.getByRole("listbox", { name: "Room list filters" });
return page.getByTestId("primary-filters");
}
function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
function getFilterExpandButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
}
function getFilterCollapseButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
}
/**
* Get the room list
* @param page
@@ -136,6 +144,7 @@ test.describe("Room list filters and sort", () => {
await tile.click();
// Enable Favourite filter
await getFilterExpandButton(page).click();
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(tile).not.toBeVisible();
@@ -223,10 +232,6 @@ test.describe("Room list filters and sort", () => {
expect(await roomList.locator("role=gridcell").count()).toBe(4);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
@@ -240,6 +245,12 @@ test.describe("Room list filters and sort", () => {
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(5);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
@@ -247,6 +258,9 @@ test.describe("Room list filters and sort", () => {
await primaryFilters.getByRole("option", { name: "Invites" }).click();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await getFilterCollapseButton(page).click();
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
});
test(
@@ -326,6 +340,8 @@ test.describe("Room list filters and sort", () => {
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);
@@ -343,6 +359,8 @@ test.describe("Room list filters and sort", () => {
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -6,7 +6,32 @@
*/
.mx_RoomListPrimaryFilters {
margin: unset;
list-style-type: none;
padding: var(--cpd-space-2x) var(--cpd-space-3x);
padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x);
.mx_RoomListPrimaryFilters_wrapping {
display: none;
}
ul {
margin: unset;
padding: unset;
list-style-type: none;
/**
* The InteractionObserver needs the height to be set to work properly.
*/
height: 100%;
flex: 1;
}
.mx_RoomListPrimaryFilters_IconButton {
svg {
transition: transform 0.1s linear;
}
}
.mx_RoomListPrimaryFilters_IconButton[aria-expanded="true"] {
svg {
transform: rotate(180deg);
}
}
}

View File

@@ -5,8 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { ChatFilter } from "@vector-im/compound-web";
import React, { type JSX, useEffect, useId, useRef, useState, type RefObject } from "react";
import { ChatFilter, IconButton } from "@vector-im/compound-web";
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { Flex } from "../../../utils/Flex";
@@ -23,23 +24,146 @@ interface RoomListPrimaryFiltersProps {
* The primary filters for the room list
*/
export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element {
const id = useId();
const [isExpanded, setIsExpanded] = useState(false);
const { ref, isWrapping: displayChevron, wrappingIndex } = useCollapseFilters<HTMLUListElement>(isExpanded);
const filters = useVisibleFilters(vm.primaryFilters, wrappingIndex);
return (
<Flex
as="ul"
role="listbox"
aria-label={_t("room_list|primary_filters")}
className="mx_RoomListPrimaryFilters"
align="center"
gap="var(--cpd-space-2x)"
wrap="wrap"
data-testid="primary-filters"
gap="var(--cpd-space-3x)"
direction="row-reverse"
>
{vm.primaryFilters.map((filter) => (
<li role="option" aria-selected={filter.active} key={filter.name}>
<ChatFilter selected={filter.active} onClick={filter.toggle}>
{filter.name}
</ChatFilter>
</li>
))}
{displayChevron && (
<IconButton
subtleBackground={true}
aria-expanded={isExpanded}
aria-controls={id}
className="mx_RoomListPrimaryFilters_IconButton"
aria-label={isExpanded ? _t("room_list|collapse_filters") : _t("room_list|expand_filters")}
size="28px"
onClick={() => setIsExpanded((_expanded) => !_expanded)}
>
<ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
</IconButton>
)}
<Flex
id={id}
as="ul"
role="listbox"
aria-label={_t("room_list|primary_filters")}
align="center"
gap="var(--cpd-space-2x)"
wrap="wrap"
ref={ref}
>
{filters.map((filter, i) => (
<li role="option" aria-selected={filter.active} key={i}>
<ChatFilter selected={filter.active} onClick={() => filter.toggle()}>
{filter.name}
</ChatFilter>
</li>
))}
</Flex>
</Flex>
);
}
/**
* A hook to manage the wrapping of filters in the room list.
* It observes the filter list and hides filters that are wrapping when the list is not expanded.
* @param isExpanded
* @returns an object containing:
* - `ref`: a ref to put on the filter list element
* - `isWrapping`: a boolean indicating if the filters are wrapping
* - `wrappingIndex`: the index of the first filter that is wrapping
*/
function useCollapseFilters<T extends HTMLElement>(
isExpanded: boolean,
): { ref: RefObject<T | null>; isWrapping: boolean; wrappingIndex: number } {
const ref = useRef<T>(null);
const [isWrapping, setIsWrapping] = useState(false);
const [wrappingIndex, setWrappingIndex] = useState(-1);
useEffect(() => {
if (!ref.current) return;
const hideFilters = (list: Element): void => {
let isWrapping = false;
Array.from(list.children).forEach((node, i): void => {
const child = node as HTMLElement;
const wrappingClass = "mx_RoomListPrimaryFilters_wrapping";
child.setAttribute("aria-hidden", "false");
child.classList.remove(wrappingClass);
// If the filter list is expanded, all filters are visible
if (isExpanded) return;
// If the previous element is on the left element of the current one, it means that the filter is wrapping
const previousSibling = child.previousElementSibling as HTMLElement | null;
if (previousSibling && child.offsetLeft < previousSibling.offsetLeft) {
if (!isWrapping) setWrappingIndex(i);
isWrapping = true;
}
// If the filter is wrapping, we hide it
child.classList.toggle(wrappingClass, isWrapping);
child.setAttribute("aria-hidden", isWrapping.toString());
});
if (!isWrapping) setWrappingIndex(-1);
setIsWrapping(isExpanded || isWrapping);
};
hideFilters(ref.current);
const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target)));
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [isExpanded]);
return { ref, isWrapping, wrappingIndex };
}
/**
* A hook to sort the filters by active state.
* The list is sorted if the current filter index is greater than or equal to the wrapping index.
* If the wrapping index is -1, the filters are not sorted.
*
* @param filters - the list of filters to sort.
* @param wrappingIndex - the index of the first filter that is wrapping.
*/
export function useVisibleFilters(
filters: RoomListViewState["primaryFilters"],
wrappingIndex: number,
): RoomListViewState["primaryFilters"] {
// By default, the filters are not sorted
const [sortedFilters, setSortedFilters] = useState(filters);
useEffect(() => {
const isActiveFilterWrapping = filters.findIndex((f) => f.active) >= wrappingIndex;
// If the active filter is not wrapping, we don't need to sort the filters
if (!isActiveFilterWrapping || wrappingIndex === -1) {
setSortedFilters(filters);
return;
}
// Sort the filters with the current filter at first position
setSortedFilters(
filters.slice().sort((filterA, filterB) => {
// If the filter is active, it should be at the top of the list
if (filterA.active && !filterB.active) return -1;
if (!filterA.active && filterB.active) return 1;
// If both filters are active or not, keep their original order
return 0;
}),
);
}, [filters, wrappingIndex]);
return sortedFilters;
}

View File

@@ -2115,6 +2115,7 @@
"add_space_label": "Add space",
"breadcrumbs_empty": "No recently visited rooms",
"breadcrumbs_label": "Recently visited rooms",
"collapse_filters": "Collapse filter list",
"empty": {
"no_chats": "No chats yet",
"no_chats_description": "Get started by messaging someone or by creating a room",
@@ -2132,6 +2133,7 @@
"show_activity": "See all activity",
"show_chats": "Show all chats"
},
"expand_filters": "Expand filter list",
"failed_add_tag": "Failed to add tag %(tagName)s to room",
"failed_remove_tag": "Failed to remove tag %(tagName)s from room",
"failed_set_dm_tag": "Failed to set direct message tag",

View File

@@ -28,16 +28,13 @@ describe("<RoomListPanel />", () => {
});
it("should render the RoomListSearch component when UIComponent.FilterContainer is at true", () => {
const { asFragment } = renderComponent();
renderComponent();
expect(screen.getByRole("button", { name: "Search Ctrl K" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should not render the RoomListSearch component when UIComponent.FilterContainer is at false", () => {
mocked(shouldShowComponent).mockReturnValue(false);
const { asFragment } = renderComponent();
renderComponent();
expect(screen.queryByRole("button", { name: "Search Ctrl K" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { act } from "react";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
@@ -15,31 +15,111 @@ import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/f
describe("<RoomListPrimaryFilters />", () => {
let vm: RoomListViewState;
const filterToggleMocks = [jest.fn(), jest.fn(), jest.fn()];
let resizeCallback: ResizeObserverCallback;
beforeEach(() => {
// Reset mocks between tests
filterToggleMocks.forEach((mock) => mock.mockClear());
// Mock ResizeObserver
global.ResizeObserver = jest.fn().mockImplementation((callback) => {
resizeCallback = callback;
return {
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
};
});
vm = {
isLoadingRooms: false,
rooms: [],
canCreateRoom: true,
createRoom: jest.fn(),
createChatRoom: jest.fn(),
primaryFilters: [
{ name: "People", active: false, toggle: jest.fn(), key: FilterKey.PeopleFilter },
{ name: "Rooms", active: true, toggle: jest.fn(), key: FilterKey.RoomsFilter },
{ name: "People", active: false, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
{ name: "Rooms", active: true, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
{ name: "Unreads", active: false, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
],
activeIndex: undefined,
};
} as unknown as RoomListViewState;
});
it("should render primary filters", async () => {
const user = userEvent.setup();
it("should renders all filters correctly", () => {
const { asFragment } = render(<RoomListPrimaryFilters vm={vm} />);
expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "true");
expect(asFragment()).toMatchSnapshot();
// Check that all filters are rendered
expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Rooms" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Unreads" })).toBeInTheDocument();
// Check that the active filter is marked as selected
expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("option", { name: "People" })).toHaveAttribute("aria-selected", "false");
expect(screen.getByRole("option", { name: "Unreads" })).toHaveAttribute("aria-selected", "false");
expect(asFragment()).toMatchSnapshot();
});
it("should call toggle function when a filter is clicked", async () => {
const user = userEvent.setup();
render(<RoomListPrimaryFilters vm={vm} />);
// Click on an inactive filter
await user.click(screen.getByRole("button", { name: "People" }));
expect(vm.primaryFilters[0].toggle).toHaveBeenCalled();
// Check that the toggle function was called
expect(filterToggleMocks[0]).toHaveBeenCalledTimes(1);
});
function mockFiltersOffsetLeft() {
jest.spyOn(screen.getByRole("option", { name: "People" }), "offsetLeft", "get").mockReturnValue(0);
jest.spyOn(screen.getByRole("option", { name: "Rooms" }), "offsetLeft", "get").mockReturnValue(30);
// Unreads is wrapping
jest.spyOn(screen.getByRole("option", { name: "Unreads" }), "offsetLeft", "get").mockReturnValue(0);
}
it("should hide or display filters if they are wrapping", async () => {
const user = userEvent.setup();
render(<RoomListPrimaryFilters vm={vm} />);
// No filter is wrapping, so chevron shouldn't be visible
expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull();
expect(screen.queryByRole("option", { name: "Unreads" })).toBeVisible();
mockFiltersOffsetLeft();
// @ts-ignore
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
// The Unreads filter is wrapping, it should not be visible
expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull();
// Now filters are wrapping, so chevron should be visible
await user.click(screen.getByRole("button", { name: "Expand filter list" }));
// The list is expanded, so Unreads should be visible
expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible();
});
it("should move the active filter if the list is collapsed and the filter is wrapping", async () => {
vm = {
primaryFilters: [
{ name: "People", active: false, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
{ name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
{ name: "Unreads", active: true, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
],
} as unknown as RoomListViewState;
const user = userEvent.setup();
render(<RoomListPrimaryFilters vm={vm} />);
mockFiltersOffsetLeft();
// @ts-ignore
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
// Unread filter should be moved to the first position
expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).toBe(
screen.getByRole("option", { name: "Unreads" }),
);
// When the list is expanded, the Unreads filter should move to its original position
await user.click(screen.getByRole("button", { name: "Expand filter list" }));
expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).not.toEqual(
screen.getByRole("option", { name: "Unreads" }),
);
});
});

View File

@@ -1,459 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomListPanel /> should not render the RoomListSearch component when UIComponent.FilterContainer is at false 1`] = `
<DocumentFragment>
<section
class="mx_Flex mx_RoomListPanel"
data-testid="room-list-panel"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<header
aria-label="Room options"
class="mx_Flex mx_RoomListHeaderView"
data-testid="room-list-header"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<div
class="mx_Flex mx_RoomListHeaderView_title"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<h1
title="Home"
>
Home
</h1>
</div>
<div
class="mx_Flex"
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: nowrap;"
>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«rc»"
class="_icon-button_m2erp_8"
data-state="closed"
id="radix-«ra»"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
color="var(--cpd-color-icon-secondary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</button>
<button
aria-label="New message"
class="_icon-button_m2erp_8"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
color="var(--cpd-color-icon-secondary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M16.937 2.82a2 2 0 0 1 2.828 0l1.415 1.414a2 2 0 0 1 0 2.829l-7.071 7.07c-.195.196-.42.342-.66.44a1 1 0 0 1-.168.072l-3.993 1.331a1 1 0 0 1-1.265-1.265l1.331-3.992q.03-.09.073-.168m10.338-4.903-6.717 6.718-1.414-1.414 6.717-6.718z"
fill-rule="evenodd"
/>
<path
d="M3 5a2 2 0 0 1 2-2h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
/>
</svg>
</div>
</button>
</div>
</header>
<ul
aria-label="Room list filters"
class="mx_Flex mx_RoomListPrimaryFilters"
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;"
>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Unreads
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
People
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Rooms
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Mentions
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Invites
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Favourites
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Low priority
</button>
</li>
</ul>
<div
class="mx_RoomListSkeleton"
/>
</section>
</DocumentFragment>
`;
exports[`<RoomListPanel /> should render the RoomListSearch component when UIComponent.FilterContainer is at true 1`] = `
<DocumentFragment>
<section
class="mx_Flex mx_RoomListPanel"
data-testid="room-list-panel"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<div
class="mx_Flex mx_RoomListSearch"
role="search"
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: nowrap;"
>
<button
class="_button_vczzf_8 mx_RoomListSearch_search _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
/>
</svg>
<span
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<span
class="mx_RoomListSearch_search_text"
>
Search
</span>
<kbd>
Ctrl K
</kbd>
</span>
</button>
<button
aria-label="Explore rooms"
class="_button_vczzf_8 mx_RoomListSearch_button _has-icon_vczzf_57 _icon-only_vczzf_50"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
/>
</svg>
</button>
</div>
<header
aria-label="Room options"
class="mx_Flex mx_RoomListHeaderView"
data-testid="room-list-header"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<div
class="mx_Flex mx_RoomListHeaderView_title"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<h1
title="Home"
>
Home
</h1>
</div>
<div
class="mx_Flex"
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: nowrap;"
>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«r2»"
class="_icon-button_m2erp_8"
data-state="closed"
id="radix-«r0»"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
color="var(--cpd-color-icon-secondary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</button>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Add"
class="_icon-button_m2erp_8"
data-state="closed"
id="radix-«r7»"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
color="var(--cpd-color-icon-secondary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M16.937 2.82a2 2 0 0 1 2.828 0l1.415 1.414a2 2 0 0 1 0 2.829l-7.071 7.07c-.195.196-.42.342-.66.44a1 1 0 0 1-.168.072l-3.993 1.331a1 1 0 0 1-1.265-1.265l1.331-3.992q.03-.09.073-.168m10.338-4.903-6.717 6.718-1.414-1.414 6.717-6.718z"
fill-rule="evenodd"
/>
<path
d="M3 5a2 2 0 0 1 2-2h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
/>
</svg>
</div>
</button>
</div>
</header>
<ul
aria-label="Room list filters"
class="mx_Flex mx_RoomListPrimaryFilters"
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;"
>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Unreads
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
People
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Rooms
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Mentions
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Invites
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Favourites
</button>
</li>
<li
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Low priority
</button>
</li>
</ul>
<div
class="mx_RoomListSkeleton"
/>
</section>
</DocumentFragment>
`;

View File

@@ -1,39 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomListPrimaryFilters /> should render primary filters 1`] = `
exports[`<RoomListPrimaryFilters /> should renders all filters correctly 1`] = `
<DocumentFragment>
<ul
aria-label="Room list filters"
<div
class="mx_Flex mx_RoomListPrimaryFilters"
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;"
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;"
>
<li
aria-selected="false"
role="option"
<ul
aria-label="Room list filters"
class="mx_Flex"
id="«r0»"
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;"
>
<button
<li
aria-hidden="false"
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
role="option"
>
People
</button>
</li>
<li
aria-selected="true"
role="option"
>
<button
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
People
</button>
</li>
<li
aria-hidden="false"
aria-selected="true"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
role="option"
>
Rooms
</button>
</li>
</ul>
<button
aria-selected="true"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Rooms
</button>
</li>
<li
aria-hidden="false"
aria-selected="false"
role="option"
>
<button
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="button"
tabindex="0"
>
Unreads
</button>
</li>
</ul>
</div>
</DocumentFragment>
`;