5
5
* Please see LICENSE files in the repository root for full details.
6
6
*/
7
7
8
- import React , { type JSX } from "react" ;
9
- import { ChatFilter } from "@vector-im/compound-web" ;
8
+ import React , { type JSX , useEffect , useId , useState } from "react" ;
9
+ import { ChatFilter , IconButton } from "@vector-im/compound-web" ;
10
+ import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down" ;
10
11
11
12
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel" ;
12
13
import { Flex } from "../../../utils/Flex" ;
13
14
import { _t } from "../../../../languageHandler" ;
15
+ import { useIsNodeVisible } from "../../../../hooks/useIsNodeVisible" ;
14
16
15
17
interface RoomListPrimaryFiltersProps {
16
18
/**
@@ -23,23 +25,105 @@ interface RoomListPrimaryFiltersProps {
23
25
* The primary filters for the room list
24
26
*/
25
27
export function RoomListPrimaryFilters ( { vm } : RoomListPrimaryFiltersProps ) : JSX . Element {
28
+ const id = useId ( ) ;
29
+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
30
+
31
+ // threshold: 0.5 means that the filter is considered visible if at least 50% of it is visible
32
+ // this value is arbitrary, we want we to have a bit of flexibility
33
+ const { isVisible, rootRef, nodeRef } = useIsNodeVisible < HTMLLIElement , HTMLUListElement > ( { threshold : 0.5 } ) ;
34
+ const { filters, onFilterChange } = useFilters ( vm . primaryFilters , isExpanded , isVisible ) ;
35
+
26
36
return (
27
- < Flex
28
- as = "ul"
29
- role = "listbox"
30
- aria-label = { _t ( "room_list|primary_filters" ) }
31
- className = "mx_RoomListPrimaryFilters"
32
- align = "center"
33
- gap = "var(--cpd-space-2x)"
34
- wrap = "wrap"
35
- >
36
- { vm . primaryFilters . map ( ( filter ) => (
37
- < li role = "option" aria-selected = { filter . active } key = { filter . name } >
38
- < ChatFilter selected = { filter . active } onClick = { filter . toggle } >
39
- { filter . name }
40
- </ ChatFilter >
41
- </ li >
42
- ) ) }
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 }
53
+ >
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 >
43
76
</ Flex >
44
77
) ;
45
78
}
79
+
80
+ /**
81
+ * A hook to sort the filters by active state.
82
+ * The list is sorted if the current filter is not visible when the list is unexpanded.
83
+ *
84
+ * @param filters - the list of filters to sort.
85
+ * @param isExpanded - the filter is expanded or not (fully visible).
86
+ * @param isVisible - `null` if there is not selected filter. `true` or `false` if the filter is visible or not.
87
+ */
88
+ function useFilters (
89
+ filters : RoomListViewState [ "primaryFilters" ] ,
90
+ isExpanded : boolean ,
91
+ isVisible : boolean | null ,
92
+ ) : {
93
+ /**
94
+ * The new list of filters.
95
+ */
96
+ filters : RoomListViewState [ "primaryFilters" ] ;
97
+ /**
98
+ * Reset the filter sorting when called.
99
+ */
100
+ onFilterChange : ( ) => void ;
101
+ } {
102
+ // By default, the filters are not sorted
103
+ const [ filterState , setFilterState ] = useState ( { filters, isSorted : false } ) ;
104
+
105
+ useEffect ( ( ) => {
106
+ // If there is no current filter (isVisible is null)
107
+ // or if the filter list is fully visible (isExpanded is true)
108
+ // or if the current filter is visible and the list isn't sorted
109
+ // then we don't need to sort the filters
110
+ if ( isVisible === null || isExpanded || ( isVisible && ! filterState . isSorted ) ) {
111
+ setFilterState ( { filters, isSorted : false } ) ;
112
+ return ;
113
+ }
114
+
115
+ // Sort the filters with the current filter at first position
116
+ setFilterState ( {
117
+ filters : filters
118
+ . slice ( )
119
+ . sort ( ( filterA , filterB ) => ( filterA . active === filterB . active ? 0 : filterA . active ? - 1 : 1 ) ) ,
120
+ isSorted : true ,
121
+ } ) ;
122
+ } , [ filters , isVisible , filterState . isSorted , isExpanded ] ) ;
123
+
124
+ const onFilterChange = ( ) : void => {
125
+ // Reset the filter sorting
126
+ setFilterState ( { filters, isSorted : false } ) ;
127
+ } ;
128
+ return { filters : filterState . filters , onFilterChange } ;
129
+ }
0 commit comments