Skip to content

Commit

Permalink
Merge pull request Expensify#32551 from pasyukevich/feature/migrate-P…
Browse files Browse the repository at this point in the history
…opoverWithMeasuredContent

[TS migration] Migrate 'PopoverWithMeasuredContent.js' component to TypeScript
  • Loading branch information
mountiny authored Dec 28, 2023
2 parents d0c3e31 + 8053058 commit 76d6f8e
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -1,56 +1,17 @@
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import React, {useMemo, useState} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import {LayoutChangeEvent, View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import PopoverWithMeasuredContentUtils from '@libs/PopoverWithMeasuredContentUtils';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import Popover from './Popover';
import {defaultProps as defaultPopoverProps, propTypes as popoverPropTypes} from './Popover/popoverPropTypes';
import {windowDimensionsPropTypes} from './withWindowDimensions';

const propTypes = {
// All popover props except:
// 1) anchorPosition (which is overridden for this component)
// 2) windowDimensionsPropTypes - we exclude them because we use useWindowDimensions hook instead
..._.omit(popoverPropTypes, ['anchorPosition', ..._.keys(windowDimensionsPropTypes)]),
import {PopoverProps} from './Popover/types';

type PopoverWithMeasuredContentProps = Omit<PopoverProps, 'anchorPosition'> & {
/** The horizontal and vertical anchors points for the popover */
anchorPosition: PropTypes.shape({
horizontal: PropTypes.number.isRequired,
vertical: PropTypes.number.isRequired,
}).isRequired,

/** How the popover should be aligned. The value you passed will is the part of the component that will be aligned to the
* anchorPosition. ie: vertical:top means the top of the menu will be positioned in the anchorPosition */
anchorAlignment: PropTypes.shape({
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
}),

/** Static dimensions for the popover.
* Note: When passed, it will skip dimensions measuring of the popover, and provided dimensions will be used to calculate the anchor position.
*/
popoverDimensions: PropTypes.shape({
height: PropTypes.number,
width: PropTypes.number,
}),
};

const defaultProps = {
...defaultPopoverProps,

// Default positioning of the popover
anchorAlignment: {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
popoverDimensions: {
height: 0,
width: 0,
},
withoutOverlay: false,
anchorPosition: AnchorPosition;
};

/**
Expand All @@ -60,73 +21,85 @@ const defaultProps = {
* anchor position.
*/

function PopoverWithMeasuredContent(props) {
function PopoverWithMeasuredContent({
popoverDimensions = {
height: 0,
width: 0,
},
anchorPosition,
isVisible,
anchorAlignment = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
children,
withoutOverlay = false,
...props
}: PopoverWithMeasuredContentProps) {
const styles = useThemeStyles();
const {windowWidth, windowHeight} = useWindowDimensions();
const [popoverWidth, setPopoverWidth] = useState(props.popoverDimensions.width);
const [popoverHeight, setPopoverHeight] = useState(props.popoverDimensions.height);
const [popoverWidth, setPopoverWidth] = useState(popoverDimensions.width);
const [popoverHeight, setPopoverHeight] = useState(popoverDimensions.height);
const [isContentMeasured, setIsContentMeasured] = useState(popoverWidth > 0 && popoverHeight > 0);
const [isVisible, setIsVisible] = useState(false);
const [isPopoverVisible, setIsPopoverVisible] = useState(false);

/**
* When Popover becomes visible, we need to recalculate the Dimensions.
* Skip render on Popover until recalculations have done by setting isContentMeasured false as early as possible.
* Skip render on Popover until recalculations are done by setting isContentMeasured to false as early as possible.
*/
if (!isVisible && props.isVisible) {
if (!isPopoverVisible && isVisible) {
// When Popover is shown recalculate
setIsContentMeasured(props.popoverDimensions.width > 0 && props.popoverDimensions.height > 0);
setIsVisible(true);
} else if (isVisible && !props.isVisible) {
setIsVisible(false);
setIsContentMeasured(popoverDimensions.width > 0 && popoverDimensions.height > 0);
setIsPopoverVisible(true);
} else if (isPopoverVisible && !isVisible) {
setIsPopoverVisible(false);
}

/**
* Measure the size of the popover's content.
*
* @param {Object} nativeEvent
*/
const measurePopover = ({nativeEvent}) => {
const measurePopover = ({nativeEvent}: LayoutChangeEvent) => {
setPopoverWidth(nativeEvent.layout.width);
setPopoverHeight(nativeEvent.layout.height);
setIsContentMeasured(true);
};

const adjustedAnchorPosition = useMemo(() => {
let horizontalConstraint;
switch (props.anchorAlignment.horizontal) {
switch (anchorAlignment.horizontal) {
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT:
horizontalConstraint = {left: props.anchorPosition.horizontal - popoverWidth};
horizontalConstraint = {left: anchorPosition.horizontal - popoverWidth};
break;
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER:
horizontalConstraint = {
left: Math.floor(props.anchorPosition.horizontal - popoverWidth / 2),
left: Math.floor(anchorPosition.horizontal - popoverWidth / 2),
};
break;
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT:
default:
horizontalConstraint = {left: props.anchorPosition.horizontal};
horizontalConstraint = {left: anchorPosition.horizontal};
}

let verticalConstraint;
switch (props.anchorAlignment.vertical) {
switch (anchorAlignment.vertical) {
case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM:
verticalConstraint = {top: props.anchorPosition.vertical - popoverHeight};
verticalConstraint = {top: anchorPosition.vertical - popoverHeight};
break;
case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER:
verticalConstraint = {
top: Math.floor(props.anchorPosition.vertical - popoverHeight / 2),
top: Math.floor(anchorPosition.vertical - popoverHeight / 2),
};
break;
case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP:
default:
verticalConstraint = {top: props.anchorPosition.vertical};
verticalConstraint = {top: anchorPosition.vertical};
}

return {
...horizontalConstraint,
...verticalConstraint,
};
}, [props.anchorPosition, props.anchorAlignment, popoverWidth, popoverHeight]);
}, [anchorPosition, anchorAlignment, popoverWidth, popoverHeight]);

const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth);
const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight);
Expand All @@ -136,11 +109,15 @@ function PopoverWithMeasuredContent(props) {
};
return isContentMeasured ? (
<Popover
popoverDimensions={popoverDimensions}
anchorAlignment={anchorAlignment}
isVisible={isVisible}
withoutOverlay={withoutOverlay}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
anchorPosition={shiftedAnchorPosition}
>
<View onLayout={measurePopover}>{props.children}</View>
<View onLayout={measurePopover}>{children}</View>
</Popover>
) : (
/*
Expand All @@ -153,18 +130,15 @@ function PopoverWithMeasuredContent(props) {
style={styles.invisiblePopover}
onLayout={measurePopover}
>
{props.children}
{children}
</View>
);
}

PopoverWithMeasuredContent.propTypes = propTypes;
PopoverWithMeasuredContent.defaultProps = defaultProps;
PopoverWithMeasuredContent.displayName = 'PopoverWithMeasuredContent';

export default React.memo(PopoverWithMeasuredContent, (prevProps, nextProps) => {
if (prevProps.isVisible === nextProps.isVisible && nextProps.isVisible === false) {
return true;
}
return _.isEqual(prevProps, nextProps);
return isEqual(prevProps, nextProps);
});
6 changes: 1 addition & 5 deletions src/libs/calculateAnchorPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@
import {View} from 'react-native';
import {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';

type AnchorOrigin = {
horizontal: ValueOf<typeof CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL>;
vertical: ValueOf<typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL>;
shiftVertical?: number;
};

type AnchorPosition = {
horizontal: number;
vertical: number;
};

/**
* Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4133,4 +4133,4 @@ const defaultStyles = styles(defaultTheme);

export default styles;
export {defaultStyles};
export type {Styles, ThemeStyles, StatusBarStyle, ColorScheme};
export type {Styles, ThemeStyles, StatusBarStyle, ColorScheme, AnchorPosition};

0 comments on commit 76d6f8e

Please sign in to comment.