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

Pay Header improvements #18883

Merged
merged 12 commits into from
May 15, 2023
12 changes: 9 additions & 3 deletions src/components/AddPaymentMethodMenu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import * as Expensicons from './Icon/Expensicons';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import compose from '../libs/compose';
Expand All @@ -12,11 +13,16 @@ import PopoverMenu from './PopoverMenu';
import paypalMeDataPropTypes from './paypalMeDataPropTypes';

const propTypes = {
/** Should the component be visible? */
isVisible: PropTypes.bool.isRequired,

/** Callback to execute when the component closes. */
onClose: PropTypes.func.isRequired,

/** Anchor position for the AddPaymentMenu. */
anchorPosition: PropTypes.shape({
top: PropTypes.number,
left: PropTypes.number,
horizontal: PropTypes.number,
vertical: PropTypes.number,
}),

/** Account details for PayPal.Me */
Expand All @@ -43,7 +49,7 @@ const AddPaymentMethodMenu = (props) => (
isVisible={props.isVisible}
onClose={props.onClose}
anchorPosition={props.anchorPosition}
onItemSelected={() => props.onClose()}
onItemSelected={props.onClose}
menuItems={[
{
text: props.translate('common.bankAccount'),
Expand Down
19 changes: 11 additions & 8 deletions src/components/ButtonWithDropdownMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import PopoverMenu from './PopoverMenu';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import themeColors from '../styles/themes/default';
import CONST from '../CONST';

const propTypes = {
/** Text to display for the menu header */
Expand Down Expand Up @@ -46,20 +47,19 @@ const ButtonWithDropdownMenu = (props) => {
const [selectedItemIndex, setSelectedItemIndex] = useState(0);
const [isMenuVisible, setIsMenuVisible] = useState(false);
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null);
const [popoverDimensions, setPopoverDimensions] = useState({});
const {width, height} = useWindowDimensions();
const {width: windowWidth, height: windowHeight} = useWindowDimensions();
const caretButton = useRef(null);
useEffect(() => {
if (!caretButton.current || !popoverDimensions.nativeEvent) {
if (!caretButton.current) {
return;
}
caretButton.current.measureInWindow((x, y, w) => {
setPopoverAnchorPosition({
left: x - popoverDimensions.nativeEvent.layout.width,
top: y + w + 10,
horizontal: x + w,
vertical: y - 16,
});
});
}, [width, height, popoverDimensions.nativeEvent]);
}, [windowWidth, windowHeight]);

const selectedItem = props.options[selectedItemIndex];
return (
Expand Down Expand Up @@ -104,20 +104,23 @@ const ButtonWithDropdownMenu = (props) => {
pressOnEnter
/>
)}
{props.options.length > 1 && (
{props.options.length > 1 && !_.isEmpty(popoverAnchorPosition) && (
<PopoverMenu
isVisible={isMenuVisible}
onClose={() => setIsMenuVisible(false)}
onItemSelected={() => setIsMenuVisible(false)}
anchorPosition={popoverAnchorPosition}
anchorOrigin={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
}}
headerText={props.menuHeaderText}
menuItems={_.map(props.options, (item, index) => ({
...item,
onSelected: () => {
setSelectedItemIndex(index);
},
}))}
onLayout={setPopoverDimensions}
/>
)}
</View>
Expand Down
16 changes: 0 additions & 16 deletions src/components/EmojiPicker/EmojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class EmojiPicker extends React.Component {
this.measureEmojiPopoverAnchorPosition = this.measureEmojiPopoverAnchorPosition.bind(this);
this.measureEmojiPopoverAnchorPositionAndUpdateState = this.measureEmojiPopoverAnchorPositionAndUpdateState.bind(this);
this.focusEmojiSearchInput = this.focusEmojiSearchInput.bind(this);
this.measureContent = this.measureContent.bind(this);
this.onModalHide = () => {};
this.onEmojiSelected = () => {};

Expand Down Expand Up @@ -131,20 +130,6 @@ class EmojiPicker extends React.Component {
});
}

/**
* Used to calculate the EmojiPicker Dimensions
*
* @returns {JSX}
*/
measureContent() {
return (
<EmojiPickerMenu
onEmojiSelected={this.selectEmoji}
ref={(el) => (this.emojiSearchInput = el)}
/>
);
}

/**
* Focus the search input in the emoji picker.
*/
Expand Down Expand Up @@ -177,7 +162,6 @@ class EmojiPicker extends React.Component {
height: CONST.EMOJI_PICKER_SIZE.HEIGHT,
}}
anchorOrigin={this.state.emojiPopoverAnchorOrigin}
measureContent={this.measureContent}
>
<EmojiPickerMenu
onEmojiSelected={this.selectEmoji}
Expand Down
1 change: 1 addition & 0 deletions src/components/KYCWall/BaseKYCWall.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class KYCWall extends React.Component {
<AddPaymentMethodMenu
isVisible={this.state.shouldShowAddPaymentMenu}
onClose={() => this.setState({shouldShowAddPaymentMenu: false})}
// TODO: Fix this anchor position
anchorPosition={{
top: this.state.anchorPositionTop,
left: this.state.anchorPositionLeft,
Expand Down
194 changes: 80 additions & 114 deletions src/components/PopoverMenu/index.js
Original file line number Diff line number Diff line change
@@ -1,136 +1,102 @@
import _ from 'underscore';
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import Popover from '../Popover';
import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent';
import styles from '../../styles/styles';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import MenuItem from '../MenuItem';
import {propTypes as createMenuPropTypes, defaultProps} from './popoverMenuPropTypes';
import ArrowKeyFocusManager from '../ArrowKeyFocusManager';
import {propTypes as createMenuPropTypes, defaultProps as createMenuDefaultProps} from './popoverMenuPropTypes';
import Text from '../Text';
import KeyboardShortcut from '../../libs/KeyboardShortcut';
import CONST from '../../CONST';
import useArrowKeyFocusManager from '../../hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '../../hooks/useKeyboardShortcut';
import useWindowDimensions from '../../hooks/useWindowDimensions';

// TODO: DRY props
const propTypes = {
...createMenuPropTypes,
...windowDimensionsPropTypes,
};

class PopoverMenu extends PureComponent {
constructor(props) {
super(props);
this.state = {
focusedIndex: -1,
};
this.resetFocusAndHideModal = this.resetFocusAndHideModal.bind(this);
this.removeKeyboardListener = this.removeKeyboardListener.bind(this);
this.attachKeyboardListener = this.attachKeyboardListener.bind(this);
this.selectedItem = null;
}

componentDidUpdate(prevProps) {
if (this.props.isVisible === prevProps.isVisible) {
return;
}

if (this.props.isVisible) {
this.attachKeyboardListener();
} else {
this.removeKeyboardListener();
}
}
/** The horizontal and vertical anchors points for the popover */
anchorPosition: PropTypes.shape({
horizontal: PropTypes.number.isRequired,
vertical: PropTypes.number.isRequired,
}).isRequired,

componentWillUnmount() {
this.removeKeyboardListener();
}
/** Where the popover should be positioned relative to the anchor points. */
anchorOrigin: PropTypes.shape({
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
}),
};

/**
* Set item to local variable to fire `onSelected` action after the menu popup closes
* @param {Object} item
*/
selectItem(item) {
this.selectedItem = item;
this.props.onItemSelected(item);
}
const defaultProps = {
...createMenuDefaultProps,
anchorOrigin: {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
};

attachKeyboardListener() {
const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ENTER;
this.unsubscribeEnterKey = KeyboardShortcut.subscribe(
shortcutConfig.shortcutKey,
() => {
if (this.state.focusedIndex === -1) {
return;
}
this.selectItem(this.props.menuItems[this.state.focusedIndex]);
this.setState({focusedIndex: -1}); // Reset the focusedIndex on selecting any menu
},
shortcutConfig.descriptionKey,
shortcutConfig.modifiers,
true,
);
}
const PopoverMenu = (props) => {
const {isSmallScreenWidth} = useWindowDimensions();

removeKeyboardListener() {
if (!this.unsubscribeEnterKey) {
return;
}
this.unsubscribeEnterKey();
}
const selectItem = (index) => {
const selectedItem = props.menuItems[index];
props.onItemSelected(selectedItem);
};

resetFocusAndHideModal() {
this.setState({focusedIndex: -1}); // Reset the focusedIndex on modal hide
this.removeKeyboardListener();
if (this.selectedItem) {
this.selectedItem.onSelected();
this.selectedItem = null;
}
}
const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1});
useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.ENTER,
() => {
if (focusedIndex === -1) {
return;
}
selectItem(focusedIndex);
setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu
},
{isActive: props.isVisible},
);
Comment on lines +51 to +61
Copy link
Contributor

@aldo-expensify aldo-expensify May 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the changes in the code that cause this event listener to constantly resubscribe (more details), make it climb to the top in the list of event handlers with the same priority. Since this event doesn't allow bubbling, it prevents any other handler. This causes this bug:

  1. Open the panel to invite new members to a workspace (/workspace/<workspace id>/invite)
  2. Verify that arrows up and down work and they change the selected row
  3. Select something with the mouse
  4. Click Next
  5. Go back
  6. BUG: The arrow keys don't work anymore

Listeners before re-subscribe:
image

Listeners after re-subscribe:

image

Context: I'm coming from debugging this other bug related to competing listeners: #18419


render() {
return (
<Popover
anchorPosition={this.props.anchorPosition}
onClose={this.props.onClose}
isVisible={this.props.isVisible}
onModalHide={this.resetFocusAndHideModal}
animationIn={this.props.animationIn}
animationOut={this.props.animationOut}
animationInTiming={this.props.animationInTiming}
disableAnimation={this.props.disableAnimation}
fromSidebarMediumScreen={this.props.fromSidebarMediumScreen}
onLayout={this.props.onLayout}
>
<View style={this.props.isSmallScreenWidth ? {} : styles.createMenuContainer}>
{!_.isEmpty(this.props.headerText) && (
<View style={styles.createMenuItem}>
<Text style={[styles.createMenuHeaderText, styles.ml3]}>{this.props.headerText}</Text>
</View>
)}
<ArrowKeyFocusManager
focusedIndex={this.state.focusedIndex}
maxIndex={this.props.menuItems.length - 1}
onFocusedIndexChanged={(index) => this.setState({focusedIndex: index})}
>
{_.map(this.props.menuItems, (item, menuIndex) => (
<MenuItem
key={item.text}
icon={item.icon}
iconWidth={item.iconWidth}
iconHeight={item.iconHeight}
title={item.text}
description={item.description}
onPress={() => this.selectItem(item)}
focused={this.state.focusedIndex === menuIndex}
/>
))}
</ArrowKeyFocusManager>
</View>
</Popover>
);
}
}
return (
<PopoverWithMeasuredContent
anchorPosition={props.anchorPosition}
anchorOrigin={props.anchorOrigin}
onClose={props.onClose}
isVisible={props.isVisible}
onModalHide={() => setFocusedIndex(-1)}
animationIn={props.animationIn}
animationOut={props.animationOut}
animationInTiming={props.animationInTiming}
// TODO: rename to `shouldDisableAnimation`
disableAnimation={props.disableAnimation}
// TODO: fuck this prop
fromSidebarMediumScreen={props.fromSidebarMediumScreen}
>
<View style={isSmallScreenWidth ? {} : styles.createMenuContainer}>
{!_.isEmpty(props.headerText) && <Text style={[styles.createMenuHeaderText, styles.ml3]}>{props.headerText}</Text>}
{_.map(props.menuItems, (item, menuIndex) => (
<MenuItem
key={item.text}
icon={item.icon}
iconWidth={item.iconWidth}
iconHeight={item.iconHeight}
title={item.text}
description={item.description}
onPress={() => selectItem(item)}
// TODO: rename to isFocused
focused={focusedIndex === menuIndex}
/>
))}
</View>
</PopoverWithMeasuredContent>
);
};

PopoverMenu.propTypes = propTypes;
PopoverMenu.defaultProps = defaultProps;
PopoverMenu.displayName = 'PopoverMenu';

export default withWindowDimensions(PopoverMenu);
export default React.memo(withWindowDimensions(PopoverMenu));
Loading
Loading