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 // 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; const previousSibling = child.previousElementSibling as HTMLElement | null;
if (previousSibling && child.offsetLeft < previousSibling.offsetLeft) { if (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) {
if (!isWrapping) setWrappingIndex(i); if (!isWrapping) setWrappingIndex(i);
isWrapping = true; isWrapping = true;
} }

View File

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