Skip to content

Commit 5fa6905

Browse files
authored
Merge pull request Expensify#89166 from mohammadjafarinejad/fix/82534-v2
feat: Add inline editing for tables on desktop (V2)
2 parents 4b4fe76 + 1abb209 commit 5fa6905

60 files changed

Lines changed: 3190 additions & 116 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import SafeArea from './components/SafeArea';
3131
import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider';
3232
import SidePanelContextProvider from './components/SidePanel/SidePanelContextProvider';
3333
import SVGDefinitionsProvider from './components/SVGDefinitionsProvider';
34+
import {EditingCellProvider} from './components/Table/EditableCell';
3435
import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider';
3536
import ThemeProvider from './components/ThemeProvider';
3637
import ThemeStylesProvider from './components/ThemeStylesContextProvider';
@@ -115,6 +116,7 @@ function App() {
115116
FullScreenLoaderContextProvider,
116117
ModalProvider,
117118
SidePanelContextProvider,
119+
EditingCellProvider,
118120
]}
119121
>
120122
<CustomStatusBarAndBackground />

src/CONST/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9004,6 +9004,9 @@ const CONST = {
90049004
SORTABLE_HEADER: 'Search-SortableHeader',
90059005
UNREPORTED_EXPENSE_LIST_ITEM: 'UnreportedExpenseListItem',
90069006
},
9007+
TABLE: {
9008+
EDITABLE_CELL: 'Table-EditableCell',
9009+
},
90079010
REPORT: {
90089011
FLOATING_MESSAGE_COUNTER: 'Report-FloatingMessageCounter',
90099012
LIST_BOUNDARY_LOADER_RETRY: 'Report-ListBoundaryLoaderRetry',
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, {useRef} from 'react';
2+
import {View} from 'react-native';
3+
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
4+
import type PopoverWithMeasuredContentProps from '@components/PopoverWithMeasuredContent/types';
5+
import type {ListItem} from '@components/SelectionList/types';
6+
import useStyleUtils from '@hooks/useStyleUtils';
7+
import useThemeStyles from '@hooks/useThemeStyles';
8+
import CONST from '@src/CONST';
9+
import CategoryPicker from '.';
10+
11+
const popoverDimensions = {
12+
width: CONST.POPOVER_DROPDOWN_WIDTH,
13+
height: CONST.POPOVER_DROPDOWN_MAX_HEIGHT,
14+
};
15+
16+
const DEFAULT_ANCHOR_ALIGNMENT = {
17+
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
18+
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
19+
};
20+
21+
type CategoryPickerModalProps = {
22+
/** Callback to close the modal */
23+
onClose: () => void;
24+
25+
/** The policy whose categories should be shown */
26+
policyID: string | undefined;
27+
28+
/** Currently selected category */
29+
selectedCategory?: string;
30+
31+
/** Called when the user confirms a category selection */
32+
onSelected?: (item: ListItem) => void;
33+
} & Omit<PopoverWithMeasuredContentProps, 'anchorRef' | 'children' | 'onClose'>;
34+
35+
function CategoryPickerModal({
36+
isVisible,
37+
onClose,
38+
anchorPosition,
39+
policyID,
40+
selectedCategory,
41+
onSelected,
42+
anchorAlignment = DEFAULT_ANCHOR_ALIGNMENT,
43+
shouldMeasureAnchorPositionFromTop = false,
44+
}: CategoryPickerModalProps) {
45+
const styles = useThemeStyles();
46+
const StyleUtils = useStyleUtils();
47+
const anchorRef = useRef<View>(null);
48+
49+
const handleCategorySelect = (item: ListItem) => {
50+
// If clicking the same category that's already selected, treat it as deselection
51+
if (item.keyForList === selectedCategory) {
52+
onSelected?.({keyForList: '', searchText: ''});
53+
} else {
54+
onSelected?.(item);
55+
}
56+
onClose();
57+
};
58+
59+
return (
60+
<PopoverWithMeasuredContent
61+
anchorRef={anchorRef}
62+
isVisible={isVisible}
63+
onClose={onClose}
64+
anchorPosition={anchorPosition}
65+
popoverDimensions={popoverDimensions}
66+
anchorAlignment={anchorAlignment}
67+
innerContainerStyle={StyleUtils.getWidthStyle(popoverDimensions.width)}
68+
restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
69+
shouldSwitchPositionIfOverflow
70+
shouldEnableNewFocusManagement
71+
shouldMeasureAnchorPositionFromTop={shouldMeasureAnchorPositionFromTop}
72+
shouldSkipRemeasurement
73+
shouldDisplayBelowModals
74+
>
75+
<View style={[StyleUtils.getHeight(popoverDimensions.height), styles.flexColumn, styles.pt4]}>
76+
<CategoryPicker
77+
selectedCategory={selectedCategory}
78+
policyID={policyID}
79+
onSubmit={handleCategorySelect}
80+
/>
81+
</View>
82+
</PopoverWithMeasuredContent>
83+
);
84+
}
85+
86+
export default CategoryPickerModal;
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import React from 'react';
2+
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
3+
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
4+
import type {ListItem} from '@components/SelectionList/types';
5+
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
26
import useAutoFocusInput from '@hooks/useAutoFocusInput';
37
import useDebouncedState from '@hooks/useDebouncedState';
48
import useLocalize from '@hooks/useLocalize';
@@ -12,10 +16,6 @@ import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils';
1216
import CONST from '@src/CONST';
1317
import ONYXKEYS from '@src/ONYXKEYS';
1418
import {isEmptyObject} from '@src/types/utils/EmptyObject';
15-
import SingleSelectListItem from './SelectionList/ListItem/SingleSelectListItem';
16-
import SelectionListWithSections from './SelectionList/SelectionListWithSections';
17-
import type {ListItem} from './SelectionList/types';
18-
import type {BaseTextInputRef} from './TextInput/BaseTextInput/types';
1919

2020
type CategoryPickerProps = {
2121
policyID: string | undefined;

src/components/DatePicker/CalendarPicker/MonthPickerModal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ type MonthPickerModalProps = {
2222

2323
/** Function to call when the user closes the month picker */
2424
onClose?: () => void;
25+
26+
/** Whether RIGHT_DOCKED modal should keep backdrop in narrow pane context */
27+
shouldEnableBackdropInNarrowPane?: boolean;
2528
};
2629

27-
function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), onMonthChange, onClose}: MonthPickerModalProps) {
30+
function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), onMonthChange, onClose, shouldEnableBackdropInNarrowPane = false}: MonthPickerModalProps) {
2831
const styles = useThemeStyles();
2932
const {translate} = useLocalize();
3033
const [searchText, setSearchText] = useState('');
@@ -66,6 +69,7 @@ function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), onMo
6669
shouldHandleNavigationBack
6770
shouldUseCustomBackdrop
6871
onBackdropPress={onClose}
72+
shouldKeepRightDockedBackdropInNarrowPane={shouldEnableBackdropInNarrowPane}
6973
enableEdgeToEdgeBottomSafeAreaPadding
7074
>
7175
<ScreenWrapper

src/components/DatePicker/CalendarPicker/YearPickerModal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ type YearPickerModalProps = {
2525

2626
/** Function to call when the user closes the year picker */
2727
onClose?: () => void;
28+
29+
/** Whether RIGHT_DOCKED modal should keep backdrop in narrow pane context */
30+
shouldEnableBackdropInNarrowPane?: boolean;
2831
};
2932

30-
function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear(), onYearChange, onClose}: YearPickerModalProps) {
33+
function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear(), onYearChange, onClose, shouldEnableBackdropInNarrowPane = false}: YearPickerModalProps) {
3134
const styles = useThemeStyles();
3235
const {translate} = useLocalize();
3336
const [searchText, setSearchText] = useState('');
@@ -67,6 +70,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear
6770
shouldHandleNavigationBack
6871
shouldUseCustomBackdrop
6972
onBackdropPress={onClose}
73+
shouldKeepRightDockedBackdropInNarrowPane={shouldEnableBackdropInNarrowPane}
7074
enableEdgeToEdgeBottomSafeAreaPadding
7175
>
7276
<ScreenWrapper

src/components/DatePicker/CalendarPicker/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type CalendarPickerProps = {
4040

4141
/** Optional style override for the header container */
4242
headerContainerStyle?: StyleProp<ViewStyle>;
43+
44+
/** Whether Month/Year right-docked picker modals should keep backdrop in narrow pane context */
45+
shouldEnableMonthYearBackdropInNarrowPane?: boolean;
4346
};
4447

4548
function getInitialCurrentDateView(value: Date | string, minDate: Date, maxDate: Date) {
@@ -71,6 +74,7 @@ function CalendarPicker({
7174
DayComponent = Day,
7275
selectableDates,
7376
headerContainerStyle,
77+
shouldEnableMonthYearBackdropInNarrowPane = false,
7478
}: CalendarPickerProps) {
7579
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
7680
const {isSmallScreenWidth} = useResponsiveLayout();
@@ -424,12 +428,14 @@ function CalendarPicker({
424428
currentYear={currentYearView}
425429
onYearChange={onYearSelected}
426430
onClose={() => setIsYearPickerVisible(false)}
431+
shouldEnableBackdropInNarrowPane={shouldEnableMonthYearBackdropInNarrowPane}
427432
/>
428433
<MonthPickerModal
429434
isVisible={isMonthPickerVisible}
430435
currentMonth={currentMonthView}
431436
onMonthChange={onMonthSelected}
432437
onClose={() => setIsMonthPickerVisible(false)}
438+
shouldEnableBackdropInNarrowPane={shouldEnableMonthYearBackdropInNarrowPane}
433439
/>
434440
</View>
435441
);

src/components/DatePicker/DatePickerModal.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const DEFAULT_ANCHOR_ORIGIN = {
1313
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
1414
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
1515
};
16+
1617
const popoverDimensions = {
1718
height: CONST.POPOVER_DATE_MIN_HEIGHT,
1819
width: CONST.POPOVER_DATE_WIDTH,
@@ -31,10 +32,12 @@ function DatePickerModal({
3132
isVisible,
3233
onClose,
3334
anchorPosition,
35+
anchorAlignment = DEFAULT_ANCHOR_ORIGIN,
3436
onSelected,
3537
shouldCloseWhenBrowserNavigationChanged = false,
3638
shouldPositionFromTop = false,
3739
forwardedFSClass,
40+
shouldEnableMonthYearBackdropInNarrowPane = false,
3841
}: DatePickerProps) {
3942
const [selectedDate, setSelectedDate] = useState(value ?? defaultValue ?? undefined);
4043
const anchorRef = useRef<View>(null);
@@ -69,7 +72,8 @@ function DatePickerModal({
6972
popoverDimensions={popoverDimensions}
7073
shouldCloseWhenBrowserNavigationChanged={shouldCloseWhenBrowserNavigationChanged}
7174
innerContainerStyle={isSmallScreenWidth ? styles.w100 : {width: CONST.POPOVER_DATE_WIDTH}}
72-
anchorAlignment={DEFAULT_ANCHOR_ORIGIN}
75+
anchorAlignment={anchorAlignment}
76+
restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
7377
shouldSwitchPositionIfOverflow
7478
shouldReturnFocus={false}
7579
shouldMeasureAnchorPositionFromTop={shouldPositionFromTop}
@@ -82,6 +86,7 @@ function DatePickerModal({
8286
maxDate={maxDate}
8387
value={selectedDate}
8488
onSelected={handleDateSelection}
89+
shouldEnableMonthYearBackdropInNarrowPane={shouldEnableMonthYearBackdropInNarrowPane}
8590
/>
8691
</PopoverWithMeasuredContent>
8792
);

src/components/DatePicker/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ type DatePickerBaseProps = ForwardedFSClassProps & {
3535

3636
/** ID of the wrapping form */
3737
formID?: keyof OnyxFormValuesMapping;
38+
39+
/**
40+
* Whether Month/Year right-docked picker modals should keep backdrop in narrow pane context.
41+
* Used by inline editing flows that require background dimming.
42+
*/
43+
shouldEnableMonthYearBackdropInNarrowPane?: boolean;
3844
};
3945

4046
type DateInputWithPickerProps = DatePickerBaseProps &
@@ -93,6 +99,12 @@ type DatePickerProps = {
9399

94100
/** If the popover will be positioned from the top */
95101
shouldPositionFromTop?: boolean;
102+
103+
/**
104+
* Whether Month/Year right-docked picker modals should keep backdrop in narrow pane context.
105+
* Used by inline editing flows that require background dimming.
106+
*/
107+
shouldEnableMonthYearBackdropInNarrowPane?: boolean;
96108
} & Omit<BaseTextInputProps & PopoverWithMeasuredContentProps, 'anchorRef' | 'children'>;
97109

98110
export type {DateInputWithPickerProps, DatePickerProps};

src/components/Modal/BaseModal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function BaseModal({
7373
forwardedFSClass = CONST.FULLSTORY.CLASS.UNMASK,
7474
ref,
7575
shouldDisplayBelowModals = false,
76+
shouldKeepRightDockedBackdropInNarrowPane = false,
7677
shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode = true,
7778
}: BaseModalProps) {
7879
// When the `enableEdgeToEdgeBottomSafeAreaPadding` prop is explicitly set, we enable edge-to-edge mode.
@@ -305,8 +306,10 @@ function BaseModal({
305306
const {originalValues} = useContext(ScreenWrapperOfflineIndicatorContext);
306307
const offlineIndicatorContextValue = useMemo(() => (isInNarrowPane ? (originalValues ?? {}) : {}), [isInNarrowPane, originalValues]);
307308

309+
const shouldSuppressRightDockedBackdrop =
310+
type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && !isSmallScreenWidth && (isInNarrowPane || isInNarrowPaneModal) && !shouldKeepRightDockedBackdropInNarrowPane;
308311
const backdropOpacityAdjusted =
309-
hideBackdrop || (type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && !isSmallScreenWidth && (isInNarrowPane || isInNarrowPaneModal)) // right_docked modals shouldn't add backdrops when opened in same-width RHP
312+
hideBackdrop || shouldSuppressRightDockedBackdrop // right_docked modals shouldn't add backdrops when opened in same-width RHP
310313
? 0
311314
: backdropOpacity;
312315

0 commit comments

Comments
 (0)