Skip to content
45 changes: 26 additions & 19 deletions src/components/DelegatorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';

import React from 'react';
import {View} from 'react-native';

import UserAvatar from './Avatar/UserAvatar';
import MenuItem from './MenuItem';
import Text from './Text';

Expand All @@ -27,25 +29,30 @@ function DelegatorList({delegators, message}: DelegatorListProps) {
return (
<>
<Text style={[styles.mh5, styles.mb4]}>{message}</Text>
{delegators?.map((delegatorEmail) => {
const delegatorDetails = personalDetailsByLogin[delegatorEmail.toLowerCase()];
const formattedLogin = formatPhoneNumber(delegatorDetails?.login ?? '');
const displayLogin = formattedLogin || delegatorEmail;

return (
<MenuItem
key={delegatorEmail}
title={delegatorDetails?.displayName ?? displayLogin}
description={displayLogin}
avatarID={delegatorDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID}
icon={delegatorDetails?.avatar ?? icons.FallbackAvatar}
iconType={CONST.ICON_TYPE_AVATAR}
numberOfLinesDescription={1}
containerStyle={[styles.pr2, styles.mt1]}
interactive={false}
/>
);
})}
<View style={[styles.mt1, styles.gap1]}>
{delegators?.map((delegatorEmail) => {
const delegatorDetails = personalDetailsByLogin[delegatorEmail.toLowerCase()];
const formattedLogin = formatPhoneNumber(delegatorDetails?.login ?? '');
const displayLogin = formattedLogin || delegatorEmail;

return (
<MenuItem.Root key={delegatorEmail}>
<MenuItem.Row>
<MenuItem.Leading>
<UserAvatar
source={delegatorDetails?.avatar ?? icons.FallbackAvatar}
accountID={delegatorDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID}
/>
</MenuItem.Leading>
<MenuItem.Content>
<MenuItem.Title>{delegatorDetails?.displayName ?? displayLogin}</MenuItem.Title>
<MenuItem.Description numberOfLines={1}>{displayLogin}</MenuItem.Description>
</MenuItem.Content>
</MenuItem.Row>
</MenuItem.Root>
);
})}
</View>
</>
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/components/MenuItem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
*
* <MenuItem.Root onPress={onNavigate}>
* <MenuItem.Row>
* <MenuItem.Icon src={icons.Gear} />
* <MenuItem.Leading>
* <MenuItem.Icon src={icons.Gear} />
* </MenuItem.Leading>
* <MenuItem.Content>
* <MenuItem.Title>{translate('common.settings')}</MenuItem.Title>
* </MenuItem.Content>
Expand All @@ -25,21 +27,25 @@
* ```
*/
import MenuItemContent from './layout/MenuItemContent';
import MenuItemLeading from './layout/MenuItemLeading';
import MenuItemRoot from './layout/MenuItemRoot';
import MenuItemRow from './layout/MenuItemRow';
import MenuItemTrailing from './layout/MenuItemTrailing';
import MenuItemIcon from './leaves/leading/MenuItemIcon';
import MenuItemDescription from './leaves/text/MenuItemDescription';
import MenuItemLabel from './leaves/text/MenuItemLabel';
import MenuItemTitle from './leaves/text/MenuItemTitle';
import MenuItemChevron from './leaves/trailing/MenuItemChevron';
import LegacyMenuItem from './MenuItem';

const MenuItem = Object.assign(LegacyMenuItem, {
Root: MenuItemRoot,
Row: MenuItemRow,
Leading: MenuItemLeading,
Content: MenuItemContent,
Trailing: MenuItemTrailing,
Icon: MenuItemIcon,
Label: MenuItemLabel,
Title: MenuItemTitle,
Description: MenuItemDescription,
Chevron: MenuItemChevron,
Expand Down
20 changes: 20 additions & 0 deletions src/components/MenuItem/layout/MenuItemLeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import useThemeStyles from '@hooks/useThemeStyles';

import type {PropsWithChildren} from 'react';

import React from 'react';
import {View} from 'react-native';

type MenuItemLeadingProps = PropsWithChildren;

/**
* The leading cell of a `MenuItem.Row`. Sets no width of its own — it centers and sizes to its
* content, so an icon, avatar or spinner brings its own footprint.
*/
function MenuItemLeading({children}: MenuItemLeadingProps) {
const styles = useThemeStyles();

return <View style={styles.menuItemLeading}>{children}</View>;
}

export default MenuItemLeading;
7 changes: 5 additions & 2 deletions src/components/MenuItem/layout/MenuItemRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import variables from '@styles/variables';

import CONST from '@src/CONST';
import type WithSentryLabel from '@src/types/utils/SentryLabel';
import type WithTestID from '@src/types/utils/TestID';

import type {PropsWithChildren} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
Expand All @@ -22,7 +23,8 @@ import React, {useRef} from 'react';
import {View} from 'react-native';

type MenuItemRootProps = PropsWithChildren &
WithSentryLabel & {
WithSentryLabel &
WithTestID & {
/** Function to fire when the row is pressed */
onPress?: (event: GestureResponderEvent | KeyboardEvent) => void | Promise<void>;

Expand All @@ -37,7 +39,7 @@ type MenuItemRootProps = PropsWithChildren &
accessibilityLabel?: string;
};

function MenuItemRoot({children, onPress, isDisabled = false, sentryLabel, accessibilityLabel}: MenuItemRootProps) {
function MenuItemRoot({children, onPress, isDisabled = false, sentryLabel, testID, accessibilityLabel}: MenuItemRootProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const pressableRef = useRef<View>(null);
Expand Down Expand Up @@ -89,6 +91,7 @@ function MenuItemRoot({children, onPress, isDisabled = false, sentryLabel, acces
accessible
tabIndex={isInteractive ? 0 : -1}
sentryLabel={sentryLabel}
testID={testID}
>
{({pressed}) => (
<MenuItemAccessibilityContext.Provider value={accessibilityLabel === undefined ? accessibilityActions : undefined}>
Expand Down
20 changes: 9 additions & 11 deletions src/components/MenuItem/leaves/leading/MenuItemIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import getButtonState from '@libs/getButtonState';
import type IconAsset from '@src/types/utils/IconAsset';

import React from 'react';
import {View} from 'react-native';

type MenuItemIconProps = {
/** Icon to display */
src: IconAsset;
};

/** The leading icon cell of a `MenuItem.Row` */
/** An icon glyph, filled from the row's interaction state */
function MenuItemIcon({src}: MenuItemIconProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -31,15 +30,14 @@ function MenuItemIcon({src}: MenuItemIconProps) {
const iconFill = StyleUtils.getIconFillColor(getButtonState(isHovered, isPressed, isComplete, isDisabled, isInteractive), isMenuIcon, isPane);

return (
<View style={[styles.popoverMenuIcon, isCompactPopover && styles.wAuto]}>
<Icon
contentFit="cover"
hovered={isHovered}
pressed={isPressed}
src={src}
fill={iconFill}
/>
</View>
<Icon
contentFit="cover"
hovered={isHovered}
pressed={isPressed}
src={src}
fill={iconFill}
additionalStyles={[styles.popoverMenuIcon, isCompactPopover && styles.wAuto]}
/>
);
}

Expand Down
41 changes: 38 additions & 3 deletions src/components/MenuItem/leaves/text/MenuItemDescription.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,64 @@
import {useMenuItemAccessibilityLabel} from '@components/MenuItem/MenuItemAccessibilityContext';
import Text from '@components/Text';

import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';

import variables from '@styles/variables';

import type {StyleProp, TextStyle} from 'react-native';
import type {ValueOf} from 'type-fest';

import React from 'react';

const MENU_ITEM_DESCRIPTION_VARIANT = {
/** The small supporting-label look, for a description that sits under a title */
SUPPORTING: 'supporting',

/** Normal-size text, for a description standing in for a value the row does not have yet */
PLACEHOLDER: 'placeholder',
} as const;

type MenuItemDescriptionVariant = ValueOf<typeof MENU_ITEM_DESCRIPTION_VARIANT>;

type MenuItemDescriptionVariantStyles = Record<MenuItemDescriptionVariant, StyleProp<TextStyle>>;

type MenuItemDescriptionProps = {
/** Text to render as the description */
children: string | number;

/** Maximum number of lines to render before the text is truncated */
numberOfLines?: number;

/**
* Typography variant. `supporting` (default) is the small label look; `placeholder` bumps the font to
* the normal size — use it on rows with no title, where the description carries the row on its own.
*/
variant?: MenuItemDescriptionVariant;
};

/** The supporting text block of a `MenuItem.Content` */
function MenuItemDescription({children}: MenuItemDescriptionProps) {
function MenuItemDescription({children, numberOfLines = 2, variant = MENU_ITEM_DESCRIPTION_VARIANT.SUPPORTING}: MenuItemDescriptionProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();

useMenuItemAccessibilityLabel('description', String(children));

/** Typography applied on top of the shared supporting-label base, keyed by variant */
const variantStyles: MenuItemDescriptionVariantStyles = {
[MENU_ITEM_DESCRIPTION_VARIANT.SUPPORTING]: styles.textLineHeightNormal,
[MENU_ITEM_DESCRIPTION_VARIANT.PLACEHOLDER]: [StyleUtils.getFontSizeStyle(variables.fontSizeNormal), StyleUtils.getLineHeightStyle(variables.fontSizeNormalHeight)],
};

return (
<Text
style={[styles.textLabelSupporting, styles.textLineHeightNormal, styles.breakWord]}
numberOfLines={2}
style={[styles.textLabelSupporting, variantStyles[variant], styles.breakWord]}
numberOfLines={numberOfLines}
>
{children}
</Text>
);
}

export default MenuItemDescription;
export {MENU_ITEM_DESCRIPTION_VARIANT};
19 changes: 19 additions & 0 deletions src/components/MenuItem/leaves/text/MenuItemLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Text from '@components/Text';

import useThemeStyles from '@hooks/useThemeStyles';

import React from 'react';

type MenuItemLabelProps = {
/** Text to render as the label */
children: string;
};

/** The label above a menu item's `Row` */
function MenuItemLabel({children}: MenuItemLabelProps) {
const styles = useThemeStyles();

return <Text style={[styles.sidebarLinkText, styles.optionAlternateText, styles.textLabelSupporting, styles.pre]}>{children}</Text>;
}

export default MenuItemLabel;
24 changes: 18 additions & 6 deletions src/components/MenuItem/leaves/text/MenuItemTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@ import convertToLTR from '@libs/convertToLTR';

import CONST from '@src/CONST';

import type {ReactElement} from 'react';

import React from 'react';

type MenuItemTitleProps = {
/** Text to render as the title */
children: string | number;
};
type MenuItemTitleProps =
| {
/** Text to render as the title */
children: string | number;

accessibilityLabel?: never;
}
| {
/** Element to render in place of plain text, e.g. a `DisplayNames` with per-name tooltips */
children: ReactElement;

/** Plain-text form of the title, contributed to the row's accessibility label */
accessibilityLabel: string;
};

/** The title block of a `MenuItem.Content`. Bold, single line */
function MenuItemTitle({children}: MenuItemTitleProps) {
function MenuItemTitle({children, accessibilityLabel}: MenuItemTitleProps) {
const styles = useThemeStyles();
const {isDisabled, isInteractive} = useMenuItemConfig();

useMenuItemAccessibilityLabel('title', String(children));
useMenuItemAccessibilityLabel('title', accessibilityLabel ?? String(children));

return (
<Text
Expand Down
30 changes: 18 additions & 12 deletions src/components/MenuItem/presets/MenuItemAction.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import MenuItemContent from '@components/MenuItem/layout/MenuItemContent';
import MenuItemLeading from '@components/MenuItem/layout/MenuItemLeading';
import MenuItemRoot from '@components/MenuItem/layout/MenuItemRoot';
import MenuItemRow from '@components/MenuItem/layout/MenuItemRow';
import MenuItemIcon from '@components/MenuItem/leaves/leading/MenuItemIcon';
Expand All @@ -8,39 +9,44 @@ import {callFunctionIfActionIsAllowed} from '@userActions/Session';

import type IconAsset from '@src/types/utils/IconAsset';
import type WithSentryLabel from '@src/types/utils/SentryLabel';
import type WithTestID from '@src/types/utils/TestID';

import type {GestureResponderEvent} from 'react-native';

import React from 'react';

type MenuItemActionProps = WithSentryLabel & {
/** The title text of the row */
title: string;
type MenuItemActionProps = WithSentryLabel &
WithTestID & {
/** The title text of the row */
title: string;

/** Leading icon to display */
icon: IconAsset;
/** Leading icon to display */
icon: IconAsset;

/** Function to fire when the row is pressed */
onPress: (event: GestureResponderEvent | KeyboardEvent) => void | Promise<void>;
/** Function to fire when the row is pressed */
onPress: (event: GestureResponderEvent | KeyboardEvent) => void | Promise<void>;

/** Whether the menu item is disabled */
isDisabled?: boolean;
};
/** Whether the menu item is disabled */
isDisabled?: boolean;
};

/**
* The action MenuItem preset — a tappable row with a leading icon and a title that performs an
* action in place (delete, select, add, etc.)
*/
function MenuItemAction({title, icon, onPress, isDisabled = false, sentryLabel}: MenuItemActionProps) {
function MenuItemAction({title, icon, onPress, isDisabled = false, sentryLabel, testID}: MenuItemActionProps) {
return (
<MenuItemRoot
onPress={callFunctionIfActionIsAllowed(onPress)}
isDisabled={isDisabled}
sentryLabel={sentryLabel}
testID={testID}
accessibilityLabel={title}
>
<MenuItemRow>
<MenuItemIcon src={icon} />
<MenuItemLeading>
<MenuItemIcon src={icon} />
</MenuItemLeading>
<MenuItemContent>
<MenuItemTitle>{title}</MenuItemTitle>
</MenuItemContent>
Expand Down
Loading
Loading