@@ -17,7 +17,7 @@ import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
17
17
import type { AnyStore , ReadableAtom , Task , WritableAtom } from 'nanostores' ;
18
18
import { atom , computed } from 'nanostores' ;
19
19
import type { StoreValues } from 'nanostores/computed' ;
20
- import type { ChangeEvent , PropsWithChildren , RefObject } from 'react' ;
20
+ import type { ChangeEvent , MouseEventHandler , PropsWithChildren , RefObject } from 'react' ;
21
21
import React , {
22
22
createContext ,
23
23
useCallback ,
@@ -29,7 +29,12 @@ import React, {
29
29
useState ,
30
30
} from 'react' ;
31
31
import { useTranslation } from 'react-i18next' ;
32
- import { PiArrowsInLineVerticalBold , PiArrowsOutLineVerticalBold , PiXBold } from 'react-icons/pi' ;
32
+ import {
33
+ PiArrowCounterClockwiseBold ,
34
+ PiArrowsInLineVerticalBold ,
35
+ PiArrowsOutLineVerticalBold ,
36
+ PiXBold ,
37
+ } from 'react-icons/pi' ;
33
38
import { assert } from 'tsafe' ;
34
39
import { useDebounce } from 'use-debounce' ;
35
40
@@ -194,7 +199,7 @@ type PickerProps<T extends object> = {
194
199
} ;
195
200
196
201
export type PickerContextState < T extends object > = {
197
- optionsOrGroups : T [ ] | Group < T > [ ] ;
202
+ $ optionsOrGroups : WritableAtom < T [ ] | Group < T > [ ] > ;
198
203
$groupStatusMap : WritableAtom < GroupStatusMap > ;
199
204
$compactView : WritableAtom < boolean > ;
200
205
$activeOptionId : WritableAtom < string | undefined > ;
@@ -490,19 +495,16 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
490
495
NextToSearchBar,
491
496
searchable,
492
497
} = props ;
493
- const [ $activeOptionId ] = useState ( ( ) => {
494
- const initialValue = getFirstOptionId ( optionsOrGroups , getOptionId ) ;
495
- return atom < string | undefined > ( initialValue ) ;
496
- } ) ;
497
498
const rootRef = useRef < HTMLDivElement > ( null ) ;
498
499
const inputRef = useRef < HTMLInputElement > ( null ) ;
499
500
const { $groupStatusMap, $areAllGroupsDisabled, toggleGroup } = useTogglableGroups ( optionsOrGroups ) ;
501
+ const $activeOptionId = useAtom ( getFirstOptionId ( optionsOrGroups , getOptionId ) ) ;
500
502
const $compactView = useAtom ( true ) ;
501
- const $filteredOptions = useAtom < T [ ] | Group < T > [ ] > ( [ ] ) ;
502
- const $flattenedFilteredOptions = useComputed ( [ $filteredOptions ] , ( filteredOptions ) =>
503
- flattenOptions ( filteredOptions )
504
- ) ;
505
- const $totalOptionCount = useComputed ( [ $filteredOptions ] , ( optionsOrGroups ) => {
503
+ const $optionsOrGroups = useAtom ( optionsOrGroups ) ;
504
+ useEffect ( ( ) => {
505
+ $optionsOrGroups . set ( optionsOrGroups ) ;
506
+ } , [ optionsOrGroups , $optionsOrGroups ] ) ;
507
+ const $totalOptionCount = useComputed ( [ $optionsOrGroups ] , ( optionsOrGroups ) => {
506
508
let count = 0 ;
507
509
for ( const optionOrGroup of optionsOrGroups ) {
508
510
if ( isGroup ( optionOrGroup ) ) {
@@ -513,6 +515,10 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
513
515
}
514
516
return count ;
515
517
} ) ;
518
+ const $filteredOptions = useAtom < T [ ] | Group < T > [ ] > ( [ ] ) ;
519
+ const $flattenedFilteredOptions = useComputed ( [ $filteredOptions ] , ( filteredOptions ) =>
520
+ flattenOptions ( filteredOptions )
521
+ ) ;
516
522
const $hasOptions = useComputed ( [ $totalOptionCount ] , ( count ) => count > 0 ) ;
517
523
const $filteredOptionsCount = useComputed ( [ $flattenedFilteredOptions ] , ( options ) => options . length ) ;
518
524
const $hasFilteredOptions = useComputed ( [ $filteredOptionsCount ] , ( count ) => count > 0 ) ;
@@ -543,7 +549,7 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
543
549
const ctx = useMemo (
544
550
( ) =>
545
551
( {
546
- optionsOrGroups,
552
+ $ optionsOrGroups,
547
553
$groupStatusMap,
548
554
$compactView,
549
555
$activeOptionId,
@@ -570,7 +576,7 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
570
576
$selectedItemId,
571
577
} ) satisfies PickerContextState < T > ,
572
578
[
573
- optionsOrGroups ,
579
+ $ optionsOrGroups,
574
580
$groupStatusMap ,
575
581
$compactView ,
576
582
$activeOptionId ,
@@ -616,7 +622,7 @@ Picker.displayName = 'Picker';
616
622
617
623
const PickerSyncer = typedMemo ( < T extends object > ( ) => {
618
624
const {
619
- optionsOrGroups,
625
+ $ optionsOrGroups,
620
626
$searchTerm,
621
627
$activeOptionId,
622
628
$groupStatusMap,
@@ -629,6 +635,7 @@ const PickerSyncer = typedMemo(<T extends object>() => {
629
635
const searchTerm = useStore ( $searchTerm ) ;
630
636
const groupStatusMap = useStore ( $groupStatusMap ) ;
631
637
const areAllGroupsDisabled = useStore ( $areAllGroupsDisabled ) ;
638
+ const optionsOrGroups = useStore ( $optionsOrGroups ) ;
632
639
const [ debouncedSearchTerm ] = useDebounce ( searchTerm , 300 ) ;
633
640
634
641
useEffect ( ( ) => {
@@ -714,13 +721,27 @@ const NoMatchesFallback = typedMemo(<T extends object>() => {
714
721
NoMatchesFallback . displayName = 'NoMatchesFallback' ;
715
722
716
723
const PickerSearchBar = typedMemo ( < T extends object > ( ) => {
717
- const { optionsOrGroups, inputRef, $searchTerm, $totalOptionCount, searchPlaceholder, NextToSearchBar } =
718
- usePickerContext < T > ( ) ;
724
+ const { NextToSearchBar } = usePickerContext < T > ( ) ;
725
+
726
+ return (
727
+ < Flex flexDir = "column" w = "full" gap = { 2 } >
728
+ < Flex gap = { 2 } alignItems = "center" >
729
+ < SearchInput />
730
+ { NextToSearchBar }
731
+ < CompactViewToggleButton />
732
+ </ Flex >
733
+ < GroupToggleButtons />
734
+ </ Flex >
735
+ ) ;
736
+ } ) ;
737
+ PickerSearchBar . displayName = 'PickerSearchBar' ;
738
+
739
+ const SearchInput = typedMemo ( < T extends object > ( ) => {
740
+ const { inputRef, $totalOptionCount, $searchTerm, searchPlaceholder } = usePickerContext < T > ( ) ;
719
741
const { t } = useTranslation ( ) ;
720
742
const searchTerm = useStore ( $searchTerm ) ;
721
743
const totalOptionCount = useStore ( $totalOptionCount ) ;
722
744
const placeholder = searchPlaceholder ?? t ( 'common.search' ) ;
723
-
724
745
const resetSearchTerm = useCallback ( ( ) => {
725
746
$searchTerm . set ( '' ) ;
726
747
inputRef . current ?. focus ( ) ;
@@ -732,53 +753,77 @@ const PickerSearchBar = typedMemo(<T extends object>() => {
732
753
} ,
733
754
[ $searchTerm ]
734
755
) ;
735
-
736
- const groups = useMemo ( ( ) => {
756
+ return (
757
+ < InputGroup >
758
+ < Input ref = { inputRef } value = { searchTerm } onChange = { onChangeSearchTerm } placeholder = { placeholder } />
759
+ { searchTerm && (
760
+ < InputRightElement h = "full" pe = { 2 } >
761
+ < IconButton
762
+ onClick = { resetSearchTerm }
763
+ size = "sm"
764
+ variant = "link"
765
+ aria-label = { t ( 'common.clear' ) }
766
+ tooltip = { t ( 'common.clear' ) }
767
+ icon = { < PiXBold /> }
768
+ isDisabled = { totalOptionCount === 0 }
769
+ disabled = { false }
770
+ />
771
+ </ InputRightElement >
772
+ ) }
773
+ </ InputGroup >
774
+ ) ;
775
+ } ) ;
776
+ SearchInput . displayName = 'SearchInput' ;
777
+ const GroupToggleButtons = typedMemo ( < T extends object > ( ) => {
778
+ const { $optionsOrGroups, $groupStatusMap, $areAllGroupsDisabled } = usePickerContext < T > ( ) ;
779
+ const { t } = useTranslation ( ) ;
780
+ const $groups = useComputed ( [ $optionsOrGroups ] , ( optionsOrGroups ) => {
737
781
const _groups : Group < T > [ ] = [ ] ;
738
782
for ( const optionOrGroup of optionsOrGroups ) {
739
783
if ( isGroup ( optionOrGroup ) ) {
740
784
_groups . push ( optionOrGroup ) ;
741
785
}
742
786
}
743
787
return _groups ;
744
- } , [ optionsOrGroups ] ) ;
788
+ } ) ;
789
+ const groups = useStore ( $groups ) ;
790
+ const areAllGroupsDisabled = useStore ( $areAllGroupsDisabled ) ;
791
+
792
+ const onClick = useCallback < MouseEventHandler > ( ( ) => {
793
+ const newMap : GroupStatusMap = { } ;
794
+ for ( const { id } of groups ) {
795
+ newMap [ id ] = false ;
796
+ }
797
+ $groupStatusMap . set ( newMap ) ;
798
+ } , [ $groupStatusMap , groups ] ) ;
799
+
800
+ if ( ! groups . length ) {
801
+ return null ;
802
+ }
745
803
746
804
return (
747
- < Flex flexDir = "column" w = "full" gap = { 2 } >
748
- < Flex gap = { 2 } alignItems = "center" >
749
- < InputGroup >
750
- < Input
751
- ref = { inputRef }
752
- value = { searchTerm }
753
- onChange = { onChangeSearchTerm }
754
- placeholder = { placeholder }
755
- isDisabled = { totalOptionCount === 0 }
756
- />
757
- { searchTerm && (
758
- < InputRightElement h = "full" pe = { 2 } >
759
- < IconButton
760
- onClick = { resetSearchTerm }
761
- size = "sm"
762
- variant = "link"
763
- aria-label = { t ( 'common.clear' ) }
764
- tooltip = { t ( 'common.clear' ) }
765
- icon = { < PiXBold /> }
766
- />
767
- </ InputRightElement >
768
- ) }
769
- </ InputGroup >
770
- { NextToSearchBar }
771
- < CompactViewToggleButton />
772
- </ Flex >
773
- < Flex gap = { 2 } alignItems = "center" >
774
- { groups . map ( ( group ) => (
775
- < GroupToggleButton key = { group . id } group = { group } />
776
- ) ) }
777
- </ Flex >
805
+ < Flex gap = { 2 } alignItems = "center" >
806
+ { groups . map ( ( group ) => (
807
+ < GroupToggleButton key = { group . id } group = { group } />
808
+ ) ) }
809
+ < Spacer />
810
+ < IconButton
811
+ icon = { < PiArrowCounterClockwiseBold /> }
812
+ aria-label = { t ( 'common.reset' ) }
813
+ tooltip = { t ( 'common.reset' ) }
814
+ size = "sm"
815
+ variant = "link"
816
+ alignSelf = "stretch"
817
+ onClick = { onClick }
818
+ // When a focused element is disabled, it blurs. This closes the popover. Fake the disabled state to prevent this.
819
+ // See: https://github.com/chakra-ui/chakra-ui/issues/7965
820
+ opacity = { areAllGroupsDisabled ? 0.5 : undefined }
821
+ pointerEvents = { areAllGroupsDisabled ? 'none' : undefined }
822
+ />
778
823
</ Flex >
779
824
) ;
780
825
} ) ;
781
- PickerSearchBar . displayName = 'PickerSearchBar ' ;
826
+ GroupToggleButtons . displayName = 'GroupToggleButtons ' ;
782
827
783
828
const CompactViewToggleButton = typedMemo ( < T extends object > ( ) => {
784
829
const { t } = useTranslation ( ) ;
0 commit comments