diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index db150d55f0d2..9faabc403c75 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -49,14 +49,14 @@ type UnresponsiveProps = { type IconProps = { /** Flag to choose between avatar image or an icon */ - iconType: typeof CONST.ICON_TYPE_ICON; + iconType?: typeof CONST.ICON_TYPE_ICON; /** Icon to display on the left side of component */ icon: IconAsset; }; type AvatarProps = { - iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; icon: AvatarSource; }; @@ -85,7 +85,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & titleStyle?: ViewStyle; /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + hoverAndPressStyle?: StyleProp>; /** Additional styles to style the description text below the title */ descriptionTextStyle?: StyleProp; @@ -175,7 +175,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & isSelected?: boolean; /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + shouldStackHorizontally?: boolean; /** Prop to represent the size of the avatar images to be shown */ avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; @@ -220,10 +220,10 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & furtherDetails?: string; /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + onSecondaryInteraction?: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + titleWithTooltips?: DisplayNameWithTooltip[]; /** Icon should be displayed in its own color */ displayInDefaultIconColor?: boolean; diff --git a/src/components/Popover/popoverPropTypes.js b/src/components/Popover/popoverPropTypes.js index c13fd8fa0b85..c758c4e6d311 100644 --- a/src/components/Popover/popoverPropTypes.js +++ b/src/components/Popover/popoverPropTypes.js @@ -26,6 +26,9 @@ const propTypes = { /** The ref of the popover */ withoutOverlayRef: refPropTypes, + + /** Whether we want to show the popover on the right side of the screen */ + fromSidebarMediumScreen: PropTypes.bool, }; const defaultProps = { diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 7f7e2829770c..7890ce5555f0 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,7 +1,15 @@ +import type {ValueOf} from 'type-fest'; import BaseModalProps, {PopoverAnchorPosition} from '@components/Modal/types'; import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; +import CONST from '@src/CONST'; -type AnchorAlignment = {horizontal: string; vertical: string}; +type AnchorAlignment = { + /** The horizontal anchor alignment of the popover */ + horizontal: ValueOf; + + /** The vertical anchor alignment of the popover */ + vertical: ValueOf; +}; type PopoverDimensions = { width: number; @@ -39,4 +47,4 @@ type PopoverProps = BaseModalProps & { type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; -export type {PopoverProps, PopoverWithWindowDimensionsProps}; +export type {PopoverProps, PopoverWithWindowDimensionsProps, AnchorAlignment}; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx new file mode 100644 index 000000000000..2c85b80534ca --- /dev/null +++ b/src/components/PopoverMenu.tsx @@ -0,0 +1,176 @@ +import type {ImageContentFit} from 'expo-image'; +import React, {RefObject, useRef} from 'react'; +import {View} from 'react-native'; +import type {ModalProps} from 'react-native-modal'; +import type {SvgProps} from 'react-native-svg'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import type {AnchorPosition} from '@src/styles'; +import MenuItem from './MenuItem'; +import type {AnchorAlignment} from './Popover/types'; +import PopoverWithMeasuredContent from './PopoverWithMeasuredContent'; +import Text from './Text'; + +type PopoverMenuItem = { + /** An icon element displayed on the left side */ + icon: React.FC; + + /** Text label */ + text: string; + + /** A callback triggered when this item is selected */ + onSelected: () => void; + + /** A description text to show under the title */ + description?: string; + + /** The fill color to pass into the icon. */ + iconFill?: string; + + /** Icon Width */ + iconWidth?: number; + + /** Icon Height */ + iconHeight?: number; + + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; + +type PopoverModalProps = Pick; + +type PopoverMenuProps = PopoverModalProps & { + /** Callback method fired when the user requests to close the modal */ + onClose: () => void; + + /** State that determines whether to display the modal or not */ + isVisible: boolean; + + /** Callback to fire when a CreateMenu item is selected */ + onItemSelected: (selectedItem: PopoverMenuItem, index: number) => void; + + /** Menu items to be rendered on the list */ + menuItems: PopoverMenuItem[]; + + /** Optional non-interactive text to display as a header for any create menu */ + headerText?: string; + + /** Whether disable the animations */ + disableAnimation?: boolean; + + /** The horizontal and vertical anchors points for the popover */ + anchorPosition: AnchorPosition; + + /** Ref of the anchor */ + anchorRef: RefObject; + + /** Where the popover should be positioned relative to the anchor points. */ + anchorAlignment?: AnchorAlignment; + + /** Whether we don't want to show overlay */ + withoutOverlay?: boolean; + + /** Should we announce the Modal visibility changes? */ + shouldSetModalVisibility?: boolean; + + /** Whether we want to show the popover on the right side of the screen */ + fromSidebarMediumScreen?: boolean; +}; + +function PopoverMenu({ + menuItems, + onItemSelected, + isVisible, + anchorPosition, + anchorRef, + onClose, + headerText, + fromSidebarMediumScreen, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, + animationIn = 'fadeIn', + animationOut = 'fadeOut', + animationInTiming = CONST.ANIMATED_TRANSITION, + disableAnimation = true, + withoutOverlay = false, + shouldSetModalVisibility = true, +}: PopoverMenuProps) { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + const selectedItemIndex = useRef(null); + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItems.length - 1, isActive: isVisible}); + + const selectItem = (index: number) => { + const selectedItem = menuItems[index]; + onItemSelected(selectedItem, index); + selectedItemIndex.current = index; + }; + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.ENTER, + () => { + if (focusedIndex === -1) { + return; + } + selectItem(focusedIndex); + setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu + }, + {isActive: isVisible}, + ); + + return ( + { + setFocusedIndex(-1); + if (selectedItemIndex.current !== null) { + menuItems[selectedItemIndex.current].onSelected(); + selectedItemIndex.current = null; + } + }} + animationIn={animationIn} + animationOut={animationOut} + animationInTiming={animationInTiming} + disableAnimation={disableAnimation} + fromSidebarMediumScreen={fromSidebarMediumScreen} + withoutOverlay={withoutOverlay} + shouldSetModalVisibility={shouldSetModalVisibility} + > + + {!!headerText && {headerText}} + {menuItems.map((item, menuIndex) => ( + selectItem(menuIndex)} + focused={focusedIndex === menuIndex} + displayInDefaultIconColor={item.displayInDefaultIconColor} + /> + ))} + + + ); +} + +PopoverMenu.displayName = 'PopoverMenu'; + +export default React.memo(PopoverMenu); diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js deleted file mode 100644 index 597105173b4c..000000000000 --- a/src/components/PopoverMenu/index.js +++ /dev/null @@ -1,114 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useRef} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import MenuItem from '@components/MenuItem'; -import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import refPropTypes from '@components/refPropTypes'; -import Text from '@components/Text'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; -import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import CONST from '@src/CONST'; -import {defaultProps as createMenuDefaultProps, propTypes as createMenuPropTypes} from './popoverMenuPropTypes'; - -const propTypes = { - ...createMenuPropTypes, - ...windowDimensionsPropTypes, - - /** Ref of the anchor */ - anchorRef: refPropTypes, - - withoutOverlay: PropTypes.bool, - - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility: PropTypes.bool, -}; - -const defaultProps = { - ...createMenuDefaultProps, - anchorAlignment: { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - }, - anchorRef: () => {}, - withoutOverlay: false, - shouldSetModalVisibility: true, -}; - -function PopoverMenu(props) { - const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); - const selectedItemIndex = useRef(null); - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1, isActive: props.isVisible}); - - const selectItem = (index) => { - const selectedItem = props.menuItems[index]; - props.onItemSelected(selectedItem, index); - selectedItemIndex.current = index; - }; - - useKeyboardShortcut( - CONST.KEYBOARD_SHORTCUTS.ENTER, - () => { - if (focusedIndex === -1) { - return; - } - selectItem(focusedIndex); - setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu - }, - {isActive: props.isVisible}, - ); - - return ( - { - setFocusedIndex(-1); - if (selectedItemIndex.current !== null) { - props.menuItems[selectedItemIndex.current].onSelected(); - selectedItemIndex.current = null; - } - }} - animationIn={props.animationIn} - animationOut={props.animationOut} - animationInTiming={props.animationInTiming} - disableAnimation={props.disableAnimation} - fromSidebarMediumScreen={props.fromSidebarMediumScreen} - withoutOverlay={props.withoutOverlay} - shouldSetModalVisibility={props.shouldSetModalVisibility} - > - - {!_.isEmpty(props.headerText) && {props.headerText}} - {_.map(props.menuItems, (item, menuIndex) => ( - selectItem(menuIndex)} - focused={focusedIndex === menuIndex} - displayInDefaultIconColor={item.displayInDefaultIconColor} - /> - ))} - - - ); -} - -PopoverMenu.propTypes = propTypes; -PopoverMenu.defaultProps = defaultProps; -PopoverMenu.displayName = 'PopoverMenu'; - -export default React.memo(withWindowDimensions(PopoverMenu)); diff --git a/src/components/PopoverMenu/popoverMenuPropTypes.js b/src/components/PopoverMenu/popoverMenuPropTypes.js deleted file mode 100644 index 53eeb63b05e7..000000000000 --- a/src/components/PopoverMenu/popoverMenuPropTypes.js +++ /dev/null @@ -1,71 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Callback method fired when the user requests to close the modal */ - onClose: PropTypes.func.isRequired, - - /** State that determines whether to display the modal or not */ - isVisible: PropTypes.bool.isRequired, - - /** Callback to fire when a CreateMenu item is selected */ - onItemSelected: PropTypes.func.isRequired, - - /** Menu items to be rendered on the list */ - menuItems: PropTypes.arrayOf( - PropTypes.shape({ - /** An icon element displayed on the left side */ - icon: sourcePropTypes, - - /** Text label */ - text: PropTypes.string.isRequired, - - /** A callback triggered when this item is selected */ - onSelected: PropTypes.func.isRequired, - }), - ).isRequired, - - /** The anchor position of the CreateMenu popover */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }).isRequired, - - /** Where the popover should be positioned relative to the anchor points. */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), - - /** The anchor reference of the CreateMenu popover */ - anchorRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - - /** A react-native-animatable animation definition for the modal display animation. */ - animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** A react-native-animatable animation definition for the modal hide animation. */ - animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** A react-native-animatable animation timing for the modal display animation. */ - animationInTiming: PropTypes.number, - - /** Optional non-interactive text to display as a header for any create menu */ - headerText: PropTypes.string, - - /** Whether disable the animations */ - disableAnimation: PropTypes.bool, -}; - -const defaultProps = { - animationIn: 'fadeIn', - animationOut: 'fadeOut', - animationInTiming: CONST.ANIMATED_TRANSITION, - headerText: undefined, - disableAnimation: true, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx index 9d10f7869f8a..206a33181605 100644 --- a/src/components/PopoverWithMeasuredContent.tsx +++ b/src/components/PopoverWithMeasuredContent.tsx @@ -8,8 +8,9 @@ import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import Popover from './Popover'; import {PopoverProps} from './Popover/types'; +import type {WindowDimensionsProps} from './withWindowDimensions/types'; -type PopoverWithMeasuredContentProps = Omit & { +type PopoverWithMeasuredContentProps = Omit & { /** The horizontal and vertical anchors points for the popover */ anchorPosition: AnchorPosition; };