55 * Please see LICENSE files in the repository root for full details.
66 */
77
8- import React , { type JSX , useEffect , useId , useRef , useState , type RefObject } from "react" ;
8+ import React , { type JSX , useId , useState } from "react" ;
99import { ChatFilter , IconButton } from "@vector-im/compound-web" ;
1010import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down" ;
1111
1212import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel" ;
1313import { Flex } from "../../../utils/Flex" ;
1414import { _t } from "../../../../languageHandler" ;
1515import { 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" ;
1620
1721interface RoomListPrimaryFiltersProps {
1822 /**
@@ -31,7 +35,7 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
3135 // threshold: 0.5 means that the filter is considered visible if at least 50% of it is visible
3236 // this value is arbitrary, we want we to have a bit of flexibility
3337 const { isVisible, rootRef, nodeRef } = useIsNodeVisible < HTMLLIElement , HTMLUListElement > ( { threshold : 0.5 } ) ;
34- const { filters, onFilterChange } = useFilters ( vm . primaryFilters , isExpanded , isVisible ) ;
38+ const { filters, onFilterChange } = useVisibleFilters ( vm . primaryFilters , isExpanded , isVisible ) ;
3539
3640 const { filterHeight, filterRef } = useFilterHeight < HTMLButtonElement > ( ) ;
3741 const { ref : containerRef , isExpanded : isSafeExpanded } = useAnimateFilter < HTMLDivElement > (
@@ -102,150 +106,3 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
102106 </ div >
103107 ) ;
104108}
105-
106- /**
107- * A hook to sort the filters by active state.
108- * The list is sorted if the current filter is not visible when the list is unexpanded.
109- *
110- * @param filters - the list of filters to sort.
111- * @param isExpanded - the filter is expanded or not (fully visible).
112- * @param isVisible - `null` if there is not selected filter. `true` or `false` if the filter is visible or not.
113- */
114- function useFilters (
115- filters : RoomListViewState [ "primaryFilters" ] ,
116- isExpanded : boolean ,
117- isVisible : boolean | null ,
118- ) : {
119- /**
120- * The new list of filters.
121- */
122- filters : RoomListViewState [ "primaryFilters" ] ;
123- /**
124- * Reset the filter sorting when called.
125- */
126- onFilterChange : ( ) => void ;
127- } {
128- // By default, the filters are not sorted
129- const [ filterState , setFilterState ] = useState ( { filters, isSorted : false } ) ;
130-
131- useEffect ( ( ) => {
132- // If there is no current filter (isVisible is null)
133- // or if the filter list is fully visible (isExpanded is true)
134- // or if the current filter is visible and the list isn't sorted
135- // then we don't need to sort the filters
136- if ( isVisible === null || isExpanded || ( isVisible && ! filterState . isSorted ) ) {
137- setFilterState ( { filters, isSorted : false } ) ;
138- return ;
139- }
140-
141- // Sort the filters with the current filter at first position
142- setFilterState ( {
143- filters : filters . slice ( ) . sort ( ( filterA , filterB ) => {
144- // If the filter is active, it should be at the top of the list
145- if ( filterA . active && ! filterB . active ) return - 1 ;
146- if ( ! filterA . active && filterB . active ) return 1 ;
147- // If both filters are active or not, keep their original order
148- return 0 ;
149- } ) ,
150- isSorted : true ,
151- } ) ;
152- } , [ filters , isVisible , filterState . isSorted , isExpanded ] ) ;
153-
154- const onFilterChange = ( ) : void => {
155- // Reset the filter sorting
156- setFilterState ( { filters, isSorted : false } ) ;
157- } ;
158- return { filters : filterState . filters , onFilterChange } ;
159- }
160-
161- /**
162- * A hook to animate the filter list when it is expanded or not.
163- * @param areFiltersExpanded
164- * @param filterHeight
165- */
166- function useAnimateFilter < T extends HTMLElement > (
167- areFiltersExpanded : boolean ,
168- filterHeight : number ,
169- ) : { ref : RefObject < T | null > ; isExpanded : boolean } {
170- const ref = useRef < T | null > ( null ) ;
171- useEffect ( ( ) => {
172- if ( ! ref . current ) return ;
173-
174- // Round to 2 decimal places and convert to integer to avoid floating point precision issues
175- const floor = ( a : number ) : number => Math . floor ( a * 100 ) / 100 || 0 ;
176- // For the animation to work, we need `grid-template-rows` to have the same unit at the beginning and the end
177- // If px is used at the beginning, we need to use px at the end.
178- // 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
179- const setRowHeight = ( ) : void =>
180- ref . current ?. style . setProperty ( "--row-height" , `${ floor ( filterHeight / ref ?. current . scrollHeight ) } fr` ) ;
181- setRowHeight ( ) ;
182-
183- const observer = new ResizeObserver ( ( ) => {
184- // Remove transition to avoid the animation to run when the new --row-height is not set yet
185- // If the animation runs at this moment, the first row will jump
186- ref . current ?. style . setProperty ( "transition" , "unset" ) ;
187- setRowHeight ( ) ;
188- } ) ;
189- observer . observe ( ref . current ) ;
190- return ( ) => observer . disconnect ( ) ;
191- } , [ ref , filterHeight ] ) ;
192-
193- // Put back the transition to the element when the expanded state changes
194- // because we want to animate it
195- const [ isExpanded , setExpanded ] = useState ( areFiltersExpanded ) ;
196- useEffect ( ( ) => {
197- ref . current ?. style . setProperty ( "transition" , "0.1s ease-in-out" ) ;
198- setExpanded ( areFiltersExpanded ) ;
199- } , [ areFiltersExpanded , ref ] ) ;
200-
201- return { ref, isExpanded } ;
202- }
203-
204- /**
205- * A hook to check if the filter list is overflowing.
206- * The list is overflowing if the scrollHeight is greater than `FILTER_HEIGHT`.
207- */
208- function useIsFilterOverflowing < T extends HTMLElement > (
209- filterHeight : number ,
210- ) : { ref : RefObject < T | undefined > ; isOverflowing : boolean } {
211- const ref = useRef < T > ( undefined ) ;
212- const [ isOverflowing , setIsOverflowing ] = useState ( false ) ;
213-
214- useEffect ( ( ) => {
215- if ( ! ref . current ) return ;
216-
217- const node = ref . current ;
218- const observer = new ResizeObserver ( ( ) => setIsOverflowing ( node . scrollHeight > filterHeight ) ) ;
219- observer . observe ( node ) ;
220- return ( ) => observer . disconnect ( ) ;
221- } , [ ref , filterHeight ] ) ;
222-
223- return { ref, isOverflowing } ;
224- }
225-
226- /**
227- * A hook to get the height of the filter list.
228- * @returns a ref that should be put on the filter button and its height.
229- */
230- function useFilterHeight < T extends HTMLElement > ( ) : { filterHeight : number ; filterRef : RefObject < T | null > } {
231- const [ filterHeight , setFilterHeight ] = useState ( 0 ) ;
232- const filterRef = useRef < T > ( null ) ;
233-
234- useEffect ( ( ) => {
235- if ( ! filterRef . current ) return ;
236-
237- const setHeight = ( ) => {
238- const height = filterRef . current ?. offsetHeight ;
239- if ( height ) setFilterHeight ( height ) ;
240- } ;
241-
242- setHeight ( ) ;
243- const observer = new ResizeObserver ( ( ) => {
244- setHeight ( ) ;
245- } ) ;
246- observer . observe ( filterRef . current ) ;
247- return ( ) => observer . disconnect ( ) ;
248- } , [ filterRef ] ) ;
249-
250- return { filterHeight, filterRef } ;
251- }
0 commit comments