Skip to content

Commit 387e69e

Browse files
committed
feat: rework collapse feature
1 parent c96d19a commit 387e69e

8 files changed

Lines changed: 134 additions & 513 deletions

File tree

res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,8 @@
88
.mx_RoomListPrimaryFilters {
99
padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x);
1010

11-
.mx_RoomListPrimaryFilters_container {
12-
/**
13-
* Set (in fr unit) at every resize of this container.
14-
*/
15-
--row-height: 30px;
16-
17-
overflow: hidden;
18-
/**
19-
* Using grid to animate the height of the container.
20-
*/
21-
display: grid;
22-
grid-template-rows: var(--row-height);
23-
transition: 0.1s ease-in-out;
24-
25-
&[data-expanded="true"] {
26-
grid-template-rows: 1fr;
27-
}
28-
}
29-
30-
.mx_RoomListPrimaryFilters_animated {
31-
/**
32-
* Required to make the collapse work
33-
*/
34-
min-height: 0;
11+
.mx_RoomListPrimaryFilters_wrapping {
12+
display: none;
3513
}
3614

3715
ul {
@@ -42,11 +20,10 @@
4220
* The InteractionObserver needs the height to be set to work properly.
4321
*/
4422
height: 100%;
23+
flex: 1;
4524
}
4625

4726
.mx_RoomListPrimaryFilters_IconButton {
48-
background-color: var(--cpd-color-bg-subtle-secondary);
49-
5027
svg {
5128
transition: transform 0.1s linear;
5229
}

src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx

Lines changed: 131 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,13 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import React, { type JSX, useId, useState } from "react";
8+
import React, { type JSX, useEffect, useId, useRef, useState, type RefObject } from "react";
99
import { ChatFilter, IconButton } from "@vector-im/compound-web";
1010
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
1111

1212
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
1313
import { Flex } from "../../../utils/Flex";
1414
import { _t } from "../../../../languageHandler";
15-
import { useIsNodeVisible } from "../../../../hooks/useIsNodeVisible";
16-
import { useFilterHeight } from "../../../../hooks/left-panel/room-list/useFilterHeight";
17-
import { useIsFilterOverflowing } from "../../../../hooks/left-panel/room-list/useIsFilterOverflowing";
18-
import { useAnimateFilter } from "../../../../hooks/left-panel/room-list/useAnimateFilter";
19-
import { useVisibleFilters } from "../../../../hooks/left-panel/room-list/useVisibleFilters";
2015

2116
interface RoomListPrimaryFiltersProps {
2217
/**
@@ -32,77 +27,138 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
3227
const id = useId();
3328
const [isExpanded, setIsExpanded] = useState(false);
3429

35-
// threshold: 0.5 means that the filter is considered visible if at least 50% of it is visible
36-
// this value is arbitrary, we want we to have a bit of flexibility
37-
const { isVisible, rootRef, nodeRef } = useIsNodeVisible<HTMLLIElement, HTMLUListElement>({ threshold: 0.5 });
38-
const { filters, onFilterChange } = useVisibleFilters(vm.primaryFilters, isExpanded, isVisible);
39-
40-
const { filterHeight, filterRef } = useFilterHeight<HTMLButtonElement>();
41-
const { ref: containerRef, isExpanded: isSafeExpanded } = useAnimateFilter<HTMLDivElement>(
42-
isExpanded,
43-
filterHeight,
44-
);
45-
const { ref, isOverflowing: displayChevron } = useIsFilterOverflowing<HTMLUListElement>(filterHeight);
30+
const { ref, isWrapping: displayChevron, wrappingIndex } = useCollapseFilters<HTMLUListElement>(isExpanded);
31+
const filters = useVisibleFilters(vm.primaryFilters, wrappingIndex);
4632

4733
return (
48-
<div className="mx_RoomListPrimaryFilters" data-testid="primary-filters">
49-
<div
50-
ref={containerRef}
51-
className="mx_RoomListPrimaryFilters_container"
52-
data-expanded={isSafeExpanded}
53-
data-testid="filter-container"
34+
<Flex className="mx_RoomListPrimaryFilters" data-testid="primary-filters" gap="var(--cpd-space-3x)">
35+
<Flex
36+
id={id}
37+
as="ul"
38+
role="listbox"
39+
aria-label={_t("room_list|primary_filters")}
40+
align="center"
41+
gap="var(--cpd-space-2x)"
42+
wrap="wrap"
43+
ref={ref}
5444
>
55-
<Flex id={id} className="mx_RoomListPrimaryFilters_animated" gap="var(--cpd-space-3x)">
56-
<Flex
57-
as="ul"
58-
role="listbox"
59-
aria-label={_t("room_list|primary_filters")}
60-
align="center"
61-
gap="var(--cpd-space-2x)"
62-
wrap="wrap"
63-
ref={(node: HTMLUListElement) => {
64-
rootRef(node);
65-
// due to https://github.com/facebook/react/issues/29196
66-
// eslint-disable-next-line react-compiler/react-compiler
67-
ref.current = node;
68-
}}
69-
>
70-
{filters.map((filter, i) => (
71-
<li
72-
ref={filter.active ? nodeRef : undefined}
73-
role="option"
74-
aria-selected={filter.active}
75-
key={filter.name}
76-
>
77-
<ChatFilter
78-
ref={i === 0 ? filterRef : undefined}
79-
selected={filter.active}
80-
onClick={() => {
81-
onFilterChange();
82-
filter.toggle();
83-
}}
84-
>
85-
{filter.name}
86-
</ChatFilter>
87-
</li>
88-
))}
89-
</Flex>
90-
{displayChevron && (
91-
<IconButton
92-
aria-expanded={isSafeExpanded}
93-
aria-controls={id}
94-
className="mx_RoomListPrimaryFilters_IconButton"
95-
aria-label={
96-
isSafeExpanded ? _t("room_list|collapse_filters") : _t("room_list|expand_filters")
97-
}
98-
size="28px"
99-
onClick={() => setIsExpanded((_expanded) => !_expanded)}
100-
>
101-
<ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
102-
</IconButton>
103-
)}
104-
</Flex>
105-
</div>
106-
</div>
45+
{filters.map((filter, i) => (
46+
<li role="option" aria-selected={filter.active} key={i}>
47+
<ChatFilter selected={filter.active} onClick={() => filter.toggle()}>
48+
{filter.name}
49+
</ChatFilter>
50+
</li>
51+
))}
52+
</Flex>
53+
{displayChevron && (
54+
<IconButton
55+
subtleBackground={true}
56+
aria-expanded={isExpanded}
57+
aria-controls={id}
58+
className="mx_RoomListPrimaryFilters_IconButton"
59+
aria-label={isExpanded ? _t("room_list|collapse_filters") : _t("room_list|expand_filters")}
60+
size="28px"
61+
onClick={() => setIsExpanded((_expanded) => !_expanded)}
62+
>
63+
<ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
64+
</IconButton>
65+
)}
66+
</Flex>
10767
);
10868
}
69+
70+
/**
71+
* A hook to manage the wrapping of filters in the room list.
72+
* It observes the filter list and hides filters that are wrapping when the list is not expanded.
73+
* @param isExpanded
74+
* @returns an object containing:
75+
* - `ref`: a ref to put on the filter list element
76+
* - `isWrapping`: a boolean indicating if the filters are wrapping
77+
* - `wrappingIndex`: the index of the first filter that is wrapping
78+
*/
79+
function useCollapseFilters<T extends HTMLElement>(
80+
isExpanded: boolean,
81+
): { ref: RefObject<T | null>; isWrapping: boolean; wrappingIndex: number } {
82+
const ref = useRef<T>(null);
83+
const [isWrapping, setIsWrapping] = useState(false);
84+
const [wrappingIndex, setWrappingIndex] = useState(-1);
85+
86+
useEffect(() => {
87+
if (!ref.current) return;
88+
89+
const hideFilters = (list: Element): void => {
90+
let isWrapping = false;
91+
Array.from(list.children).forEach((node, i): void => {
92+
const child = node as HTMLElement;
93+
const wrappingClass = "mx_RoomListPrimaryFilters_wrapping";
94+
child.setAttribute("aria-hidden", "false");
95+
child.classList.remove(wrappingClass);
96+
97+
// If the filter list is expanded, all filters are visible
98+
if (isExpanded) return;
99+
100+
// If the previous element is on the left element of the current one, it means that the filter is wrapping
101+
const previousSibling = child.previousElementSibling as HTMLElement | null;
102+
if (previousSibling && child.offsetLeft < previousSibling.offsetLeft) {
103+
if (!isWrapping) setWrappingIndex(i);
104+
isWrapping = true;
105+
}
106+
107+
// If the filter is wrapping, we hide it
108+
child.classList.toggle(wrappingClass, isWrapping);
109+
child.setAttribute("aria-hidden", isWrapping.toString());
110+
});
111+
112+
if (!isWrapping) setWrappingIndex(-1);
113+
setIsWrapping(isExpanded || isWrapping);
114+
};
115+
116+
hideFilters(ref.current);
117+
const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target)));
118+
119+
observer.observe(ref.current);
120+
return () => {
121+
observer.disconnect();
122+
};
123+
}, [isExpanded]);
124+
125+
return { ref, isWrapping, wrappingIndex };
126+
}
127+
128+
/**
129+
* A hook to sort the filters by active state.
130+
* The list is sorted if the current filter index is greater than or equal to the wrapping index.
131+
* If the wrapping index is -1, the filters are not sorted.
132+
*
133+
* @param filters - the list of filters to sort.
134+
* @param wrappingIndex - the index of the first filter that is wrapping.
135+
*/
136+
export function useVisibleFilters(
137+
filters: RoomListViewState["primaryFilters"],
138+
wrappingIndex: number,
139+
): RoomListViewState["primaryFilters"] {
140+
// By default, the filters are not sorted
141+
const [sortedFilters, setSortedFilters] = useState(filters);
142+
143+
useEffect(() => {
144+
const isActiveFilterWrapping = filters.findIndex((f) => f.active) >= wrappingIndex;
145+
// If the active filter is not wrapping, we don't need to sort the filters
146+
if (!isActiveFilterWrapping || wrappingIndex === -1) {
147+
setSortedFilters(filters);
148+
return;
149+
}
150+
151+
// Sort the filters with the current filter at first position
152+
setSortedFilters(
153+
filters.slice().sort((filterA, filterB) => {
154+
// If the filter is active, it should be at the top of the list
155+
if (filterA.active && !filterB.active) return -1;
156+
if (!filterA.active && filterB.active) return 1;
157+
// If both filters are active or not, keep their original order
158+
return 0;
159+
}),
160+
);
161+
}, [filters, wrappingIndex]);
162+
163+
return sortedFilters;
164+
}

src/hooks/left-panel/room-list/useAnimateFilter.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

src/hooks/left-panel/room-list/useFilterHeight.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/hooks/left-panel/room-list/useIsFilterOverflowing.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)