Skip to content

feat: migrate BannerBase from mobile into MMDS#955

Merged
georgewrmarshall merged 2 commits into
mainfrom
banner-base
Mar 6, 2026
Merged

feat: migrate BannerBase from mobile into MMDS#955
georgewrmarshall merged 2 commits into
mainfrom
banner-base

Conversation

@georgewrmarshall

@georgewrmarshall georgewrmarshall commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Description

  • Migrates BannerBase into MMDS for @metamask/design-system-react-native.
  • Adds shared cross-platform props in @metamask/design-system-shared via BannerBasePropsShared (ADR-0004 layering).
  • Implements BannerBase using MMDS primitives (Box, Text, Button, ButtonIcon) with default primary action button behavior.
  • Adds React Native stories, README documentation, tests, and exports.
  • Aligns Storybook/docs conventions with component-documentation rules (prop-focused stories, handler-specific action story, prop-object guidance).

Scope note:

  • This PR is intentionally scoped to React Native + shared types to make review easier.
  • React web parity is handled in the corresponding React PR using the same reference assets.

Related issues

Fixes:

  • DSYS-321
  • DSYS-299

Manual testing steps

  1. Run yarn build from repo root.
  2. Run yarn test from repo root.
  3. Run yarn lint from repo root.
  4. Open React Native Storybook and verify BannerBase stories:
    • Default, Title, Description, Children, ActionButtonOnPress, OnClose, StartAccessory.
  5. Confirm only Default and ActionButtonOnPress render an action button.

Screenshots/Recordings

Before

BannerBase in extension

bannerbase.ext720.mov

BannerBase in mobile

bannerbasemobile.720.mov

After

  • BannerBase is available and documented in @metamask/design-system-react-native.
  • Shared BannerBase props are available in @metamask/design-system-shared.
  • React parity references (stories/readmes/behavior) are preserved in the corresponding React PR.

Stories

storybook720.mov

READMES

docs720.mov

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs
  • I've completed the PR template to the best of my ability
  • I’ve included tests if applicable
  • I’ve documented my code using JSDoc format if applicable
  • I’ve applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Low Risk
Mostly additive: introduces a new component and shared types with Storybook/docs/tests, and updates public exports. Low risk aside from potential minor API/export or styling regressions for new consumers.

Overview
Adds a new BannerBase component to @metamask/design-system-react-native, including its typed API (BannerBase.types.ts), unit tests, README documentation, and Storybook stories, and wires it into the package exports.

Introduces cross-platform BannerBasePropsShared in @metamask/design-system-shared and exports it, and updates React Native Storybook’s auto-generated storybook.requires.js to include the new stories.

Written by Cursor Bugbot for commit bd3012a. This will update automatically on new commits. Configure here.

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: RN close button missing accessibility label
    • Added DEFAULT_BANNERBASE_CLOSE_BUTTON_ARIA_LABEL and passed it via accessibilityLabel to the RN close ButtonIcon with a default of "Close banner".
  • ✅ Fixed: Close button spread overrides structural layout classes
    • Destructured and merged className/twClassName so structural classes (ml-3/self-start) are preserved and user classes are merged instead of overwritten on both web and RN.
  • ✅ Fixed: BannerBase exports placed before Badge breaking alphabetical order*
    • Moved BannerBase exports after BadgeWrapper in both packages’ index files to restore alphabetical ordering and grouping.

Create PR

Or push these changes by commenting:

@cursor push 8d0969d66e
Preview (8d0969d66e)
diff --git a/packages/design-system-react-native/src/components/BannerBase/BannerBase.constants.ts b/packages/design-system-react-native/src/components/BannerBase/BannerBase.constants.ts
--- a/packages/design-system-react-native/src/components/BannerBase/BannerBase.constants.ts
+++ b/packages/design-system-react-native/src/components/BannerBase/BannerBase.constants.ts
@@ -5,5 +5,6 @@
 export const DEFAULT_BANNERBASE_ACTION_BUTTON_SIZE = ButtonSize.Md;
 export const DEFAULT_BANNERBASE_CLOSE_BUTTON_SIZE = ButtonIconSize.Sm;
 export const DEFAULT_BANNERBASE_CLOSE_BUTTON_ICON_NAME = IconName.Close;
