Skip to content
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
12 changes: 6 additions & 6 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there any specific reason this is optional?

@blazejkustra blazejkustra Jan 3, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, because iconType should be optional, there is even a default value for it inside the component (it was a mistake when we migrated MenuItem)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, you can also see that iconType isn't passed as a prop to the MenuItem in this file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks for explaining


icon: AvatarSource;
};
Expand Down Expand Up @@ -85,7 +85,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) &
titleStyle?: ViewStyle;

/** Any adjustments to style when menu item is hovered or pressed */
hoverAndPressStyle: StyleProp<AnimatedStyle<ViewStyle>>;
hoverAndPressStyle?: StyleProp<AnimatedStyle<ViewStyle>>;

/** Additional styles to style the description text below the title */
descriptionTextStyle?: StyleProp<TextStyle>;
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/components/Popover/popoverPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
12 changes: 10 additions & 2 deletions src/components/Popover/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL>;

/** The vertical anchor alignment of the popover */
vertical: ValueOf<typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL>;
};

type PopoverDimensions = {
width: number;
Expand Down Expand Up @@ -39,4 +47,4 @@ type PopoverProps = BaseModalProps & {

type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps;

export type {PopoverProps, PopoverWithWindowDimensionsProps};
export type {PopoverProps, PopoverWithWindowDimensionsProps, AnchorAlignment};
176 changes: 176 additions & 0 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
@@ -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<SvgProps>;

/** 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<ModalProps, 'animationIn' | 'animationOut' | 'animationInTiming'>;

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<HTMLDivElement>;

/** 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<number | null>(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 (
<PopoverWithMeasuredContent
anchorPosition={anchorPosition}
anchorRef={anchorRef}
anchorAlignment={anchorAlignment}
onClose={onClose}
isVisible={isVisible}
onModalHide={() => {
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}
>
<View style={isSmallScreenWidth ? {} : styles.createMenuContainer}>
{!!headerText && <Text style={[styles.createMenuHeaderText, styles.ml3]}>{headerText}</Text>}
{menuItems.map((item, menuIndex) => (
<MenuItem
key={item.text}
icon={item.icon}
iconWidth={item.iconWidth}
iconHeight={item.iconHeight}
iconFill={item.iconFill}
contentFit={item.contentFit}
title={item.text}
shouldCheckActionAllowedOnPress={false}
description={item.description}
onPress={() => selectItem(menuIndex)}
focused={focusedIndex === menuIndex}
displayInDefaultIconColor={item.displayInDefaultIconColor}
/>
))}
</View>
</PopoverWithMeasuredContent>
);
}

PopoverMenu.displayName = 'PopoverMenu';

export default React.memo(PopoverMenu);
114 changes: 0 additions & 114 deletions src/components/PopoverMenu/index.js

This file was deleted.

Loading