Skip to content
Closed
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
32 changes: 32 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9216,6 +9216,38 @@ const CONST = {
NEWDOT: 83,
OAUTH: 86,
},

MENU_ITEM: {
BADGE_POSITION: {
RIGHT: 'right',
BELOW_TITLE: 'belowTitle',
SEPARATE_ROW: 'separateRow',
},
TITLE_FORMAT: {
MARKDOWN: 'markdown',
HTML: 'html',
},
TITLE_VARIANT: {
NORMAL: 'normal',
STRONG: 'strong',
},
VARIANT: {
DEFAULT: 'default',
SECTION: 'section',
},
DESCRIPTION_VARIANT: {
SUPPORTING: 'supporting',
PROMINENT: 'prominent',
},
ICON_VARIANT: {
DEFAULT: 'default',
COMPACT: 'compact',
},
RIGHT_LABEL_VARIANT: {
LABEL: 'label',
SUBTITLE: 'subtitle',
},
},
} as const;

/** Upgrade intro feature ids from UPGRADE_FEATURE_INTRO_MAPPING for Submit workspace */
Expand Down
1 change: 1 addition & 0 deletions src/components/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,5 @@ function Badge({
);
}

export type {BadgeProps};
export default Badge;
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
import ActivityIndicator from '@components/ActivityIndicator';
import Avatar from '@components/Avatar';
import Badge from '@components/Badge';
import {useIsCompactMenu} from '@components/CompactMenuContext';
import CopyTextToClipboard from '@components/CopyTextToClipboard';
import DisplayNames from '@components/DisplayNames';
import type {DisplayNameWithTooltip} from '@components/DisplayNames/types';
import FormHelpMessage from '@components/FormHelpMessage';
import Hoverable from '@components/Hoverable';
import type HoverableProps from '@components/Hoverable/types';
import Icon from '@components/Icon';
import InlineIcon from '@components/Icon/InlineIcon';
import {useMenuItemGroupActions, useMenuItemGroupState} from '@components/MenuItemGroup';
import PlaidCardFeedIcon from '@components/PlaidCardFeedIcon';
import type {PressableRef} from '@components/Pressable/GenericPressable/types';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
import RadioButton from '@components/RadioButton';
import RenderHTML from '@components/RenderHTML';
import ReportActionAvatars from '@components/ReportActionAvatars';
import Text from '@components/Text';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import getContextMenuAccessibilityHint from '@components/utils/getContextMenuAccessibilityHint';
import getContextMenuAccessibilityProps from '@components/utils/getContextMenuAccessibilityProps';

