Skip to content
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

[TS migration] Migrate 'PopoverWithMeasuredContent.js' component to TypeScript #32551

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -4054,4 +4054,4 @@ const defaultStyles = styles(defaultTheme);

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