From 003debb97efacc432e2d0322af62e6e0aaa7cd53 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 30 Jan 2026 09:43:03 +0000 Subject: [PATCH] Add RoomListPrimaryFilters component Add filter chips component for filtering the room list by unread, people, rooms, favourites, mentions, invites, and low priority. --- .../default-auto.png | Bin 0 -> 10666 bytes .../narrow-container-auto.png | Bin 0 -> 6620 bytes ...arrow-with-active-wrapping-filter-auto.png | Bin 0 -> 6777 bytes .../no-filters-auto.png | Bin 0 -> 3522 bytes .../people-selected-auto.png | Bin 0 -> 11211 bytes .../RoomListPrimaryFilters.module.css | 32 ++ .../RoomListPrimaryFilters.stories.tsx | 85 ++++ .../RoomListPrimaryFilters.test.tsx | 140 +++++++ .../RoomListPrimaryFilters.tsx | 116 ++++++ .../RoomListPrimaryFilters.test.tsx.snap | 388 ++++++++++++++++++ .../RoomListPrimaryFilters/index.tsx | 12 + .../useCollapseFilters.ts | 71 ++++ .../useVisibleFilters.ts | 55 +++ 13 files changed, 899 insertions(+) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/default-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-container-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-with-active-wrapping-filter-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/no-filters-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/people-selected-auto.png create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts create mode 100644 packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/default-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..45c41520e201a4f8a65192026172cee38c5bfd6a GIT binary patch literal 10666 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 z z_)v&A=-)wrilT~%bGoo|#>n6x7Au6^`Uzt7i~3@7-X#@_ZOTEdQ;S{q5Dh~m2j zo=s0H5%+ta8(^+E2y*tb-g#y8P_j=y2td&2uui>Vu6!u@OVgqu9@@g&GIA8&$-qeF zZY*4z!0%qPePQBlMUcPKUSzfV=BFVgyv=c@5EI@$IsiG ztt1JCIj-nxpdglKMoZUpih>;~f{$nPA}C9z^xakh)c1(DqGXMig-tn#qqK7GghiugBvo*8<; zjLImbL~gsfj-bw_6;J#0RNkyN^g{Mh`^lA>yOpI#QNrailA-+kQ}*P46&PuNxm(J* zfzEJsl;su=H3?=OQp>)rL&S)t5$9O>VROR|5vTEp_^m7Xo{niT^Ph&AG!~nu#L1un zK#C4xC5CD3D;}Z8d*2nWD(k0vAW*@f?enR^h;ROC%n_MNQFR0~Hl1^oK!Z5dYU-J5>I4LHWjo zNB>g|wkJSEWzY9tZ~Qu*|DzlIDvGcFng6RW|6hoL*ZwI{Mde%jf6fJb^`Zaa5W@dr zreEjyb&mgigypMxzS{Kv3!DCGDPJw+tEK#Zz6p||lm{&pihidwTjh)0|3oTPm20bq W`yDCBdgT?B>sOpE*I&B-$A19IJWo;p literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-container-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-container-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..d392b616c77c74d78296ba6a43e7278019ce70a0 GIT binary patch literal 6620 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 e$#BOO4FDA*R0q5z@_gzzpc3tdh0(>b>zwb literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/no-filters-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/no-filters-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..9f58a62407b42321e1382e699e8f73b601b1bbd6 GIT binary patch literal 3522 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 S>#Vq!vAA~D+AOA1)Y@k;F literal 0 HcmV?d00001 diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css new file mode 100644 index 0000000000..29db6d1bd6 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.roomListPrimaryFilters { + padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x); +} + +/* Hide filters that are wrapping when collapsed */ +.roomListPrimaryFilters :global(.wrapping) { + display: none; +} + +.list { + /** + * The InteractionObserver needs the height to be set to work properly. + */ + height: 100%; + flex: 1; +} + +/* IconButton styles for chevron */ +.iconButton svg { + transition: transform 0.1s linear; +} + +.iconButton[aria-expanded="true"] svg { + transform: rotate(180deg); +} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx new file mode 100644 index 0000000000..65cf75e132 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +import type { FilterId } from "./useVisibleFilters"; + +const meta: Meta = { + title: "Room List/RoomListPrimaryFilters", + component: RoomListPrimaryFilters, + tags: ["autodocs"], + args: { + onToggleFilter: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +// All available filter IDs +const allFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite", "mentions", "invites", "low_priority"]; + +// Subset of filters for narrow container tests +const fewFilterIds: FilterId[] = ["people", "rooms", "unread"]; + +export const Default: Story = { + args: { + filterIds: allFilterIds, + }, +}; + +export const PeopleSelected: Story = { + args: { + filterIds: allFilterIds, + activeFilterId: "people", + }, +}; + +export const NoFilters: Story = { + args: { + filterIds: [], + }, +}; + +/** + * Narrow container that causes filters to wrap. + * The chevron button should appear to expand/collapse the filter list. + */ +export const NarrowContainer: Story = { + args: { + filterIds: fewFilterIds, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +/** + * Narrow container with active filter that would wrap. + * When collapsed, the active filter should move to the front. + */ +export const NarrowWithActiveWrappingFilter: Story = { + args: { + filterIds: fewFilterIds, + activeFilterId: "unread", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx new file mode 100644 index 0000000000..a86181da15 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { act } from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import * as stories from "./RoomListPrimaryFilters.stories"; + +const { Default, PeopleSelected, NoFilters, NarrowContainer, NarrowWithActiveWrappingFilter } = composeStories(stories); + +describe(" stories", () => { + describe("snapshots", () => { + it("renders Default story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders PeopleSelected story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NoFilters story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NarrowContainer story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NarrowWithActiveWrappingFilter story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe("behavior", () => { + it("should call onToggleFilter when a filter is clicked", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("option", { name: "People" })); + + expect(Default.args.onToggleFilter).toHaveBeenCalled(); + }); + }); + + describe("resize behavior", () => { + let resizeCallback: ResizeObserverCallback; + + beforeEach(() => { + globalThis.ResizeObserver = class MockResizeObserver { + public constructor(callback: ResizeObserverCallback) { + resizeCallback = callback; + } + public observe = vi.fn(); + public unobserve = vi.fn(); + public disconnect = vi.fn(); + } as unknown as typeof ResizeObserver; + }); + + function mockFiltersNotWrapping(): void { + vi.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); + vi.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); + vi.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + act(() => resizeCallback([{ target: listbox } as any], {} as ResizeObserver)); + } + + function mockUnreadWrapping(): void { + vi.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); + vi.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); + vi.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + act(() => resizeCallback([{ target: listbox } as any], {} as ResizeObserver)); + } + + it("should hide wrapping filters and show chevron", () => { + render(); + mockUnreadWrapping(); + + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + expect(screen.getByRole("button", { name: "Expand filter list" })).toBeInTheDocument(); + }); + + it("should expand and collapse filter list with chevron button", async () => { + const user = userEvent.setup(); + render(); + mockUnreadWrapping(); + + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + + await user.click(screen.getByRole("button", { name: "Expand filter list" })); + expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible(); + + await user.click(screen.getByRole("button", { name: "Collapse filter list" })); + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + }); + + it("should move active filter to front when collapsed and wrapping", () => { + render(); + mockUnreadWrapping(); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + expect(listbox.children[0]).toBe(screen.getByRole("option", { name: "Unreads" })); + }); + + it("should restore original filter order when expanded", async () => { + const user = userEvent.setup(); + render(); + mockUnreadWrapping(); + + await user.click(screen.getByRole("button", { name: "Expand filter list" })); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + expect(listbox.children[0]).toBe(screen.getByRole("option", { name: "People" })); + }); + + it("should handle resize from non-wrapping to wrapping", () => { + render(); + mockFiltersNotWrapping(); + + expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull(); + + mockUnreadWrapping(); + expect(screen.getByRole("button", { name: "Expand filter list" })).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx new file mode 100644 index 0000000000..561544a3a5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, useId, useState } from "react"; +import { ChatFilter, IconButton } from "@vector-im/compound-web"; +import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; + +import { Flex } from "../../utils/Flex"; +import { _t } from "../../utils/i18n"; +import { useCollapseFilters } from "./useCollapseFilters"; +import { useVisibleFilters, type FilterId } from "./useVisibleFilters"; +import styles from "./RoomListPrimaryFilters.module.css"; + +/** + * Maps filter IDs to translated labels + */ +const filterIdToLabel = (filterId: FilterId): string => { + switch (filterId) { + case "unread": + return _t("room_list|filters|unread"); + case "people": + return _t("room_list|filters|people"); + case "rooms": + return _t("room_list|filters|rooms"); + case "favourite": + return _t("room_list|filters|favourite"); + case "mentions": + return _t("room_list|filters|mentions"); + case "invites": + return _t("room_list|filters|invites"); + case "low_priority": + return _t("room_list|filters|low_priority"); + } +}; + +/** + * Props for RoomListPrimaryFilters component + */ +export interface RoomListPrimaryFiltersProps { + /** Array of filter IDs to display */ + filterIds: FilterId[]; + /** Currently active filter ID (if any) */ + activeFilterId?: FilterId; + /** Callback when a filter is toggled */ + onToggleFilter: (filterId: FilterId) => void; +} + +/** + * The primary filters component for the room list. + * Displays a collapsible list of filters with expand/collapse functionality. + */ +export const RoomListPrimaryFilters: React.FC = ({ + filterIds, + activeFilterId, + onToggleFilter, +}): JSX.Element | null => { + const id = useId(); + const [isExpanded, setIsExpanded] = useState(false); + + const { + ref, + isWrapping: displayChevron, + wrappingIndex, + } = useCollapseFilters(isExpanded, "wrapping"); + const visibleFilterIds = useVisibleFilters(filterIds, activeFilterId, wrappingIndex); + + return ( + + {displayChevron && ( + setIsExpanded((expanded) => !expanded)} + > + + + )} + + {visibleFilterIds.map((filterId, index) => ( + onToggleFilter(filterId)} + > + {filterIdToLabel(filterId)} + + ))} + + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap b/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap new file mode 100644 index 0000000000..74c281bde5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap @@ -0,0 +1,388 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` stories > snapshots > renders Default story 1`] = ` +
+
+ +
+ + + + + + + +
+
+
+`; + +exports[` stories > snapshots > renders NarrowContainer story 1`] = ` +
+
+
+ +
+ + + +
+
+
+
+`; + +exports[` stories > snapshots > renders NarrowWithActiveWrappingFilter story 1`] = ` +
+
+
+ +
+ + + +
+
+
+
+`; + +exports[` stories > snapshots > renders NoFilters story 1`] = ` +
+
+
+
+
+`; + +exports[` stories > snapshots > renders PeopleSelected story 1`] = ` +
+
+ +
+ + + + + + + +
+
+
+`; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx new file mode 100644 index 0000000000..7697d4829c --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +export type { RoomListPrimaryFiltersProps } from "./RoomListPrimaryFilters"; +export { useCollapseFilters } from "./useCollapseFilters"; +export { useVisibleFilters } from "./useVisibleFilters"; +export type { FilterId } from "./useVisibleFilters"; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts new file mode 100644 index 0000000000..e3fbf74e54 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useEffect, useRef, useState, type RefObject } from "react"; + +/** + * 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 + * @param wrappingClassName - the CSS class to apply to wrapping filters + * @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 + */ +export function useCollapseFilters( + isExpanded: boolean, + wrappingClassName: string, +): { + ref: RefObject; + isWrapping: boolean; + wrappingIndex: number; +} { + const ref = useRef(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; + child.setAttribute("aria-hidden", "false"); + child.classList.remove(wrappingClassName); + + // 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(wrappingClassName, 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, wrappingClassName]); + + return { ref, isWrapping, wrappingIndex }; +} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts new file mode 100644 index 0000000000..73a580b4d9 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useEffect, useState } from "react"; + +/** + * Standard filter identifiers that can be used across implementations. + * These are stable keys - the view layer maps them to translated labels. + */ +export type FilterId = "unread" | "people" | "rooms" | "favourite" | "mentions" | "invites" | "low_priority"; + +/** + * A hook to sort the filter IDs by active state. + * The list is sorted if the active filter index is greater than or equal to the wrapping index. + * If the wrapping index is -1, the filters are not sorted. + * + * @param filterIds - the list of filter IDs to sort. + * @param activeFilterId - the currently active filter ID (if any). + * @param wrappingIndex - the index of the first filter that is wrapping. + */ +export function useVisibleFilters( + filterIds: FilterId[], + activeFilterId: FilterId | undefined, + wrappingIndex: number, +): FilterId[] { + // By default, the filters are not sorted + const [sortedFilterIds, setSortedFilterIds] = useState(filterIds); + + useEffect(() => { + const activeIndex = activeFilterId ? filterIds.indexOf(activeFilterId) : -1; + const isActiveFilterWrapping = activeIndex >= wrappingIndex; + // If the active filter is not wrapping, we don't need to sort the filters + if (!isActiveFilterWrapping || wrappingIndex === -1) { + setSortedFilterIds(filterIds); + return; + } + + // Sort the filters with the active filter at first position + setSortedFilterIds( + filterIds.slice().sort((filterA, filterB) => { + // If the filter is active, it should be at the top of the list + if (filterA === activeFilterId && filterB !== activeFilterId) return -1; + if (filterA !== activeFilterId && filterB === activeFilterId) return 1; + // If both filters are active or not, keep their original order + return 0; + }), + ); + }, [filterIds, activeFilterId, wrappingIndex]); + + return sortedFilterIds; +}