-
-
Notifications
You must be signed in to change notification settings - Fork 11
feat: Add Tag component for React Native #1053
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
Changes from all commits
2524b7c
31473dc
4b4bc84
55a1aac
98b6cd0
338b885
087b91e
e561a9d
0aca470
6e60b43
6ee5cf7
55754c2
3b6ee42
5399006
38d1a14
5ac9e7a
6d575c8
a409bac
cdbff40
7dd19be
aa96b26
c464af6
7d5d89b
ca8046c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| # Tag | ||
|
|
||
| A tag is a compact, non-interactive or interactive label used to categorize, annotate, or highlight metadata. Tags help users quickly scan, filter, and understand content relationships at a glance. | ||
|
|
||
| **Figma:** [MMDS Components — Tag](https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12339-1167) | ||
|
|
||
| ```tsx | ||
| import { Tag } from '@metamask/design-system-react-native'; | ||
|
|
||
| <Tag>Default Example</Tag>; | ||
| ``` | ||
|
|
||
| ## Props | ||
|
|
||
| ### `severity` | ||
|
|
||
| Semantic emphasis for background, string child text color, and icons. Values use `TagSeverity` from `@metamask/design-system-shared` (same pattern as `BannerAlert` / `IconAlert`). | ||
|
|
||
| Available values: | ||
|
|
||
| - `TagSeverity.Neutral` | ||
| - `TagSeverity.Success` | ||
| - `TagSeverity.Error` | ||
| - `TagSeverity.Warning` | ||
| - `TagSeverity.Info` | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | -------------- | -------- | --------------------- | | ||
| | `TagSeverity?` | No | `TagSeverity.Neutral` | | ||
|
|
||
| ```tsx | ||
| import { Tag } from '@metamask/design-system-react-native'; | ||
| import { TagSeverity } from '@metamask/design-system-shared'; | ||
|
|
||
| <Tag severity={TagSeverity.Success}>Success</Tag> | ||
| <Tag severity={TagSeverity.Error}>Error</Tag> | ||
| ``` | ||
|
|
||
| ### `children` | ||
|
|
||
| Main content. String children are rendered with design-system `Text` (`BodyXs`, medium weight, severity-based color). Other React nodes are rendered as-is (use your own `Text` or layout inside when needed). | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | ----------- | -------- | ----------- | | ||
| | `ReactNode` | No | `undefined` | | ||
|
|
||
| ```tsx | ||
| import { Tag } from '@metamask/design-system-react-native'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| <Tag> | ||
| <Text>Custom children content</Text> | ||
| </Tag>; | ||
| ``` | ||
|
|
||
| ### `startIconName` / `endIconName` | ||
|
|
||
| Optional `IconName` for small icons at the start or end of the tag (`IconSize.Xs` by default). Prefer these when using built-in icons; use `startIconProps` / `endIconProps` for overrides (including `name` instead of `startIconName` / `endIconName`). | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | ----------- | -------- | ----------- | | ||
| | `IconName?` | No | `undefined` | | ||
|
|
||
| ```tsx | ||
| import { IconName, Tag } from '@metamask/design-system-react-native'; | ||
|
|
||
| <Tag startIconName={IconName.Warning}>With start icon</Tag> | ||
| <Tag endIconName={IconName.ArrowRight}>With end icon</Tag> | ||
| ``` | ||
|
|
||
| ### `startIconProps` / `endIconProps` | ||
|
|
||
| Optional partial `IconProps` passed through to the underlying `Icon`. You may set `name` here instead of `startIconName` / `endIconName`. | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | --------------------- | -------- | ----------- | | ||
| | `Partial<IconProps>?` | No | `undefined` | | ||
|
|
||
| ### `startAccessory` / `endAccessory` | ||
|
|
||
| Optional React nodes shown when no start/end icon is resolved (e.g. custom glyph or badge). Icons take precedence when `startIconName` / `endIconName` (or `name` in icon props) is set. | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | ----------- | -------- | ----------- | | ||
| | `ReactNode` | No | `undefined` | | ||
|
|
||
| ```tsx | ||
| import { Tag } from '@metamask/design-system-react-native'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| <Tag startAccessory={<Text>→</Text>}>With accessory</Tag>; | ||
| ``` | ||
|
|
||
| ### `testID` | ||
|
|
||
| Test identifier for selecting the component in tests. | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | -------- | -------- | ----------- | | ||
| | `string` | No | `undefined` | | ||
|
|
||
| ### `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 `twMerge`, 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 | ||
| import { Tag } from '@metamask/design-system-react-native'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| // Add additional styles | ||
| <Tag twClassName="mt-4"> | ||
| <Text>Custom Background</Text> | ||
| </Tag> | ||
|
|
||
| // Override default styles | ||
| <Tag twClassName="bg-error-default"> | ||
| <Text>Override Background</Text> | ||
| </Tag> | ||
| ``` | ||
|
|
||
| ### `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. | ||
|
|
||
| | TYPE | REQUIRED | DEFAULT | | ||
| | ---------------------- | -------- | ----------- | | ||
| | `StyleProp<ViewStyle>` | No | `undefined` | | ||
|
|
||
| ```tsx | ||
| import { useTailwind } from '@metamask/design-system-twrnc-preset'; | ||
| import { Tag } from '@metamask/design-system-react-native'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| export const ConditionalExample = ({ isActive }: { isActive: boolean }) => { | ||
| const tw = useTailwind(); | ||
|
|
||
| return ( | ||
| <Tag style={tw.style('bg-default', isActive && 'bg-success-default')}> | ||
| <Text>Conditional styling</Text> | ||
| </Tag> | ||
| ); | ||
| }; | ||
| ``` | ||
|
|
||
| ## Accessibility | ||
|
|
||
| - String `children` are rendered with design-system typography inside the layout row, so assistive technologies can treat visible label text like normal copy. Prefer clear, concise labels; do not rely on color or icons alone to convey meaning. | ||
| - Icons are decorative unless your app assigns accessibility labels on the underlying `Icon` via `startIconProps` / `endIconProps` when needed. | ||
| - `testID` is intended for automated tests only; it does not replace accessible names for users. | ||
|
|
||
| ## References | ||
|
|
||
| [MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { TagSeverity, TextColor } from '@metamask/design-system-shared'; | ||
|
|
||
| import { BoxBackgroundColor, IconColor } from '../../types'; | ||
|
|
||
| export const MAP_TAG_SEVERITY_BACKGROUND: Record< | ||
| TagSeverity, | ||
| BoxBackgroundColor | ||
| > = { | ||
| [TagSeverity.Neutral]: BoxBackgroundColor.BackgroundMuted, | ||
| [TagSeverity.Success]: BoxBackgroundColor.SuccessMuted, | ||
| [TagSeverity.Error]: BoxBackgroundColor.ErrorMuted, | ||
| [TagSeverity.Warning]: BoxBackgroundColor.WarningMuted, | ||
| [TagSeverity.Info]: BoxBackgroundColor.InfoMuted, | ||
| }; | ||
|
|
||
| export const MAP_TAG_SEVERITY_TEXT_COLOR: Record<TagSeverity, TextColor> = { | ||
| [TagSeverity.Neutral]: TextColor.TextDefault, | ||
| [TagSeverity.Success]: TextColor.SuccessDefault, | ||
| [TagSeverity.Error]: TextColor.ErrorDefault, | ||
| [TagSeverity.Warning]: TextColor.WarningDefault, | ||
| [TagSeverity.Info]: TextColor.InfoDefault, | ||
| }; | ||
|
|
||
| export const MAP_TAG_SEVERITY_ICON_COLOR: Record<TagSeverity, IconColor> = { | ||
| [TagSeverity.Neutral]: IconColor.IconDefault, | ||
| [TagSeverity.Success]: IconColor.SuccessDefault, | ||
| [TagSeverity.Error]: IconColor.ErrorDefault, | ||
| [TagSeverity.Warning]: IconColor.WarningDefault, | ||
| [TagSeverity.Info]: IconColor.InfoDefault, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // import figma needs to remain as figma otherwise it breaks code connect | ||
| // eslint-disable-next-line import-x/no-named-as-default | ||
| import figma from '@figma/code-connect'; | ||
| import { TagSeverity } from '@metamask/design-system-shared'; | ||
| import React from 'react'; | ||
|
|
||
| import { IconName } from '../Icon'; | ||
|
|
||
| import { Tag } from './Tag'; | ||
|
|
||
| /** | ||
| * -- This file was auto-generated by Code Connect -- | ||
| * React Native implementation of Tag (`figma.connect` for Figma Dev Mode). | ||
| * | ||
| * [MMDS Tag in Figma](https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12339-6553) | ||
| * | ||
| * Root Figma props: `severity`, `state`, `icons`. The first argument to each `figma.*` | ||
| * helper must match Dev Mode property names exactly. | ||
| * | ||
| * - **`state`** (default / hover / pressed) is visual-only in Figma; `Tag` has no matching prop yet. | ||
| * - **`icons`** drives `startIconName` / `endIconName` with a placeholder icon (`IconName.Tag`). | ||
| * - Label copy comes from the nested **Label** instance (`Label text`), same pattern as `Checkbox.figma.tsx`. | ||
| */ | ||
| figma.connect( | ||
| Tag, | ||
| 'https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12339-6553', | ||
| { | ||
| props: { | ||
| severity: figma.enum('severity', { | ||
| neutral: TagSeverity.Neutral, | ||
| error: TagSeverity.Error, | ||
| info: TagSeverity.Info, | ||
| success: TagSeverity.Success, | ||
| warning: TagSeverity.Warning, | ||
| }), | ||
| state: figma.enum('state', { | ||
| default: 'default', | ||
| hover: 'hover', | ||
| pressed: 'pressed', | ||
| }), | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| icons: figma.enum('icons', { | ||
| none: 'none', | ||
| start: 'start', | ||
| 'start & end': 'both', | ||
| }), | ||
| label: figma.nestedProps('Label', { | ||
| text: figma.string('Label text'), | ||
| }), | ||
| }, | ||
| example: ({ severity, state: _figmaState, icons, label }) => { | ||
| const startIconName = | ||
| icons === 'start' || icons === 'both' ? IconName.Tag : undefined; | ||
| const endIconName = icons === 'both' ? IconName.Tag : undefined; | ||
|
|
||
| return ( | ||
| <Tag | ||
| severity={severity} | ||
| startIconName={startIconName} | ||
| endIconName={endIconName} | ||
| > | ||
| {label.text} | ||
| </Tag> | ||
| ); | ||
| }, | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { TagSeverity } from '@metamask/design-system-shared'; | ||
| import type { Meta, StoryObj } from '@storybook/react-native'; | ||
| import React from 'react'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| import { IconName } from '../Icon'; | ||
|
|
||
| import { Tag } from './Tag'; | ||
| import type { TagProps } from './Tag.types'; | ||
|
|
||
| const meta: Meta<TagProps> = { | ||
| title: 'Components/Tag', | ||
| component: Tag, | ||
| parameters: {}, | ||
| argTypes: { | ||
| severity: { | ||
| control: 'select', | ||
| options: Object.values(TagSeverity), | ||
| }, | ||
| children: { | ||
| control: 'text', | ||
| }, | ||
| startIconName: { | ||
| control: 'select', | ||
| options: Object.values(IconName), | ||
| }, | ||
| endIconName: { | ||
| control: 'select', | ||
| options: Object.values(IconName), | ||
| }, | ||
| }, | ||
| }; | ||
|
Contributor
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. Per our component documentation standards, the first story must always be const meta: Meta<typeof Tag> = {
title: 'Components/Tag',
component: Tag,
argTypes: {
severity: {
control: 'select',
options: Object.values(TagSeverity),
},
startIconName: { control: 'select', options: Object.values(IconName) },
endIconName: { control: 'select', options: Object.values(IconName) },
},
};
export const Default: Story = {
args: {
children: 'Tag',
severity: TagSeverity.Neutral,
},
};non-blocking:
Contributor
Author
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. |
||
|
|
||
| export default meta; | ||
| type Story = StoryObj<TagProps>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| children: 'Tag', | ||
| severity: TagSeverity.Neutral, | ||
| }, | ||
| }; | ||
|
|
||
| export const Severity: Story = { | ||
| render: () => ( | ||
| <> | ||
| <Tag severity={TagSeverity.Neutral}>Neutral</Tag> | ||
| <Tag severity={TagSeverity.Success} twClassName="mt-2"> | ||
| Success | ||
| </Tag> | ||
| <Tag severity={TagSeverity.Error} twClassName="mt-2"> | ||
| Error | ||
| </Tag> | ||
| <Tag severity={TagSeverity.Warning} twClassName="mt-2"> | ||
| Warning | ||
| </Tag> | ||
| <Tag severity={TagSeverity.Info} twClassName="mt-2"> | ||
| Info | ||
| </Tag> | ||
| </> | ||
| ), | ||
| }; | ||
|
|
||
| export const StartIconName: Story = { | ||
| render: () => <Tag startIconName={IconName.Warning}>Tag</Tag>, | ||
| }; | ||
|
|
||
| export const EndIconName: Story = { | ||
| render: () => <Tag endIconName={IconName.ArrowRight}>Tag</Tag>, | ||
| }; | ||
|
|
||
| export const StartAccessory: Story = { | ||
| render: () => ( | ||
| <Tag startAccessory={<Text testID="tag-story-start-accessory">→</Text>}> | ||
| Tag | ||
| </Tag> | ||
| ), | ||
| }; | ||
|
|
||
| export const EndAccessory: Story = { | ||
| render: () => ( | ||
| <Tag endAccessory={<Text testID="tag-story-end-accessory">←</Text>}> | ||
| Tag | ||
| </Tag> | ||
| ), | ||
| }; | ||
|
|
||
| export const StartAndEndIconNames: Story = { | ||
| render: () => ( | ||
| <Tag startIconName={IconName.Warning} endIconName={IconName.ArrowRight}> | ||
| Tag | ||
| </Tag> | ||
| ), | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
Added this so there is better connection between code:design. It'll help with maintainability in the long term. I can remove it if this is premature at this stage of our DS.
If/when this PR is approved, I'll need to add the code link in Figma.