@@ -36,8 +36,6 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
3636 const [ chosenLeafIds , setChosenLeafIds ] = React . useState < string [ ] > ( [ 'beans' , 'beef' , 'chicken' , 'tofu' ] ) ;
3737 const [ chosenFilter , setChosenFilter ] = React . useState < string > ( '' ) ;
3838 const [ availableFilter , setAvailableFilter ] = React . useState < string > ( '' ) ;
39- let hiddenChosen : string [ ] = [ ] ;
40- let hiddenAvailable : string [ ] = [ ] ;
4139
4240 // helper function to build memoized lists
4341 const buildTextById = ( node : FoodNode ) : { [ key : string ] : string } => {
@@ -82,7 +80,7 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
8280 } ;
8381
8482 // Builds a map of child leaf nodes by node id - memoized so that it only rebuilds the list if the data changes.
85- const { memoizedLeavesById, memoizedAllLeaves, memoizedNodeText } = React . useMemo ( ( ) => {
83+ const { memoizedLeavesById, memoizedAllLeaves, memoizedNodeTexts } = React . useMemo ( ( ) => {
8684 let leavesById : { [ key : string ] : string [ ] } = { } ;
8785 let allLeaves : string [ ] = [ ] ;
8886 let nodeTexts : { [ key : string ] : string } = { } ;
@@ -94,32 +92,49 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
9492 return {
9593 memoizedLeavesById : leavesById ,
9694 memoizedAllLeaves : allLeaves ,
97- memoizedNodeText : nodeTexts
95+ memoizedNodeTexts : nodeTexts
9896 } ;
9997 // eslint-disable-next-line react-hooks/exhaustive-deps
10098 } , [ data ] ) ;
10199
100+ const matchesFilter = ( value : string , filter : string ) => value . toLowerCase ( ) . includes ( filter . trim ( ) . toLowerCase ( ) ) ;
101+
102+ const getVisibleLeafIds = ( leafIds : string [ ] , filter : string ) => {
103+ const filterMatchingNodeIds = Object . keys ( memoizedLeavesById ) . filter ( ( nodeId ) =>
104+ matchesFilter ( memoizedNodeTexts [ nodeId ] , filter )
105+ ) ;
106+ const filterMatchingLeafIds = filterMatchingNodeIds . map ( ( nodeId ) => memoizedLeavesById [ nodeId ] ) . flat ( ) ;
107+ return leafIds . filter ( ( leafId ) => filterMatchingLeafIds . includes ( leafId ) ) ;
108+ } ;
109+
110+ const availableLeafIds = memoizedAllLeaves . filter ( ( leafId ) => ! chosenLeafIds . includes ( leafId ) ) ;
111+ const visibleChosenLeafIds = getVisibleLeafIds ( chosenLeafIds , chosenFilter ) ;
112+ const visibleAvailableLeafIds = getVisibleLeafIds ( availableLeafIds , availableFilter ) ;
113+
102114 const moveChecked = ( toChosen : boolean ) => {
115+ const visibleCheckedChosenLeafIds = checkedLeafIds . filter ( ( leafId ) => visibleChosenLeafIds . includes ( leafId ) ) ;
116+ const visibleCheckedAvailableLeafIds = checkedLeafIds . filter ( ( leafId ) => visibleAvailableLeafIds . includes ( leafId ) ) ;
117+
103118 setChosenLeafIds (
104119 ( prevChosenIds ) =>
105120 toChosen
106- ? [ ...prevChosenIds , ...checkedLeafIds ] // add checked ids to chosen list
107- : [ ... prevChosenIds . filter ( ( x ) => ! checkedLeafIds . includes ( x ) ) ] // remove checked ids from chosen list
121+ ? [ ...prevChosenIds , ...visibleCheckedAvailableLeafIds ] // add visible checked ids to chosen list
122+ : prevChosenIds . filter ( ( x ) => ! visibleCheckedChosenLeafIds . includes ( x ) ) // remove visible checked ids from chosen list
108123 ) ;
109124
110125 // uncheck checked ids that just moved
111126 setCheckedLeafIds ( ( prevChecked ) =>
112127 toChosen
113- ? [ ... prevChecked . filter ( ( x ) => chosenLeafIds . includes ( x ) ) ]
114- : [ ... prevChecked . filter ( ( x ) => ! chosenLeafIds . includes ( x ) ) ]
128+ ? prevChecked . filter ( ( x ) => ! visibleCheckedAvailableLeafIds . includes ( x ) )
129+ : prevChecked . filter ( ( x ) => ! visibleCheckedChosenLeafIds . includes ( x ) )
115130 ) ;
116131 } ;
117132
118133 const moveAll = ( toChosen : boolean ) => {
119134 if ( toChosen ) {
120- setChosenLeafIds ( memoizedAllLeaves ) ;
135+ setChosenLeafIds ( ( prevChosenIds ) => [ ... prevChosenIds , ... visibleAvailableLeafIds ] ) ;
121136 } else {
122- setChosenLeafIds ( [ ] ) ;
137+ setChosenLeafIds ( ( prevChosenIds ) => prevChosenIds . filter ( ( id ) => ! visibleChosenLeafIds . includes ( id ) ) ) ;
123138 }
124139 } ;
125140
@@ -149,15 +164,9 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
149164 isChosen : boolean
150165 ) => {
151166 const nodeIdsToCheck = memoizedLeavesById [ node . id ] . filter ( ( id ) =>
152- isChosen
153- ? chosenLeafIds . includes ( id ) && ! hiddenChosen . includes ( id )
154- : ! chosenLeafIds . includes ( id ) && ! hiddenAvailable . includes ( id )
167+ isChosen ? visibleChosenLeafIds . includes ( id ) : visibleAvailableLeafIds . includes ( id )
155168 ) ;
156- if ( isChosen ) {
157- hiddenChosen = [ ] ;
158- } else {
159- hiddenAvailable = [ ] ;
160- }
169+
161170 setCheckedLeafIds ( ( prevChecked ) => {
162171 const otherCheckedNodeNames = prevChecked . filter ( ( id ) => ! nodeIdsToCheck . includes ( id ) ) ;
163172 return ! isChecked ? otherCheckedNodeNames : [ ...otherCheckedNodeNames , ...nodeIdsToCheck ] ;
@@ -189,16 +198,15 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
189198
190199 const isChecked = isNodeChecked ( node , isChosen ) ;
191200
192- const filterValue = ( isChosen ? chosenFilter : availableFilter ) . toLowerCase ( ) . trim ( ) ;
201+ const filterValue = isChosen ? chosenFilter : availableFilter ;
193202 const descendentLeafIds = memoizedLeavesById [ node . id ] ;
194203 const descendentsOnThisPane = isChosen
195204 ? descendentLeafIds . filter ( ( id ) => chosenLeafIds . includes ( id ) )
196205 : descendentLeafIds . filter ( ( id ) => ! chosenLeafIds . includes ( id ) ) ;
197206
198207 const hasMatchingChildren =
199- filterValue && descendentsOnThisPane . some ( ( id ) => memoizedNodeText [ id ] . toLowerCase ( ) . includes ( filterValue ) ) ;
200- const isFilterMatch =
201- filterValue && node . text . toLowerCase ( ) . includes ( filterValue ) && descendentsOnThisPane . length > 0 ;
208+ filterValue && descendentsOnThisPane . some ( ( id ) => matchesFilter ( memoizedNodeTexts [ id ] , filterValue ) ) ;
209+ const isFilterMatch = filterValue && matchesFilter ( node . text , filterValue ) && descendentsOnThisPane . length > 0 ;
202210
203211 // A node is displayed if either of the following is true:
204212 // - There is no filter value and this node or its descendents belong on this pane
@@ -209,14 +217,6 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
209217 ( hasParentMatch && descendentsOnThisPane . length > 0 ) ||
210218 isFilterMatch ;
211219
212- if ( ! isDisplayed ) {
213- if ( isChosen ) {
214- hiddenChosen . push ( node . id ) ;
215- } else {
216- hiddenAvailable . push ( node . id ) ;
217- }
218- }
219-
220220 return [
221221 ...( isDisplayed
222222 ? [
@@ -243,9 +243,9 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
243243
244244 const buildPane = ( isChosen : boolean ) : React . ReactNode => {
245245 const options : DualListSelectorTreeItemData [ ] = buildOptions ( isChosen , data , false ) ;
246- const numOptions = isChosen ? chosenLeafIds . length : memoizedAllLeaves . length - chosenLeafIds . length ;
246+ const numOptions = isChosen ? visibleChosenLeafIds . length : visibleAvailableLeafIds . length ;
247247 const numSelected = checkedLeafIds . filter ( ( id ) =>
248- isChosen ? chosenLeafIds . includes ( id ) : ! chosenLeafIds . includes ( id )
248+ isChosen ? visibleChosenLeafIds . includes ( id ) : visibleAvailableLeafIds . includes ( id )
249249 ) . length ;
250250 const status = `${ numSelected } of ${ numOptions } options selected` ;
251251 const filterApplied = isChosen ? chosenFilter !== '' : availableFilter !== '' ;
@@ -286,7 +286,7 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
286286 { buildPane ( false ) }
287287 < DualListSelectorControlsWrapper >
288288 < DualListSelectorControl
289- isDisabled = { ! checkedLeafIds . filter ( ( x ) => ! chosenLeafIds . includes ( x ) ) . length }
289+ isDisabled = { ! checkedLeafIds . filter ( ( x ) => visibleAvailableLeafIds . includes ( x ) ) . length }
290290 onClick = { ( ) => moveChecked ( true ) }
291291 aria-label = "Add selected"
292292 icon = { < AngleRightIcon /> }
@@ -305,7 +305,7 @@ const DualListSelectorComposableTree: React.FunctionComponent<ExampleProps> = ({
305305 />
306306 < DualListSelectorControl
307307 onClick = { ( ) => moveChecked ( false ) }
308- isDisabled = { ! checkedLeafIds . filter ( ( x ) => ! ! chosenLeafIds . includes ( x ) ) . length }
308+ isDisabled = { ! checkedLeafIds . filter ( ( x ) => visibleChosenLeafIds . includes ( x ) ) . length }
309309 aria-label = "Remove selected"
310310 icon = { < AngleLeftIcon /> }
311311 />
0 commit comments