diff --git a/apps/storybook-react-native/.storybook/storybook.requires.js b/apps/storybook-react-native/.storybook/storybook.requires.js index 599589f3e..5d001b859 100644 --- a/apps/storybook-react-native/.storybook/storybook.requires.js +++ b/apps/storybook-react-native/.storybook/storybook.requires.js @@ -88,6 +88,7 @@ const getStories = () => { "./../../packages/design-system-react-native/src/components/ButtonSemantic/ButtonSemantic.stories.tsx": require("../../../packages/design-system-react-native/src/components/ButtonSemantic/ButtonSemantic.stories.tsx"), "./../../packages/design-system-react-native/src/components/Card/Card.stories.tsx": require("../../../packages/design-system-react-native/src/components/Card/Card.stories.tsx"), "./../../packages/design-system-react-native/src/components/Checkbox/Checkbox.stories.tsx": require("../../../packages/design-system-react-native/src/components/Checkbox/Checkbox.stories.tsx"), + "./../../packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.stories.tsx": require("../../../packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.stories.tsx"), "./../../packages/design-system-react-native/src/components/HeaderBase/HeaderBase.stories.tsx": require("../../../packages/design-system-react-native/src/components/HeaderBase/HeaderBase.stories.tsx"), "./../../packages/design-system-react-native/src/components/HeaderRoot/HeaderRoot.stories.tsx": require("../../../packages/design-system-react-native/src/components/HeaderRoot/HeaderRoot.stories.tsx"), "./../../packages/design-system-react-native/src/components/HeaderSearch/HeaderSearch.stories.tsx": require("../../../packages/design-system-react-native/src/components/HeaderSearch/HeaderSearch.stories.tsx"), diff --git a/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.stories.tsx b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.stories.tsx new file mode 100644 index 000000000..f51b25b9f --- /dev/null +++ b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.stories.tsx @@ -0,0 +1,49 @@ +import { IconAlertSeverity } from '@metamask/design-system-shared'; +import type { Meta, StoryObj } from '@storybook/react-native'; +import React from 'react'; + +import { Box, BoxAlignItems } from '../Box'; + +import { HeaderAlert } from './HeaderAlert'; +import type { HeaderAlertProps } from './HeaderAlert.types'; + +const meta: Meta = { + title: 'Components/HeaderAlert', + component: HeaderAlert, + argTypes: { + severity: { + control: 'select', + options: Object.values(IconAlertSeverity), + }, + twClassName: { control: 'text' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + severity: IconAlertSeverity.Info, + onBack: () => null, + onClose: () => null, + }, +}; + +export const Severity: Story = { + render: () => ( + + {Object.values(IconAlertSeverity).map((severity) => ( + + null} + onClose={() => null} + iconAlertProps={{ testID: `header-alert-icon-${severity}` }} + /> + + ))} + + ), +}; diff --git a/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.test.tsx b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.test.tsx new file mode 100644 index 000000000..ea1003388 --- /dev/null +++ b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.test.tsx @@ -0,0 +1,81 @@ +import { IconAlertSeverity } from '@metamask/design-system-shared'; +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 { IconSize } from '../../types'; +import { TWCLASSMAP_ICON_SIZE_DIMENSION } from '../Icon/Icon.constants'; +import type { IconAlertProps } from '../IconAlert'; +import { ICON_ALERT_SEVERITY_MAP } from '../IconAlert/IconAlert.constants'; + +import { HeaderAlert } from './HeaderAlert'; + +type IconAlertSeverityUnion = + (typeof IconAlertSeverity)[keyof typeof IconAlertSeverity]; + +describe('HeaderAlert', () => { + let tw: ReturnType; + + beforeAll(() => { + tw = renderHook(() => useTailwind()).result.current; + }); + + describe('when a severity is provided', () => { + it.each(Object.values(IconAlertSeverity) as IconAlertSeverityUnion[])( + 'renders IconAlert at Lg with mapped color for %s', + (severity) => { + const { getByTestId } = render( + , + ); + + const icon = getByTestId('header-alert-icon'); + const { color } = ICON_ALERT_SEVERITY_MAP[severity]; + + expect(icon).toBeOnTheScreen(); + expect(icon).toHaveStyle( + tw.style(color, TWCLASSMAP_ICON_SIZE_DIMENSION[IconSize.Lg]), + ); + }, + ); + }); + + describe('when iconAlertProps includes a severity at runtime', () => { + it('uses HeaderAlert severity for icon mapping', () => { + const { getByTestId } = render( + , + ); + + const icon = getByTestId('header-alert-icon'); + const { color } = ICON_ALERT_SEVERITY_MAP[IconAlertSeverity.Error]; + + expect(icon).toHaveStyle( + tw.style(color, TWCLASSMAP_ICON_SIZE_DIMENSION[IconSize.Lg]), + ); + }); + }); + + it('forwards HeaderStandard props such as testID', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('header-alert-root')).toBeOnTheScreen(); + expect(getByTestId('header-alert-icon')).toBeOnTheScreen(); + }); +}); diff --git a/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.tsx b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.tsx new file mode 100644 index 000000000..359459c06 --- /dev/null +++ b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { IconSize } from '../../types'; +import { HeaderStandard } from '../HeaderStandard'; +import { IconAlert } from '../IconAlert'; + +import type { HeaderAlertProps } from './HeaderAlert.types'; + +export const HeaderAlert: React.FC = ({ + severity, + iconAlertProps, + ...headerStandardProps +}) => ( + + + +); + +HeaderAlert.displayName = 'HeaderAlert'; diff --git a/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.types.ts b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.types.ts new file mode 100644 index 000000000..a5ece823d --- /dev/null +++ b/packages/design-system-react-native/src/components/HeaderAlert/HeaderAlert.types.ts @@ -0,0 +1,19 @@ +import type { HeaderAlertPropsShared } from '@metamask/design-system-shared'; + +import type { HeaderStandardProps } from '../HeaderStandard'; +import type { IconAlertProps } from '../IconAlert'; + +/** + * HeaderAlert component props (React Native). + */ +export type HeaderAlertProps = Omit< + HeaderStandardProps, + 'children' | 'title' | 'titleProps' | 'subtitle' | 'subtitleProps' +> & + HeaderAlertPropsShared & { + /** + * Props for the inner IconAlert. `severity` and `size` are always set by + * HeaderAlert and are omitted from this type. + */ + iconAlertProps?: Omit; + }; diff --git a/packages/design-system-react-native/src/components/HeaderAlert/README.md b/packages/design-system-react-native/src/components/HeaderAlert/README.md new file mode 100644 index 000000000..e17f2b3c3 --- /dev/null +++ b/packages/design-system-react-native/src/components/HeaderAlert/README.md @@ -0,0 +1,63 @@ +# HeaderAlert + +HeaderAlert is a [`HeaderStandard`](./../HeaderStandard/README.md) layout whose center content is always an [`IconAlert`](./../IconAlert/README.md) at `IconSize.Lg`, driven by a required `severity` that matches [`IconAlert`](./../IconAlert/README.md) semantics. + +The center slot is always the `IconAlert`; `title`, `titleProps`, `subtitle`, `subtitleProps`, and `children` are not part of the public API. Pass actions such as `onBack` or `onClose` as you would on `HeaderStandard`. + +```tsx +import { + HeaderAlert, + IconAlertSeverity, +} from '@metamask/design-system-react-native'; + + {}} />; +``` + +## Props + +### `severity` + +Same values and icon/color mapping as [`IconAlert` `severity`](./../IconAlert/README.md). This value is always passed to the inner `IconAlert` after `iconAlertProps`, so it overrides any `severity` inside `iconAlertProps`. + +| TYPE | REQUIRED | DEFAULT | +| ------------------- | -------- | ------- | +| `IconAlertSeverity` | Yes | - | + +```tsx +import { + HeaderAlert, + IconAlertSeverity, +} from '@metamask/design-system-react-native'; + +; +``` + +### `iconAlertProps` + +Optional props spread onto the inner `IconAlert` (for example `testID`, `twClassName`, `accessible`). The type is `IconAlert` props with `severity` and `size` omitted, because `HeaderAlert` always supplies those (`severity` from this component; `size` is `IconSize.Lg`). + +| TYPE | REQUIRED | DEFAULT | +| -------------------------------------------- | -------- | ----------- | +| `Omit` | No | `undefined` | + +```tsx +import { + HeaderAlert, + IconAlertSeverity, +} from '@metamask/design-system-react-native'; + +; +``` + +### Other props + +All [`HeaderStandard`](./../HeaderStandard/README.md) props except `children`, `title`, `titleProps`, `subtitle`, and `subtitleProps` are supported (`onBack`, `onClose`, `twClassName`, `testID`, etc.). + +## References + +- Storybook: **Components / HeaderAlert** — `Default`, `Severity` +- [MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940) diff --git a/packages/design-system-react-native/src/components/HeaderAlert/index.ts b/packages/design-system-react-native/src/components/HeaderAlert/index.ts new file mode 100644 index 000000000..e9bb803ad --- /dev/null +++ b/packages/design-system-react-native/src/components/HeaderAlert/index.ts @@ -0,0 +1,3 @@ +export { IconAlertSeverity } from '@metamask/design-system-shared'; +export { HeaderAlert } from './HeaderAlert'; +export type { HeaderAlertProps } from './HeaderAlert.types'; diff --git a/packages/design-system-react-native/src/components/index.ts b/packages/design-system-react-native/src/components/index.ts index e057985d2..7da6e0608 100644 --- a/packages/design-system-react-native/src/components/index.ts +++ b/packages/design-system-react-native/src/components/index.ts @@ -127,6 +127,9 @@ export type { CheckboxProps } from './Checkbox'; export { HeaderBase, HeaderBaseVariant } from './HeaderBase'; export type { HeaderBaseProps } from './HeaderBase'; +export { HeaderAlert } from './HeaderAlert'; +export type { HeaderAlertProps } from './HeaderAlert'; + export { HeaderRoot } from './HeaderRoot'; export type { HeaderRootProps } from './HeaderRoot'; diff --git a/packages/design-system-shared/src/index.ts b/packages/design-system-shared/src/index.ts index 3868af23e..9a994c51f 100644 --- a/packages/design-system-shared/src/index.ts +++ b/packages/design-system-shared/src/index.ts @@ -29,6 +29,9 @@ export { type IconAlertPropsShared, } from './types/IconAlert'; +// HeaderAlert types (ADR-0004) +export { type HeaderAlertPropsShared } from './types/HeaderAlert'; + // BannerBase types (ADR-0004) export { type BannerBasePropsShared } from './types/BannerBase'; diff --git a/packages/design-system-shared/src/types/HeaderAlert/HeaderAlert.types.ts b/packages/design-system-shared/src/types/HeaderAlert/HeaderAlert.types.ts new file mode 100644 index 000000000..65d88d0ef --- /dev/null +++ b/packages/design-system-shared/src/types/HeaderAlert/HeaderAlert.types.ts @@ -0,0 +1,7 @@ +import type { IconAlertPropsShared } from '../IconAlert'; + +/** + * HeaderAlert shared props (ADR-0004). + * Reuses IconAlert severity semantics. + */ +export type HeaderAlertPropsShared = IconAlertPropsShared; diff --git a/packages/design-system-shared/src/types/HeaderAlert/index.ts b/packages/design-system-shared/src/types/HeaderAlert/index.ts new file mode 100644 index 000000000..9dcc96f62 --- /dev/null +++ b/packages/design-system-shared/src/types/HeaderAlert/index.ts @@ -0,0 +1 @@ +export type { HeaderAlertPropsShared } from './HeaderAlert.types';