generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: adding button icon to design system react #387
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0ba620e
chore: adding button icon to design system react
georgewrmarshall 5b5c7f8
chore: fixing interative styles when disabled
georgewrmarshall f5b4dd0
chore: updating constant names to be consistent with other components
georgewrmarshall 4fe9ff9
chore: import icon name in docs
georgewrmarshall dfffc10
chore: updating floating button to be default if is inverse
georgewrmarshall 7fb04cc
chore: updating test
georgewrmarshall 01f3a09
chore: updating is inverse story
georgewrmarshall File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
packages/design-system-react/src/components/button-icon/ButtonIcon.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,14 @@ | ||
import { IconSize } from '../icon'; | ||
import { ButtonIconSize } from './ButtonIcon.types'; | ||
|
||
export const BUTTON_ICON_SIZE_CLASS_MAP = { | ||
[ButtonIconSize.Sm]: 'h-6 w-6', | ||
[ButtonIconSize.Md]: 'h-8 w-8', | ||
[ButtonIconSize.Lg]: 'h-10 w-10', | ||
} as const; | ||
|
||
export const BUTTON_ICON_SIZE_TO_ICON_SIZE_CLASS_MAP = { | ||
[ButtonIconSize.Sm]: IconSize.Sm, | ||
[ButtonIconSize.Md]: IconSize.Md, | ||
[ButtonIconSize.Lg]: IconSize.Lg, | ||
} as const; |
118 changes: 118 additions & 0 deletions
118
packages/design-system-react/src/components/button-icon/ButtonIcon.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,118 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import React from 'react'; | ||
|
||
import { IconName } from '..'; | ||
import { ButtonIcon } from './ButtonIcon'; | ||
import { ButtonIconSize } from './ButtonIcon.types'; | ||
import README from './README.mdx'; | ||
|
||
const meta: Meta<typeof ButtonIcon> = { | ||
title: 'React Components/ButtonIcon', | ||
component: ButtonIcon, | ||
parameters: { | ||
docs: { | ||
page: README, | ||
}, | ||
}, | ||
argTypes: { | ||
iconName: { | ||
control: 'select', | ||
options: Object.keys(IconName), | ||
mapping: IconName, | ||
description: 'Required prop to specify the icon to show', | ||
}, | ||
size: { | ||
control: 'select', | ||
options: Object.keys(ButtonIconSize), | ||
mapping: ButtonIconSize, | ||
description: 'Optional prop to control the size of the ButtonIcon', | ||
}, | ||
isDisabled: { | ||
control: 'boolean', | ||
description: 'Optional prop that when true, disables the button', | ||
}, | ||
isInverse: { | ||
control: 'boolean', | ||
description: | ||
'Optional prop that when true, applies inverse styling to the button', | ||
}, | ||
isFloating: { | ||
control: 'boolean', | ||
description: | ||
'Optional prop that when true, applies floating/contained styling to the button', | ||
}, | ||
className: { | ||
control: 'text', | ||
description: | ||
'Optional prop for additional CSS classes to be applied to the ButtonIcon', | ||
}, | ||
ariaLabel: { | ||
control: 'text', | ||
description: | ||
'Required prop to provide an accessible label for the button', | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof ButtonIcon>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
iconName: IconName.Close, | ||
ariaLabel: 'Close', | ||
}, | ||
}; | ||
|
||
export const Size: Story = { | ||
render: () => ( | ||
<div className="flex gap-2"> | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
size={ButtonIconSize.Sm} | ||
ariaLabel="Close small" | ||
/> | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
size={ButtonIconSize.Md} | ||
ariaLabel="Close medium" | ||
/> | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
size={ButtonIconSize.Lg} | ||
ariaLabel="Close large" | ||
/> | ||
</div> | ||
), | ||
}; | ||
|
||
export const IsFloating: Story = { | ||
render: () => ( | ||
<div className="flex gap-2"> | ||
<ButtonIcon iconName={IconName.Close} isFloating ariaLabel="Close" /> | ||
</div> | ||
), | ||
}; | ||
|
||
export const IsInverse: Story = { | ||
render: () => ( | ||
<div className="bg-primary-default p-4"> | ||
<ButtonIcon iconName={IconName.Close} isInverse ariaLabel="Close" /> | ||
</div> | ||
), | ||
}; | ||
|
||
export const IsDisabled: Story = { | ||
args: { | ||
iconName: IconName.Close, | ||
isDisabled: true, | ||
ariaLabel: 'Close', | ||
}, | ||
}; | ||
|
||
export const AriaLabel: Story = { | ||
args: { | ||
iconName: IconName.Close, | ||
ariaLabel: 'Close dialog', | ||
}, | ||
}; |
149 changes: 149 additions & 0 deletions
149
packages/design-system-react/src/components/button-icon/ButtonIcon.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,149 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import React from 'react'; | ||
|
||
import { IconName, IconSize } from '..'; | ||
import { ButtonIcon } from './ButtonIcon'; | ||
import { ButtonIconSize } from './ButtonIcon.types'; | ||
|
||
describe('ButtonIcon', () => { | ||
it('renders with default props', () => { | ||
render(<ButtonIcon iconName={IconName.Close} ariaLabel="Close" />); | ||
const button = screen.getByRole('button', { name: 'Close' }); | ||
expect(button).toHaveClass( | ||
'h-8', | ||
'w-8', | ||
'rounded', | ||
'bg-transparent', | ||
'hover:bg-hover', | ||
'active:bg-pressed', | ||
'text-icon-default', | ||
); | ||
}); | ||
|
||
it('renders with different sizes', () => { | ||
const { rerender } = render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
size={ButtonIconSize.Sm} | ||
ariaLabel="Close small" | ||
iconProps={{ 'data-testid': 'button-icon' }} | ||
/>, | ||
); | ||
expect(screen.getByRole('button')).toHaveClass('h-6', 'w-6'); | ||
const icon = screen.getByTestId('button-icon'); | ||
expect(icon).toHaveClass('text-inherit'); | ||
|
||
rerender( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
size={ButtonIconSize.Md} | ||
ariaLabel="Close medium" | ||
iconProps={{ 'data-testid': 'button-icon' }} | ||
/>, | ||
); | ||
expect(screen.getByRole('button')).toHaveClass('h-8', 'w-8'); | ||
|
||
rerender( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
size={ButtonIconSize.Lg} | ||
ariaLabel="Close large" | ||
iconProps={{ 'data-testid': 'button-icon' }} | ||
/>, | ||
); | ||
expect(screen.getByRole('button')).toHaveClass('h-10', 'w-10'); | ||
}); | ||
|
||
it('applies floating styles correctly', () => { | ||
render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
isFloating | ||
ariaLabel="Close floating" | ||
/>, | ||
); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveClass( | ||
'rounded-full', | ||
'bg-icon-default', | ||
'text-background-default', | ||
); | ||
}); | ||
|
||
it('applies inverse styles correctly', () => { | ||
render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
isInverse | ||
ariaLabel="Close inverse" | ||
/>, | ||
); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveClass('text-background-default'); | ||
}); | ||
|
||
it('applies disabled styles correctly', () => { | ||
render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
isDisabled | ||
ariaLabel="Close disabled" | ||
/>, | ||
); | ||
const button = screen.getByRole('button'); | ||
expect(button).toBeDisabled(); | ||
expect(button).toHaveClass('opacity-50', 'cursor-not-allowed'); | ||
expect(button).not.toHaveClass('hover:bg-hover', 'active:bg-pressed'); | ||
}); | ||
|
||
it('merges custom className with default styles', () => { | ||
render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
className="custom-class" | ||
ariaLabel="Close custom" | ||
/>, | ||
); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveClass('custom-class'); | ||
}); | ||
|
||
it('passes additional iconProps correctly', () => { | ||
render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
iconProps={{ | ||
className: 'custom-icon-class', | ||
'data-testid': 'custom-icon', | ||
}} | ||
ariaLabel="Close with custom icon" | ||
/>, | ||
); | ||
const icon = screen.getByTestId('custom-icon'); | ||
expect(icon).toHaveClass('custom-icon-class'); | ||
}); | ||
|
||
it('applies aria-label correctly', () => { | ||
render(<ButtonIcon iconName={IconName.Close} ariaLabel="Close dialog" />); | ||
expect( | ||
screen.getByRole('button', { name: 'Close dialog' }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('applies floating and inverse styles correctly when both are true', () => { | ||
render( | ||
<ButtonIcon | ||
iconName={IconName.Close} | ||
isFloating | ||
isInverse | ||
ariaLabel="Close floating inverse" | ||
/>, | ||
); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveClass( | ||
'rounded-full', | ||
'bg-icon-default', | ||
'text-background-default', | ||
); | ||
}); | ||
}); |
72 changes: 72 additions & 0 deletions
72
packages/design-system-react/src/components/button-icon/ButtonIcon.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,72 @@ | ||
import React from 'react'; | ||
|
||
import { twMerge } from '../../utils/tw-merge'; | ||
import { ButtonBase } from '../button-base'; | ||
import { Icon } from '../icon'; | ||
import { | ||
BUTTON_ICON_SIZE_CLASS_MAP, | ||
BUTTON_ICON_SIZE_TO_ICON_SIZE_CLASS_MAP, | ||
} from './ButtonIcon.constants'; | ||
import type { ButtonIconProps } from './ButtonIcon.types'; | ||
import { ButtonIconSize } from './ButtonIcon.types'; | ||
|
||
export const ButtonIcon = React.forwardRef<HTMLButtonElement, ButtonIconProps>( | ||
( | ||
{ | ||
className, | ||
iconName, | ||
iconProps, | ||
ariaLabel, | ||
isDisabled = false, | ||
isInverse = false, | ||
isFloating = false, | ||
size = ButtonIconSize.Md, | ||
style, | ||
...props | ||
}, | ||
ref, | ||
) => { | ||
const isInteractive = !isDisabled; | ||
|
||
const mergedClassName = twMerge( | ||
// Base styles | ||
'p-0', | ||
// Size styles | ||
BUTTON_ICON_SIZE_CLASS_MAP[size], | ||
// Floating styles | ||
isFloating && [ | ||
'rounded-full', | ||
!isInverse && 'bg-icon-default text-background-default', | ||
isInverse && 'bg-icon-default text-background-default', | ||
], | ||
Comment on lines
+37
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Continue with no hover for |
||
// Non-floating styles | ||
!isFloating && [ | ||
'rounded bg-transparent ', | ||
// Only apply hover/active styles when interactive | ||
isInteractive && 'hover:bg-hover active:bg-pressed', | ||
!isInverse && 'text-icon-default', | ||
isInverse && 'text-background-default', | ||
], | ||
className, | ||
); | ||
|
||
return ( | ||
<ButtonBase | ||
ref={ref} | ||
className={mergedClassName} | ||
isDisabled={isDisabled} | ||
aria-label={ariaLabel} | ||
{...props} | ||
> | ||
<Icon | ||
name={iconName} | ||
size={BUTTON_ICON_SIZE_TO_ICON_SIZE_CLASS_MAP[size]} | ||
className={twMerge('text-inherit', iconProps?.className)} | ||
{...iconProps} | ||
/> | ||
</ButtonBase> | ||
); | ||
}, | ||
); | ||
|
||
ButtonIcon.displayName = 'ButtonIcon'; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updates
isFloating
andisInverse
to default toisFloating
.This story has been updated to no longer show the
isInverse, isFloating
button to avoid confusion.