generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DSRN] Add ButtonBase component (#384)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds the `ButtonBase` component to the `Primitives` folder <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Related issues** Fixes: #176 ## **Manual testing steps** 1. Run `yarn storybook:ios` from root 2. Go to Primitives > ButtonBase 3. Observe component ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** https://github.com/user-attachments/assets/5488cb0c-27b7-4d6f-9f9a-55a3e8304699 <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). 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.
- Loading branch information
1 parent
1e76a9b
commit 2564ee9
Showing
10 changed files
with
783 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { IconSize, IconColor } from '../../components/Icon'; | ||
import { TextColor, TextVariant, FontWeight } from '../../components/Text'; | ||
import type { ButtonBaseProps } from './ButtonBase.types'; | ||
import { ButtonBaseSize } from './ButtonBase.types'; | ||
|
||
// Defaults | ||
export const DEFAULT_BUTTONBASE_PROPS: Partial<ButtonBaseProps> = { | ||
textProps: { | ||
variant: TextVariant.BodyMd, | ||
fontWeight: FontWeight.Medium, | ||
color: TextColor.TextDefault, | ||
numberOfLines: 1, | ||
ellipsizeMode: 'clip', | ||
}, | ||
size: ButtonBaseSize.Lg, | ||
isLoading: false, | ||
spinnerProps: { | ||
color: IconColor.IconDefault, | ||
}, | ||
startIconProps: { | ||
size: IconSize.Sm, | ||
testID: 'start-icon', | ||
}, | ||
endIconProps: { | ||
size: IconSize.Sm, | ||
testID: 'end-icon', | ||
}, | ||
isDisabled: false, | ||
isFullWidth: false, | ||
}; |
135 changes: 135 additions & 0 deletions
135
packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import type { Meta, StoryObj } from '@storybook/react-native'; | ||
import { View } from 'react-native'; | ||
|
||
import { IconName } from '../../components/Icon'; | ||
import ButtonBase from './ButtonBase'; | ||
import { DEFAULT_BUTTONBASE_PROPS } from './ButtonBase.constants'; | ||
import type { ButtonBaseProps } from './ButtonBase.types'; | ||
import { ButtonBaseSize } from './ButtonBase.types'; | ||
|
||
const meta: Meta<ButtonBaseProps> = { | ||
title: 'Primitives/ButtonBase', | ||
component: ButtonBase, | ||
argTypes: { | ||
children: { | ||
control: 'text', | ||
}, | ||
size: { | ||
control: 'select', | ||
options: ButtonBaseSize, | ||
}, | ||
isLoading: { | ||
control: 'boolean', | ||
}, | ||
loadingText: { | ||
control: 'text', | ||
}, | ||
startIconName: { | ||
control: 'select', | ||
options: IconName, | ||
}, | ||
endIconName: { | ||
control: 'select', | ||
options: IconName, | ||
}, | ||
isDisabled: { | ||
control: 'boolean', | ||
}, | ||
isFullWidth: { | ||
control: 'boolean', | ||
}, | ||
twClassName: { | ||
control: 'text', | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<ButtonBaseProps>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
children: 'ButtonBase', | ||
size: DEFAULT_BUTTONBASE_PROPS.size, | ||
isLoading: DEFAULT_BUTTONBASE_PROPS.isLoading, | ||
loadingText: 'Loading', | ||
startIconName: IconName.Add, | ||
endIconName: IconName.AddSquare, | ||
isDisabled: DEFAULT_BUTTONBASE_PROPS.isDisabled, | ||
isFullWidth: DEFAULT_BUTTONBASE_PROPS.isFullWidth, | ||
}, | ||
}; | ||
|
||
export const Sizes: Story = { | ||
render: () => ( | ||
<View style={{ gap: 16 }}> | ||
<ButtonBase size={ButtonBaseSize.Sm}>ButtonBaseSize Sm</ButtonBase> | ||
<ButtonBase size={ButtonBaseSize.Md}>ButtonBaseSize Md</ButtonBase> | ||
<ButtonBase size={ButtonBaseSize.Lg}> | ||
ButtonBaseSize Lg (Default) | ||
</ButtonBase> | ||
</View> | ||
), | ||
}; | ||
|
||
export const IsLoading: Story = { | ||
render: () => ( | ||
<View style={{ gap: 16 }}> | ||
<ButtonBase isLoading>ButtonBase</ButtonBase> | ||
<ButtonBase isLoading loadingText="With Loading Text"> | ||
ButtonBase | ||
</ButtonBase> | ||
</View> | ||
), | ||
}; | ||
|
||
export const WithStartAccessory: Story = { | ||
render: () => ( | ||
<ButtonBase startIconName={IconName.Add}>ButtonBase</ButtonBase> | ||
), | ||
}; | ||
|
||
export const WithEndAccessory: Story = { | ||
render: () => <ButtonBase endIconName={IconName.Add}>ButtonBase</ButtonBase>, | ||
}; | ||
|
||
export const WithStartAndEndAccessory: Story = { | ||
render: () => ( | ||
<ButtonBase startIconName={IconName.Add} endIconName={IconName.AddSquare}> | ||
ButtonBase | ||
</ButtonBase> | ||
), | ||
}; | ||
|
||
export const isDisabled: Story = { | ||
render: () => <ButtonBase isDisabled>ButtonBase</ButtonBase>, | ||
}; | ||
|
||
export const isFullWidth: Story = { | ||
render: () => ( | ||
<View style={{ gap: 16 }}> | ||
<ButtonBase>ButtonBase</ButtonBase> | ||
<ButtonBase isFullWidth>ButtonBase</ButtonBase> | ||
</View> | ||
), | ||
}; | ||
|
||
export const WithLongText: Story = { | ||
render: () => ( | ||
<View style={{ paddingHorizontal: 32 }}> | ||
<ButtonBase | ||
startIconName={IconName.Add} | ||
endIconName={IconName.ArrowRight} | ||
> | ||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod | ||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim | ||
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea | ||
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate | ||
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint | ||
occaecat cupidatat non proident, sunt in culpa qui officia deserunt | ||
mollit anim id est laborum. | ||
</ButtonBase> | ||
</View> | ||
), | ||
}; |
120 changes: 120 additions & 0 deletions
120
packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { render, fireEvent } from '@testing-library/react-native'; | ||
import React from 'react'; | ||
|
||
import { IconName } from '../../components/Icon'; | ||
import ButtonBase from './ButtonBase'; | ||
import { ButtonBaseSize } from './ButtonBase.types'; | ||
import { generateButtonBaseContainerClassNames } from './ButtonBase.utilities'; | ||
|
||
describe('ButtonBase', () => { | ||
describe('generateButtonBaseContainerClassNames', () => { | ||
it('returns correct class names for default state', () => { | ||
const classNames = generateButtonBaseContainerClassNames({}); | ||
expect(classNames).toContain( | ||
'flex-row items-center justify-center rounded-full bg-background-muted px-4', | ||
); | ||
expect(classNames).toContain('opacity-100'); | ||
expect(classNames).toContain('self-start'); | ||
}); | ||
|
||
it('applies correct class names when disabled', () => { | ||
const classNames = generateButtonBaseContainerClassNames({ | ||
isDisabled: true, | ||
}); | ||
expect(classNames).toContain('opacity-50'); | ||
}); | ||
|
||
it('applies correct class names when full width', () => { | ||
const classNames = generateButtonBaseContainerClassNames({ | ||
isFullWidth: true, | ||
}); | ||
expect(classNames).toContain('self-stretch'); | ||
}); | ||
|
||
it('applies correct size class from TWCLASSMAP_BUTTONBASE_SIZE', () => { | ||
const size = ButtonBaseSize.Lg; | ||
const expectedSizeClass = `h-[${size}px]`; | ||
const classNames = generateButtonBaseContainerClassNames({ size }); | ||
expect(classNames).toContain(expectedSizeClass); | ||
}); | ||
|
||
it('appends additional Tailwind class names', () => { | ||
const classNames = generateButtonBaseContainerClassNames({ | ||
twClassName: 'border border-blue-500', | ||
}); | ||
expect(classNames).toContain('border border-blue-500'); | ||
}); | ||
|
||
it('applies all styles together correctly', () => { | ||
const classNames = generateButtonBaseContainerClassNames({ | ||
size: ButtonBaseSize.Sm, | ||
isDisabled: true, | ||
isFullWidth: true, | ||
twClassName: 'shadow-md', | ||
}); | ||
expect(classNames).toContain(`h-[${ButtonBaseSize.Sm}px]`); | ||
expect(classNames).toContain('opacity-50'); | ||
expect(classNames).toContain('self-stretch'); | ||
expect(classNames).toContain('shadow-md'); | ||
}); | ||
}); | ||
describe('ButtonBase Component', () => { | ||
it('renders correctly with default props', () => { | ||
const { getByText } = render(<ButtonBase>Default Button</ButtonBase>); | ||
expect(getByText('Default Button')).not.toBeNull(); | ||
}); | ||
|
||
it('disables interaction when `isDisabled` is true', () => { | ||
const onPressMock = jest.fn(); | ||
const { getByText } = render( | ||
<ButtonBase isDisabled onPress={onPressMock}> | ||
Disabled Button | ||
</ButtonBase>, | ||
); | ||
|
||
const button = getByText('Disabled Button'); | ||
fireEvent.press(button); | ||
expect(onPressMock).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('shows loading spinner when `isLoading` is true', () => { | ||
const { getByTestId } = render( | ||
<ButtonBase isLoading loadingText="Loading..."> | ||
Default Button | ||
</ButtonBase>, | ||
); | ||
|
||
// Ensure the spinner is visible with the correct opacity | ||
const spinnerContainer = getByTestId('spinner-container'); | ||
expect(spinnerContainer.props.style.opacity).toBe(1); | ||
|
||
// Ensure the content container is hidden with the correct opacity | ||
const contentContainer = getByTestId('content-container'); | ||
expect(contentContainer.props.style.opacity).toBe(0); | ||
}); | ||
|
||
it('renders start and end icons correctly', () => { | ||
const { getByTestId } = render( | ||
<ButtonBase | ||
startIconName={IconName.Add} | ||
endIconName={IconName.ArrowRight} | ||
> | ||
Button with Icons | ||
</ButtonBase>, | ||
); | ||
|
||
expect(getByTestId('content-container')).not.toBeNull(); | ||
}); | ||
|
||
it('triggers onPress when clicked', () => { | ||
const onPressMock = jest.fn(); | ||
const { getByText } = render( | ||
<ButtonBase onPress={onPressMock}>Press Me</ButtonBase>, | ||
); | ||
|
||
const button = getByText('Press Me'); | ||
fireEvent.press(button); | ||
expect(onPressMock).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.