+export const DEFAULT_BANNERBASE_CLOSE_BUTTON_ARIA_LABEL = 'Close banner';
 
 export const TESTID_BANNERBASE_CLOSE_BUTTON = 'banner-base-close-button';

diff --git a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
@@ -14,6 +14,7 @@
 
 import {
   DEFAULT_BANNERBASE_ACTION_BUTTON_SIZE,
+  DEFAULT_BANNERBASE_CLOSE_BUTTON_ARIA_LABEL,
   DEFAULT_BANNERBASE_CLOSE_BUTTON_ICON_NAME,
   DEFAULT_BANNERBASE_CLOSE_BUTTON_SIZE,
   DEFAULT_BANNERBASE_DESCRIPTION_VARIANT,
@@ -48,8 +49,13 @@
     ...resolvedActionButtonProps
   } = actionButtonProps ?? {};
 
-  const { onPress: closeButtonPropsOnPress, ...resolvedCloseButtonProps } =
-    closeButtonProps ?? {};
+  const {
+    accessibilityLabel:
+      closeButtonAccessibilityLabel = DEFAULT_BANNERBASE_CLOSE_BUTTON_ARIA_LABEL,
+    onPress: closeButtonPropsOnPress,
+    twClassName: closeButtonTwClassName,
+    ...resolvedCloseButtonProps
+  } = closeButtonProps ?? {};
 
   const resolvedActionButtonLabel =
     actionButtonLabel ??
@@ -71,6 +77,10 @@
         }
       : undefined;
 
+  const mergedCloseButtonTwClassName = closeButtonTwClassName
+    ? `ml-3 ${closeButtonTwClassName}`
+    : 'ml-3';
+
   return (
     <Box
       flexDirection={BoxFlexDirection.Row}
@@ -128,9 +138,10 @@
       {shouldShowCloseButton && (
         <ButtonIcon
           testID={TESTID_BANNERBASE_CLOSE_BUTTON}
-          twClassName="ml-3"
+          twClassName={mergedCloseButtonTwClassName}
           iconName={DEFAULT_BANNERBASE_CLOSE_BUTTON_ICON_NAME}
           size={DEFAULT_BANNERBASE_CLOSE_BUTTON_SIZE}
+          accessibilityLabel={closeButtonAccessibilityLabel}
           onPress={handleClosePress}
           {...resolvedCloseButtonProps}
         />

diff --git a/packages/design-system-react-native/src/components/index.ts b/packages/design-system-react-native/src/components/index.ts
--- a/packages/design-system-react-native/src/components/index.ts
+++ b/packages/design-system-react-native/src/components/index.ts
@@ -30,9 +30,6 @@
 export { AvatarToken, AvatarTokenSize } from './AvatarToken';
 export type { AvatarTokenProps } from './AvatarToken';
 
-export { BannerBase } from './BannerBase';
-export type { BannerBaseProps } from './BannerBase';
-
 export { BadgeCount, BadgeCountSize } from './BadgeCount';
 export type { BadgeCountProps } from './BadgeCount';
 
@@ -55,6 +52,9 @@
   BadgeWrapperCustomPosition,
 } from './BadgeWrapper';
 
+export { BannerBase } from './BannerBase';
+export type { BannerBaseProps } from './BannerBase';
+
 export { BottomSheetFooter, ButtonsAlignment } from './BottomSheetFooter';
 export type {
   BottomSheetFooterProps,

diff --git a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
@@ -57,6 +57,7 @@
       ariaLabel:
         closeButtonAriaLabel = DEFAULT_BANNERBASE_CLOSE_BUTTON_ARIA_LABEL,
       onClick: closeButtonPropsOnClick,
+      className: closeButtonClassName,
       ...resolvedCloseButtonProps
     } = closeButtonProps ?? {};
 
@@ -127,7 +128,7 @@
         {shouldShowCloseButton && (
           <ButtonIcon
             data-testid={TESTID_BANNERBASE_CLOSE_BUTTON}
-            className="ml-3 self-start"
+            className={twMerge('ml-3 self-start', closeButtonClassName)}
             iconName={DEFAULT_BANNERBASE_CLOSE_BUTTON_ICON_NAME}
             size={DEFAULT_BANNERBASE_CLOSE_BUTTON_SIZE}
             ariaLabel={closeButtonAriaLabel}

diff --git a/packages/design-system-react/src/components/index.ts b/packages/design-system-react/src/components/index.ts
--- a/packages/design-system-react/src/components/index.ts
+++ b/packages/design-system-react/src/components/index.ts
@@ -27,9 +27,6 @@
 export { AvatarToken, AvatarTokenSize } from './AvatarToken';
 export type { AvatarTokenProps } from './AvatarToken';
 
-export { BannerBase } from './BannerBase';
-export type { BannerBaseProps } from './BannerBase';
-
 export { BadgeCount } from './BadgeCount';
 export type { BadgeCountProps } from './BadgeCount';
 export { BadgeCountSize } from './BadgeCount';
@@ -52,6 +49,9 @@
 } from './BadgeWrapper';
 export type { BadgeWrapperCustomPosition } from './BadgeWrapper';
 
+export { BannerBase } from './BannerBase';
+export type { BannerBaseProps } from './BannerBase';
+
 export { Blockies } from './temp-components/Blockies';
 export type { BlockiesProps } from './temp-components/Blockies';

Comment thread packages/design-system-react/src/components/BannerBase/BannerBase.tsx Outdated
Comment thread packages/design-system-react/src/components/index.ts Outdated
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Falsy numeric values bypass Text wrapping in RN
    • Replaced truthy guards with explicit null checks so 0 is wrapped in Text for title, description, and children in both RN and Web components.

Create PR

Or push these changes by commenting:

@cursor push de46f72021
Preview (de46f72021)
diff --git a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
@@ -69,7 +69,8 @@
       {startAccessory}
 
       <Box twClassName="flex-1">
-        {title &&
+        {title !== null &&
+          title !== undefined &&
           (isTextContent(title) ? (
             <Text
               variant={TextVariant.BodyMd}
@@ -82,7 +83,7 @@
             title
           ))}
 
-        {description && (
+        {description !== null && description !== undefined && (
           <Box twClassName={title ? 'mt-1' : undefined}>
             {isTextContent(description) ? (
               <Text variant={TextVariant.BodySm} {...descriptionProps}>
@@ -94,7 +95,8 @@
           </Box>
         )}
 
-        {children &&
+        {children !== null &&
+          children !== undefined &&
           (isTextContent(children) ? (
             <Text variant={TextVariant.BodyMd} {...childrenWrapperProps}>
               {children}

diff --git a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
@@ -66,7 +66,8 @@
         {startAccessory}
 
         <Box className="min-w-0 flex-1">
-          {title &&
+          {title !== null &&
+            title !== undefined &&
             (isTextContent(title) ? (
               <Text
                 variant={TextVariant.BodyMd}
@@ -79,7 +80,8 @@
               title
             ))}
 
-          {description && (
+          {description !== null &&
+            description !== undefined && (
             <Box className={title ? 'mt-1' : undefined}>
               {isTextContent(description) ? (
                 <Text variant={TextVariant.BodySm} {...descriptionProps}>
@@ -91,7 +93,8 @@
             </Box>
           )}
 
-          {children &&
+          {children !== null &&
+            children !== undefined &&
             (isTextContent(children) ? (
               <Text variant={TextVariant.BodyMd} {...childrenWrapperProps}>
                 {children}

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Comment thread .cursor/rules/component-documentation.md
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: BannerBase exports break alphabetical ordering convention
    • Moved the BannerBase export to follow all Badge* exports in both React and React Native component barrel files to restore alphabetical order.

Create PR

Or push these changes by commenting:

@cursor push e46c799a0f
Preview (e46c799a0f)
diff --git a/packages/design-system-react-native/src/components/index.ts b/packages/design-system-react-native/src/components/index.ts
--- a/packages/design-system-react-native/src/components/index.ts
+++ b/packages/design-system-react-native/src/components/index.ts
@@ -30,9 +30,6 @@
 export { AvatarToken, AvatarTokenSize } from './AvatarToken';
 export type { AvatarTokenProps } from './AvatarToken';
 
-export { BannerBase } from './BannerBase';
-export type { BannerBaseProps } from './BannerBase';
-
 export { BadgeCount, BadgeCountSize } from './BadgeCount';
 export type { BadgeCountProps } from './BadgeCount';
 
@@ -55,6 +52,9 @@
   BadgeWrapperCustomPosition,
 } from './BadgeWrapper';
 
+export { BannerBase } from './BannerBase';
+export type { BannerBaseProps } from './BannerBase';
+
 export { BottomSheetFooter, ButtonsAlignment } from './BottomSheetFooter';
 export type {
   BottomSheetFooterProps,

diff --git a/packages/design-system-react/src/components/index.ts b/packages/design-system-react/src/components/index.ts
--- a/packages/design-system-react/src/components/index.ts
+++ b/packages/design-system-react/src/components/index.ts
@@ -27,9 +27,6 @@
 export { AvatarToken, AvatarTokenSize } from './AvatarToken';
 export type { AvatarTokenProps } from './AvatarToken';
 
-export { BannerBase } from './BannerBase';
-export type { BannerBaseProps } from './BannerBase';
-
 export { BadgeCount } from './BadgeCount';
 export type { BadgeCountProps } from './BadgeCount';
 export { BadgeCountSize } from './BadgeCount';
@@ -52,6 +49,9 @@
 } from './BadgeWrapper';
 export type { BadgeWrapperCustomPosition } from './BadgeWrapper';
 
+export { BannerBase } from './BannerBase';
+export type { BannerBaseProps } from './BannerBase';
+
 export { Blockies } from './temp-components/Blockies';
 export type { BlockiesProps } from './temp-components/Blockies';

Comment thread packages/design-system-react-native/src/components/index.ts Outdated
@georgewrmarshall georgewrmarshall self-assigned this Mar 4, 2026
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Falsy numeric title breaks description margin spacing
    • Replaced the truthiness check with a nullish check so a numeric title of 0 still applies the mt-1 spacing for the description in both React and React Native components.

Create PR

Or push these changes by commenting:

@cursor push 75330899a4
Preview (75330899a4)
diff --git a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
@@ -92,7 +92,11 @@
           ))}
 
         {description !== null && description !== undefined && (
-          <Box twClassName={title ? 'mt-1' : undefined}>
+          <Box
+            twClassName={
+              title !== null && title !== undefined ? 'mt-1' : undefined
+            }
+          >
             {isTextContent(description) ? (
               <Text variant={TextVariant.BodySm} {...descriptionProps}>
                 {description}

diff --git a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
@@ -82,7 +82,11 @@
             ))}
 
           {description !== null && description !== undefined && (
-            <Box className={title ? 'mt-1' : undefined}>
+            <Box
+              className={
+                title !== null && title !== undefined ? 'mt-1' : undefined
+              }
+            >
               {isTextContent(description) ? (
                 <Text variant={TextVariant.BodySm} {...descriptionProps}>
                   {description}

Comment thread packages/design-system-react/src/components/BannerBase/BannerBase.tsx Outdated
@georgewrmarshall

Copy link
Copy Markdown
Contributor Author

@cursor push 7533089

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: hasContent mishandles boolean ReactNode values causing incorrect spacing
    • Updated hasContent to treat boolean values as no content in both BannerBase implementations, preventing false titles from triggering spacing.

Create PR

Or push these changes by commenting:

@cursor push 777bf4a8a2
Preview (777bf4a8a2)
diff --git a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
@@ -22,7 +22,7 @@
   typeof content === 'string' || typeof content === 'number';
 
 const hasContent = (content: React.ReactNode) =>
-  content !== null && content !== undefined;
+  content !== null && content !== undefined && typeof content !== 'boolean';
 
 export const BannerBase: React.FC<BannerBaseProps> = ({
   title,

diff --git a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
@@ -22,7 +22,7 @@
   typeof content === 'string' || typeof content === 'number';
 
 const hasContent = (content: React.ReactNode) =>
-  content !== null && content !== undefined;
+  content !== null && content !== undefined && typeof content !== 'boolean';
 
 export const BannerBase = forwardRef<HTMLDivElement, BannerBaseProps>(
   (

typeof content === 'string' || typeof content === 'number';

const hasContent = (content: React.ReactNode) =>
content !== null && content !== undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasContent mishandles boolean ReactNode values causing incorrect spacing

Low Severity

The hasContent helper only excludes null and undefined, so false (and true) pass the check. React renders booleans as nothing, but the component treats them as present content. This matters for the common title={condition && "Title"} pattern — when condition is falsy, title becomes false, the title section renders an invisible node, and the description incorrectly receives mt-1 spacing as if a title is visible above it.

Additional Locations (1)

Fix in Cursor Fix in Web

@georgewrmarshall

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/design-system-react": "0.9.0-preview.694a8ed",
  "@metamask-previews/design-system-react-native": "0.9.0-preview.694a8ed",
  "@metamask-previews/design-system-shared": "0.2.0-preview.694a8ed",
  "@metamask-previews/design-system-tailwind-preset": "0.6.1-preview.694a8ed",
  "@metamask-previews/design-system-twrnc-preset": "0.3.0-preview.694a8ed",
  "@metamask-previews/design-tokens": "8.2.2-preview.694a8ed"
}

@georgewrmarshall georgewrmarshall left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a suggestion regarding close button test id


{shouldShowCloseButton && (
<ButtonIcon
testID="banner-base-close-button"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not have a default close button here


{shouldShowCloseButton && (
<ButtonIcon
data-testid="banner-base-close-button"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not have a default test id here should be applied using closeButtonProps update component and tests and code example in both react and react native docs

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Duplicate utility functions across platform packages
    • Moved isTextContent/hasContent to @metamask/design-system-shared and updated both platform BannerBase components to import them.
  • ✅ Fixed: Close button missing self-start in React Native
    • Added 'self-start' to the React Native close ButtonIcon twClassName to match web alignment behavior.

Create PR

Or push these changes by commenting:

@cursor push 4f2bc1c17e
Preview (4f2bc1c17e)
diff --git a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react-native/src/components/BannerBase/BannerBase.tsx
@@ -1,3 +1,4 @@
+import { hasContent, isTextContent } from '@metamask/design-system-shared';
 import React from 'react';
 import { GestureResponderEvent } from 'react-native';
 
@@ -18,12 +19,6 @@
 
 import type { BannerBaseProps } from './BannerBase.types';
 
-const isTextContent = (content: React.ReactNode): content is string | number =>
-  typeof content === 'string' || typeof content === 'number';
-
-const hasContent = (content: React.ReactNode) =>
-  content !== null && content !== undefined;
-
 export const BannerBase: React.FC<BannerBaseProps> = ({
   title,
   titleProps,
@@ -64,8 +59,8 @@
       : undefined;
 
   const mergedCloseButtonTwClassName = closeButtonTwClassName
-    ? `ml-3 ${closeButtonTwClassName}`
-    : 'ml-3';
+    ? `ml-3 self-start ${closeButtonTwClassName}`
+    : 'ml-3 self-start';
 
   return (
     <Box

diff --git a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
--- a/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
+++ b/packages/design-system-react/src/components/BannerBase/BannerBase.tsx
@@ -1,3 +1,4 @@
+import { hasContent, isTextContent } from '@metamask/design-system-shared';
 import React, { forwardRef } from 'react';
 
 import {
@@ -18,12 +19,6 @@
 
 import type { BannerBaseProps } from './BannerBase.types';
 
-const isTextContent = (content: React.ReactNode): content is string | number =>
-  typeof content === 'string' || typeof content === 'number';
-
-const hasContent = (content: React.ReactNode) =>
-  content !== null && content !== undefined;
-
 export const BannerBase = forwardRef<HTMLDivElement, BannerBaseProps>(
   (
     {

diff --git a/packages/design-system-shared/src/index.ts b/packages/design-system-shared/src/index.ts
--- a/packages/design-system-shared/src/index.ts
+++ b/packages/design-system-shared/src/index.ts
@@ -6,6 +6,9 @@
   generateIconSeed,
 } from './utils/caip-address';
 
+// BannerBase utils (ADR-0004)
+export { isTextContent, hasContent } from './utils/banner-base';
+
 // BadgeCount types (ADR-0003 + ADR-0004)
 export { BadgeCountSize, type BadgeCountPropsShared } from './types/BadgeCount';
 

diff --git a/packages/design-system-shared/src/utils/banner-base.ts b/packages/design-system-shared/src/utils/banner-base.ts
new file mode 100644
--- /dev/null
+++ b/packages/design-system-shared/src/utils/banner-base.ts
@@ -1,0 +1,27 @@
+import type { ReactNode } from 'react';
+
+/**
+ * Returns true when the provided ReactNode is plain text content that should be
+ * wrapped with a Text component for consistent typography.
+ */
+/**
+ * @param content - The ReactNode to test.
+ * @returns True if content is a string or number (textual), false otherwise.
+ */
+export const isTextContent = (
+  content: ReactNode,
+): content is string | number => {
+  return typeof content === 'string' || typeof content === 'number';
+};
+
+/**
+ * Returns true when the provided ReactNode is present (not null/undefined).
+ * This intentionally allows values like 0 or empty string, which are valid content.
+ */
+/**
+ * @param content - The ReactNode to test for presence.
+ * @returns True if content is neither null nor undefined.
+ */
+export const hasContent = (content: ReactNode): boolean => {
+  return content !== null && content !== undefined;
+};

@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: HTML title attribute narrows shared title prop type
    • Updated BannerBase props to omit native HTML title from BoxProps, preserving shared title?: ReactNode.

Create PR

Or push these changes by commenting:

@cursor push c32b4393c5
Preview (c32b4393c5)
diff --git a/packages/design-system-react/src/components/BannerBase/BannerBase.types.ts b/packages/design-system-react/src/components/BannerBase/BannerBase.types.ts
--- a/packages/design-system-react/src/components/BannerBase/BannerBase.types.ts
+++ b/packages/design-system-react/src/components/BannerBase/BannerBase.types.ts
@@ -26,7 +26,7 @@
 };
 
 type BannerBasePropsBase = BannerBasePropsShared &
-  Omit<BoxProps, 'children'> & {
+  Omit<BoxProps, 'children' | 'title'> & {
     /**
      * Optional props for the title `Text` when the title is a string.
      */

Comment thread packages/design-system-react/src/components/BannerBase/BannerBase.types.ts Outdated
@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall marked this pull request as ready for review March 5, 2026 21:47
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner March 5, 2026 21:47
@georgewrmarshall georgewrmarshall changed the title feat: migrate BannerBase from extension/mobile into MMDS feat: migrate BannerBase from mobile into MMDS Mar 5, 2026
))}

{shouldShowActionButton && (
<Box twClassName="mt-4">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably could remove some nesting here will do in follow up polish

@georgewrmarshall georgewrmarshall enabled auto-merge (squash) March 6, 2026 00:44
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall merged commit b6a071b into main Mar 6, 2026
43 checks passed
@georgewrmarshall georgewrmarshall deleted the banner-base branch March 6, 2026 00:47

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

typeof content === 'string' || typeof content === 'number';

const hasContent = (content: React.ReactNode) =>
content !== null && content !== undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasContent mishandles false, causing incorrect description spacing

Medium Severity

hasContent only rejects null and undefined, so hasContent(false) returns true. A common React pattern like title={condition && "Title"} passes false when the condition fails. On line 97, hasContent(title) then incorrectly evaluates to true, applying mt-1 margin to the description even though no title is visually rendered — producing incorrect spacing.

Additional Locations (1)

Fix in Cursor Fix in Web

@georgewrmarshall georgewrmarshall mentioned this pull request Mar 10, 2026
6 tasks
georgewrmarshall added a commit that referenced this pull request Mar 10, 2026
## Release 25.0.0

This release includes new components migrated from Extension and Mobile,
breaking API improvements to ButtonIcon and Input components, and
continued ADR-0003/0004 type migrations.

### 📦 Package Versions

- `@metamask/design-system-shared`: **0.4.0**
- `@metamask/design-system-react`: **0.11.0**
- `@metamask/design-system-react-native`: **0.11.0**

### 🔄 Shared Type Updates (0.4.0)

#### Component Type Additions (#964, #955)

Added shared types for newly migrated components following ADR-0003 and
ADR-0004 patterns:

**What Changed:**
- Added `ButtonFilter` shared types with `ButtonFilterVariant` const
object and `ButtonFilterPropsShared`
- Added `BannerBase` shared types with `BannerBaseSeverity` const object
and `BannerBasePropsShared`

**Impact:**
- Enables consistent ButtonFilter and BannerBase implementations across
React and React Native
- Continues ADR-0003/0004 const-object + string-union pattern adoption

### 🌐 React Web Updates (0.11.0)

#### Added
- Added `ButtonFilter` component for filter button functionality (#964)
- Added `BannerBase` component for creating custom banner notifications
(#961)

#### Changed
- **BREAKING:** Updated `ButtonIcon` API to use `variant` prop instead
of `isInverse` and `isFloating` boolean props (#948)
- Updated `Ai` icon to filled version for visual consistency (#970)

### 📱 React Native Updates (0.11.0)

#### Added
- Added `ButtonFilter` component (#964)
- Added `BottomSheet` component for modal interactions (#963)
- Added `MainActionButton` component for primary CTAs (#952)
- Added `BannerBase` component for custom banners (#955)
- Added `ListItem` component for standardized list rows (#958)
- Added `TabEmptyState` component for empty states (#949)
- Added `BottomSheetDialog` component for dialogs (#905)

#### Changed
- **BREAKING:** Updated `ButtonIcon` API to use `variant` prop instead
of `isInverse` and `isFloating` boolean props (#948)
- **BREAKING:** `Input` component now requires `value` prop and is
controlled-only (#960)
- Updated `Ai` icon to filled version (#970)

#### Fixed
- Fixed iOS placeholder alignment issue in `Input` component (#960)
- Fixed missing component exports (#967)

### ⚠️ Breaking Changes

#### ButtonIcon Variant Prop (Both Platforms)

**What Changed:**
- Removed `isInverse` and `isFloating` boolean props
- Added `variant` prop with three options:
  - `ButtonIconVariant.Default` (transparent background - default)
- `ButtonIconVariant.Filled` (muted background with rounded corners -
new)
- `ButtonIconVariant.Floating` (colored background with inverse icon -
replaces `isFloating`)

**Migration:**
```tsx
// Before
<ButtonIcon name={IconName.Add} isFloating />

// After
<ButtonIcon name={IconName.Add} variant={ButtonIconVariant.Floating} />
```

See migration guides for complete instructions:
- [React Migration
Guide](./packages/design-system-react/MIGRATION.md#from-version-0100-to-0110)
- [React Native Migration
Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0100-to-0110)

#### Input Controlled-Only (React Native Only)

**What Changed:**
- Removed `defaultValue` prop
- `value` prop is now required
- All Input instances must be controlled with state management

**Migration:**
```tsx
// Before
<Input placeholder="Enter text" defaultValue="Initial" onChange={handleChange} />

// After
const [text, setText] = useState('Initial');
<Input placeholder="Enter text" value={text} onChange={setText} />
```

**Impact:**
- Affects `Input` and `TextField` components
- Provides consistent behavior and fixes iOS placeholder alignment

See [React Native Migration
Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0100-to-0110)
for complete instructions.

### ✅ Checklist

- [x] Changelogs updated with human-readable descriptions
- [x] Changelog validation passed (`yarn changelog:validate`)
- [x] Version bumps follow semantic versioning
  - design-system-shared: minor (0.3.0 → 0.4.0) - new shared types
- design-system-react: minor (0.10.0 → 0.11.0) - new components +
breaking ButtonIcon change
- design-system-react-native: minor (0.10.0 → 0.11.0) - new components +
breaking ButtonIcon and Input changes
- [x] Breaking changes documented with migration guidance
- [x] Migration guides updated with before/after examples
- [x] PR references included in changelog entries

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Although this PR is mostly versioning/docs, it formalizes breaking
public API changes (`ButtonIcon` and RN `Input`) that will require
downstream updates and could cause consumer build failures if missed.
> 
> **Overview**
> Bumps the monorepo release to `25.0.0` and publishes new package
versions (`@metamask/design-system-react@0.11.0`,
`@metamask/design-system-react-native@0.11.0`,
`@metamask/design-system-shared@0.4.0`) with updated changelogs.
> 
> Documents newly added components/types (`ButtonFilter`, `BannerBase`,
plus several React Native-only additions like `BottomSheet`, `ListItem`,
etc.) and **breaking API changes**: `ButtonIcon` replaces
`isInverse`/`isFloating` with a `variant` prop (React + RN), and React
Native `Input` becomes controlled-only (requires `value`, removes
`defaultValue`). Migration guides are updated with before/after examples
and new compare links.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0148a27. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants