Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ee29cf1
feat: enable new bottom modal in emoji picker
BartoszGrajdek Feb 10, 2025
a6bd34f
fix: eslint problems
BartoszGrajdek Feb 11, 2025
df606fb
fix: LayoutAnimationConfig mock
BartoszGrajdek Feb 11, 2025
446234b
chore: resolve merge conflicts
BartoszGrajdek Feb 27, 2025
7b82b78
Merge remote-tracking branch 'origin/main' into @BartoszGrajdek/new-e…
BartoszGrajdek Mar 4, 2025
20f84c0
Merge branch 'main' of github.com:Expensify/App into @BartoszGrajdek/…
blazejkustra Mar 28, 2025
f4df8b7
Merge branch 'main' of github.com:Expensify/App into @BartoszGrajdek/…
blazejkustra Apr 1, 2025
ab9f94c
Remove unnecessary comment and prop
blazejkustra Apr 2, 2025
8a82036
Use KeyboardAvoidingView from our codebase
blazejkustra Apr 2, 2025
3065947
Add background color
blazejkustra Apr 2, 2025
897f8c0
Remove maxHeight restriction from popoverInnerContainer style
blazejkustra Apr 2, 2025
9a64992
Fix keyboard avoiding view on mobile web
blazejkustra Apr 3, 2025
4eec6d4
Merge branch 'main' of github.com:Expensify/App into feature/new-emoj…
blazejkustra Apr 3, 2025
eaf5191
Refactor getModalPaddingStyles and keyboard context
blazejkustra Apr 3, 2025
ea3cb90
Replace Animated.View with View in BaseModal component
blazejkustra Apr 3, 2025
93d9d43
Merge branch 'main' of github.com:Expensify/App into feature/new-emoj…
blazejkustra Apr 4, 2025
bcc5485
Bring back old code
blazejkustra Apr 4, 2025
d5ef18e
Fix ios warning
blazejkustra Apr 4, 2025
b12864f
Use padding on android too
blazejkustra Apr 4, 2025
f5f59ca
Merge branch 'main' of github.com:Expensify/App into feature/new-emoj…
blazejkustra Apr 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jest.mock('react-native-reanimated', () => ({
...jest.requireActual<typeof Animated>('react-native-reanimated/mock'),
createAnimatedPropAdapter: jest.fn,
useReducedMotion: jest.fn,
LayoutAnimationConfig: jest.fn,
}));

jest.mock('react-native-keyboard-controller', () => require<typeof RNKeyboardController>('react-native-keyboard-controller/jest'));
Expand Down
14 changes: 5 additions & 9 deletions src/components/EmojiPicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import type {AnchorOrigin, EmojiPickerRef, EmojiPopoverAnchor, OnEmojiSelected, OnModalHideValue, OnWillShowPicker} from '@libs/actions/EmojiPickerAction';
import * as Browser from '@libs/Browser';
import {isMobileChrome} from '@libs/Browser';
import calculateAnchorPosition from '@libs/calculateAnchorPosition';
import * as Modal from '@userActions/Modal';
import {close} from '@userActions/Modal';
import CONST from '@src/CONST';
import EmojiPickerMenu from './EmojiPickerMenu';

