From 22350b8ae19b0926d75de63a3e77c1ca4f22bf1c Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Tue, 7 Apr 2026 19:08:27 -0700 Subject: [PATCH 1/4] 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/4] 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 14e03a28986088a6954d53fc291e5f67ba94e572 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Wed, 8 Apr 2026 20:47:58 -0700 Subject: [PATCH 3/4] Added end Accessory to both title and bottomLabel --- .../design-system-react-native/MIGRATION.md | 24 ++++++- .../src/components/TitleStandard/README.md | 39 +++++++++++- .../TitleStandard/TitleStandard.stories.tsx | 22 ++++++- .../TitleStandard/TitleStandard.test.tsx | 63 ++++++++++++++++--- .../TitleStandard/TitleStandard.tsx | 24 ++++--- .../TitleStandard/TitleStandard.types.ts | 4 +- .../TitleStandard/TitleStandard.types.ts | 7 ++- 7 files changed, 156 insertions(+), 27 deletions(-) diff --git a/packages/design-system-react-native/MIGRATION.md b/packages/design-system-react-native/MIGRATION.md index 0813bf1ae..dfa6f47cf 100644 --- a/packages/design-system-react-native/MIGRATION.md +++ b/packages/design-system-react-native/MIGRATION.md @@ -33,7 +33,7 @@ If you adopted `TitleStandard` from a prerelease or internal branch that used `t **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`. +- `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 `titleEndAccessory`. - **`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. @@ -94,7 +94,7 @@ import { } + titleEndAccessory={} /> ``` @@ -180,6 +180,26 @@ import { BoxRow, BoxColumn } from '@metamask/design-system-react-native'; - Any import of `BoxHorizontal` or `BoxVertical` must be renamed +#### TitleStandard `titleAccessory` renamed to `titleEndAccessory` + +**What changed:** + +- `TitleStandard` and `TitleStandardPropsShared` prop **`titleAccessory`** is now **`titleEndAccessory`**. + +**Migration:** + +```tsx +// Before (0.15.0) +} /> + +// After (0.16.0) +} /> +``` + +**Impact:** + +- Any `TitleStandard` usage that passed `titleAccessory` + #### KeyValueRow API **What changed:** diff --git a/packages/design-system-react-native/src/components/TitleStandard/README.md b/packages/design-system-react-native/src/components/TitleStandard/README.md index 98a08b26c..8128ea129 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/README.md +++ b/packages/design-system-react-native/src/components/TitleStandard/README.md @@ -1,6 +1,6 @@ # 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. +TitleStandard is used to display a required primary title with optional rows above and below the title, optional inline accessories next to the title and bottom label, and optional bottom label or custom bottom content. ```tsx import { TitleStandard } from '@metamask/design-system-react-native'; @@ -24,7 +24,7 @@ The primary title. The title row always renders. When `title` is a string, it is ``` -### `titleAccessory` +### `titleEndAccessory` Optional node rendered to the right of the title (for example an info icon). @@ -43,7 +43,7 @@ import { @@ -95,6 +95,39 @@ Optional bottom row with secondary label typography when the value is a string ( ``` +### `bottomLabelEndAccessory` + +Optional node rendered to the right of the bottom label. Only used when `bottomLabel` is renderable (same row as the default bottom label typography). + +| TYPE | REQUIRED | DEFAULT | +| ----------- | -------- | ----------- | +| `ReactNode` | No | `undefined` | + +```tsx +import { + TitleStandard, + Box, + Icon, + IconName, + IconSize, + IconColor, +} from '@metamask/design-system-react-native'; + + + + + } +/>; +``` + ### `bottomAccessory` Optional custom bottom row when `bottomLabel` is not renderable. Renders without default label typography; compose layout inside the node. 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 index 9c6da99d3..19605c16f 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.stories.tsx @@ -44,11 +44,11 @@ export const Title: Story = { render: () => , }; -export const TitleAccessory: Story = { +export const TitleEndAccessory: Story = { render: () => ( ( + + + + } + /> + ), +}; + export const BottomAccessory: Story = { render: () => ( { expect(getByTestId(BOTTOM_LABEL_TEST_ID)).toBeOnTheScreen(); }); + + it('renders bottomLabel and bottomLabelEndAccessory', () => { + const { getByText } = render( + Extra} + />, + ); + + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + expect(getByText('Extra')).toBeOnTheScreen(); + }); + + it('does not render bottomLabelEndAccessory when it is false', () => { + const { getByText } = render( + , + ); + + expect(getByText('0.002 ETH')).toBeOnTheScreen(); + }); }); describe('when bottomAccessory is provided', () => { @@ -135,42 +160,62 @@ describe('TitleStandard', () => { }); }); - describe('when titleAccessory is provided', () => { - it('renders title and titleAccessory', () => { + describe('when bottomLabelEndAccessory is provided without bottomLabel', () => { + it('does not render bottomLabelEndAccessory', () => { + const { getByText, queryByTestId } = render( + Custom Bottom} + bottomLabelEndAccessory={ + End + } + />, + ); + + expect(getByText('Custom Bottom')).toBeOnTheScreen(); + expect(queryByTestId('bottom-label-end-only')).not.toBeOnTheScreen(); + }); + }); + + describe('when titleEndAccessory is provided', () => { + it('renders title and titleEndAccessory', () => { const { getByText } = render( - Info} />, + Info} />, ); expect(getByText('$4.42')).toBeOnTheScreen(); expect(getByText('Info')).toBeOnTheScreen(); }); - it('renders titleAccessory when title is an empty string', () => { + it('renders titleEndAccessory when title is an empty string', () => { const { getByText } = render( - Accessory only} />, + Accessory only} + />, ); expect(getByText('Accessory only')).toBeOnTheScreen(); }); }); - describe('when titleAccessory is false', () => { + describe('when titleEndAccessory is false', () => { it('renders title only', () => { const { getByText } = render( - , + , ); expect(getByText('$4.42')).toBeOnTheScreen(); }); }); - describe('when topAccessory, titleAccessory, and bottomLabel are provided', () => { + describe('when topAccessory, titleEndAccessory, and bottomLabel are provided', () => { it('renders all slots', () => { const { getByText } = render( Send} title="$4.42" - titleAccessory={i} + titleEndAccessory={i} bottomLabel="0.002 ETH" />, ); 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 f78ed7187..1beb9e685 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx +++ b/packages/design-system-react-native/src/components/TitleStandard/TitleStandard.tsx @@ -15,10 +15,11 @@ import type { TitleStandardProps } from './TitleStandard.types'; * * @param props - Component props * @param props.title - Primary title content - * @param props.titleAccessory - Optional inline accessory to the right of the title + * @param props.titleEndAccessory - 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.bottomLabelEndAccessory - Optional inline accessory to the right of the bottom label * @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 @@ -27,18 +28,28 @@ import type { TitleStandardProps } from './TitleStandard.types'; */ export const TitleStandard: React.FC = ({ title, - titleAccessory, + titleEndAccessory, topAccessory, bottomAccessory, bottomLabel, + bottomLabelEndAccessory, titleProps, bottomLabelProps, twClassName = '', ...props }) => { - const titleEndAccessoryNode = isReactNodeRenderable(titleAccessory) - ? titleAccessory + const titleEndAccessoryNode = isReactNodeRenderable(titleEndAccessory) + ? titleEndAccessory : undefined; + const bottomLabelEndAccessoryNode = isReactNodeRenderable( + bottomLabelEndAccessory, + ) + ? bottomLabelEndAccessory + : undefined; + + const renderBottomLabel = isReactNodeRenderable(bottomLabel); + const renderBottomAccessory = + !renderBottomLabel && isReactNodeRenderable(bottomAccessory); const titleRow = ( = ({ ); - const renderBottomLabel = isReactNodeRenderable(bottomLabel); - const renderBottomAccessory = - !renderBottomLabel && isReactNodeRenderable(bottomAccessory); - const bottomLabelRow = ( ; /** - * Optional props merged into {@link BoxHorizontal} `textProps` when `bottomLabel` is a string. + * Optional props merged into {@link BoxRow} `textProps` when `bottomLabel` is a string. */ bottomLabelProps?: Partial; /** diff --git a/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts b/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts index 2a8336bad..b2a4e4ff3 100644 --- a/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts +++ b/packages/design-system-shared/src/types/TitleStandard/TitleStandard.types.ts @@ -13,7 +13,7 @@ export type TitleStandardPropsShared = { /** * Optional accessory rendered inline to the right of the title. */ - titleAccessory?: ReactNode; + titleEndAccessory?: ReactNode; /** * Optional accessory rendered in its own row above the title. */ @@ -28,4 +28,9 @@ export type TitleStandardPropsShared = { * If renderable, it is shown instead of `bottomAccessory`. */ bottomLabel?: ReactNode; + /** + * Optional accessory rendered inline to the right of the bottom label row. + * Only applies when `bottomLabel` is renderable. + */ + bottomLabelEndAccessory?: ReactNode; }; From 5b517ef27db890a8a820bb5e86bbe1da58ddc872 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Thu, 9 Apr 2026 16:16:11 -0700 Subject: [PATCH 4/4] Updated readmes --- .../design-system-react-native/MIGRATION.md | 98 ------------------- .../src/components/TitleStandard/README.md | 2 +- 2 files changed, 1 insertion(+), 99 deletions(-) diff --git a/packages/design-system-react-native/MIGRATION.md b/packages/design-system-react-native/MIGRATION.md index dfa6f47cf..088b38811 100644 --- a/packages/design-system-react-native/MIGRATION.md +++ b/packages/design-system-react-native/MIGRATION.md @@ -25,84 +25,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 `titleEndAccessory`. -- **`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 @@ -180,26 +102,6 @@ import { BoxRow, BoxColumn } from '@metamask/design-system-react-native'; - Any import of `BoxHorizontal` or `BoxVertical` must be renamed -#### TitleStandard `titleAccessory` renamed to `titleEndAccessory` - -**What changed:** - -- `TitleStandard` and `TitleStandardPropsShared` prop **`titleAccessory`** is now **`titleEndAccessory`**. - -**Migration:** - -```tsx -// Before (0.15.0) -} /> - -// After (0.16.0) -} /> -``` - -**Impact:** - -- Any `TitleStandard` usage that passed `titleAccessory` - #### KeyValueRow API **What changed:** diff --git a/packages/design-system-react-native/src/components/TitleStandard/README.md b/packages/design-system-react-native/src/components/TitleStandard/README.md index 8128ea129..6fbc36108 100644 --- a/packages/design-system-react-native/src/components/TitleStandard/README.md +++ b/packages/design-system-react-native/src/components/TitleStandard/README.md @@ -45,7 +45,7 @@ import { title="$4.42" titleEndAccessory={ - + } />;