From 22350b8ae19b0926d75de63a3e77c1ca4f22bf1c Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Tue, 7 Apr 2026 19:08:27 -0700 Subject: [PATCH 1/5] Added TitleStandard to DSRN --- .../.storybook/storybook.requires.js | 1 + .../design-system-react-native/MIGRATION.md | 79 +++++++ .../src/components/TitleStandard/README.md | 208 +++++++++++++++++ .../TitleStandard/TitleStandard.stories.tsx | 121 ++++++++++ .../TitleStandard/TitleStandard.test.tsx | 214 ++++++++++++++++++ .../TitleStandard/TitleStandard.tsx | 81 +++++++ .../TitleStandard/TitleStandard.types.ts | 27 +++ .../src/components/TitleStandard/index.ts | 3 + .../src/components/index.ts | 6 + packages/design-system-shared/src/index.ts | 3 + .../TitleStandard/TitleStandard.types.ts | 31 +++ .../src/types/TitleStandard/index.ts | 1 + 12 files changed, 775 insertions(+) create mode 100644 packages/design-system-react-native/src/components/TitleStandard/README.md create mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx create mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx create mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx create mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts create mode 100644 packages/design-system-react-native/src/components/TitleStandard/index.ts create mode 100644 packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts create mode 100644 packages/design-system-shared/src/types/TitleStandard/index.ts diff --git a/apps/storybook-react-native/.storybook/storybook.requires.js b/apps/storybook-react-native/.storybook/storybook.requires.js index 68c86a9c1..bd57088d7 100644 --- a/apps/storybook-react-native/.storybook/storybook.requires.js +++ b/apps/storybook-react-native/.storybook/storybook.requires.js @@ -112,6 +112,7 @@ const getStories = () => { "./../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx"), "./../../packages/design-system-react-native/src/components/TextField/TextField.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextField/TextField.stories.tsx"), "./../../packages/design-system-react-native/src/components/TextFieldSearch/TextFieldSearch.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextFieldSearch/TextFieldSearch.stories.tsx"), + "./../../packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx": require("../../../packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx"), "./../../packages/design-system-react-native/src/components/Toast/Toast.stories.tsx": require("../../../packages/design-system-react-native/src/components/Toast/Toast.stories.tsx"), "./stories/Backgrounds.stories.tsx": require("../stories/Backgrounds.stories.tsx"), "./stories/WalletHome.stories.tsx": require("../stories/WalletHome.stories.tsx"), diff --git a/packages/design-system-react-native/MIGRATION.md b/packages/design-system-react-native/MIGRATION.md index 0a8fa81c0..d2c9a154c 100644 --- a/packages/design-system-react-native/MIGRATION.md +++ b/packages/design-system-react-native/MIGRATION.md @@ -16,6 +16,7 @@ This guide provides detailed instructions for migrating your project from one ve - [Icon Component](#icon-component) - [Checkbox Component](#checkbox-component) - [Version Updates](#version-updates) + - [From version 0.14.0 to 0.15.0](#from-version-0140-to-0150) - [From version 0.13.0 to 0.14.0](#from-version-0130-to-0140) - [From version 0.12.0 to 0.13.0](#from-version-0120-to-0130) - [From version 0.11.0 to 0.12.0](#from-version-0110-to-0120) @@ -24,6 +25,84 @@ This guide provides detailed instructions for migrating your project from one ve ## Version Updates +### From version 0.14.0 to 0.15.0 + +#### TitleStandard API + +If you adopted `TitleStandard` from a prerelease or internal branch that used `topLabel` or an optional `title`, apply the following when upgrading to **0.15.0+** (the first stable release that includes this component with the API below). + +**What changed:** + +- `title` is **required**. Omitting it is a type error; if you only need a trailing inline control, pass an empty string or another minimal `ReactNode` and use `titleAccessory`. +- **`topLabel`** and **`topLabelProps`** are removed. Use **`topAccessory`** with a [`Text`](./src/components/Text/README.md) node (or any `ReactNode`) for content above the title row. +- **`title`** and **`bottomLabel`** are typed as **`ReactNode`** (strings still receive default typography via [`BoxHorizontal`](./src/components/BoxHorizontal/README.md) / `TextOrChildren`). +- The bottom row shows **`bottomLabel` or `bottomAccessory`**, not both: if `bottomLabel` is renderable, it is the only bottom row; otherwise a renderable `bottomAccessory` is rendered on its own. + +**Migration:** + +Before (preview / earlier API): + +```tsx + + + + +} /> +``` + +After (0.15.0+): + +```tsx +import { + TitleStandard, + Text, + TextVariant, + TextColor, + FontWeight, + Icon, + IconName, + IconSize, +} from '@metamask/design-system-react-native'; + + + Send + + } + title="$4.42" + bottomLabel="0.002 ETH" +/> + + + Send + + } + title="$4.42" +/> + +} +/> +``` + +**Impact:** + +- Affects any screen using `TitleStandard` with `topLabel` / `topLabelProps` or without `title`. +- Shared types: import `TitleStandardPropsShared` from `@metamask/design-system-shared` if you extend the layout contract across platforms. + ### From version 0.13.0 to 0.14.0 #### BottomSheet navigation callback change diff --git a/packages/design-system-react-native/src/components/TitleStandard/README.md b/packages/design-system-react-native/src/components/TitleStandard/README.md new file mode 100644 index 000000000..98a08b26c --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleStandard/README.md @@ -0,0 +1,208 @@ +# TitleStandard + +TitleStandard is used to display a required primary title with optional rows above and below the title, an optional inline accessory next to the title, and optional bottom label or custom bottom content. + +```tsx +import { TitleStandard } from '@metamask/design-system-react-native'; + +; +``` + +Cross-platform layout props are defined as `TitleStandardPropsShared` in `@metamask/design-system-shared`. This package adds `twClassName`, React Native `View` props, and `titleProps` / `bottomLabelProps` for the platform `Text` component. + +## Props + +### `title` + +The primary title. The title row always renders. When `title` is a string, it is wrapped with heading typography (`TextVariant.HeadingLg` and `titleProps`); other `ReactNode` values render as provided. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ------- | +| `ReactNode` | Yes | — | + +```tsx + +``` + +### `titleAccessory` + +Optional node rendered to the right of the title (for example an info icon). + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleStandard, + Box, + Icon, + IconName, + IconSize, +} from '@metamask/design-system-react-native'; + + + + + } +/>; +``` + +### `topAccessory` + +Optional row above the title (for example secondary label text or a row with icons). + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleStandard, + Text, + TextVariant, + TextColor, + FontWeight, +} from '@metamask/design-system-react-native'; + + + Send + + } + title="$4.42" + bottomLabel="0.002 ETH" +/>; +``` + +### `bottomLabel` + +Optional bottom row with secondary label typography when the value is a string (`BodySm`, medium, `TextColor.TextAlternative`). If `bottomLabel` is renderable, `bottomAccessory` is not shown. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx + +``` + +### `bottomAccessory` + +Optional custom bottom row when `bottomLabel` is not renderable. Renders without default label typography; compose layout inside the node. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleStandard, + Box, + BoxFlexDirection, + BoxAlignItems, + Icon, + IconName, + IconSize, + Text, + TextVariant, +} from '@metamask/design-system-react-native'; + + + + ~$0.50 fee + + } +/>; +``` + +### `titleProps` + +Optional props merged into the heading `Text` when `title` is a string. Use for `testID` or typography overrides. + +| TYPE | REQUIRED | DEFAULT | +| -------------------- | -------- | ----------- | +| `Partial` | No | `undefined` | + +```tsx + +``` + +### `bottomLabelProps` + +Optional props merged into the bottom label `Text` when `bottomLabel` is a string. + +| TYPE | REQUIRED | DEFAULT | +| -------------------- | -------- | ----------- | +| `Partial` | No | `undefined` | + +```tsx + +``` + +### `twClassName` + +Use the `twClassName` prop to add Tailwind CSS classes to the component. These classes will be merged with the component's default classes using `tw.style()`, allowing you to: + +- Add new styles that don't exist in the default component +- Override the component's default styles when needed + +| TYPE | REQUIRED | DEFAULT | +| -------- | -------- | ----------- | +| `string` | No | `undefined` | + +```tsx +// Add additional styles + + +// Override default styles + +``` + +### `style` + +Use the `style` prop to customize the component's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values. Other `View` props (for example `testID` and accessibility fields) are also accepted on the root container. + +| TYPE | REQUIRED | DEFAULT | +| ---------------------- | -------- | ----------- | +| `StyleProp` | No | `undefined` | + +```tsx +import { useTailwind } from '@metamask/design-system-twrnc-preset'; + +import { TitleStandard } from '@metamask/design-system-react-native'; + +export const ConditionalExample = ({ isActive }: { isActive: boolean }) => { + const tw = useTailwind(); + + return ( + + ); +}; +``` + +## References + +[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940) diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx new file mode 100644 index 000000000..9c6da99d3 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx @@ -0,0 +1,121 @@ +import type { Meta, StoryObj } from '@storybook/react-native'; +import React from 'react'; +import { View } from 'react-native'; + +import { Box, BoxFlexDirection, BoxAlignItems } from '../Box'; +import { Icon, IconName, IconSize, IconColor } from '../Icon'; +import { Text, TextVariant, TextColor, FontWeight } from '../Text'; + +import { TitleStandard } from './TitleStandard'; +import type { TitleStandardProps } from './TitleStandard.types'; + +const meta: Meta = { + title: 'Components/TitleStandard', + component: TitleStandard, + argTypes: { + title: { + control: 'text', + }, + bottomLabel: { + control: 'text', + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Import a wallet', + bottomLabel: 'Enter your Secret Recovery Phrase', + }, +}; + +export const Title: Story = { + render: () => , +}; + +export const TitleAccessory: Story = { + render: () => ( + + + + } + /> + ), +}; + +export const TopAccessory: Story = { + render: () => ( + + + Step 2 of 3 + + } + title="Create your wallet password" + /> + + + Import from mobile + + } + title="Enter your Secret Recovery Phrase" + /> + + ), +}; + +export const BottomLabel: Story = { + args: { + title: 'Import a wallet', + bottomLabel: 'Enter your Secret Recovery Phrase', + }, +}; + +export const BottomAccessory: Story = { + render: () => ( + + + + MetaMask support will never ask for your phrase. + + + } + /> + ), +}; diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx new file mode 100644 index 000000000..8e456c89e --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx @@ -0,0 +1,214 @@ +// Third party dependencies. +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { Text } from 'react-native'; + +// Internal dependencies. +import { TitleStandard } from './TitleStandard'; + +const CONTAINER_TEST_ID = 'title-standard-container'; +const TITLE_TEST_ID = 'title-standard-title'; +const BOTTOM_LABEL_TEST_ID = 'title-standard-bottom-label'; + +describe('TitleStandard', () => { + let tw: ReturnType; + + beforeAll(() => { + tw = renderHook(() => useTailwind()).result.current; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('rendering', () => { + it('renders string title', () => { + const { getByText } = render(); + + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + + it('renders React node title', () => { + const { getByTestId } = render( + Custom title} + />, + ); + + expect(getByTestId('title-standard-title-node')).toBeOnTheScreen(); + }); + + it('renders container with testID when provided', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(CONTAINER_TEST_ID)).toBeOnTheScreen(); + }); + + it('forwards titleProps testID to title Text when title is a string', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TITLE_TEST_ID)).toBeOnTheScreen(); + }); + }); + + describe('when topAccessory is provided', () => { + it('renders topAccessory and title', () => { + const { getByText } = render( + Custom Top} />, + ); + + expect(getByText('Custom Top')).toBeOnTheScreen(); + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + }); + + describe('when topAccessory is false', () => { + it('does not render topAccessory node', () => { + const showTop = false; + const { getByText, queryByTestId } = render( + Top : false + } + />, + ); + + expect(getByText('$4.42')).toBeOnTheScreen(); + expect(queryByTestId('title-standard-top-slot')).not.toBeOnTheScreen(); + }); + }); + + describe('when bottomLabel is provided', () => { + it('renders bottomLabel text', () => { + const { getByText } = render( + , + ); + + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + }); + + it('forwards bottomLabelProps testID to bottom label Text', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(BOTTOM_LABEL_TEST_ID)).toBeOnTheScreen(); + }); + }); + + describe('when bottomAccessory is provided', () => { + it('renders bottomAccessory when bottomLabel is omitted', () => { + const { getByText } = render( + Custom Bottom} + />, + ); + + expect(getByText('Custom Bottom')).toBeOnTheScreen(); + }); + }); + + describe('when bottomLabel and bottomAccessory are both provided', () => { + it('renders only bottomLabel', () => { + const { getByText, queryByText } = render( + Accessory} + />, + ); + + expect(getByText('Label Priority')).toBeOnTheScreen(); + expect(queryByText('Accessory')).not.toBeOnTheScreen(); + }); + }); + + describe('when titleAccessory is provided', () => { + it('renders title and titleAccessory', () => { + const { getByText } = render( + Info} />, + ); + + expect(getByText('$4.42')).toBeOnTheScreen(); + expect(getByText('Info')).toBeOnTheScreen(); + }); + + it('renders titleAccessory when title is an empty string', () => { + const { getByText } = render( + Accessory only} />, + ); + + expect(getByText('Accessory only')).toBeOnTheScreen(); + }); + }); + + describe('when titleAccessory is false', () => { + it('renders title only', () => { + const { getByText } = render( + , + ); + + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + }); + + describe('when topAccessory, titleAccessory, and bottomLabel are provided', () => { + it('renders all slots', () => { + const { getByText } = render( + Send} + title="$4.42" + titleAccessory={i} + bottomLabel="0.002 ETH" + />, + ); + + expect(getByText('Send')).toBeOnTheScreen(); + expect(getByText('$4.42')).toBeOnTheScreen(); + expect(getByText('i')).toBeOnTheScreen(); + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + }); + }); + + describe('style and twClassName', () => { + it('applies custom style to root container', () => { + const customStyle = { opacity: 0.5 }; + const { getByTestId } = render( + , + ); + + expect(getByTestId(CONTAINER_TEST_ID)).toHaveStyle(customStyle); + }); + + it('merges twClassName with base styles', () => { + const { getByTestId } = render( + , + ); + + const container = getByTestId(CONTAINER_TEST_ID); + + expect(container).toHaveStyle(tw`gap-1`); + expect(container).toHaveStyle(tw`bg-default`); + }); + }); +}); diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx new file mode 100644 index 000000000..47ff5ca83 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx @@ -0,0 +1,81 @@ +// Third party dependencies. +import React from 'react'; + +// External dependencies. +import { isReactNodeRenderable } from '@metamask/design-system-shared'; + +// Internal dependencies. +import { Box } from '../Box'; +import { BoxHorizontal } from '../BoxHorizontal'; +import { TextVariant, TextColor, FontWeight } from '../Text'; + +import type { TitleStandardProps } from './TitleStandard.types'; + +/** + * TitleStandard is a component that displays a title with optional accessories + * in a left-aligned layout. + * + * @example + * ```tsx + * Send} + * title="$4.42" + * titleAccessory={} + * /> + * ``` + */ +export const TitleStandard: React.FC = ({ + title, + titleAccessory, + topAccessory, + bottomAccessory, + bottomLabel, + titleProps, + bottomLabelProps, + twClassName = '', + ...props +}) => { + const titleEndAccessoryNode = isReactNodeRenderable(titleAccessory) + ? titleAccessory + : undefined; + + const titleRow = ( + + {title} + + ); + + const renderBottomLabel = isReactNodeRenderable(bottomLabel); + const renderBottomAccessory = + !renderBottomLabel && isReactNodeRenderable(bottomAccessory); + + const bottomLabelRow = ( + + {bottomLabel} + + ); + + return ( + + {isReactNodeRenderable(topAccessory) ? topAccessory : null} + {titleRow} + {renderBottomLabel ? bottomLabelRow : null} + {renderBottomAccessory ? bottomAccessory : null} + + ); +}; + +TitleStandard.displayName = 'TitleStandard'; diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts new file mode 100644 index 000000000..cf6f48839 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts @@ -0,0 +1,27 @@ +// Third party dependencies. +import type { ViewProps } from 'react-native'; + +// External dependencies. +import type { TitleStandardPropsShared } from '@metamask/design-system-shared'; + +// Internal dependencies. +import type { TextProps } from '../Text/Text.types'; + +/** + * TitleStandard component props (React Native). + * Extends {@link TitleStandardPropsShared} (requires `title`) with platform `Text` passthroughs, `twClassName`, and `View` props. + */ +export type TitleStandardProps = TitleStandardPropsShared & { + /** + * Optional props merged into {@link BoxHorizontal} `textProps` when `title` is a string. + */ + titleProps?: Partial; + /** + * Optional props merged into {@link BoxHorizontal} `textProps` when `bottomLabel` is a string. + */ + bottomLabelProps?: Partial; + /** + * Optional Tailwind class name to apply to the container. + */ + twClassName?: string; +} & Omit; diff --git a/packages/design-system-react-native/src/components/TitleStandard/index.ts b/packages/design-system-react-native/src/components/TitleStandard/index.ts new file mode 100644 index 000000000..422cfee30 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleStandard/index.ts @@ -0,0 +1,3 @@ +export type { TitleStandardPropsShared } from '@metamask/design-system-shared'; +export { TitleStandard } from './TitleStandard'; +export type { TitleStandardProps } from './TitleStandard.types'; diff --git a/packages/design-system-react-native/src/components/index.ts b/packages/design-system-react-native/src/components/index.ts index 64fcf61c0..0197e8c30 100644 --- a/packages/design-system-react-native/src/components/index.ts +++ b/packages/design-system-react-native/src/components/index.ts @@ -188,6 +188,12 @@ export type { TextFieldSearchProps } from './TextFieldSearch'; export { TextOrChildren } from './temp-components/TextOrChildren'; export type { TextOrChildrenProps } from './temp-components/TextOrChildren'; +export { TitleStandard } from './TitleStandard'; +export type { + TitleStandardProps, + TitleStandardPropsShared, +} from './TitleStandard'; + export { Toast, ToastVariant, diff --git a/packages/design-system-shared/src/index.ts b/packages/design-system-shared/src/index.ts index 5aaa91632..fde9e49d9 100644 --- a/packages/design-system-shared/src/index.ts +++ b/packages/design-system-shared/src/index.ts @@ -31,6 +31,9 @@ export { type BannerBasePropsShared } from './types/BannerBase'; // TextOrChildren types (ADR-0004) export { type TextOrChildrenPropsShared } from './types/TextOrChildren'; +// TitleStandard types (ADR-0004) +export { type TitleStandardPropsShared } from './types/TitleStandard'; + // BoxHorizontal types (ADR-0004) export { type BoxHorizontalPropsShared } from './types/BoxHorizontal'; diff --git a/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts b/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts new file mode 100644 index 000000000..2a8336bad --- /dev/null +++ b/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts @@ -0,0 +1,31 @@ +import type { ReactNode } from 'react'; + +/** + * TitleStandard component shared props (ADR-0004). + * Platform-independent properties; platform packages extend with `ViewProps` / `className`, + * `twClassName`, and platform `Text` prop passthroughs. + */ +export type TitleStandardPropsShared = { + /** + * Primary title content. When a string, platforms typically wrap with large heading styles via `textProps`. + */ + title: ReactNode; + /** + * Optional accessory rendered inline to the right of the title. + */ + titleAccessory?: ReactNode; + /** + * Optional accessory rendered in its own row above the title. + */ + topAccessory?: ReactNode; + /** + * Optional custom bottom row when `bottomLabel` is not renderable. + * Mutually exclusive with a renderable `bottomLabel`: only one bottom row is shown. + */ + bottomAccessory?: ReactNode; + /** + * Optional bottom row with secondary label styling when a string (via platform `textProps`). + * If renderable, it is shown instead of `bottomAccessory`. + */ + bottomLabel?: ReactNode; +}; diff --git a/packages/design-system-shared/src/types/TitleStandard/index.ts b/packages/design-system-shared/src/types/TitleStandard/index.ts new file mode 100644 index 000000000..25c27b58a --- /dev/null +++ b/packages/design-system-shared/src/types/TitleStandard/index.ts @@ -0,0 +1 @@ +export type { TitleStandardPropsShared } from './TitleStandard.types'; From ec63d5b7eaa14b38d9090458ba6f9509c4ec8cb9 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Tue, 7 Apr 2026 19:20:24 -0700 Subject: [PATCH 2/5] Fixed lint errors --- .../TitleStandard/TitleStandard.test.tsx | 2 +- .../TitleStandard/TitleStandard.tsx | 27 ++++++++++--------- .../TitleStandard/TitleStandard.types.ts | 4 +-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx index 8e456c89e..32aca9c88 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx @@ -1,8 +1,8 @@ // Third party dependencies. import { useTailwind } from '@metamask/design-system-twrnc-preset'; import { renderHook } from '@testing-library/react-hooks'; -import React from 'react'; import { render } from '@testing-library/react-native'; +import React from 'react'; import { Text } from 'react-native'; // Internal dependencies. diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx index 47ff5ca83..6e6c8ce61 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx @@ -1,8 +1,6 @@ // Third party dependencies. -import React from 'react'; - -// External dependencies. import { isReactNodeRenderable } from '@metamask/design-system-shared'; +import React from 'react'; // Internal dependencies. import { Box } from '../Box'; @@ -12,17 +10,20 @@ import { TextVariant, TextColor, FontWeight } from '../Text'; import type { TitleStandardProps } from './TitleStandard.types'; /** - * TitleStandard is a component that displays a title with optional accessories - * in a left-aligned layout. + * Displays a primary title with optional top, inline, and bottom rows in a left-aligned layout. + * Remaining `View` props are forwarded to the root `Box`. + * + * @param props - Component props + * @param props.title - Primary title content + * @param props.titleAccessory - Optional inline accessory to the right of the title + * @param props.topAccessory - Optional row above the title + * @param props.bottomAccessory - Optional custom bottom row when `bottomLabel` is not renderable + * @param props.bottomLabel - Optional secondary label below the title + * @param props.titleProps - Optional props merged into title `Text` when `title` is a string + * @param props.bottomLabelProps - Optional props merged into bottom label `Text` when `bottomLabel` is a string + * @param props.twClassName - Optional Tailwind classes on the root container * - * @example - * ```tsx - * Send} - * title="$4.42" - * titleAccessory={} - * /> - * ``` + * @returns The rendered TitleStandard layout. */ export const TitleStandard: React.FC = ({ title, diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts index cf6f48839..d33c55430 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts @@ -1,8 +1,6 @@ // Third party dependencies. -import type { ViewProps } from 'react-native'; - -// External dependencies. import type { TitleStandardPropsShared } from '@metamask/design-system-shared'; +import type { ViewProps } from 'react-native'; // Internal dependencies. import type { TextProps } from '../Text/Text.types'; From 04388658ac6645ad3bd240eb4ffeebabbf626fee Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Tue, 7 Apr 2026 23:28:02 -0700 Subject: [PATCH 3/5] Added TitleHub to DSRN --- .../.storybook/storybook.requires.js | 2 +- .../design-system-react-native/MIGRATION.md | 79 ----- .../src/components/TitleHub/README.md | 283 +++++++++++++++ .../components/TitleHub/TitleHub.stories.tsx | 146 ++++++++ .../src/components/TitleHub/TitleHub.test.tsx | 321 ++++++++++++++++++ .../src/components/TitleHub/TitleHub.tsx | 118 +++++++ .../TitleHub.types.ts} | 12 +- .../src/components/TitleHub/index.ts | 3 + .../src/components/TitleStandard/README.md | 208 ------------ .../TitleStandard/TitleStandard.stories.tsx | 121 ------- .../TitleStandard/TitleStandard.test.tsx | 214 ------------ .../TitleStandard/TitleStandard.tsx | 82 ----- .../src/components/TitleStandard/index.ts | 3 - .../src/components/index.ts | 7 +- packages/design-system-shared/src/index.ts | 4 +- .../src/types/TitleHub/TitleHub.types.ts | 41 +++ .../src/types/TitleHub/index.ts | 1 + .../TitleStandard/TitleStandard.types.ts | 31 -- .../src/types/TitleStandard/index.ts | 1 - 19 files changed, 926 insertions(+), 751 deletions(-) create mode 100644 packages/design-system-react-native/src/components/TitleHub/README.md create mode 100644 packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx create mode 100644 packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx create mode 100644 packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx rename packages/design-system-react-native/src/components/{TitleStandard/TitleStandard.types.ts => TitleHub/TitleHub.types.ts} (57%) create mode 100644 packages/design-system-react-native/src/components/TitleHub/index.ts delete mode 100644 packages/design-system-react-native/src/components/TitleStandard/README.md delete mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx delete mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx delete mode 100644 packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx delete mode 100644 packages/design-system-react-native/src/components/TitleStandard/index.ts create mode 100644 packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts create mode 100644 packages/design-system-shared/src/types/TitleHub/index.ts delete mode 100644 packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts delete mode 100644 packages/design-system-shared/src/types/TitleStandard/index.ts diff --git a/apps/storybook-react-native/.storybook/storybook.requires.js b/apps/storybook-react-native/.storybook/storybook.requires.js index bd57088d7..00da3a435 100644 --- a/apps/storybook-react-native/.storybook/storybook.requires.js +++ b/apps/storybook-react-native/.storybook/storybook.requires.js @@ -112,7 +112,7 @@ const getStories = () => { "./../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx"), "./../../packages/design-system-react-native/src/components/TextField/TextField.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextField/TextField.stories.tsx"), "./../../packages/design-system-react-native/src/components/TextFieldSearch/TextFieldSearch.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextFieldSearch/TextFieldSearch.stories.tsx"), - "./../../packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx": require("../../../packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx"), + "./../../packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx": require("../../../packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx"), "./../../packages/design-system-react-native/src/components/Toast/Toast.stories.tsx": require("../../../packages/design-system-react-native/src/components/Toast/Toast.stories.tsx"), "./stories/Backgrounds.stories.tsx": require("../stories/Backgrounds.stories.tsx"), "./stories/WalletHome.stories.tsx": require("../stories/WalletHome.stories.tsx"), diff --git a/packages/design-system-react-native/MIGRATION.md b/packages/design-system-react-native/MIGRATION.md index d2c9a154c..0a8fa81c0 100644 --- a/packages/design-system-react-native/MIGRATION.md +++ b/packages/design-system-react-native/MIGRATION.md @@ -16,7 +16,6 @@ This guide provides detailed instructions for migrating your project from one ve - [Icon Component](#icon-component) - [Checkbox Component](#checkbox-component) - [Version Updates](#version-updates) - - [From version 0.14.0 to 0.15.0](#from-version-0140-to-0150) - [From version 0.13.0 to 0.14.0](#from-version-0130-to-0140) - [From version 0.12.0 to 0.13.0](#from-version-0120-to-0130) - [From version 0.11.0 to 0.12.0](#from-version-0110-to-0120) @@ -25,84 +24,6 @@ This guide provides detailed instructions for migrating your project from one ve ## Version Updates -### From version 0.14.0 to 0.15.0 - -#### TitleStandard API - -If you adopted `TitleStandard` from a prerelease or internal branch that used `topLabel` or an optional `title`, apply the following when upgrading to **0.15.0+** (the first stable release that includes this component with the API below). - -**What changed:** - -- `title` is **required**. Omitting it is a type error; if you only need a trailing inline control, pass an empty string or another minimal `ReactNode` and use `titleAccessory`. -- **`topLabel`** and **`topLabelProps`** are removed. Use **`topAccessory`** with a [`Text`](./src/components/Text/README.md) node (or any `ReactNode`) for content above the title row. -- **`title`** and **`bottomLabel`** are typed as **`ReactNode`** (strings still receive default typography via [`BoxHorizontal`](./src/components/BoxHorizontal/README.md) / `TextOrChildren`). -- The bottom row shows **`bottomLabel` or `bottomAccessory`**, not both: if `bottomLabel` is renderable, it is the only bottom row; otherwise a renderable `bottomAccessory` is rendered on its own. - -**Migration:** - -Before (preview / earlier API): - -```tsx - - - - -} /> -``` - -After (0.15.0+): - -```tsx -import { - TitleStandard, - Text, - TextVariant, - TextColor, - FontWeight, - Icon, - IconName, - IconSize, -} from '@metamask/design-system-react-native'; - - - Send - - } - title="$4.42" - bottomLabel="0.002 ETH" -/> - - - Send - - } - title="$4.42" -/> - -} -/> -``` - -**Impact:** - -- Affects any screen using `TitleStandard` with `topLabel` / `topLabelProps` or without `title`. -- Shared types: import `TitleStandardPropsShared` from `@metamask/design-system-shared` if you extend the layout contract across platforms. - ### From version 0.13.0 to 0.14.0 #### BottomSheet navigation callback change diff --git a/packages/design-system-react-native/src/components/TitleHub/README.md b/packages/design-system-react-native/src/components/TitleHub/README.md new file mode 100644 index 000000000..4cd018e40 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleHub/README.md @@ -0,0 +1,283 @@ +# TitleHub + +TitleHub is used to display a **required** title row with an optional amount line below it, optional rows beneath that, optional inline accessories next to each row, and optional bottom label or custom bottom content. + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +## Props + +### `title` + +Title row (required). When `title` is a string, it uses `TextVariant.HeadingMd` and `TextColor.TextDefault` (merged with `titleProps`). For custom layout, pass a `ReactNode`. The row also renders when only `titleAccessory` is renderable (for example `title={false}` with an end accessory). + +Legacy **`TitleStandard`** **`topLabel`** maps to **`title`** on `TitleHub`. The old main-line value (large amount) maps to **`amount`**, not `title`. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ------- | +| `ReactNode` | Yes | — | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +### `titleAccessory` + +Optional node to the right of `title` in the title row (same pattern as `amountAccessory`). + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleHub, + Box, + Icon, + IconName, + IconSize, +} from '@metamask/design-system-react-native'; + + + + + } + amount="$4.42" +/>; +``` + +### `amount` + +Optional primary amount line below the title. The amount row renders when `amount` or `amountAccessory` is renderable. When `amount` is a string, it is wrapped with display typography (`TextVariant.DisplayLg` and `amountProps`); other `ReactNode` values render as provided. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +### `amountAccessory` + +Optional node rendered to the right of the amount (for example an info icon). + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleHub, + Box, + Icon, + IconName, + IconSize, +} from '@metamask/design-system-react-native'; + + + + + } +/>; +``` + +### `bottomLabel` + +Optional bottom label row with secondary typography when the value is a string (`BodySm`, medium, `TextColor.TextAlternative`). If `bottomLabel` or `bottomLabelAccessory` is renderable, that row is shown and `bottomAccessory` is not used. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +### `bottomLabelAccessory` + +Optional node to the right of `bottomLabel` in the bottom label row (same pattern as `amountAccessory`). + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleHub, + Box, + Icon, + IconName, + IconSize, +} from '@metamask/design-system-react-native'; + + + + + } +/>; +``` + +### `bottomAccessory` + +Optional custom bottom row when neither `bottomLabel` nor `bottomLabelAccessory` is renderable. Renders without default label typography; compose layout inside the node. + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleHub, + Box, + BoxFlexDirection, + BoxAlignItems, + Icon, + IconName, + IconSize, + Text, + TextVariant, +} from '@metamask/design-system-react-native'; + + + + ~$0.50 fee + + } +/>; +``` + +### `amountProps` + +Optional props merged into the amount `Text` when `amount` is a string. Use for `testID` or typography overrides. + +| TYPE | REQUIRED | DEFAULT | +| -------------------- | -------- | ----------- | +| `Partial` | No | `undefined` | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +### `titleProps` + +Optional props merged into the title row `Text` when `title` is a string. + +| TYPE | REQUIRED | DEFAULT | +| -------------------- | -------- | ----------- | +| `Partial` | No | `undefined` | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +### `bottomLabelProps` + +Optional props merged into the bottom label `Text` when `bottomLabel` is a string. + +| TYPE | REQUIRED | DEFAULT | +| -------------------- | -------- | ----------- | +| `Partial` | No | `undefined` | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +; +``` + +### `twClassName` + +Use the `twClassName` prop to add Tailwind CSS classes to the component. These classes will be merged with the component's default classes using `tw.style()`, allowing you to: + +- Add new styles that don't exist in the default component +- Override the component's default styles when needed + +| TYPE | REQUIRED | DEFAULT | +| -------- | -------- | ----------- | +| `string` | No | `undefined` | + +```tsx +import { TitleHub } from '@metamask/design-system-react-native'; + +// Add additional styles + + +// Override default styles + +``` + +### `style` + +Use the `style` prop to customize the component's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values. Other `View` props (for example `testID` and accessibility fields) are also accepted on the root container. + +| TYPE | REQUIRED | DEFAULT | +| ---------------------- | -------- | ----------- | +| `StyleProp` | No | `undefined` | + +```tsx +import { useTailwind } from '@metamask/design-system-twrnc-preset'; + +import { TitleHub } from '@metamask/design-system-react-native'; + +export const ConditionalExample = ({ isActive }: { isActive: boolean }) => { + const tw = useTailwind(); + + return ( + + ); +}; +``` + +## References + +[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940) diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx new file mode 100644 index 000000000..8e7ef46ad --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx @@ -0,0 +1,146 @@ +import type { Meta, StoryObj } from '@storybook/react-native'; +import React from 'react'; + +import { Box, BoxAlignItems, BoxFlexDirection } from '../Box'; +import { Icon, IconName, IconSize, IconColor } from '../Icon'; +import { Text, TextColor, FontWeight, TextVariant } from '../Text'; + +import { TitleHub } from './TitleHub'; +import type { TitleHubProps } from './TitleHub.types'; + +/** Pill badge: dot + label (e.g. network), for `titleAccessory`. */ +const TestnetBadge = () => ( + + + + Testnet + + +); + +const meta: Meta = { + title: 'Components/TitleHub', + component: TitleHub, + argTypes: { + title: { + control: 'text', + }, + amount: { + control: 'text', + }, + bottomLabel: { + control: 'text', + }, + twClassName: { control: 'text' }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Perps', + amount: '$336.21', + bottomLabel: '$336.21 available', + twClassName: '', + }, + render: (args) => } />, +}; + +export const Amount: Story = { + render: () => , +}; + +export const AmountAccessory: Story = { + render: () => ( + + + + } + /> + ), +}; + +export const Title: Story = { + render: () => ( + + + + ), +}; + +export const TitleAccessory: Story = { + render: () => } />, +}; + +export const BottomLabel: Story = { + args: { + title: 'Perps', + amount: '$336.21', + bottomLabel: '$336.21 available', + twClassName: '', + }, +}; + +export const BottomLabelAccessory: Story = { + render: () => ( + + + + } + /> + ), +}; + +export const BottomAccessory: Story = { + render: () => ( + + + + Perps use isolated margin. Liquidation can occur if collateral falls + below maintenance. + + + } + /> + ), +}; diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx new file mode 100644 index 000000000..3392d7ac2 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx @@ -0,0 +1,321 @@ +// Third party dependencies. +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import { renderHook } from '@testing-library/react-hooks'; +import { render } from '@testing-library/react-native'; +import React from 'react'; +import { Text } from 'react-native'; + +// Internal dependencies. +import { TitleHub } from './TitleHub'; + +const CONTAINER_TEST_ID = 'title-hub-container'; +const AMOUNT_TEST_ID = 'title-hub-amount'; +const TITLE_ROW_TEST_ID = 'title-hub-title'; +const BOTTOM_LABEL_TEST_ID = 'title-hub-bottom-label'; + +describe('TitleHub', () => { + let tw: ReturnType; + + beforeAll(() => { + tw = renderHook(() => useTailwind()).result.current; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('rendering', () => { + it('renders string title', () => { + const { getByText } = render(); + + expect(getByText('Section')).toBeOnTheScreen(); + }); + + it('renders string amount when provided', () => { + const { getByText } = render(); + + expect(getByText('Send')).toBeOnTheScreen(); + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + + it('renders React node amount', () => { + const { getByTestId } = render( + Custom amount} + />, + ); + + expect(getByTestId('title-hub-amount-node')).toBeOnTheScreen(); + }); + + it('renders container with testID when provided', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(CONTAINER_TEST_ID)).toBeOnTheScreen(); + }); + + it('forwards amountProps testID to amount Text when amount is a string', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(AMOUNT_TEST_ID)).toBeOnTheScreen(); + }); + }); + + describe('when title is provided', () => { + it('renders title and amount', () => { + const { getByText } = render( + Custom Top} amount="$4.42" />, + ); + + expect(getByText('Custom Top')).toBeOnTheScreen(); + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + + it('renders title and titleAccessory', () => { + const { getByText } = render( + Title extra} + />, + ); + + expect(getByText('Step 1')).toBeOnTheScreen(); + expect(getByText('Title extra')).toBeOnTheScreen(); + }); + + it('forwards titleProps testID to title row Text when title is a string', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TITLE_ROW_TEST_ID)).toBeOnTheScreen(); + }); + }); + + describe('when title is false', () => { + it('does not render title node', () => { + const showTitle = false; + const { getByText, queryByTestId } = render( + Top : false + } + amount="$4.42" + />, + ); + + expect(getByText('$4.42')).toBeOnTheScreen(); + expect(queryByTestId('title-hub-title-slot')).not.toBeOnTheScreen(); + }); + }); + + describe('when titleAccessory is false', () => { + it('renders title only', () => { + const { getByText } = render( + , + ); + + expect(getByText('Hi')).toBeOnTheScreen(); + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + }); + + describe('when amount is false', () => { + it('does not render amount node', () => { + const showAmount = false; + const { getByText, queryByTestId } = render( + $1 : false + } + />, + ); + + expect(getByText('Send')).toBeOnTheScreen(); + expect(queryByTestId('title-hub-amount-slot')).not.toBeOnTheScreen(); + }); + }); + + describe('when bottomLabel is provided', () => { + it('renders bottomLabel text', () => { + const { getByText } = render( + , + ); + + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + }); + + it('renders bottomLabel and bottomLabelAccessory', () => { + const { getByText } = render( + Fee info} + />, + ); + + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + expect(getByText('Fee info')).toBeOnTheScreen(); + }); + + it('forwards bottomLabelProps testID to bottom label Text', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(BOTTOM_LABEL_TEST_ID)).toBeOnTheScreen(); + }); + }); + + describe('when bottomAccessory is provided', () => { + it('renders bottomAccessory when bottomLabel is omitted', () => { + const { getByText } = render( + Custom Bottom} + />, + ); + + expect(getByText('Custom Bottom')).toBeOnTheScreen(); + }); + }); + + describe('when bottomLabel and bottomAccessory are both provided', () => { + it('renders only bottomLabel', () => { + const { getByText, queryByText } = render( + Accessory} + />, + ); + + expect(getByText('Label Priority')).toBeOnTheScreen(); + expect(queryByText('Accessory')).not.toBeOnTheScreen(); + }); + }); + + describe('when only bottomLabelAccessory is provided', () => { + it('renders bottom label row with accessory and not bottomAccessory', () => { + const { getByText, queryByText } = render( + Only accessory} + bottomAccessory={Full row} + />, + ); + + expect(getByText('Only accessory')).toBeOnTheScreen(); + expect(queryByText('Full row')).not.toBeOnTheScreen(); + }); + }); + + describe('when amountAccessory is provided', () => { + it('renders amount and amountAccessory', () => { + const { getByText } = render( + Info} + />, + ); + + expect(getByText('$4.42')).toBeOnTheScreen(); + expect(getByText('Info')).toBeOnTheScreen(); + }); + + it('renders amountAccessory when amount is an empty string', () => { + const { getByText } = render( + Accessory only} + />, + ); + + expect(getByText('Accessory only')).toBeOnTheScreen(); + }); + }); + + describe('when amountAccessory is false', () => { + it('renders amount only', () => { + const { getByText } = render( + , + ); + + expect(getByText('$4.42')).toBeOnTheScreen(); + }); + }); + + describe('when title, amountAccessory, and bottomLabel are provided', () => { + it('renders all slots', () => { + const { getByText } = render( + Send} + amount="$4.42" + amountAccessory={i} + bottomLabel="0.002 ETH" + />, + ); + + expect(getByText('Send')).toBeOnTheScreen(); + expect(getByText('$4.42')).toBeOnTheScreen(); + expect(getByText('i')).toBeOnTheScreen(); + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + }); + }); + + describe('style and twClassName', () => { + it('applies custom style to root container', () => { + const customStyle = { opacity: 0.5 }; + const { getByTestId } = render( + , + ); + + expect(getByTestId(CONTAINER_TEST_ID)).toHaveStyle(customStyle); + }); + + it('merges twClassName with base styles', () => { + const { getByTestId } = render( + , + ); + + const container = getByTestId(CONTAINER_TEST_ID); + + expect(container).toHaveStyle(tw`gap-1`); + expect(container).toHaveStyle(tw`bg-default`); + }); + }); +}); diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx new file mode 100644 index 000000000..9397f3229 --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx @@ -0,0 +1,118 @@ +// Third party dependencies. +import { isReactNodeRenderable } from '@metamask/design-system-shared'; +import React from 'react'; + +// Internal dependencies. +import { Box } from '../Box'; +import { BoxHorizontal } from '../BoxHorizontal'; +import { TextVariant, TextColor, FontWeight } from '../Text'; + +import type { TitleHubProps } from './TitleHub.types'; + +/** + * Displays a required title row with optional amount, inline accessories, and bottom rows in a left-aligned layout. + * Remaining `View` props are forwarded to the root `Box`. + * + * @param props - Component props + * @param props.title - Title row content (required) + * @param props.titleAccessory - Optional inline accessory to the right of `title` + * @param props.amount - Optional primary amount below the title + * @param props.amountAccessory - Optional inline accessory to the right of the amount + * @param props.bottomAccessory - Optional custom bottom row when the bottom label row is not shown + * @param props.bottomLabel - Optional secondary label below the amount row + * @param props.bottomLabelAccessory - Optional inline accessory to the right of `bottomLabel` + * @param props.titleProps - Optional props merged into title row `Text` when `title` is a string + * @param props.amountProps - Optional props merged into amount `Text` when `amount` is a string + * @param props.bottomLabelProps - Optional props merged into bottom label `Text` when `bottomLabel` is a string + * @param props.twClassName - Optional Tailwind classes on the root container + * + * @returns The rendered TitleHub layout. + */ +export const TitleHub: React.FC = ({ + amount, + amountAccessory, + title, + titleAccessory, + bottomAccessory, + bottomLabel, + bottomLabelAccessory, + amountProps, + titleProps, + bottomLabelProps, + twClassName = '', + ...props +}) => { + const amountEndAccessoryNode = isReactNodeRenderable(amountAccessory) + ? amountAccessory + : undefined; + + const titleEndAccessoryNode = isReactNodeRenderable(titleAccessory) + ? titleAccessory + : undefined; + + const bottomLabelEndAccessoryNode = isReactNodeRenderable( + bottomLabelAccessory, + ) + ? bottomLabelAccessory + : undefined; + + const renderTitleRow = + isReactNodeRenderable(title) || isReactNodeRenderable(titleAccessory); + const renderAmountRow = + isReactNodeRenderable(amount) || isReactNodeRenderable(amountAccessory); + const renderBottomLabelRow = + isReactNodeRenderable(bottomLabel) || + isReactNodeRenderable(bottomLabelAccessory); + const renderBottomAccessory = + !renderBottomLabelRow && isReactNodeRenderable(bottomAccessory); + + const titleRow = ( + + {title} + + ); + + const amountRow = ( + + {amount} + + ); + + const bottomLabelRow = ( + + {bottomLabel} + + ); + + return ( + + {renderTitleRow ? titleRow : null} + {renderAmountRow ? amountRow : null} + {renderBottomLabelRow ? bottomLabelRow : null} + {renderBottomAccessory ? bottomAccessory : null} + + ); +}; + +TitleHub.displayName = 'TitleHub'; diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts b/packages/design-system-react-native/src/components/TitleHub/TitleHub.types.ts similarity index 57% rename from packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts rename to packages/design-system-react-native/src/components/TitleHub/TitleHub.types.ts index d33c55430..d6c98126c 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.types.ts +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.types.ts @@ -1,15 +1,19 @@ // Third party dependencies. -import type { TitleStandardPropsShared } from '@metamask/design-system-shared'; +import type { TitleHubPropsShared } from '@metamask/design-system-shared'; import type { ViewProps } from 'react-native'; // Internal dependencies. import type { TextProps } from '../Text/Text.types'; /** - * TitleStandard component props (React Native). - * Extends {@link TitleStandardPropsShared} (requires `title`) with platform `Text` passthroughs, `twClassName`, and `View` props. + * TitleHub component props (React Native). + * Extends {@link TitleHubPropsShared} (requires `title`) with platform `Text` passthroughs, `twClassName`, and `View` props. */ -export type TitleStandardProps = TitleStandardPropsShared & { +export type TitleHubProps = TitleHubPropsShared & { + /** + * Optional props merged into {@link BoxHorizontal} `textProps` when `amount` is a string. + */ + amountProps?: Partial; /** * Optional props merged into {@link BoxHorizontal} `textProps` when `title` is a string. */ diff --git a/packages/design-system-react-native/src/components/TitleHub/index.ts b/packages/design-system-react-native/src/components/TitleHub/index.ts new file mode 100644 index 000000000..67231dc5a --- /dev/null +++ b/packages/design-system-react-native/src/components/TitleHub/index.ts @@ -0,0 +1,3 @@ +export type { TitleHubPropsShared } from '@metamask/design-system-shared'; +export { TitleHub } from './TitleHub'; +export type { TitleHubProps } from './TitleHub.types'; diff --git a/packages/design-system-react-native/src/components/TitleStandard/README.md b/packages/design-system-react-native/src/components/TitleStandard/README.md deleted file mode 100644 index 98a08b26c..000000000 --- a/packages/design-system-react-native/src/components/TitleStandard/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# TitleStandard - -TitleStandard is used to display a required primary title with optional rows above and below the title, an optional inline accessory next to the title, and optional bottom label or custom bottom content. - -```tsx -import { TitleStandard } from '@metamask/design-system-react-native'; - -; -``` - -Cross-platform layout props are defined as `TitleStandardPropsShared` in `@metamask/design-system-shared`. This package adds `twClassName`, React Native `View` props, and `titleProps` / `bottomLabelProps` for the platform `Text` component. - -## Props - -### `title` - -The primary title. The title row always renders. When `title` is a string, it is wrapped with heading typography (`TextVariant.HeadingLg` and `titleProps`); other `ReactNode` values render as provided. - -| TYPE | REQUIRED | DEFAULT | -| ----------- | -------- | ------- | -| `ReactNode` | Yes | — | - -```tsx - -``` - -### `titleAccessory` - -Optional node rendered to the right of the title (for example an info icon). - -| TYPE | REQUIRED | DEFAULT | -| ----------- | -------- | ----------- | -| `ReactNode` | No | `undefined` | - -```tsx -import { - TitleStandard, - Box, - Icon, - IconName, - IconSize, -} from '@metamask/design-system-react-native'; - - - - - } -/>; -``` - -### `topAccessory` - -Optional row above the title (for example secondary label text or a row with icons). - -| TYPE | REQUIRED | DEFAULT | -| ----------- | -------- | ----------- | -| `ReactNode` | No | `undefined` | - -```tsx -import { - TitleStandard, - Text, - TextVariant, - TextColor, - FontWeight, -} from '@metamask/design-system-react-native'; - - - Send - - } - title="$4.42" - bottomLabel="0.002 ETH" -/>; -``` - -### `bottomLabel` - -Optional bottom row with secondary label typography when the value is a string (`BodySm`, medium, `TextColor.TextAlternative`). If `bottomLabel` is renderable, `bottomAccessory` is not shown. - -| TYPE | REQUIRED | DEFAULT | -| ----------- | -------- | ----------- | -| `ReactNode` | No | `undefined` | - -```tsx - -``` - -### `bottomAccessory` - -Optional custom bottom row when `bottomLabel` is not renderable. Renders without default label typography; compose layout inside the node. - -| TYPE | REQUIRED | DEFAULT | -| ----------- | -------- | ----------- | -| `ReactNode` | No | `undefined` | - -```tsx -import { - TitleStandard, - Box, - BoxFlexDirection, - BoxAlignItems, - Icon, - IconName, - IconSize, - Text, - TextVariant, -} from '@metamask/design-system-react-native'; - - - - ~$0.50 fee - - } -/>; -``` - -### `titleProps` - -Optional props merged into the heading `Text` when `title` is a string. Use for `testID` or typography overrides. - -| TYPE | REQUIRED | DEFAULT | -| -------------------- | -------- | ----------- | -| `Partial` | No | `undefined` | - -```tsx - -``` - -### `bottomLabelProps` - -Optional props merged into the bottom label `Text` when `bottomLabel` is a string. - -| TYPE | REQUIRED | DEFAULT | -| -------------------- | -------- | ----------- | -| `Partial` | No | `undefined` | - -```tsx - -``` - -### `twClassName` - -Use the `twClassName` prop to add Tailwind CSS classes to the component. These classes will be merged with the component's default classes using `tw.style()`, allowing you to: - -- Add new styles that don't exist in the default component -- Override the component's default styles when needed - -| TYPE | REQUIRED | DEFAULT | -| -------- | -------- | ----------- | -| `string` | No | `undefined` | - -```tsx -// Add additional styles - - -// Override default styles - -``` - -### `style` - -Use the `style` prop to customize the component's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values. Other `View` props (for example `testID` and accessibility fields) are also accepted on the root container. - -| TYPE | REQUIRED | DEFAULT | -| ---------------------- | -------- | ----------- | -| `StyleProp` | No | `undefined` | - -```tsx -import { useTailwind } from '@metamask/design-system-twrnc-preset'; - -import { TitleStandard } from '@metamask/design-system-react-native'; - -export const ConditionalExample = ({ isActive }: { isActive: boolean }) => { - const tw = useTailwind(); - - return ( - - ); -}; -``` - -## References - -[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940) diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx deleted file mode 100644 index 9c6da99d3..000000000 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-native'; -import React from 'react'; -import { View } from 'react-native'; - -import { Box, BoxFlexDirection, BoxAlignItems } from '../Box'; -import { Icon, IconName, IconSize, IconColor } from '../Icon'; -import { Text, TextVariant, TextColor, FontWeight } from '../Text'; - -import { TitleStandard } from './TitleStandard'; -import type { TitleStandardProps } from './TitleStandard.types'; - -const meta: Meta = { - title: 'Components/TitleStandard', - component: TitleStandard, - argTypes: { - title: { - control: 'text', - }, - bottomLabel: { - control: 'text', - }, - }, - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - title: 'Import a wallet', - bottomLabel: 'Enter your Secret Recovery Phrase', - }, -}; - -export const Title: Story = { - render: () => , -}; - -export const TitleAccessory: Story = { - render: () => ( - - - - } - /> - ), -}; - -export const TopAccessory: Story = { - render: () => ( - - - Step 2 of 3 - - } - title="Create your wallet password" - /> - - - Import from mobile - - } - title="Enter your Secret Recovery Phrase" - /> - - ), -}; - -export const BottomLabel: Story = { - args: { - title: 'Import a wallet', - bottomLabel: 'Enter your Secret Recovery Phrase', - }, -}; - -export const BottomAccessory: Story = { - render: () => ( - - - - MetaMask support will never ask for your phrase. - - - } - /> - ), -}; diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx deleted file mode 100644 index 32aca9c88..000000000 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.test.tsx +++ /dev/null @@ -1,214 +0,0 @@ -// Third party dependencies. -import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import { renderHook } from '@testing-library/react-hooks'; -import { render } from '@testing-library/react-native'; -import React from 'react'; -import { Text } from 'react-native'; - -// Internal dependencies. -import { TitleStandard } from './TitleStandard'; - -const CONTAINER_TEST_ID = 'title-standard-container'; -const TITLE_TEST_ID = 'title-standard-title'; -const BOTTOM_LABEL_TEST_ID = 'title-standard-bottom-label'; - -describe('TitleStandard', () => { - let tw: ReturnType; - - beforeAll(() => { - tw = renderHook(() => useTailwind()).result.current; - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('rendering', () => { - it('renders string title', () => { - const { getByText } = render(); - - expect(getByText('$4.42')).toBeOnTheScreen(); - }); - - it('renders React node title', () => { - const { getByTestId } = render( - Custom title} - />, - ); - - expect(getByTestId('title-standard-title-node')).toBeOnTheScreen(); - }); - - it('renders container with testID when provided', () => { - const { getByTestId } = render( - , - ); - - expect(getByTestId(CONTAINER_TEST_ID)).toBeOnTheScreen(); - }); - - it('forwards titleProps testID to title Text when title is a string', () => { - const { getByTestId } = render( - , - ); - - expect(getByTestId(TITLE_TEST_ID)).toBeOnTheScreen(); - }); - }); - - describe('when topAccessory is provided', () => { - it('renders topAccessory and title', () => { - const { getByText } = render( - Custom Top} />, - ); - - expect(getByText('Custom Top')).toBeOnTheScreen(); - expect(getByText('$4.42')).toBeOnTheScreen(); - }); - }); - - describe('when topAccessory is false', () => { - it('does not render topAccessory node', () => { - const showTop = false; - const { getByText, queryByTestId } = render( - Top : false - } - />, - ); - - expect(getByText('$4.42')).toBeOnTheScreen(); - expect(queryByTestId('title-standard-top-slot')).not.toBeOnTheScreen(); - }); - }); - - describe('when bottomLabel is provided', () => { - it('renders bottomLabel text', () => { - const { getByText } = render( - , - ); - - expect(getByText('0.002 ETH')).toBeOnTheScreen(); - }); - - it('forwards bottomLabelProps testID to bottom label Text', () => { - const { getByTestId } = render( - , - ); - - expect(getByTestId(BOTTOM_LABEL_TEST_ID)).toBeOnTheScreen(); - }); - }); - - describe('when bottomAccessory is provided', () => { - it('renders bottomAccessory when bottomLabel is omitted', () => { - const { getByText } = render( - Custom Bottom} - />, - ); - - expect(getByText('Custom Bottom')).toBeOnTheScreen(); - }); - }); - - describe('when bottomLabel and bottomAccessory are both provided', () => { - it('renders only bottomLabel', () => { - const { getByText, queryByText } = render( - Accessory} - />, - ); - - expect(getByText('Label Priority')).toBeOnTheScreen(); - expect(queryByText('Accessory')).not.toBeOnTheScreen(); - }); - }); - - describe('when titleAccessory is provided', () => { - it('renders title and titleAccessory', () => { - const { getByText } = render( - Info} />, - ); - - expect(getByText('$4.42')).toBeOnTheScreen(); - expect(getByText('Info')).toBeOnTheScreen(); - }); - - it('renders titleAccessory when title is an empty string', () => { - const { getByText } = render( - Accessory only} />, - ); - - expect(getByText('Accessory only')).toBeOnTheScreen(); - }); - }); - - describe('when titleAccessory is false', () => { - it('renders title only', () => { - const { getByText } = render( - , - ); - - expect(getByText('$4.42')).toBeOnTheScreen(); - }); - }); - - describe('when topAccessory, titleAccessory, and bottomLabel are provided', () => { - it('renders all slots', () => { - const { getByText } = render( - Send} - title="$4.42" - titleAccessory={i} - bottomLabel="0.002 ETH" - />, - ); - - expect(getByText('Send')).toBeOnTheScreen(); - expect(getByText('$4.42')).toBeOnTheScreen(); - expect(getByText('i')).toBeOnTheScreen(); - expect(getByText('0.002 ETH')).toBeOnTheScreen(); - }); - }); - - describe('style and twClassName', () => { - it('applies custom style to root container', () => { - const customStyle = { opacity: 0.5 }; - const { getByTestId } = render( - , - ); - - expect(getByTestId(CONTAINER_TEST_ID)).toHaveStyle(customStyle); - }); - - it('merges twClassName with base styles', () => { - const { getByTestId } = render( - , - ); - - const container = getByTestId(CONTAINER_TEST_ID); - - expect(container).toHaveStyle(tw`gap-1`); - expect(container).toHaveStyle(tw`bg-default`); - }); - }); -}); diff --git a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx deleted file mode 100644 index 6e6c8ce61..000000000 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// Third party dependencies. -import { isReactNodeRenderable } from '@metamask/design-system-shared'; -import React from 'react'; - -// Internal dependencies. -import { Box } from '../Box'; -import { BoxHorizontal } from '../BoxHorizontal'; -import { TextVariant, TextColor, FontWeight } from '../Text'; - -import type { TitleStandardProps } from './TitleStandard.types'; - -/** - * Displays a primary title with optional top, inline, and bottom rows in a left-aligned layout. - * Remaining `View` props are forwarded to the root `Box`. - * - * @param props - Component props - * @param props.title - Primary title content - * @param props.titleAccessory - Optional inline accessory to the right of the title - * @param props.topAccessory - Optional row above the title - * @param props.bottomAccessory - Optional custom bottom row when `bottomLabel` is not renderable - * @param props.bottomLabel - Optional secondary label below the title - * @param props.titleProps - Optional props merged into title `Text` when `title` is a string - * @param props.bottomLabelProps - Optional props merged into bottom label `Text` when `bottomLabel` is a string - * @param props.twClassName - Optional Tailwind classes on the root container - * - * @returns The rendered TitleStandard layout. - */ -export const TitleStandard: React.FC = ({ - title, - titleAccessory, - topAccessory, - bottomAccessory, - bottomLabel, - titleProps, - bottomLabelProps, - twClassName = '', - ...props -}) => { - const titleEndAccessoryNode = isReactNodeRenderable(titleAccessory) - ? titleAccessory - : undefined; - - const titleRow = ( - - {title} - - ); - - const renderBottomLabel = isReactNodeRenderable(bottomLabel); - const renderBottomAccessory = - !renderBottomLabel && isReactNodeRenderable(bottomAccessory); - - const bottomLabelRow = ( - - {bottomLabel} - - ); - - return ( - - {isReactNodeRenderable(topAccessory) ? topAccessory : null} - {titleRow} - {renderBottomLabel ? bottomLabelRow : null} - {renderBottomAccessory ? bottomAccessory : null} - - ); -}; - -TitleStandard.displayName = 'TitleStandard'; diff --git a/packages/design-system-react-native/src/components/TitleStandard/index.ts b/packages/design-system-react-native/src/components/TitleStandard/index.ts deleted file mode 100644 index 422cfee30..000000000 --- a/packages/design-system-react-native/src/components/TitleStandard/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { TitleStandardPropsShared } from '@metamask/design-system-shared'; -export { TitleStandard } from './TitleStandard'; -export type { TitleStandardProps } from './TitleStandard.types'; diff --git a/packages/design-system-react-native/src/components/index.ts b/packages/design-system-react-native/src/components/index.ts index 0197e8c30..77ec54b84 100644 --- a/packages/design-system-react-native/src/components/index.ts +++ b/packages/design-system-react-native/src/components/index.ts @@ -188,11 +188,8 @@ export type { TextFieldSearchProps } from './TextFieldSearch'; export { TextOrChildren } from './temp-components/TextOrChildren'; export type { TextOrChildrenProps } from './temp-components/TextOrChildren'; -export { TitleStandard } from './TitleStandard'; -export type { - TitleStandardProps, - TitleStandardPropsShared, -} from './TitleStandard'; +export { TitleHub } from './TitleHub'; +export type { TitleHubProps, TitleHubPropsShared } from './TitleHub'; export { Toast, diff --git a/packages/design-system-shared/src/index.ts b/packages/design-system-shared/src/index.ts index fde9e49d9..071e582d2 100644 --- a/packages/design-system-shared/src/index.ts +++ b/packages/design-system-shared/src/index.ts @@ -31,8 +31,8 @@ export { type BannerBasePropsShared } from './types/BannerBase'; // TextOrChildren types (ADR-0004) export { type TextOrChildrenPropsShared } from './types/TextOrChildren'; -// TitleStandard types (ADR-0004) -export { type TitleStandardPropsShared } from './types/TitleStandard'; +// TitleHub types (ADR-0004) +export { type TitleHubPropsShared } from './types/TitleHub'; // BoxHorizontal types (ADR-0004) export { type BoxHorizontalPropsShared } from './types/BoxHorizontal'; diff --git a/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts b/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts new file mode 100644 index 000000000..7727d3cfe --- /dev/null +++ b/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts @@ -0,0 +1,41 @@ +import type { ReactNode } from 'react'; + +/** + * TitleHub component shared props (ADR-0004). + * Platform-independent properties; platform packages extend with `ViewProps` / `className`, + * `twClassName`, and platform `Text` prop passthroughs. + */ +export type TitleHubPropsShared = { + /** + * Optional primary amount line below the title (for example a fiat or token value). + * When a string, platforms typically wrap with large display styles via `textProps`. + * The amount row renders when `amount` or `amountAccessory` is renderable. + */ + amount?: ReactNode; + /** + * Optional accessory rendered inline to the right of the amount. + */ + amountAccessory?: ReactNode; + /** + * Title row above the optional amount (via platform `textProps` when a string). Required. + */ + title: ReactNode; + /** + * Optional accessory rendered inline to the right of `title` in the title row. + */ + titleAccessory?: ReactNode; + /** + * Optional custom bottom row when neither `bottomLabel` nor `bottomLabelAccessory` is renderable. + * Mutually exclusive with the bottom label row: only one bottom row is shown. + */ + bottomAccessory?: ReactNode; + /** + * Optional bottom row with secondary label styling when a string (via platform `textProps`). + * If `bottomLabel` or `bottomLabelAccessory` is renderable, that row is shown instead of `bottomAccessory`. + */ + bottomLabel?: ReactNode; + /** + * Optional accessory rendered inline to the right of `bottomLabel` in the bottom label row. + */ + bottomLabelAccessory?: ReactNode; +}; diff --git a/packages/design-system-shared/src/types/TitleHub/index.ts b/packages/design-system-shared/src/types/TitleHub/index.ts new file mode 100644 index 000000000..4eb38613e --- /dev/null +++ b/packages/design-system-shared/src/types/TitleHub/index.ts @@ -0,0 +1 @@ +export type { TitleHubPropsShared } from './TitleHub.types'; diff --git a/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts b/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts deleted file mode 100644 index 2a8336bad..000000000 --- a/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ReactNode } from 'react'; - -/** - * TitleStandard component shared props (ADR-0004). - * Platform-independent properties; platform packages extend with `ViewProps` / `className`, - * `twClassName`, and platform `Text` prop passthroughs. - */ -export type TitleStandardPropsShared = { - /** - * Primary title content. When a string, platforms typically wrap with large heading styles via `textProps`. - */ - title: ReactNode; - /** - * Optional accessory rendered inline to the right of the title. - */ - titleAccessory?: ReactNode; - /** - * Optional accessory rendered in its own row above the title. - */ - topAccessory?: ReactNode; - /** - * Optional custom bottom row when `bottomLabel` is not renderable. - * Mutually exclusive with a renderable `bottomLabel`: only one bottom row is shown. - */ - bottomAccessory?: ReactNode; - /** - * Optional bottom row with secondary label styling when a string (via platform `textProps`). - * If renderable, it is shown instead of `bottomAccessory`. - */ - bottomLabel?: ReactNode; -}; diff --git a/packages/design-system-shared/src/types/TitleStandard/index.ts b/packages/design-system-shared/src/types/TitleStandard/index.ts deleted file mode 100644 index 25c27b58a..000000000 --- a/packages/design-system-shared/src/types/TitleStandard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { TitleStandardPropsShared } from './TitleStandard.types'; From 7e64aebaa8ab98452cb8151fada88fa6e6768d88 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Wed, 8 Apr 2026 20:54:20 -0700 Subject: [PATCH 4/5] Updated accessories to endAccessories for TitleHub --- .../src/components/TitleHub/README.md | 24 ++++++------- .../components/TitleHub/TitleHub.stories.tsx | 10 +++--- .../src/components/TitleHub/TitleHub.test.tsx | 34 +++++++++---------- .../src/components/TitleHub/TitleHub.tsx | 30 ++++++++-------- .../src/types/TitleHub/TitleHub.types.ts | 12 +++---- 5 files changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/design-system-react-native/src/components/TitleHub/README.md b/packages/design-system-react-native/src/components/TitleHub/README.md index 4cd018e40..61ccd0504 100644 --- a/packages/design-system-react-native/src/components/TitleHub/README.md +++ b/packages/design-system-react-native/src/components/TitleHub/README.md @@ -12,7 +12,7 @@ import { TitleHub } from '@metamask/design-system-react-native'; ### `title` -Title row (required). When `title` is a string, it uses `TextVariant.HeadingMd` and `TextColor.TextDefault` (merged with `titleProps`). For custom layout, pass a `ReactNode`. The row also renders when only `titleAccessory` is renderable (for example `title={false}` with an end accessory). +Title row (required). When `title` is a string, it uses `TextVariant.HeadingMd` and `TextColor.TextDefault` (merged with `titleProps`). For custom layout, pass a `ReactNode`. The row also renders when only `titleEndAccessory` is renderable (for example `title={false}` with an end accessory). Legacy **`TitleStandard`** **`topLabel`** maps to **`title`** on `TitleHub`. The old main-line value (large amount) maps to **`amount`**, not `title`. @@ -26,9 +26,9 @@ import { TitleHub } from '@metamask/design-system-react-native'; ; ``` -### `titleAccessory` +### `titleEndAccessory` -Optional node to the right of `title` in the title row (same pattern as `amountAccessory`). +Optional node to the right of `title` in the title row (same pattern as `amountEndAccessory`). | TYPE | REQUIRED | DEFAULT | | ----------- | -------- | ----------- | @@ -45,7 +45,7 @@ import { @@ -56,7 +56,7 @@ import { ### `amount` -Optional primary amount line below the title. The amount row renders when `amount` or `amountAccessory` is renderable. When `amount` is a string, it is wrapped with display typography (`TextVariant.DisplayLg` and `amountProps`); other `ReactNode` values render as provided. +Optional primary amount line below the title. The amount row renders when `amount` or `amountEndAccessory` is renderable. When `amount` is a string, it is wrapped with display typography (`TextVariant.DisplayLg` and `amountProps`); other `ReactNode` values render as provided. | TYPE | REQUIRED | DEFAULT | | ----------- | -------- | ----------- | @@ -68,7 +68,7 @@ import { TitleHub } from '@metamask/design-system-react-native'; ; ``` -### `amountAccessory` +### `amountEndAccessory` Optional node rendered to the right of the amount (for example an info icon). @@ -88,7 +88,7 @@ import { @@ -98,7 +98,7 @@ import { ### `bottomLabel` -Optional bottom label row with secondary typography when the value is a string (`BodySm`, medium, `TextColor.TextAlternative`). If `bottomLabel` or `bottomLabelAccessory` is renderable, that row is shown and `bottomAccessory` is not used. +Optional bottom label row with secondary typography when the value is a string (`BodySm`, medium, `TextColor.TextAlternative`). If `bottomLabel` or `bottomLabelEndAccessory` is renderable, that row is shown and `bottomAccessory` is not used. | TYPE | REQUIRED | DEFAULT | | ----------- | -------- | ----------- | @@ -110,9 +110,9 @@ import { TitleHub } from '@metamask/design-system-react-native'; ; ``` -### `bottomLabelAccessory` +### `bottomLabelEndAccessory` -Optional node to the right of `bottomLabel` in the bottom label row (same pattern as `amountAccessory`). +Optional node to the right of `bottomLabel` in the bottom label row (same pattern as `amountEndAccessory`). | TYPE | REQUIRED | DEFAULT | | ----------- | -------- | ----------- | @@ -131,7 +131,7 @@ import { title="Send" amount="$4.42" bottomLabel="0.002 ETH" - bottomLabelAccessory={ + bottomLabelEndAccessory={ @@ -141,7 +141,7 @@ import { ### `bottomAccessory` -Optional custom bottom row when neither `bottomLabel` nor `bottomLabelAccessory` is renderable. Renders without default label typography; compose layout inside the node. +Optional custom bottom row when neither `bottomLabel` nor `bottomLabelEndAccessory` is renderable. Renders without default label typography; compose layout inside the node. | TYPE | REQUIRED | DEFAULT | | ----------- | -------- | ----------- | diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx index 8e7ef46ad..9dbb7a11d 100644 --- a/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx @@ -8,7 +8,7 @@ import { Text, TextColor, FontWeight, TextVariant } from '../Text'; import { TitleHub } from './TitleHub'; import type { TitleHubProps } from './TitleHub.types'; -/** Pill badge: dot + label (e.g. network), for `titleAccessory`. */ +/** Pill badge: dot + label (e.g. network), for `titleEndAccessory`. */ const TestnetBadge = () => ( } />, + render: (args) => } />, }; export const Amount: Story = { @@ -74,7 +74,7 @@ export const AmountAccessory: Story = { } />, + render: () => } />, }; export const BottomLabel: Story = { @@ -114,7 +114,7 @@ export const BottomLabelAccessory: Story = { title="Perps" amount="$336.21" bottomLabel="$336.21 available" - bottomLabelAccessory={ + bottomLabelEndAccessory={ diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx index 3392d7ac2..461633fd1 100644 --- a/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx @@ -80,12 +80,12 @@ describe('TitleHub', () => { expect(getByText('$4.42')).toBeOnTheScreen(); }); - it('renders title and titleAccessory', () => { + it('renders title and titleEndAccessory', () => { const { getByText } = render( Title extra} + titleEndAccessory={Title extra} />, ); @@ -123,10 +123,10 @@ describe('TitleHub', () => { }); }); - describe('when titleAccessory is false', () => { + describe('when titleEndAccessory is false', () => { it('renders title only', () => { const { getByText } = render( - , + , ); expect(getByText('Hi')).toBeOnTheScreen(); @@ -160,13 +160,13 @@ describe('TitleHub', () => { expect(getByText('0.002 ETH')).toBeOnTheScreen(); }); - it('renders bottomLabel and bottomLabelAccessory', () => { + it('renders bottomLabel and bottomLabelEndAccessory', () => { const { getByText } = render( Fee info} + bottomLabelEndAccessory={Fee info} />, ); @@ -218,13 +218,13 @@ describe('TitleHub', () => { }); }); - describe('when only bottomLabelAccessory is provided', () => { + describe('when only bottomLabelEndAccessory is provided', () => { it('renders bottom label row with accessory and not bottomAccessory', () => { const { getByText, queryByText } = render( Only accessory} + bottomLabelEndAccessory={Only accessory} bottomAccessory={Full row} />, ); @@ -234,13 +234,13 @@ describe('TitleHub', () => { }); }); - describe('when amountAccessory is provided', () => { - it('renders amount and amountAccessory', () => { + describe('when amountEndAccessory is provided', () => { + it('renders amount and amountEndAccessory', () => { const { getByText } = render( Info} + amountEndAccessory={Info} />, ); @@ -248,12 +248,12 @@ describe('TitleHub', () => { expect(getByText('Info')).toBeOnTheScreen(); }); - it('renders amountAccessory when amount is an empty string', () => { + it('renders amountEndAccessory when amount is an empty string', () => { const { getByText } = render( Accessory only} + amountEndAccessory={Accessory only} />, ); @@ -261,23 +261,23 @@ describe('TitleHub', () => { }); }); - describe('when amountAccessory is false', () => { + describe('when amountEndAccessory is false', () => { it('renders amount only', () => { const { getByText } = render( - , + , ); expect(getByText('$4.42')).toBeOnTheScreen(); }); }); - describe('when title, amountAccessory, and bottomLabel are provided', () => { + describe('when title, amountEndAccessory, and bottomLabel are provided', () => { it('renders all slots', () => { const { getByText } = render( Send} amount="$4.42" - amountAccessory={i} + amountEndAccessory={i} bottomLabel="0.002 ETH" />, ); diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx index b285aaea0..6d12393bc 100644 --- a/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.tsx @@ -15,12 +15,12 @@ import type { TitleHubProps } from './TitleHub.types'; * * @param props - Component props * @param props.title - Title row content (required) - * @param props.titleAccessory - Optional inline accessory to the right of `title` + * @param props.titleEndAccessory - Optional inline accessory to the right of `title` * @param props.amount - Optional primary amount below the title - * @param props.amountAccessory - Optional inline accessory to the right of the amount + * @param props.amountEndAccessory - Optional inline accessory to the right of the amount * @param props.bottomAccessory - Optional custom bottom row when the bottom label row is not shown * @param props.bottomLabel - Optional secondary label below the amount row - * @param props.bottomLabelAccessory - Optional inline accessory to the right of `bottomLabel` + * @param props.bottomLabelEndAccessory - Optional inline accessory to the right of `bottomLabel` * @param props.titleProps - Optional props merged into title row `Text` when `title` is a string * @param props.amountProps - Optional props merged into amount `Text` when `amount` is a string * @param props.bottomLabelProps - Optional props merged into bottom label `Text` when `bottomLabel` is a string @@ -30,39 +30,39 @@ import type { TitleHubProps } from './TitleHub.types'; */ export const TitleHub: React.FC = ({ amount, - amountAccessory, + amountEndAccessory, title, - titleAccessory, + titleEndAccessory, bottomAccessory, bottomLabel, - bottomLabelAccessory, + bottomLabelEndAccessory, amountProps, titleProps, bottomLabelProps, twClassName = '', ...props }) => { - const amountEndAccessoryNode = isReactNodeRenderable(amountAccessory) - ? amountAccessory + const amountEndAccessoryNode = isReactNodeRenderable(amountEndAccessory) + ? amountEndAccessory : undefined; - const titleEndAccessoryNode = isReactNodeRenderable(titleAccessory) - ? titleAccessory + const titleEndAccessoryNode = isReactNodeRenderable(titleEndAccessory) + ? titleEndAccessory : undefined; const bottomLabelEndAccessoryNode = isReactNodeRenderable( - bottomLabelAccessory, + bottomLabelEndAccessory, ) - ? bottomLabelAccessory + ? bottomLabelEndAccessory : undefined; const renderTitleRow = - isReactNodeRenderable(title) || isReactNodeRenderable(titleAccessory); + isReactNodeRenderable(title) || isReactNodeRenderable(titleEndAccessory); const renderAmountRow = - isReactNodeRenderable(amount) || isReactNodeRenderable(amountAccessory); + isReactNodeRenderable(amount) || isReactNodeRenderable(amountEndAccessory); const renderBottomLabelRow = isReactNodeRenderable(bottomLabel) || - isReactNodeRenderable(bottomLabelAccessory); + isReactNodeRenderable(bottomLabelEndAccessory); const renderBottomAccessory = !renderBottomLabelRow && isReactNodeRenderable(bottomAccessory); diff --git a/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts b/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts index 7727d3cfe..1b5afe233 100644 --- a/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts +++ b/packages/design-system-shared/src/types/TitleHub/TitleHub.types.ts @@ -9,13 +9,13 @@ export type TitleHubPropsShared = { /** * Optional primary amount line below the title (for example a fiat or token value). * When a string, platforms typically wrap with large display styles via `textProps`. - * The amount row renders when `amount` or `amountAccessory` is renderable. + * The amount row renders when `amount` or `amountEndAccessory` is renderable. */ amount?: ReactNode; /** * Optional accessory rendered inline to the right of the amount. */ - amountAccessory?: ReactNode; + amountEndAccessory?: ReactNode; /** * Title row above the optional amount (via platform `textProps` when a string). Required. */ @@ -23,19 +23,19 @@ export type TitleHubPropsShared = { /** * Optional accessory rendered inline to the right of `title` in the title row. */ - titleAccessory?: ReactNode; + titleEndAccessory?: ReactNode; /** - * Optional custom bottom row when neither `bottomLabel` nor `bottomLabelAccessory` is renderable. + * Optional custom bottom row when neither `bottomLabel` nor `bottomLabelEndAccessory` is renderable. * Mutually exclusive with the bottom label row: only one bottom row is shown. */ bottomAccessory?: ReactNode; /** * Optional bottom row with secondary label styling when a string (via platform `textProps`). - * If `bottomLabel` or `bottomLabelAccessory` is renderable, that row is shown instead of `bottomAccessory`. + * If `bottomLabel` or `bottomLabelEndAccessory` is renderable, that row is shown instead of `bottomAccessory`. */ bottomLabel?: ReactNode; /** * Optional accessory rendered inline to the right of `bottomLabel` in the bottom label row. */ - bottomLabelAccessory?: ReactNode; + bottomLabelEndAccessory?: ReactNode; }; From 578735024ca401a9332ae7edba131bf3cef44592 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Thu, 9 Apr 2026 16:44:31 -0700 Subject: [PATCH 5/5] Addressed comments and fixed lint errors --- .../src/components/TitleHub/TitleHub.stories.tsx | 15 ++++++++------- .../src/components/TitleHub/TitleHub.test.tsx | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx index 9dbb7a11d..dcc7ac290 100644 --- a/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.stories.tsx @@ -8,7 +8,12 @@ import { Text, TextColor, FontWeight, TextVariant } from '../Text'; import { TitleHub } from './TitleHub'; import type { TitleHubProps } from './TitleHub.types'; -/** Pill badge: dot + label (e.g. network), for `titleEndAccessory`. */ +/** + * Pill badge: dot + label (e.g. network), for `titleEndAccessory`. + * TODO: Temporary until a Tag component exists. + * + * @returns Story-only testnet badge UI. + */ const TestnetBadge = () => ( + - - - } + bottomLabelEndAccessory={} /> ), }; diff --git a/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx b/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx index 461633fd1..78fc97dd2 100644 --- a/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx +++ b/packages/design-system-react-native/src/components/TitleHub/TitleHub.test.tsx @@ -314,7 +314,6 @@ describe('TitleHub', () => { const container = getByTestId(CONTAINER_TEST_ID); - expect(container).toHaveStyle(tw`gap-1`); expect(container).toHaveStyle(tw`bg-default`); }); });