import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand Down Expand Up @@ -38,31 +62,6 @@ import type {ValueOf} from 'type-fest';
import React, {useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';

import type {DisplayNameWithTooltip} from './DisplayNames/types';
import type HoverableProps from './Hoverable/types';
import type {PressableRef} from './Pressable/GenericPressable/types';

import ActivityIndicator from './ActivityIndicator';
import Avatar from './Avatar';
import Badge from './Badge';
import {useIsCompactMenu} from './CompactMenuContext';
import CopyTextToClipboard from './CopyTextToClipboard';
import DisplayNames from './DisplayNames';
import FormHelpMessage from './FormHelpMessage';
import Hoverable from './Hoverable';
import Icon from './Icon';
import InlineIcon from './Icon/InlineIcon';
import {useMenuItemGroupActions, useMenuItemGroupState} from './MenuItemGroup';
import PlaidCardFeedIcon from './PlaidCardFeedIcon';
import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction';
import RadioButton from './RadioButton';
import RenderHTML from './RenderHTML';
import ReportActionAvatars from './ReportActionAvatars';
import Text from './Text';
import EducationalTooltip from './Tooltip/EducationalTooltip';
import getContextMenuAccessibilityHint from './utils/getContextMenuAccessibilityHint';
import getContextMenuAccessibilityProps from './utils/getContextMenuAccessibilityProps';

type IconProps = {
/** Flag to choose between avatar image or an icon */
iconType?: typeof CONST.ICON_TYPE_ICON;
Expand Down
42 changes: 42 additions & 0 deletions src/components/MenuItem/MenuItemContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {createContext, useContext} from 'react';

/**
* Interaction state of the menu item row, provided by `<MenuItem>` (the Root) and consumed by leaf
* sub-components (e.g. `MenuItem.Icon` derives its fill color, `MenuItem.CopyButton` reveals itself on hover).
*/
type MenuItemState = {
/** Whether the row is currently hovered */
isHovered: boolean;

/** Whether the row is currently pressed */
isPressed: boolean;

/** Whether the row is focused/active (the selected-row state, not screen focus) */
isFocused: boolean;

/** Whether the row is disabled */
isDisabled: boolean;

/** Whether the row responds to interactions */
isInteractive: boolean;

/** Whether the row uses success (green) styling */
isSuccess: boolean;

/** Whether the row is rendered inside a compact popover menu */
isCompact: boolean;
};

const MenuItemContext = createContext<MenuItemState | undefined>(undefined);

function useMenuItemState(): MenuItemState {
const state = useContext(MenuItemContext);
if (!state) {
throw new Error('MenuItem sub-components must be rendered inside <MenuItem>');
}
return state;
}

export default MenuItemContext;
export {useMenuItemState};
export type {MenuItemState};
117 changes: 117 additions & 0 deletions src/components/MenuItem/compound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* MenuItem (compound)
*
* A composable decomposition of the classic `MenuItem`, following the composition-over-configuration
* pattern: instead of ~135 props configuring one monolith, the consumer assembles the row from
* sub-components and interaction state (hover/press/focus/disabled) is shared through context.
*
* @example Simple navigation row
* ```tsx
* import MenuItem from '@components/MenuItem/compound';
*
* <MenuItem onPress={onNavigate} accessibilityLabel={translate('common.settings')}>
* <MenuItem.Row>
* <MenuItem.Icon src={icons.Gear} />
* <MenuItem.Content>
* <MenuItem.Title>{translate('common.settings')}</MenuItem.Title>
* </MenuItem.Content>
* <MenuItem.Trailing>
* <MenuItem.Chevron />
* </MenuItem.Trailing>
* </MenuItem.Row>
* </MenuItem>
* ```
*
* @example Field row with a description on top, an error below, and helper text outside the pressable
* ```tsx
* <MenuItem onPress={onEdit} accessibilityLabel={`${description}, ${title}`}>
* <MenuItem.Row>
* <MenuItem.Content>
* <MenuItem.Description>{description}</MenuItem.Description>
* <MenuItem.Title>{title}</MenuItem.Title>
* </MenuItem.Content>
* <MenuItem.Trailing>
* <MenuItem.BrickRoadIndicator status={CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR} />
* <MenuItem.Chevron />
* </MenuItem.Trailing>
* </MenuItem.Row>
* <MenuItem.Error>{errorText}</MenuItem.Error>
* </MenuItem>
* <MenuItem.HelperText>{helperText}</MenuItem.HelperText>
* ```
*
* See README.md in this directory for the full design notes and the legacy-prop → composition mapping.
*/
import MenuItemContent from './layout/MenuItemContent';
import MenuItemRoot from './layout/MenuItemRoot';
import MenuItemRow from './layout/MenuItemRow';
import MenuItemTrailing from './layout/MenuItemTrailing';
import MenuItemAvatar from './leaves/leading/MenuItemAvatar';
import MenuItemIcon from './leaves/leading/MenuItemIcon';
import MenuItemError from './leaves/messages/MenuItemError';
import MenuItemHelperText from './leaves/messages/MenuItemHelperText';
import MenuItemHint from './leaves/messages/MenuItemHint';
import MenuItemDescription from './leaves/text/MenuItemDescription';
import MenuItemLabel from './leaves/text/MenuItemLabel';
import MenuItemTitle from './leaves/text/MenuItemTitle';
import MenuItemBadge from './leaves/trailing/MenuItemBadge';
import MenuItemBrickRoadIndicator from './leaves/trailing/MenuItemBrickRoadIndicator';
import MenuItemChevron from './leaves/trailing/MenuItemChevron';
import MenuItemCopyButton from './leaves/trailing/MenuItemCopyButton';
import MenuItemRightLabel from './leaves/trailing/MenuItemRightLabel';
import {useMenuItemState} from './MenuItemContext';

const MenuItem = Object.assign(MenuItemRoot, {
/** The main horizontal line holding the leading, content and trailing cells */
Row: MenuItemRow,

/** The flexible middle cell — stacks Title/Description (in any order) vertically */
Content: MenuItemContent,

/** The right-side cluster for indicators and actions */
Trailing: MenuItemTrailing,

/** Leading icon whose fill follows the row's interaction state */
Icon: MenuItemIcon,

/** Leading user/workspace avatar */
Avatar: MenuItemAvatar,

/** Small supporting label rendered above the main line */
Label: MenuItemLabel,

/** The (bold) title text */
Title: MenuItemTitle,

/** The supporting description text — above or below the title depending on declaration order */
Description: MenuItemDescription,

/** Badge that follows the row's focused state */
Badge: MenuItemBadge,

/** Right arrow (or custom) navigation indicator, dimmed until hovered */
Chevron: MenuItemChevron,

/** Right-aligned supporting text (covers legacy `rightLabel` and `subtitle`) */
RightLabel: MenuItemRightLabel,

/** Red/green dot signalling the row needs attention */
BrickRoadIndicator: MenuItemBrickRoadIndicator,

/** Hover-revealed copy-to-clipboard button (devices with hover support) */
CopyButton: MenuItemCopyButton,

/** Error message rendered under the main line (inside the pressable) */
Error: MenuItemError,

/** Hint message rendered under the main line (inside the pressable) */
Hint: MenuItemHint,

/** Non-interactive helper text — place it AFTER the root, outside the pressable */
HelperText: MenuItemHelperText,
});

export default MenuItem;
export {useMenuItemState};
export type {MenuItemRootProps} from './layout/MenuItemRoot';
export type {MenuItemState} from './MenuItemContext';
36 changes: 36 additions & 0 deletions src/components/MenuItem/layout/MenuItemContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {useMenuItemState} from '@components/MenuItem/MenuItemContext';

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

import type {ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';

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

type MenuItemContentProps = {
/** The text blocks of the row (`MenuItem.Title`, `MenuItem.Description`, or any custom content),
* stacked vertically in the order they are declared */
children: ReactNode;

/** Any additional styles to apply to the content container */
style?: StyleProp<ViewStyle>;
};

/**
* The flexible middle cell of a `MenuItem.Row`. Stacks its children vertically and takes up all
* the horizontal space left over by the leading and trailing cells.
*/
function MenuItemContent({children, style}: MenuItemContentProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isCompact} = useMenuItemState();

return <View style={[styles.justifyContentCenter, styles.flex1, styles.gap1, StyleUtils.getMenuItemTextContainerStyle(isCompact), style]}>{children}</View>;
}

MenuItemContent.displayName = 'MenuItemContent';

export type {MenuItemContentProps};
export default MenuItemContent;
Loading
Loading