@@ -9,18 +9,20 @@ import {
9
9
PrefetchPageLinks ,
10
10
useLocation ,
11
11
useNavigate ,
12
- useNavigation ,
13
12
useSearchParams ,
14
13
} from '@remix-run/react' ;
15
14
import { useCallback , useMemo , useState } from 'react' ;
16
15
import useDebounce from 'react-use/lib/useDebounce' ;
17
16
17
+ import { useOptimisticNavigationData } from '~/hooks/useOptimisticNavigationData' ;
18
18
import { cn } from '~/lib/utils' ;
19
19
20
+ import type { AppliedFilter } from './SortFilterLayout' ;
21
+
20
22
import { Checkbox } from '../ui/Checkbox' ;
21
23
import { Input } from '../ui/Input' ;
22
24
import { Label } from '../ui/Label' ;
23
- import { type AppliedFilter , FILTER_URL_PREFIX } from './SortFilterLayout' ;
25
+ import { FILTER_URL_PREFIX } from './SortFilterLayout' ;
24
26
25
27
export function DefaultFilter ( props : {
26
28
appliedFilters : AppliedFilter [ ] ;
@@ -29,70 +31,106 @@ export function DefaultFilter(props: {
29
31
const { appliedFilters, option} = props ;
30
32
const [ params ] = useSearchParams ( ) ;
31
33
const [ prefetchPage , setPrefetchPage ] = useState < null | string > ( null ) ;
32
- const navigate = useNavigate ( ) ;
33
- const navigation = useNavigation ( ) ;
34
34
const location = useLocation ( ) ;
35
35
const addFilterLink = getFilterLink ( option . input as string , params , location ) ;
36
36
const appliedFilter = getAppliedFilter ( option , appliedFilters ) ;
37
- const isNavigationPending = navigation . state !== 'idle' ;
38
37
39
- const getRemoveFilterLink = useCallback ( ( ) => {
38
+ const removeFilterLink = useMemo ( ( ) => {
40
39
if ( ! appliedFilter ) {
41
- return null ;
40
+ return location . pathname ;
42
41
}
43
42
return getAppliedFilterLink ( appliedFilter , params , location ) ;
44
43
} , [ appliedFilter , params , location ] ) ;
45
44
46
- const handleToggleFilter = useCallback ( ( ) => {
47
- if ( appliedFilter ) {
48
- const removeFilterLink = getRemoveFilterLink ( ) ;
49
- if ( removeFilterLink ) {
50
- navigate ( removeFilterLink , {
51
- preventScrollReset : true ,
52
- replace : true ,
53
- } ) ;
54
- }
55
- return ;
56
- }
57
-
58
- navigate ( addFilterLink , {
59
- preventScrollReset : true ,
60
- replace : true ,
61
- } ) ;
62
- } , [ addFilterLink , appliedFilter , navigate , getRemoveFilterLink ] ) ;
63
-
64
45
// Prefetch the page that will be navigated to when the user hovers or touches the filter
65
46
const handleSetPrefetch = useCallback ( ( ) => {
66
- const removeFilterLink = getRemoveFilterLink ( ) ;
67
47
if ( appliedFilter ) {
68
48
setPrefetchPage ( removeFilterLink ) ;
69
49
return ;
70
50
}
71
51
72
52
setPrefetchPage ( addFilterLink ) ;
73
- } , [ getRemoveFilterLink , addFilterLink , appliedFilter ] ) ;
53
+ } , [ removeFilterLink , addFilterLink , appliedFilter ] ) ;
54
+
55
+ return (
56
+ < div onMouseEnter = { handleSetPrefetch } onTouchStart = { handleSetPrefetch } >
57
+ < FilterCheckbox
58
+ addFilterLink = { addFilterLink }
59
+ filterIsApplied = { Boolean ( appliedFilter ) }
60
+ option = { option }
61
+ removeFilterLink = { removeFilterLink }
62
+ />
63
+ { prefetchPage && < PrefetchPageLinks page = { prefetchPage } /> }
64
+ </ div >
65
+ ) ;
66
+ }
67
+
68
+ function FilterCheckbox ( {
69
+ addFilterLink,
70
+ filterIsApplied,
71
+ option,
72
+ removeFilterLink,
73
+ } : {
74
+ addFilterLink : string ;
75
+ filterIsApplied : boolean ;
76
+ option : Filter [ 'values' ] [ 0 ] ;
77
+ removeFilterLink : string ;
78
+ } ) {
79
+ const navigate = useNavigate ( ) ;
80
+ const optionId = option . id ;
81
+ const { optimisticData, pending} = useOptimisticNavigationData < {
82
+ isFilterChecked : boolean ;
83
+ } > ( optionId ) ;
84
+ const { optimisticData : clearFilters } =
85
+ useOptimisticNavigationData < boolean > ( 'clear-all-filters' ) ;
86
+
87
+ // Use optimistic checked state while the navigation is pending
88
+ if ( optimisticData ) {
89
+ filterIsApplied = optimisticData . isFilterChecked ;
90
+ }
91
+ // Here we can optimistically clear all filters
92
+ else if ( clearFilters ) {
93
+ filterIsApplied = false ;
94
+ }
95
+
96
+ const handleToggleFilter = useCallback ( ( ) => {
97
+ const navigateTo = filterIsApplied ? removeFilterLink : addFilterLink ;
98
+ const optimisticChecked = ! filterIsApplied ;
99
+
100
+ navigate ( navigateTo , {
101
+ preventScrollReset : true ,
102
+ replace : true ,
103
+ state : {
104
+ optimisticData : {
105
+ isFilterChecked : optimisticChecked ,
106
+ } ,
107
+ optimisticId : optionId ,
108
+ } ,
109
+ } ) ;
110
+ } , [ filterIsApplied , removeFilterLink , addFilterLink , navigate , optionId ] ) ;
74
111
75
112
return (
76
113
< div
77
114
className = { cn ( [
78
115
'flex items-center gap-2' ,
79
- isNavigationPending && 'lg:animate-pulse' ,
116
+ // If the navigation is pending, animate after a delay
117
+ // to avoid flickering when navigation is fast
118
+ pending && 'pointer-events-none animate-pulse delay-500' ,
80
119
] ) }
81
- onMouseEnter = { handleSetPrefetch }
82
- onTouchStart = { handleSetPrefetch }
83
120
>
84
121
< Checkbox
85
- checked = { Boolean ( appliedFilter ) }
86
- id = { option . id }
122
+ checked = { filterIsApplied }
123
+ id = { optionId }
87
124
onCheckedChange = { handleToggleFilter }
88
125
/>
89
126
< Label
90
- className = "w-full cursor-pointer lg:transition-opacity lg:hover:opacity-70"
91
- htmlFor = { option . id }
127
+ className = { cn ( [
128
+ 'w-full cursor-pointer lg:transition-opacity lg:hover:opacity-70' ,
129
+ ] ) }
130
+ htmlFor = { optionId }
92
131
>
93
132
{ option . label }
94
133
</ Label >
95
- { prefetchPage && < PrefetchPageLinks page = { prefetchPage } /> }
96
134
</ div >
97
135
) ;
98
136
}
0 commit comments