Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-loops-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add leadingVisual to InlineMessage component.
8 changes: 7 additions & 1 deletion packages/react/src/InlineMessage/InlineMessage.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
"description": "Specify the type of the inline message",
"type": "'critical' | 'success' | 'unvailable' | 'warning'",
"required": true
},
{
"name": "leadingVisual",
"description": "A custom leading visual to display instead of the default variant icon.",
"type": "React.ElementType | React.ReactNode",
"required": false
}
]
}
}
39 changes: 38 additions & 1 deletion packages/react/src/InlineMessage/InlineMessage.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import type {Meta, StoryObj} from '@storybook/react-vite'
import {
AlertIcon,
CheckCircleIcon,
InfoIcon,
LockIcon,
RocketIcon,
XCircleIcon,
HeartIcon,
StarIcon,
} from '@primer/octicons-react'
import {InlineMessage} from '../InlineMessage'

const meta = {
Expand All @@ -12,9 +22,27 @@ export const Default = () => {
return <InlineMessage variant="unavailable">An example inline message</InlineMessage>
}

const iconMap = {
default: undefined,
InfoIcon,
LockIcon,
RocketIcon,
AlertIcon,
CheckCircleIcon,
XCircleIcon,
HeartIcon,
StarIcon,
} as const

export const Playground: StoryObj<typeof InlineMessage> = {
render(args) {
return <InlineMessage {...args}>An example inline message</InlineMessage>
const {leadingVisual: leadingVisualOption, ...rest} = args
const leadingVisual = leadingVisualOption ? iconMap[leadingVisualOption as keyof typeof iconMap] : undefined
return (
<InlineMessage {...rest} leadingVisual={leadingVisual}>
An example inline message
</InlineMessage>
)
},
argTypes: {
size: {
Expand All @@ -29,9 +57,18 @@ export const Playground: StoryObj<typeof InlineMessage> = {
},
options: ['critical', 'success', 'unavailable', 'warning'],
},
leadingVisual: {
name: 'leadingVisual',
control: {
type: 'select',
},
options: Object.keys(iconMap),
description: 'Select a custom icon to override the default variant icon',
},
},
args: {
size: 'medium',
variant: 'success',
leadingVisual: 'default',
},
}
39 changes: 39 additions & 0 deletions packages/react/src/InlineMessage/InlineMessage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {render, screen} from '@testing-library/react'
import {describe, expect, it, test} from 'vitest'
import {InfoIcon} from '@primer/octicons-react'
import {InlineMessage} from '../InlineMessage'
import React from 'react'

describe('InlineMessage', () => {
it('should render content passed as `children`', () => {
Expand Down Expand Up @@ -79,4 +81,41 @@ describe('InlineMessage', () => {
)
expect(screen.getByTestId('container')).toHaveAttribute('data-variant', 'warning')
})

it('should render leading visual', () => {
render(
<>
<InlineMessage variant="critical" leadingVisual={<InfoIcon data-testid="info-icon" />}>
test with custom icon
</InlineMessage>
<InlineMessage
variant="critical"
leadingVisual={React.memo(() => (
<div data-testid="memo">leadingVisual</div>
))}
>
test with memo icon
</InlineMessage>
<InlineMessage
variant="critical"
leadingVisual={React.forwardRef(() => (
<div data-testid="forward-ref">leadingVisual</div>
))}
>
test with forward ref icon
</InlineMessage>
</>,
)
expect(screen.getByTestId('info-icon')).toBeInTheDocument()
expect(screen.getByTestId('memo')).toBeInTheDocument()
expect(screen.getByTestId('forward-ref')).toBeInTheDocument()
})

it('should use default icon when `leadingVisual` is not provided', () => {
const {container} = render(<InlineMessage variant="success">test with default icon</InlineMessage>)
expect(screen.getByText('test with default icon')).toBeInTheDocument()
// Default icon should be rendered
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
})
28 changes: 26 additions & 2 deletions packages/react/src/InlineMessage/InlineMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {AlertFillIcon, AlertIcon, CheckCircleFillIcon, CheckCircleIcon} from '@primer/octicons-react'
import {clsx} from 'clsx'
import type React from 'react'
import {isValidElementType} from 'react-is'
import classes from './InlineMessage.module.css'
type MessageVariant = 'critical' | 'success' | 'unavailable' | 'warning'

Expand All @@ -14,6 +15,11 @@ export type InlineMessageProps = React.ComponentPropsWithoutRef<'div'> & {
* Specify the type of the InlineMessage
*/
variant: MessageVariant

/**
* A custom leading visual (icon or other element) to display instead of the default variant icon.
*/
leadingVisual?: React.ElementType | React.ReactNode
}

const icons: Record<MessageVariant, React.ReactNode> = {
Expand All @@ -30,8 +36,26 @@ const smallIcons: Record<MessageVariant, React.ReactNode> = {
unavailable: <AlertFillIcon className={classes.InlineMessageIcon} size={12} />,
}

export function InlineMessage({children, className, size = 'medium', variant, ...rest}: InlineMessageProps) {
const icon = size === 'small' ? smallIcons[variant] : icons[variant]
export function InlineMessage({
children,
className,
size = 'medium',
variant,
leadingVisual: LeadingVisual,
...rest
}: InlineMessageProps) {
let icon: React.ReactNode

if (LeadingVisual !== undefined) {
if (typeof LeadingVisual !== 'string' && isValidElementType(LeadingVisual)) {
icon = <LeadingVisual className={classes.InlineMessageIcon} />
} else {
icon = LeadingVisual
}
} else {
// Use default icon based on variant and size
icon = size === 'small' ? smallIcons[variant] : icons[variant]
}

return (
<div {...rest} className={clsx(className, classes.InlineMessage)} data-size={size} data-variant={variant}>
Expand Down
Loading