-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add Badge component to UI package #73875
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
b6377a1
46d619e
009a83f
ead071b
46aa19e
920b9dc
f3b2f13
baaebe0
5ed8020
28e31d9
9f3fdfa
50049b8
c319801
ab5c1a7
f3bdae6
a86f883
f7b0765
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,98 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { forwardRef } from '@wordpress/element'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { Box } from '../box'; | ||
| import { type BoxProps } from '../box/types'; | ||
| import { type BadgeProps } from './types'; | ||
|
|
||
| /** | ||
| * Default render function that renders a span element with the given props. | ||
| * | ||
| * @param props The props to apply to the HTML element. | ||
| */ | ||
| const DEFAULT_RENDER = ( props: React.ComponentPropsWithoutRef< 'span' > ) => ( | ||
| <span { ...props } /> | ||
| ); | ||
|
|
||
| /** | ||
| * Maps intent values to Box backgroundColor and color props. | ||
| * Uses strong emphasis styles (as emphasis prop has been removed). | ||
| * @param intent | ||
| */ | ||
| const getIntentStyles = ( | ||
| intent: BadgeProps[ 'intent' ] | ||
| ): Partial< BoxProps > => { | ||
| switch ( intent ) { | ||
| case 'high': | ||
| return { | ||
| backgroundColor: 'error', | ||
| color: 'error', | ||
| }; | ||
| case 'medium': | ||
| return { | ||
| backgroundColor: 'warning', | ||
| color: 'warning', | ||
| }; | ||
| case 'low': | ||
| return { | ||
| backgroundColor: 'caution', | ||
| color: 'caution', | ||
| }; | ||
| case 'stable': | ||
| return { | ||
| backgroundColor: 'success', | ||
| color: 'success', | ||
| }; | ||
| case 'informational': | ||
| return { | ||
| backgroundColor: 'info', | ||
| color: 'info', | ||
| }; | ||
| case 'draft': | ||
| return { | ||
| backgroundColor: 'neutral-weak', | ||
| color: 'neutral', | ||
| }; | ||
| case 'none': | ||
| default: | ||
| return { | ||
| backgroundColor: 'neutral', | ||
| color: 'neutral-weak', | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * A badge component for displaying labels with semantic intent. | ||
| * Built on the Box primitive for consistent theming and accessibility. | ||
| */ | ||
| export const Badge = forwardRef< HTMLDivElement, BadgeProps >( function Badge( | ||
| { children, intent = 'none', render = DEFAULT_RENDER, ...props }, | ||
| ref | ||
| ) { | ||
| const intentStyles = getIntentStyles( intent ); | ||
|
|
||
| return ( | ||
| <Box | ||
| { ...intentStyles } | ||
| padding={ { inline: 'xs', block: '2xs' } } | ||
| borderRadius="lg" | ||
| render={ render } | ||
| style={ { | ||
|
Member
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. I feel like we should use a stylesheet instead of inlining all these?
Member
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. I expected we'd be able to get rid of most of them through e.g. the There's a few that wouldn't be covered:
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.
Member
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. I agree with @mirka in having a light preference to a stylesheet since it's a simple shift and general preference, but honestly I think it's fine to just leave these since we'll refactor them away after |
||
| fontFamily: 'var(--wpds-font-family-body)', | ||
| fontSize: 'var(--wpds-font-size-small)', | ||
| fontWeight: '400', | ||
| lineHeight: 'var(--wpds-font-line-height-x-small)', | ||
| ...props.style, | ||
| } } | ||
| ref={ ref } | ||
| > | ||
| { children } | ||
| </Box> | ||
| ); | ||
| } ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { Badge } from './badge'; | ||
jameskoster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { Meta } from '@storybook/blocks'; | ||
| import { Badge } from '../index'; | ||
|
|
||
| <Meta title="Design System/Components/Badge/Choosing intent" /> | ||
|
|
||
| # Choosing intent | ||
|
|
||
| <div style={ { padding: '2rem', display: 'flex', justifyContent: 'center', gap: '0.5rem', flexWrap: 'wrap', border: '1px solid #e0e0e0', borderRadius: '0.5rem' } }> | ||
| <Badge intent="high">high</Badge> | ||
| <Badge intent="medium">medium</Badge> | ||
| <Badge intent="low">low</Badge> | ||
| <Badge intent="stable">stable</Badge> | ||
| <Badge intent="informational">informational</Badge> | ||
| <Badge intent="draft">draft</Badge> | ||
| <Badge intent="none">none</Badge> | ||
| </div> | ||
|
|
||
| It can be difficult to determine which badge intent to use because the component's properties are not tied to any specific product view. Those properties should be balanced against the requirements of the view in which the badge appears, all while keeping an eye on high-level consistency (global statuses that appear across multiple views). | ||
|
|
||
| Here is a decision tree to help identify which badge to use. | ||
|
|
||
| ## 1. Ask first: should this draw the eye? | ||
|
|
||
| If the user scans this screen, should their attention be drawn to this badge? | ||
|
|
||
| - If **no** → use `none` (or even just plain text; no badge), even if the state is positive or "stable". | ||
| - If **yes** → pick an intent based on how important the action or awareness is. | ||
|
|
||
| ## 2. High / Medium / Low = action priority | ||
|
|
||
| Use when there's something for the user to act on. | ||
|
|
||
| ### `high` – Critical / top priority | ||
|
|
||
| * Needs attention as soon as possible | ||
| * _E.g. "Payment declined", "Security issue"_ | ||
|
|
||
| <Badge intent="high">Payment declined</Badge> <Badge intent="high">Security issue</Badge> | ||
|
|
||
| ### `medium` – Important / blocks progress | ||
|
|
||
| * Blocks a key task, should be handled soon | ||
| * _E.g. "Approval required", "Review needed"_ | ||
|
|
||
| <Badge intent="medium">Approval required</Badge> <Badge intent="medium">Review needed</Badge> | ||
|
|
||
| ### `low` – Worth noticing / non‑urgent | ||
|
|
||
| * Good to be aware of; action may be optional or later | ||
| * _E.g. "Pending", "Queued", "Minor issues", "Optional setup"_ | ||
|
|
||
| <Badge intent="low">Pending</Badge> <Badge intent="low">Queued</Badge> | ||
|
|
||
| ## 3. Informational / draft = special non-final states | ||
|
|
||
| ### `informational` – Notable, no action / fix needed | ||
|
|
||
| * Context only; no clear action | ||
| * _E.g. "Scheduled", "Beta", "Internal only"_ | ||
|
|
||
| <Badge intent="informational">Scheduled</Badge> <Badge intent="informational">Beta</Badge> | ||
|
|
||
| ### `draft` – Not final / work in progress | ||
|
|
||
| * _E.g. "Draft", "Unpublished", "Work in progress"_ | ||
|
|
||
| <Badge intent="draft">Draft</Badge> <Badge intent="draft">Unpublished</Badge> | ||
|
|
||
| ## 4. Stable / none = normal states | ||
|
|
||
| ### `stable` – Positive / "healthy" state | ||
|
|
||
| * Use when confirming success or "all good" is important in that view | ||
| * _E.g. "Healthy", "Active", "Live"_ | ||
|
|
||
| <Badge intent="stable">Healthy</Badge> <Badge intent="stable">Active</Badge> | ||
|
|
||
| ### `none` – Default for normal / background states | ||
|
|
||
| * Especially in dense lists where too much color creates visual noise | ||
| * _E.g. "Inactive", "Expired"_ | ||
|
|
||
| <Badge intent="none">Inactive</Badge> <Badge intent="none">Expired</Badge> | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Comment status: | ||
|
|
||
| - "Approved" → `none`: <Badge intent="none">Approved</Badge> | ||
| - "Approval required" → `medium`: <Badge intent="medium">Approval required</Badge> | ||
|
|
||
| ### Page status: | ||
|
|
||
| - "Published" → `none`: <Badge intent="none">Published</Badge> | ||
| - "Pending" → `low`: <Badge intent="low">Pending</Badge> | ||
| - "Draft" → `draft`: <Badge intent="draft">Draft</Badge> | ||
| - "Scheduled" → `informational`: <Badge intent="informational">Scheduled</Badge> | ||
| - "Private" → `informational`: <Badge intent="informational">Private</Badge> | ||
|
|
||
| ### Plugin status: | ||
|
|
||
| - "Active" → `stable`: <Badge intent="stable">Active</Badge> | ||
| - "Inactive" → `none`: <Badge intent="none">Inactive</Badge> | ||
|
|
||
| ## 5. When in doubt… | ||
|
|
||
| Use the least attention‑grabbing intent that still: | ||
|
|
||
| - Makes it clear what needs attention, | ||
| - Marks what isn't final, or | ||
| - Confirms a key success state in that context. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { Fragment } from '@wordpress/element'; | ||
| import '@wordpress/theme/design-tokens.css'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { Badge } from '../index'; | ||
|
|
||
| const meta: Meta< typeof Badge > = { | ||
| title: 'Design System/Components/Badge', | ||
| component: Badge, | ||
| tags: [ 'status-experimental' ], | ||
| }; | ||
| export default meta; | ||
|
|
||
| type Story = StoryObj< typeof Badge >; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| children: 'Badge', | ||
| }, | ||
| }; | ||
|
|
||
| export const High: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'high', | ||
| }, | ||
| }; | ||
|
|
||
| export const Medium: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'medium', | ||
| }, | ||
| }; | ||
|
|
||
| export const Low: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'low', | ||
| }, | ||
| }; | ||
|
|
||
| export const Stable: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'stable', | ||
| }, | ||
| }; | ||
|
|
||
| export const Informational: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'informational', | ||
| }, | ||
| }; | ||
|
|
||
| export const Draft: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'draft', | ||
| }, | ||
| }; | ||
|
|
||
| export const None: Story = { | ||
| ...Default, | ||
| args: { | ||
| ...Default.args, | ||
| intent: 'none', | ||
| }, | ||
| }; | ||
|
|
||
| export const AllIntents: Story = { | ||
| ...Default, | ||
| render: ( args ) => ( | ||
| <div | ||
| style={ { | ||
| display: 'grid', | ||
| gridTemplateColumns: 'max-content min-content', | ||
| gap: '1rem', | ||
| color: 'var(--wpds-color-fg-content-neutral)', | ||
jameskoster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| backgroundColor: 'var(--wpds-color-bg-surface-neutral-strong)', | ||
| } } | ||
| > | ||
| { ( | ||
| [ | ||
| 'high', | ||
| 'medium', | ||
| 'low', | ||
| 'stable', | ||
| 'informational', | ||
| 'draft', | ||
| 'none', | ||
| ] as const | ||
| ).map( ( intent ) => ( | ||
| <Fragment key={ intent }> | ||
| <div | ||
| style={ { | ||
| paddingInlineEnd: '1rem', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| } } | ||
| > | ||
| { intent } | ||
| </div> | ||
| <div | ||
| style={ { | ||
| padding: '0.5rem 1rem', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| } } | ||
| > | ||
| <Badge { ...args } intent={ intent } /> | ||
| </div> | ||
| </Fragment> | ||
| ) ) } | ||
| </div> | ||
| ), | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { type ComponentProps } from '../utils/types'; | ||
|
|
||
| export interface BadgeProps extends ComponentProps< 'span' > { | ||
| /** | ||
| * The text to display in the badge. | ||
| */ | ||
| children: string; | ||
jameskoster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * The semantic intent of the badge, communicating its meaning through color. | ||
| * | ||
| * @default "none" | ||
| */ | ||
| intent?: | ||
| | 'high' | ||
| | 'medium' | ||
| | 'low' | ||
| | 'stable' | ||
| | 'informational' | ||
| | 'draft' | ||
| | 'none'; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export * from './box'; | ||
| export * from './badge'; | ||
| export * from './stack'; |
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.
I wonder if others would find it a little counterintuitive that the "weak" background is a darker gray 🤔 I guess it's in terms of "lightness", i.e. "stronger lightness" = lighter vs. "weaker lightness" = darker.
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.
Yeah there's a nuance here that doesn't really carry over without documentation.