Expand Down Expand Up @@ -92,7 +92,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef<Em
// It's possible that the anchor is inside an active modal (e.g., add emoji reaction in report context menu).
// So, we need to get the anchor position first before closing the active modal which will also destroy the anchor.
calculateAnchorPosition(emojiPopoverAnchor?.current, anchorOriginValue).then((value) => {
Modal.close(() => {
close(() => {
onWillShow?.();
setIsEmojiPickerVisible(true);
setEmojiPopoverAnchorPosition({
Expand Down Expand Up @@ -190,19 +190,14 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef<Em
};
}, [isEmojiPickerVisible, shouldUseNarrowLayout, emojiPopoverAnchorOrigin, getEmojiPopoverAnchor]);

// There is no way to disable animations, and they are really laggy, because there are so many
// emojis. The best alternative is to set it to 1ms so it just "pops" in and out
return (
<PopoverWithMeasuredContent
shouldHandleNavigationBack={Browser.isMobileChrome()}
shouldHandleNavigationBack={isMobileChrome()}
isVisible={isEmojiPickerVisible}
onClose={hideEmojiPicker}
onModalShow={focusEmojiSearchInput}
onModalHide={onModalHide.current}
hideModalContentWhileAnimating
shouldSetModalVisibility={false}
animationInTiming={1}
animationOutTiming={1}
anchorPosition={{
vertical: emojiPopoverAnchorPosition.vertical,
horizontal: emojiPopoverAnchorPosition.horizontal,
Expand All @@ -221,6 +216,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef<Em
shoudSwitchPositionIfOverflow
shouldEnableNewFocusManagement
restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
shouldUseNewModal
>
<FocusTrapForModal active={isEmojiPickerVisible}>
<View>
Expand Down
54 changes: 25 additions & 29 deletions src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function BaseModal(
const sidePaneStyle = shouldApplySidePaneOffset && !isSmallScreenWidth ? {paddingRight: sidePaneOffset.current} : undefined;
const keyboardStateContextValue = useKeyboardState();

const safeAreaInsets = useSafeAreaInsets();
const insets = useSafeAreaInsets();

const isVisibleRef = useRef(isVisible);
const hideModalCallbackRef = useRef<(callHideCallback: boolean) => void>();
Expand Down Expand Up @@ -211,34 +211,30 @@ function BaseModal(
[StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle],
);

const {
paddingTop: safeAreaPaddingTop,
paddingBottom: safeAreaPaddingBottom,
paddingLeft: safeAreaPaddingLeft,
paddingRight: safeAreaPaddingRight,
} = StyleUtils.getPlatformSafeAreaPadding(safeAreaInsets);

const modalPaddingStyles = shouldUseModalPaddingStyle
? StyleUtils.getModalPaddingStyles({
safeAreaPaddingTop,
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaMargin,
// enableEdgeToEdgeBottomSafeAreaPadding is used as a temporary solution to disable safe area bottom spacing on modals, to allow edge-to-edge content
shouldAddBottomSafeAreaPadding: !enableEdgeToEdgeBottomSafeAreaPadding && (!avoidKeyboard || !keyboardStateContextValue?.isKeyboardShown) && shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
modalContainerStyleMarginTop: modalContainerStyle.marginTop,
modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
insets: safeAreaInsets,
})
: {
paddingLeft: safeAreaPaddingLeft ?? 0,
paddingRight: safeAreaPaddingRight ?? 0,
};
const modalPaddingStyles = useMemo(() => {
const paddings = StyleUtils.getModalPaddingStyles({
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaMargin,
// enableEdgeToEdgeBottomSafeAreaPadding is used as a temporary solution to disable safe area bottom spacing on modals, to allow edge-to-edge content
shouldAddBottomSafeAreaPadding: !enableEdgeToEdgeBottomSafeAreaPadding && (!avoidKeyboard || !keyboardStateContextValue.isKeyboardActive) && shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
modalContainerStyle,
insets,
});
return shouldUseModalPaddingStyle ? paddings : {paddingLeft: paddings.paddingLeft, paddingRight: paddings.paddingRight};
}, [
StyleUtils,
avoidKeyboard,
enableEdgeToEdgeBottomSafeAreaPadding,
insets,
keyboardStateContextValue.isKeyboardActive,
modalContainerStyle,
shouldAddBottomSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaMargin,
shouldAddTopSafeAreaPadding,
shouldUseModalPaddingStyle,
]);

const modalContextValue = useMemo(
() => ({
Expand Down
5 changes: 2 additions & 3 deletions src/components/Modal/BottomDockedModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function BottomDockedModal({
width: deviceWidthProp ?? deviceWidth,
height: deviceHeightProp ?? deviceHeight,
backgroundColor: backdropColor,
opacity: getPlatform() === CONST.PLATFORM.WEB ? backdropOpacity : 1,
...(getPlatform() === CONST.PLATFORM.WEB ? {opacity: backdropOpacity} : {}),
};
}, [deviceHeightProp, deviceWidthProp, deviceWidth, deviceHeight, backdropColor, backdropOpacity]);

Expand Down Expand Up @@ -215,10 +215,9 @@ function BottomDockedModal({
{...props}
>
{isVisibleState && hasBackdrop && backdropView}

{avoidKeyboard ? (
<KeyboardAvoidingView
behavior={getPlatform() === CONST.PLATFORM.IOS ? 'padding' : undefined}
behavior="padding"
pointerEvents="box-none"
style={[style, {margin: 0}]}
>
Expand Down
33 changes: 2 additions & 31 deletions src/components/PopoverWithoutOverlay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,46 +72,17 @@ function PopoverWithoutOverlay(
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isVisible]);

const {
paddingTop: safeAreaPaddingTop,
paddingBottom: safeAreaPaddingBottom,
paddingLeft: safeAreaPaddingLeft,
paddingRight: safeAreaPaddingRight,
} = useMemo(() => StyleUtils.getPlatformSafeAreaPadding(insets), [StyleUtils, insets]);

const modalPaddingStyles = useMemo(
() =>
StyleUtils.getModalPaddingStyles({
safeAreaPaddingTop,
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
modalContainerStyleMarginTop: modalContainerStyle.marginTop,
modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
modalContainerStyle,
insets,
}),
[
StyleUtils,
insets,
modalContainerStyle.marginBottom,
modalContainerStyle.marginTop,
modalContainerStyle.paddingBottom,
modalContainerStyle.paddingTop,
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
safeAreaPaddingTop,
shouldAddBottomSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaMargin,
shouldAddTopSafeAreaPadding,
],
[StyleUtils, insets, modalContainerStyle, shouldAddBottomSafeAreaMargin, shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaMargin, shouldAddTopSafeAreaPadding],
);

if (!isVisible) {
Expand Down
17 changes: 16 additions & 1 deletion src/components/withKeyboardState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ type KeyboardStateContextValue = {
/** Whether the keyboard is open */
isKeyboardShown: boolean;

/** Whether the keyboard is animating or shown */
isKeyboardActive: boolean;

/** Height of the keyboard in pixels */
keyboardHeight: number;

/** Ref to check if the keyboard is animating */
isKeyboardAnimatingRef: MutableRefObject<boolean>;
};

const KeyboardStateContext = createContext<KeyboardStateContextValue>({
isKeyboardShown: false,
isKeyboardActive: false,
keyboardHeight: 0,
isKeyboardAnimatingRef: {current: false},
});
Expand All @@ -26,6 +31,7 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
const {bottom} = useSafeAreaInsets();
const [keyboardHeight, setKeyboardHeight] = useState(0);
const isKeyboardAnimatingRef = useRef(false);
const [isKeyboardActive, setIsKeyboardActive] = useState(false);

useEffect(() => {
const keyboardDidShowListener = KeyboardEvents.addListener('keyboardDidShow', (e) => {
Expand All @@ -34,10 +40,18 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
const keyboardDidHideListener = KeyboardEvents.addListener('keyboardDidHide', () => {
setKeyboardHeight(0);
});
const keyboardWillShowListener = KeyboardEvents.addListener('keyboardWillShow', () => {
setIsKeyboardActive(true);
});
const keyboardWillHideListener = KeyboardEvents.addListener('keyboardWillHide', () => {
setIsKeyboardActive(false);
});

return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
keyboardWillShowListener.remove();
keyboardWillHideListener.remove();
};
}, [bottom]);

Expand Down Expand Up @@ -66,8 +80,9 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
keyboardHeight,
isKeyboardShown: keyboardHeight !== 0,
isKeyboardAnimatingRef,
isKeyboardActive,
}),
[keyboardHeight],
[isKeyboardActive, keyboardHeight],
);
return <KeyboardStateContext.Provider value={contextValue}>{children}</KeyboardStateContext.Provider>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@ const styles = (theme: ThemeColors) =>

popoverInnerContainer: {
paddingTop: 0, // adjusting this because the mobile modal adds additional padding that we don't need for our layout
maxHeight: '95%',
backgroundColor: theme.modalBackground,
},

menuItemTextContainer: {
Expand Down
30 changes: 9 additions & 21 deletions src/styles/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,14 +589,7 @@ type ModalPaddingStylesParams = {
shouldAddTopSafeAreaMargin: boolean;
shouldAddBottomSafeAreaPadding: boolean;
shouldAddTopSafeAreaPadding: boolean;
safeAreaPaddingTop: number;
safeAreaPaddingBottom: number;
safeAreaPaddingLeft: number;
safeAreaPaddingRight: number;
modalContainerStyleMarginTop: MarginPaddingValue;
modalContainerStyleMarginBottom: MarginPaddingValue;
modalContainerStylePaddingTop: MarginPaddingValue;
modalContainerStylePaddingBottom: MarginPaddingValue;
modalContainerStyle: ViewStyle;
insets: EdgeInsets;
};

Expand All @@ -605,24 +598,19 @@ function getModalPaddingStyles({
shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
safeAreaPaddingTop,
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
modalContainerStyleMarginTop,
modalContainerStyleMarginBottom,
modalContainerStylePaddingTop,
modalContainerStylePaddingBottom,
modalContainerStyle,
insets,
}: ModalPaddingStylesParams): ViewStyle {
const {paddingTop: safeAreaPaddingTop, paddingBottom: safeAreaPaddingBottom, paddingLeft: safeAreaPaddingLeft, paddingRight: safeAreaPaddingRight} = getPlatformSafeAreaPadding(insets);

// use fallback value for safeAreaPaddingBottom to keep padding bottom consistent with padding top.
// More info: issue #17376
const safeAreaPaddingBottomWithFallback = insets.bottom === 0 && typeof modalContainerStylePaddingTop === 'number' ? modalContainerStylePaddingTop ?? 0 : safeAreaPaddingBottom;
const safeAreaPaddingBottomWithFallback = insets.bottom === 0 && typeof modalContainerStyle.paddingTop === 'number' ? modalContainerStyle.paddingTop ?? 0 : safeAreaPaddingBottom;
return {
marginTop: getCombinedSpacing(modalContainerStyleMarginTop, safeAreaPaddingTop, shouldAddTopSafeAreaMargin),
marginBottom: getCombinedSpacing(modalContainerStyleMarginBottom, safeAreaPaddingBottomWithFallback, shouldAddBottomSafeAreaMargin),
paddingTop: getCombinedSpacing(modalContainerStylePaddingTop, safeAreaPaddingTop, shouldAddTopSafeAreaPadding),
paddingBottom: getCombinedSpacing(modalContainerStylePaddingBottom, safeAreaPaddingBottomWithFallback, shouldAddBottomSafeAreaPadding),
marginTop: getCombinedSpacing(modalContainerStyle.marginTop, safeAreaPaddingTop, shouldAddTopSafeAreaMargin),
marginBottom: getCombinedSpacing(modalContainerStyle.marginBottom, safeAreaPaddingBottomWithFallback, shouldAddBottomSafeAreaMargin),
paddingTop: getCombinedSpacing(modalContainerStyle.paddingTop, safeAreaPaddingTop, shouldAddTopSafeAreaPadding),
paddingBottom: getCombinedSpacing(modalContainerStyle.paddingBottom, safeAreaPaddingBottomWithFallback, shouldAddBottomSafeAreaPadding),
paddingLeft: safeAreaPaddingLeft ?? 0,
paddingRight: safeAreaPaddingRight ?? 0,
};
Expand Down