Avoid flicker of the room list filter on resize (#30787)

* fix: avoid flicker of the room list filter on resize

* test: update existing test to render correctly

* test: add test to avoid flicker regression
This commit is contained in:
Florian Duros 2025-09-16 17:52:36 +02:00 committed by GitHub
parent 6f41ac58bc
commit 841f12bd46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 47 additions and 17 deletions

View File

@ -103,7 +103,7 @@ function useCollapseFilters<T extends HTMLElement>(
// 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 (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) {
if (!isWrapping) setWrappingIndex(i);
isWrapping = true;
}

View File

@ -35,15 +35,26 @@ describe("<RoomListPrimaryFilters />", () => {
vm = {
primaryFilters: [
{ name: "People", active: false, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
{ name: "Rooms", active: true, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
{ name: "People", active: true, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
{ name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
{ name: "Unreads", active: false, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
],
} as unknown as RoomListViewState;
});
function mockFiltersOffsetLeft() {
// Use `getByText` instead of `getByRole` to bypass the aria-hidden
jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0);
jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30);
jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60);
// @ts-ignore
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
}
it("should renders all filters correctly", () => {
const { asFragment } = render(<RoomListPrimaryFilters vm={vm} />);
mockFiltersOffsetLeft();
// Check that all filters are rendered
expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument();
@ -51,8 +62,8 @@ describe("<RoomListPrimaryFilters />", () => {
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: "People" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "false");
expect(screen.getByRole("option", { name: "Unreads" })).toHaveAttribute("aria-selected", "false");
expect(asFragment()).toMatchSnapshot();
@ -61,6 +72,7 @@ describe("<RoomListPrimaryFilters />", () => {
it("should call toggle function when a filter is clicked", async () => {
const user = userEvent.setup();
render(<RoomListPrimaryFilters vm={vm} />);
mockFiltersOffsetLeft();
// Click on an inactive filter
await user.click(screen.getByRole("option", { name: "People" }));
@ -69,24 +81,27 @@ describe("<RoomListPrimaryFilters />", () => {
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);
function makeUnreadWrapping() {
// Use `getByText` instead of `getByRole` to bypass the aria-hidden
jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0);
jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30);
// Unreads is wrapping
jest.spyOn(screen.getByRole("option", { name: "Unreads" }), "offsetLeft", "get").mockReturnValue(0);
jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0);
// @ts-ignore
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
}
it("should hide or display filters if they are wrapping", async () => {
const user = userEvent.setup();
render(<RoomListPrimaryFilters vm={vm} />);
mockFiltersOffsetLeft();
// 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" }) }]));
makeUnreadWrapping();
// The Unreads filter is wrapping, it should not be visible
expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull();
@ -107,9 +122,7 @@ describe("<RoomListPrimaryFilters />", () => {
const user = userEvent.setup();
render(<RoomListPrimaryFilters vm={vm} />);
mockFiltersOffsetLeft();
// @ts-ignore
act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
makeUnreadWrapping();
// Unread filter should be moved to the first position
expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).toBe(
@ -122,4 +135,21 @@ describe("<RoomListPrimaryFilters />", () => {
screen.getByRole("option", { name: "Unreads" }),
);
});
it("should hide the filter is the previous is on the same vertical position", async () => {
render(<RoomListPrimaryFilters vm={vm} />);
mockFiltersOffsetLeft();
jest.spyOn(screen.getByRole("option", { name: "People" }), "offsetLeft", "get").mockReturnValue(0);
// Rooms is wrapping
jest.spyOn(screen.getByRole("option", { name: "Rooms" }), "offsetLeft", "get").mockReturnValue(0);
// @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: "Rooms" })).toBeNull();
// Now filters are wrapping, so chevron should be visible
expect(screen.getByRole("button", { name: "Expand filter list" })).toBeVisible();
});
});

View File

@ -16,7 +16,7 @@ exports[`<RoomListPrimaryFilters /> should renders all filters correctly 1`] = `
>
<button
aria-hidden="false"
aria-selected="false"
aria-selected="true"
class="_chat-filter_5qdp0_8"
role="option"
tabindex="0"
@ -25,7 +25,7 @@ exports[`<RoomListPrimaryFilters /> should renders all filters correctly 1`] = `
</button>
<button
aria-hidden="false"
aria-selected="true"
aria-selected="false"
class="_chat-filter_5qdp0_8"
role="option"
tabindex="0"