diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 0148d564e88c..2f105594cf98 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -50,6 +50,9 @@ type CheckboxProps = Partial & { /** stop propagation of the mouse down event */ shouldStopMouseDownPropagation?: boolean; + + /** Whether the checkbox should be selected when pressing Enter key */ + shouldSelectOnPressEnter?: boolean; }; function Checkbox( @@ -68,6 +71,7 @@ function Checkbox( onPress, accessibilityLabel, shouldStopMouseDownPropagation, + shouldSelectOnPressEnter, }: CheckboxProps, ref: ForwardedRef, ) { @@ -75,8 +79,14 @@ function Checkbox( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const handleSpaceKey = (event?: ReactKeyboardEvent) => { - if (event?.code !== 'Space') { + const handleSpaceOrEnterKey = (event?: ReactKeyboardEvent) => { + if (event?.code !== 'Space' && event?.code !== 'Enter') { + return; + } + + if (event?.code === 'Enter' && !shouldSelectOnPressEnter) { + // If the checkbox should not be selected on Enter key press, we do not want to + // toggle it, so we return early. return; } @@ -105,7 +115,7 @@ function Checkbox( }} ref={ref} style={[StyleUtils.getCheckboxPressableStyle(containerBorderRadius + 2), style]} // to align outline on focus, border-radius of pressable should be 2px more than Checkbox - onKeyDown={handleSpaceKey} + onKeyDown={handleSpaceOrEnterKey} role={CONST.ROLE.CHECKBOX} /* true → checked false → unchecked diff --git a/src/components/Search/SearchBooleanFilterBase.tsx b/src/components/Search/SearchBooleanFilterBase.tsx index a648f8f814f1..e03c7cc1a311 100644 --- a/src/components/Search/SearchBooleanFilterBase.tsx +++ b/src/components/Search/SearchBooleanFilterBase.tsx @@ -1,11 +1,12 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; +import SingleSelectListItem from '@components/SelectionList/SingleSelectListItem'; import type {ListItem} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SearchFilterPageFooterButtons from './SearchFilterPageFooterButtons'; import type {SearchBooleanFilterKeys} from './types'; type BooleanFilterItem = ListItem & { @@ -34,11 +36,11 @@ function SearchBooleanFilterBase({booleanKey, titleKey}: SearchBooleanFilterBase const {translate} = useLocalize(); const booleanValues = Object.values(CONST.SEARCH.BOOLEAN); - const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); - const selectedItem = useMemo(() => { + const [selectedItem, setSelectedItem] = useState(() => { return booleanValues.find((value) => searchAdvancedFiltersForm?.[booleanKey] === value) ?? null; - }, [booleanKey, searchAdvancedFiltersForm, booleanValues]); + }); const items = useMemo(() => { return booleanValues.map((value) => ({ @@ -49,22 +51,26 @@ function SearchBooleanFilterBase({booleanKey, titleKey}: SearchBooleanFilterBase })); }, [selectedItem, translate, booleanValues]); - const updateFilter = useCallback( - (selectedFilter: BooleanFilterItem) => { - const newValue = selectedFilter.isSelected ? null : selectedFilter.value; + const updateFilter = useCallback((selectedFilter: BooleanFilterItem) => { + const newValue = selectedFilter.isSelected ? null : selectedFilter.value; + setSelectedItem(newValue); + }, []); - updateAdvancedFilters({[booleanKey]: newValue}); - Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); - }, - [booleanKey], - ); + const resetChanges = useCallback(() => { + setSelectedItem(null); + }, []); + + const applyChanges = useCallback(() => { + updateAdvancedFilters({[booleanKey]: selectedItem}); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }, [booleanKey, selectedItem]); return ( + + + ); } diff --git a/src/components/Search/SearchDateFilterBase.tsx b/src/components/Search/SearchDateFilterBase.tsx deleted file mode 100644 index 483a1ccc0c9f..000000000000 --- a/src/components/Search/SearchDateFilterBase.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import {format} from 'date-fns'; -import React from 'react'; -import {useOnyx} from 'react-native-onyx'; -import DatePicker from '@components/DatePicker'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {updateAdvancedFilters} from '@libs/actions/Search'; -import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type {SearchDateFilterKeys} from './types'; - -type SearchDateFilterBaseProps = { - /** Key used for the date filter */ - dateKey: SearchDateFilterKeys; - - /** The translation key for the page title */ - titleKey: TranslationPaths; -}; - -function SearchDateFilterBase({dateKey, titleKey}: SearchDateFilterBaseProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); - const unformattedDateAfter = searchAdvancedFiltersForm?.[`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.AFTER}`]; - const unformattedDateBefore = searchAdvancedFiltersForm?.[`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.BEFORE}`]; - const dateAfter = unformattedDateAfter ? format(unformattedDateAfter, 'yyyy-MM-dd') : undefined; - const dateBefore = unformattedDateBefore ? format(unformattedDateBefore, 'yyyy-MM-dd') : undefined; - - const updateDateFilter = (values: FormOnyxValues) => { - updateAdvancedFilters(values); - Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); - }; - - return ( - - { - Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); - }} - /> - - - - - - ); -} - -SearchDateFilterBase.displayName = 'SearchDateFilterBase'; - -export default SearchDateFilterBase; diff --git a/src/components/Search/SearchDateFilterBase/CalendarView.tsx b/src/components/Search/SearchDateFilterBase/CalendarView.tsx new file mode 100644 index 000000000000..16beaaa9c753 --- /dev/null +++ b/src/components/Search/SearchDateFilterBase/CalendarView.tsx @@ -0,0 +1,70 @@ +import React, {useState} from 'react'; +import CalendarPicker from '@components/DatePicker/CalendarPicker'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchFilterPageFooterButtons from '@components/Search/SearchFilterPageFooterButtons'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SearchDateModifier, SearchDateModifierLower} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; + +type CalendarViewProps = { + view: SearchDateModifier; + value: string | null; + navigateBack: () => void; + setValue: (key: SearchDateModifier, value: string | null) => void; +}; + +function SearchDateFilterBaseCalendarView({view, value, navigateBack, setValue}: CalendarViewProps) { + const initialValue = value === CONST.SEARCH.NEVER ? null : value; + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [localDateValue, setLocalDateValue] = useState(initialValue); + + const lowerDateModifier = view.toLowerCase() as SearchDateModifierLower; + + const resetChanges = () => { + setValue(view, null); + navigateBack(); + }; + + const applyChanges = () => { + setValue(view, localDateValue); + navigateBack(); + }; + + return ( + + + + + + + + + + ); +} + +SearchDateFilterBaseCalendarView.displayName = 'SearchDateFilterBaseCalendarView'; + +export default SearchDateFilterBaseCalendarView; diff --git a/src/components/Search/SearchDateFilterBase/RootView.tsx b/src/components/Search/SearchDateFilterBase/RootView.tsx new file mode 100644 index 000000000000..ca6271cd6fd1 --- /dev/null +++ b/src/components/Search/SearchDateFilterBase/RootView.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import {View} from 'react-native'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItem from '@components/MenuItem'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchFilterPageFooterButtons from '@components/Search/SearchFilterPageFooterButtons'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SearchDateModifier} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {SearchDateFilterBaseProps, SearchFiltersDatePageValues} from '.'; + +type RootViewProps = Pick & { + value: SearchFiltersDatePageValues; + applyChanges: () => void; + resetChanges: () => void; + setView: (view: SearchDateModifier) => void; +}; + +function SearchDateFilterBaseRootView({titleKey, value, applyChanges, resetChanges, setView}: RootViewProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const dateOn = value?.[CONST.SEARCH.DATE_MODIFIERS.ON] ?? undefined; + const dateAfter = value?.[CONST.SEARCH.DATE_MODIFIERS.AFTER] ?? undefined; + const dateBefore = value?.[CONST.SEARCH.DATE_MODIFIERS.BEFORE] ?? undefined; + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + + setView(CONST.SEARCH.DATE_MODIFIERS.ON)} + /> + setView(CONST.SEARCH.DATE_MODIFIERS.AFTER)} + /> + setView(CONST.SEARCH.DATE_MODIFIERS.BEFORE)} + /> + + + + + + + ); +} + +SearchDateFilterBaseRootView.displayName = 'SearchDateFilterBaseRootView'; + +export default SearchDateFilterBaseRootView; diff --git a/src/components/Search/SearchDateFilterBase/index.tsx b/src/components/Search/SearchDateFilterBase/index.tsx new file mode 100644 index 000000000000..49216c5a4f72 --- /dev/null +++ b/src/components/Search/SearchDateFilterBase/index.tsx @@ -0,0 +1,106 @@ +import React, {useCallback, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import type {SearchDateFilterKeys} from '@components/Search/types'; +import {updateAdvancedFilters} from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SearchDateModifier} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import SearchDateFilterBaseCalendarView from './CalendarView'; +import SearchDateFilterBaseRootView from './RootView'; + +type SearchFiltersDatePageValues = Record; + +type SearchDateFilterBaseProps = { + /** Key used for the date filter */ + dateKey: SearchDateFilterKeys; + + /** The translation key for the page title */ + titleKey: TranslationPaths; +}; + +function SearchDateFilterBase({dateKey, titleKey}: SearchDateFilterBaseProps) { + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); + + const [view, setView] = useState(null); + const [localDateValues, setLocalDateValues] = useState({ + After: searchAdvancedFiltersForm?.[`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.AFTER}`] ?? null, + Before: searchAdvancedFiltersForm?.[`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.BEFORE}`] ?? null, + On: searchAdvancedFiltersForm?.[`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.ON}`] ?? null, + }); + + const setDateValue = (key: SearchDateModifier, dateValue: string | null) => { + setLocalDateValues((currentValue) => { + // If we are setting the 'on' to 'never', reset the other dates + if (key === CONST.SEARCH.DATE_MODIFIERS.ON && dateValue === CONST.SEARCH.NEVER) { + return { + [CONST.SEARCH.DATE_MODIFIERS.ON]: dateValue, + [CONST.SEARCH.DATE_MODIFIERS.AFTER]: null, + [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: null, + }; + } + + // If we are setting any other value while 'on' is set to 'never', reset 'on' to null + if (key !== CONST.SEARCH.DATE_MODIFIERS.ON && currentValue?.[CONST.SEARCH.DATE_MODIFIERS.ON] === CONST.SEARCH.NEVER) { + return { + ...currentValue, + [key]: dateValue, + [CONST.SEARCH.DATE_MODIFIERS.ON]: null, + }; + } + + return { + ...currentValue, + [key]: dateValue, + }; + }); + }; + + const navigateToRootView = useCallback(() => { + setView(null); + }, []); + + const resetChanges = useCallback(() => { + // Reset each field back to null + setLocalDateValues((prev) => { + return Object.fromEntries(Object.entries(prev).map(([key]) => [key, null])) as SearchFiltersDatePageValues; + }); + }, []); + + const applyChanges = useCallback(() => { + updateAdvancedFilters({ + [`${dateKey}On`]: localDateValues?.[CONST.SEARCH.DATE_MODIFIERS.ON], + [`${dateKey}After`]: localDateValues?.[CONST.SEARCH.DATE_MODIFIERS.AFTER], + [`${dateKey}Before`]: localDateValues?.[CONST.SEARCH.DATE_MODIFIERS.BEFORE], + }); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }, [dateKey, localDateValues]); + + if (!view) { + return ( + + ); + } + + return ( + + ); +} + +SearchDateFilterBase.displayName = 'SearchDateFilterBase'; + +export type {SearchFiltersDatePageValues, SearchDateFilterBaseProps}; +export default SearchDateFilterBase; diff --git a/src/components/Search/SearchFilterPageFooterButtons.tsx b/src/components/Search/SearchFilterPageFooterButtons.tsx new file mode 100644 index 000000000000..78aeed8d42ab --- /dev/null +++ b/src/components/Search/SearchFilterPageFooterButtons.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type SearchFilterPageFooterButtonsProps = { + /** Function to reset changes made in the filter */ + resetChanges: () => void; + + /** Function to apply changes made in the filter */ + applyChanges: () => void; +}; + +function SearchFilterPageFooterButtons({resetChanges, applyChanges}: SearchFilterPageFooterButtonsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + +