Skip to content

Commit 74c4fad

Browse files
committed
feat: add animation to filter list
1 parent 15a4637 commit 74c4fad

File tree

2 files changed

+109
-49
lines changed

2 files changed

+109
-49
lines changed

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,31 @@
77

88
.mx_RoomListPrimaryFilters {
99
padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x);
10-
min-height: 46px;
11-
max-height: 46px;
12-
overflow: hidden;
13-
box-sizing: border-box;
14-
15-
&[data-expanded="true"] {
16-
min-height: unset;
17-
max-height: unset;
18-
overflow: unset;
10+
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;
1935
}
2036

2137
ul {
@@ -26,6 +42,10 @@
2642
* The InteractionObserver needs the height to be set for to work properly.
2743
*/
2844
height: 100%;
45+
46+
li {
47+
height: 30px;
48+
}
2949
}
3050

3151
.mx_RoomListPrimaryFilters_IconButton {

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

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

8-
import React, { type JSX, useEffect, 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

@@ -33,47 +33,53 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
3333
const { isVisible, rootRef, nodeRef } = useIsNodeVisible<HTMLLIElement, HTMLUListElement>({ threshold: 0.5 });
3434
const { filters, onFilterChange } = useFilters(vm.primaryFilters, isExpanded, isVisible);
3535

36+
const { ref: containerRef, isExpanded: isSafeExpanded } = useAnimateFilter<HTMLDivElement>(isExpanded);
37+
3638
return (
37-
<Flex id={id} className="mx_RoomListPrimaryFilters" gap="var(--cpd-space-3x)" data-expanded={isExpanded}>
38-
<Flex
39-
as="ul"
40-
role="listbox"
41-
aria-label={_t("room_list|primary_filters")}
42-
align="center"
43-
gap="var(--cpd-space-2x)"
44-
wrap="wrap"
45-
ref={rootRef}
46-
>
47-
{filters.map((filter) => (
48-
<li
49-
ref={filter.active ? nodeRef : undefined}
50-
role="option"
51-
aria-selected={filter.active}
52-
key={filter.name}
39+
<div className="mx_RoomListPrimaryFilters">
40+
<div ref={containerRef} className="mx_RoomListPrimaryFilters_container" data-expanded={isSafeExpanded}>
41+
<Flex id={id} className="mx_RoomListPrimaryFilters_animated" gap="var(--cpd-space-3x)">
42+
<Flex
43+
as="ul"
44+
role="listbox"
45+
aria-label={_t("room_list|primary_filters")}
46+
align="center"
47+
gap="var(--cpd-space-2x)"
48+
wrap="wrap"
49+
ref={rootRef}
50+
>
51+
{filters.map((filter) => (
52+
<li
53+
ref={filter.active ? nodeRef : undefined}
54+
role="option"
55+
aria-selected={filter.active}
56+
key={filter.name}
57+
>
58+
<ChatFilter
59+
selected={filter.active}
60+
onClick={() => {
61+
onFilterChange();
62+
filter.toggle();
63+
}}
64+
>
65+
{filter.name}
66+
</ChatFilter>
67+
</li>
68+
))}
69+
</Flex>
70+
<IconButton
71+
aria-expanded={isSafeExpanded}
72+
aria-controls={id}
73+
className="mx_RoomListPrimaryFilters_IconButton"
74+
aria-label={_t("room_list|room_options")}
75+
size="28px"
76+
onClick={() => setIsExpanded((_expanded) => !_expanded)}
5377
>
54-
<ChatFilter
55-
selected={filter.active}
56-
onClick={() => {
57-
onFilterChange();
58-
filter.toggle();
59-
}}
60-
>
61-
{filter.name}
62-
</ChatFilter>
63-
</li>
64-
))}
65-
</Flex>
66-
<IconButton
67-
aria-expanded={isExpanded}
68-
aria-controls={id}
69-
className="mx_RoomListPrimaryFilters_IconButton"
70-
aria-label={_t("room_list|room_options")}
71-
size="28px"
72-
onClick={() => setIsExpanded((_expanded) => !_expanded)}
73-
>
74-
<ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
75-
</IconButton>
76-
</Flex>
78+
<ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
79+
</IconButton>
80+
</Flex>
81+
</div>
82+
</div>
7783
);
7884
}
7985

@@ -127,3 +133,37 @@ function useFilters(
127133
};
128134
return { filters: filterState.filters, onFilterChange };
129135
}
136+
137+
/**
138+
* A hook to animate the filter list when it is expanded or not.
139+
* @param areFiltersExpanded
140+
*/
141+
function useAnimateFilter<T extends HTMLElement>(
142+
areFiltersExpanded: boolean,
143+
): { ref: RefObject<T | null>; isExpanded: boolean } {
144+
const ref = useRef<T | null>(null);
145+
useEffect(() => {
146+
if (!ref.current) return;
147+
148+
const observer = new ResizeObserver(() => {
149+
// Remove transition to avoid the animation to run when the new --row-height is not set yet
150+
// If the animation runs at this moment, the first row will jump
151+
ref.current?.style.setProperty("transition", "unset");
152+
// For the animation to work, we need `grid-template-rows` to have the same unit at the beginning and the end
153+
// If px is used at the beginning, we need to use px at the end.
154+
// In our case, we use fr unit to fully grow when expanded (1fr) so we need to compute the value in fr when the filters are not expanded
155+
ref.current?.style.setProperty("--row-height", `${30 / ref?.current.scrollHeight}fr`);
156+
});
157+
observer.observe(ref.current);
158+
return () => observer.disconnect();
159+
}, [ref]);
160+
161+
// We put back the transition to the element when the filters are expanded
162+
const [isExpanded, setExpanded] = useState(areFiltersExpanded);
163+
useEffect(() => {
164+
ref.current?.style.setProperty("transition", "0.1s ease-in-out");
165+
setExpanded(areFiltersExpanded);
166+
}, [areFiltersExpanded, ref]);
167+
168+
return { ref, isExpanded };
169+
}

0 commit comments

Comments
 (0)