-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Issue #1491]: Setup states to hide clear all and select all in filter accordion #1495
Changes from all commits
e196e3d
c2f270e
6c9ee88
bf8a5f5
a6a8caf
0e3dd15
f59293e
c0c22ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,10 @@ export function SearchFilterAccordion({ | |
toggleSelectAll, | ||
incrementTotal, | ||
decrementTotal, | ||
isAllSelected, | ||
isNoneSelected, | ||
isSectionAllSelected, | ||
isSectionNoneSelected, | ||
} = useSearchFilter( | ||
initialFilterOptions, | ||
initialQueryParams, | ||
|
@@ -70,6 +74,8 @@ export function SearchFilterAccordion({ | |
<SearchFilterToggleAll | ||
onSelectAll={() => toggleSelectAll(true)} | ||
onClearAll={() => toggleSelectAll(false)} | ||
isAllSelected={isAllSelected} | ||
isNoneSelected={isNoneSelected} | ||
/> | ||
<ul className="usa-list usa-list--unstyled"> | ||
{options.map((option) => ( | ||
|
@@ -84,6 +90,8 @@ export function SearchFilterAccordion({ | |
mounted={mounted} | ||
updateCheckedOption={toggleOptionChecked} | ||
toggleSelectAll={toggleSelectAll} | ||
isSectionAllSelected={isSectionAllSelected[option.id]} | ||
isSectionNoneSelected={isSectionNoneSelected[option.id]} | ||
/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea the logic to setup those values is kind of a lot but once you have it it's very easy to use in the components |
||
) : ( | ||
<SearchFilterCheckbox | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,48 @@ | ||
"use client"; | ||
|
||
interface SearchFilterToggleAllProps { | ||
isAllSelected: boolean; | ||
isNoneSelected: boolean; | ||
onSelectAll?: () => void; | ||
onClearAll?: () => void; | ||
} | ||
|
||
const SearchFilterToggleAll: React.FC<SearchFilterToggleAllProps> = ({ | ||
onSelectAll, | ||
onClearAll, | ||
}) => ( | ||
<div className="grid-row"> | ||
<div className="grid-col-fill"> | ||
<button | ||
className="usa-button usa-button--unstyled font-sans-xs" | ||
onClick={(event) => { | ||
// form submission is done in useSearchFilter, so | ||
// prevent the onClick from submitting here. | ||
event.preventDefault(); | ||
onSelectAll?.(); | ||
}} | ||
> | ||
Select All | ||
</button> | ||
isAllSelected, | ||
isNoneSelected, | ||
}) => { | ||
return ( | ||
<div className="grid-row"> | ||
<div className="grid-col-fill"> | ||
<button | ||
className="usa-button usa-button--unstyled font-sans-xs" | ||
onClick={(event) => { | ||
// form submission is done in useSearchFilter, so | ||
// prevent the onClick from submitting here. | ||
event.preventDefault(); | ||
onSelectAll?.(); | ||
}} | ||
disabled={isAllSelected} | ||
> | ||
Select All | ||
</button> | ||
</div> | ||
<div className="grid-col-fill text-right"> | ||
<button | ||
className="usa-button usa-button--unstyled font-sans-xs" | ||
onClick={(event) => { | ||
event.preventDefault(); | ||
onClearAll?.(); | ||
}} | ||
disabled={isNoneSelected} | ||
> | ||
Clear All | ||
</button> | ||
</div> | ||
</div> | ||
<div className="grid-col-fill text-right"> | ||
<button | ||
className="usa-button usa-button--unstyled font-sans-xs" | ||
onClick={(event) => { | ||
event.preventDefault(); | ||
onClearAll?.(); | ||
}} | ||
> | ||
Clear All | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
); | ||
}; | ||
|
||
export default SearchFilterToggleAll; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ import { useSearchParamUpdater } from "./useSearchParamUpdater"; | |
|
||
// Encapsulate core search filter accordion logic: | ||
// - Keep track of checked count | ||
// - Increment/decrement functions | ||
// - Provide increment/decrement functions | ||
// - Toggle one or all checkboxes | ||
// - Run debounced function that updates query params and submits form | ||
// (Does not cover opportunity status checkbox logic) | ||
|
@@ -19,6 +19,7 @@ function useSearchFilter( | |
queryParamKey: QueryParamKey, // agency, fundingInstrument, eligibility, or category | ||
formRef: React.RefObject<HTMLFormElement>, | ||
) { | ||
const { updateQueryParams } = useSearchParamUpdater(); | ||
const [options, setOptions] = useState<FilterOption[]>(() => | ||
initializeOptions(initialFilterOptions, initialQueryParams), | ||
); | ||
|
@@ -43,6 +44,69 @@ function useSearchFilter( | |
})); | ||
} | ||
|
||
// Recursively count checked options | ||
const countChecked = useCallback((optionsList: FilterOption[]): number => { | ||
return optionsList.reduce((acc, option) => { | ||
return option.children | ||
? acc + countChecked(option.children) | ||
: acc + (option.isChecked ? 1 : 0); | ||
}, 0); | ||
}, []); | ||
|
||
// Used for disabled select all / clear all states | ||
const determineInitialSelectionStates = useCallback( | ||
(options: FilterOption[]) => { | ||
const totalOptions = options.reduce((total, option) => { | ||
return total + (option.children ? option.children.length : 1); | ||
}, 0); | ||
|
||
const totalChecked = countChecked(options); | ||
const allSelected = totalChecked === totalOptions; | ||
const noneSelected = totalChecked === 0; | ||
|
||
type SectionStates = { | ||
isSectionAllSelected: { [key: string]: boolean }; | ||
isSectionNoneSelected: { [key: string]: boolean }; | ||
}; | ||
|
||
const sectionStates = options.reduce<SectionStates>( | ||
(acc, option) => { | ||
if (option.children) { | ||
const totalInSection = option.children.length; | ||
const checkedInSection = countChecked(option.children); | ||
acc.isSectionAllSelected[option.id] = | ||
totalInSection === checkedInSection; | ||
acc.isSectionNoneSelected[option.id] = checkedInSection === 0; | ||
} | ||
return acc; | ||
}, | ||
{ isSectionAllSelected: {}, isSectionNoneSelected: {} }, | ||
); | ||
|
||
return { | ||
allSelected, | ||
noneSelected, | ||
...sectionStates, | ||
}; | ||
}, | ||
[countChecked], | ||
); | ||
|
||
const initialSelectionStates = determineInitialSelectionStates(options); | ||
|
||
const [isAllSelected, setIsAllSelected] = useState<boolean>( | ||
initialSelectionStates.allSelected, | ||
); | ||
const [isNoneSelected, setIsNoneSelected] = useState<boolean>( | ||
initialSelectionStates.noneSelected, | ||
); | ||
const [isSectionAllSelected, setIsSectionAllSelected] = useState<{ | ||
[key: string]: boolean; | ||
}>(initialSelectionStates.isSectionAllSelected); | ||
const [isSectionNoneSelected, setIsSectionNoneSelected] = useState<{ | ||
[key: string]: boolean; | ||
}>(initialSelectionStates.isSectionNoneSelected); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. loads of logic to manage the {select,clear} all disabled state for both sections and non-sections |
||
const [checkedTotal, setCheckedTotal] = useState<number>(0); | ||
const incrementTotal = () => { | ||
setCheckedTotal(checkedTotal + 1); | ||
|
@@ -51,22 +115,11 @@ function useSearchFilter( | |
setCheckedTotal(checkedTotal - 1); | ||
}; | ||
|
||
const { updateQueryParams } = useSearchParamUpdater(); | ||
|
||
const [mounted, setMounted] = useState<boolean>(false); | ||
useEffect(() => { | ||
setMounted(true); | ||
}, []); | ||
|
||
// Recursively count checked options | ||
const countChecked = useCallback((optionsList: FilterOption[]): number => { | ||
return optionsList.reduce((acc, option) => { | ||
return option.children | ||
? acc + countChecked(option.children) | ||
: acc + (option.isChecked ? 1 : 0); | ||
}, 0); | ||
}, []); | ||
|
||
// Recursively toggle options | ||
const recursiveToggle = useCallback( | ||
( | ||
|
@@ -119,6 +172,30 @@ function useSearchFilter( | |
formRef.current?.requestSubmit(); | ||
}, 500); | ||
|
||
// Extracted function to calculate and update selection states | ||
const updateSelectionStates = useCallback( | ||
(newOptions: FilterOption[], sectionId?: string | undefined) => { | ||
const newSelectionStates = determineInitialSelectionStates(newOptions); | ||
setIsAllSelected(newSelectionStates.allSelected); | ||
setIsNoneSelected(newSelectionStates.noneSelected); | ||
|
||
if (sectionId) { | ||
setIsSectionAllSelected((prevState) => ({ | ||
...prevState, | ||
[sectionId]: newSelectionStates.isSectionAllSelected[sectionId], | ||
})); | ||
setIsSectionNoneSelected((prevState) => ({ | ||
...prevState, | ||
[sectionId]: newSelectionStates.isSectionNoneSelected[sectionId], | ||
})); | ||
} else { | ||
setIsSectionAllSelected(newSelectionStates.isSectionAllSelected); | ||
setIsSectionNoneSelected(newSelectionStates.isSectionNoneSelected); | ||
} | ||
}, | ||
[determineInitialSelectionStates], | ||
); | ||
|
||
// Toggle all checkbox options on the accordion, or all within a section | ||
const toggleSelectAll = useCallback( | ||
(isSelected: boolean, sectionId?: string) => { | ||
|
@@ -129,11 +206,13 @@ function useSearchFilter( | |
sectionId, | ||
); | ||
|
||
updateSelectionStates(newOptions, sectionId); | ||
|
||
debouncedUpdateQueryParams(); | ||
return newOptions; | ||
}); | ||
}, | ||
[recursiveToggle, debouncedUpdateQueryParams], | ||
[recursiveToggle, debouncedUpdateQueryParams, updateSelectionStates], | ||
); | ||
|
||
// Toggle a single option | ||
|
@@ -147,13 +226,20 @@ function useSearchFilter( | |
children: opt.children ? updateChecked(opt.children) : undefined, | ||
})); | ||
}; | ||
const newOptions = updateChecked(prevOptions); | ||
updateSelectionStates(newOptions); | ||
|
||
// If the option being toggled has children, pass the option's id to update its specific section state | ||
if (prevOptions.find((opt) => opt.id === optionId)?.children) { | ||
updateSelectionStates(newOptions, optionId); | ||
} | ||
|
||
// Trigger the debounced update when options/checkboxes change | ||
debouncedUpdateQueryParams(); | ||
return updateChecked(prevOptions); | ||
return newOptions; | ||
}); | ||
}, | ||
[debouncedUpdateQueryParams], | ||
[debouncedUpdateQueryParams, updateSelectionStates], | ||
); | ||
|
||
// The total count of checked options | ||
|
@@ -168,6 +254,10 @@ function useSearchFilter( | |
totalCheckedCount, | ||
incrementTotal, | ||
decrementTotal, | ||
isAllSelected, | ||
isNoneSelected, | ||
isSectionAllSelected, | ||
isSectionNoneSelected, | ||
}; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also moved the
SearchFetcher
classes from/services/searchfetcher
toservices/search/searchfetcher/
so there's a bunch of updates in this PR for that