Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
70ad8a1
feat: add new hook to check if a node is visible
florianduros May 21, 2025
15a4637
feat: filters in new room list can be collapsed
florianduros May 21, 2025
def54b8
feat: add animation to filter list
florianduros May 23, 2025
d354977
feat: hide chevron when list fit on one line
florianduros May 23, 2025
135a75f
fix: use correct label for expand button
florianduros May 26, 2025
6200a7d
test: update room list panel snapshots
florianduros May 23, 2025
5f7fb62
test: add tests for useIsNodeVisible
florianduros May 23, 2025
40df1b1
chore: update i18n
florianduros May 26, 2025
892053e
test: add tests for primary filters
florianduros May 26, 2025
86a0ea3
test(e2e): update existing screenshots
florianduros May 26, 2025
f3674ce
test(e2e): update primary filter tests
florianduros May 26, 2025
5f90a09
chore: typo in css file
florianduros May 28, 2025
7679b7c
refactor: replace ternary by if in filter condition
florianduros May 28, 2025
7692f23
feat: compute filter height instead of hardcoded value
florianduros Jun 6, 2025
9a289a2
Merge branch 'develop' into florianduros/new-room-list/merge-filters
florianduros Jun 9, 2025
5b8622a
fix: floor floating computation on filter
florianduros Jun 10, 2025
e5ab8b8
refactor: move hooks to dedicated files
florianduros Jun 10, 2025
c96d19a
test: update tests
florianduros Jun 10, 2025
387e69e
feat: rework collapse feature
florianduros Jun 11, 2025
fa159aa
test: remove room list panel snapshot
florianduros Jun 11, 2025
d74d231
test: update room list primary filter tests
florianduros Jun 11, 2025
ea718d8
test(e2e): update screenshots
florianduros Jun 11, 2025
1003d7a
Merge branch 'develop' into florianduros/new-room-list/merge-filters
florianduros Jun 11, 2025
0622cce
test(e2e): update screenshots
florianduros Jun 11, 2025
e73ff18
test(e2e): fix favourite filter in scroll behaviour test
florianduros Jun 11, 2025
ac09cd1
fix: accessibility order when tabbing
florianduros Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -240,13 +245,22 @@ 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);

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(
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 28 additions & 3 deletions res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
154 changes: 139 additions & 15 deletions src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading
Loading