@@ -17,7 +17,7 @@ import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
1717import type { AnyStore , ReadableAtom , Task , WritableAtom } from 'nanostores' ;
1818import { atom , computed } from 'nanostores' ;
1919import type { StoreValues } from 'nanostores/computed' ;
20- import type { ChangeEvent , PropsWithChildren , RefObject } from 'react' ;
20+ import type { ChangeEvent , MouseEventHandler , PropsWithChildren , RefObject } from 'react' ;
2121import React , {
2222 createContext ,
2323 useCallback ,
@@ -29,7 +29,12 @@ import React, {
2929 useState ,
3030} from 'react' ;
3131import { 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' ;
3338import { assert } from 'tsafe' ;
3439import { useDebounce } from 'use-debounce' ;
3540
@@ -194,7 +199,7 @@ type PickerProps<T extends object> = {
194199} ;
195200
196201export type PickerContextState < T extends object > = {
197- optionsOrGroups : T [ ] | Group < T > [ ] ;
202+ $ optionsOrGroups : WritableAtom < T [ ] | Group < T > [ ] > ;
198203 $groupStatusMap : WritableAtom < GroupStatusMap > ;
199204 $compactView : WritableAtom < boolean > ;
200205 $activeOptionId : WritableAtom < string | undefined > ;
@@ -490,19 +495,16 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
490495 NextToSearchBar,
491496 searchable,
492497 } = props ;
493- const [ $activeOptionId ] = useState ( ( ) => {
494- const initialValue = getFirstOptionId ( optionsOrGroups , getOptionId ) ;
495- return atom < string | undefined > ( initialValue ) ;
496- } ) ;
497498 const rootRef = useRef < HTMLDivElement > ( null ) ;
498499 const inputRef = useRef < HTMLInputElement > ( null ) ;
499500 const { $groupStatusMap, $areAllGroupsDisabled, toggleGroup } = useTogglableGroups ( optionsOrGroups ) ;
501+ const $activeOptionId = useAtom ( getFirstOptionId ( optionsOrGroups , getOptionId ) ) ;
500502 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 ) => {
506508 let count = 0 ;
507509 for ( const optionOrGroup of optionsOrGroups ) {
508510 if ( isGroup ( optionOrGroup ) ) {
@@ -513,6 +515,10 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
513515 }
514516 return count ;
515517 } ) ;
518+ const $filteredOptions = useAtom < T [ ] | Group < T > [ ] > ( [ ] ) ;
519+ const $flattenedFilteredOptions = useComputed ( [ $filteredOptions ] , ( filteredOptions ) =>
520+ flattenOptions ( filteredOptions )
521+ ) ;
516522 const $hasOptions = useComputed ( [ $totalOptionCount ] , ( count ) => count > 0 ) ;
517523 const $filteredOptionsCount = useComputed ( [ $flattenedFilteredOptions ] , ( options ) => options . length ) ;
518524 const $hasFilteredOptions = useComputed ( [ $filteredOptionsCount ] , ( count ) => count > 0 ) ;
@@ -543,7 +549,7 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
543549 const ctx = useMemo (
544550 ( ) =>
545551 ( {
546- optionsOrGroups,
552+ $ optionsOrGroups,
547553 $groupStatusMap,
548554 $compactView,
549555 $activeOptionId,
@@ -570,7 +576,7 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
570576 $selectedItemId,
571577 } ) satisfies PickerContextState < T > ,
572578 [
573- optionsOrGroups ,
579+ $ optionsOrGroups,
574580 $groupStatusMap ,
575581 $compactView ,
576582 $activeOptionId ,
@@ -616,7 +622,7 @@ Picker.displayName = 'Picker';
616622
617623const PickerSyncer = typedMemo ( < T extends object > ( ) => {
618624 const {
619- optionsOrGroups,
625+ $ optionsOrGroups,
620626 $searchTerm,
621627 $activeOptionId,
622628 $groupStatusMap,
@@ -629,6 +635,7 @@ const PickerSyncer = typedMemo(<T extends object>() => {
629635 const searchTerm = useStore ( $searchTerm ) ;
630636 const groupStatusMap = useStore ( $groupStatusMap ) ;
631637 const areAllGroupsDisabled = useStore ( $areAllGroupsDisabled ) ;
638+ const optionsOrGroups = useStore ( $optionsOrGroups ) ;
632639 const [ debouncedSearchTerm ] = useDebounce ( searchTerm , 300 ) ;
633640
634641 useEffect ( ( ) => {
@@ -714,13 +721,27 @@ const NoMatchesFallback = typedMemo(<T extends object>() => {
714721NoMatchesFallback . displayName = 'NoMatchesFallback' ;
715722
716723const 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 > ( ) ;
719741 const { t } = useTranslation ( ) ;
720742 const searchTerm = useStore ( $searchTerm ) ;
721743 const totalOptionCount = useStore ( $totalOptionCount ) ;
722744 const placeholder = searchPlaceholder ?? t ( 'common.search' ) ;
723-
724745 const resetSearchTerm = useCallback ( ( ) => {
725746 $searchTerm . set ( '' ) ;
726747 inputRef . current ?. focus ( ) ;
@@ -732,53 +753,77 @@ const PickerSearchBar = typedMemo(<T extends object>() => {
732753 } ,
733754 [ $searchTerm ]
734755 ) ;
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 ) => {
737781 const _groups : Group < T > [ ] = [ ] ;
738782 for ( const optionOrGroup of optionsOrGroups ) {
739783 if ( isGroup ( optionOrGroup ) ) {
740784 _groups . push ( optionOrGroup ) ;
741785 }
742786 }
743787 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+ }
745803
746804 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+ />
778823 </ Flex >
779824 ) ;
780825} ) ;
781- PickerSearchBar . displayName = 'PickerSearchBar ' ;
826+ GroupToggleButtons . displayName = 'GroupToggleButtons ' ;
782827
783828const CompactViewToggleButton = typedMemo ( < T extends object > ( ) => {
784829 const { t } = useTranslation ( ) ;
0 commit comments