Skip to content

Commit e1021e7

Browse files
authored
Add leadingVisual to InlineMessage (#7247)
1 parent d973c5a commit e1021e7

File tree

5 files changed

+115
-4
lines changed

5 files changed

+115
-4
lines changed

.changeset/giant-loops-send.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Add leadingVisual to InlineMessage component.

packages/react/src/InlineMessage/InlineMessage.docs.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
"description": "Specify the type of the inline message",
3838
"type": "'critical' | 'success' | 'unvailable' | 'warning'",
3939
"required": true
40+
},
41+
{
42+
"name": "leadingVisual",
43+
"description": "A custom leading visual to display instead of the default variant icon.",
44+
"type": "React.ElementType | React.ReactNode",
45+
"required": false
4046
}
4147
]
42-
}
48+
}

packages/react/src/InlineMessage/InlineMessage.stories.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
import type {Meta, StoryObj} from '@storybook/react-vite'
2+
import {
3+
AlertIcon,
4+
CheckCircleIcon,
5+
InfoIcon,
6+
LockIcon,
7+
RocketIcon,
8+
XCircleIcon,
9+
HeartIcon,
10+
StarIcon,
11+
} from '@primer/octicons-react'
212
import {InlineMessage} from '../InlineMessage'
313

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

25+
const iconMap = {
26+
default: undefined,
27+
InfoIcon,
28+
LockIcon,
29+
RocketIcon,
30+
AlertIcon,
31+
CheckCircleIcon,
32+
XCircleIcon,
33+
HeartIcon,
34+
StarIcon,
35+
} as const
36+
1537
export const Playground: StoryObj<typeof InlineMessage> = {
1638
render(args) {
17-
return <InlineMessage {...args}>An example inline message</InlineMessage>
39+
const {leadingVisual: leadingVisualOption, ...rest} = args
40+
const leadingVisual = leadingVisualOption ? iconMap[leadingVisualOption as keyof typeof iconMap] : undefined
41+
return (
42+
<InlineMessage {...rest} leadingVisual={leadingVisual}>
43+
An example inline message
44+
</InlineMessage>
45+
)
1846
},
1947
argTypes: {
2048
size: {
@@ -29,9 +57,18 @@ export const Playground: StoryObj<typeof InlineMessage> = {
2957
},
3058
options: ['critical', 'success', 'unavailable', 'warning'],
3159
},
60+
leadingVisual: {
61+
name: 'leadingVisual',
62+
control: {
63+
type: 'select',
64+
},
65+
options: Object.keys(iconMap),
66+
description: 'Select a custom icon to override the default variant icon',
67+
},
3268
},
3369
args: {
3470
size: 'medium',
3571
variant: 'success',
72+
leadingVisual: 'default',
3673
},
3774
}

packages/react/src/InlineMessage/InlineMessage.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {render, screen} from '@testing-library/react'
22
import {describe, expect, it, test} from 'vitest'
3+
import {InfoIcon} from '@primer/octicons-react'
34
import {InlineMessage} from '../InlineMessage'
5+
import React from 'react'
46

57
describe('InlineMessage', () => {
68
it('should render content passed as `children`', () => {
@@ -79,4 +81,41 @@ describe('InlineMessage', () => {
7981
)
8082
expect(screen.getByTestId('container')).toHaveAttribute('data-variant', 'warning')
8183
})
84+
85+
it('should render leading visual', () => {
86+
render(
87+
<>
88+
<InlineMessage variant="critical" leadingVisual={<InfoIcon data-testid="info-icon" />}>
89+
test with custom icon
90+
</InlineMessage>
91+
<InlineMessage
92+
variant="critical"
93+
leadingVisual={React.memo(() => (
94+
<div data-testid="memo">leadingVisual</div>
95+
))}
96+
>
97+
test with memo icon
98+
</InlineMessage>
99+
<InlineMessage
100+
variant="critical"
101+
leadingVisual={React.forwardRef(() => (
102+
<div data-testid="forward-ref">leadingVisual</div>
103+
))}
104+
>
105+
test with forward ref icon
106+
</InlineMessage>
107+
</>,
108+
)
109+
expect(screen.getByTestId('info-icon')).toBeInTheDocument()
110+
expect(screen.getByTestId('memo')).toBeInTheDocument()
111+
expect(screen.getByTestId('forward-ref')).toBeInTheDocument()
112+
})
113+
114+
it('should use default icon when `leadingVisual` is not provided', () => {
115+
const {container} = render(<InlineMessage variant="success">test with default icon</InlineMessage>)
116+
expect(screen.getByText('test with default icon')).toBeInTheDocument()
117+
// Default icon should be rendered
118+
const svg = container.querySelector('svg')
119+
expect(svg).toBeInTheDocument()
120+
})
82121
})

packages/react/src/InlineMessage/InlineMessage.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {AlertFillIcon, AlertIcon, CheckCircleFillIcon, CheckCircleIcon} from '@primer/octicons-react'
22
import {clsx} from 'clsx'
33
import type React from 'react'
4+
import {isValidElementType} from 'react-is'
45
import classes from './InlineMessage.module.css'
56
type MessageVariant = 'critical' | 'success' | 'unavailable' | 'warning'
67

@@ -14,6 +15,11 @@ export type InlineMessageProps = React.ComponentPropsWithoutRef<'div'> & {
1415
* Specify the type of the InlineMessage
1516
*/
1617
variant: MessageVariant
18+
19+
/**
20+
* A custom leading visual (icon or other element) to display instead of the default variant icon.
21+
*/
22+
leadingVisual?: React.ElementType | React.ReactNode
1723
}
1824

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

33-
export function InlineMessage({children, className, size = 'medium', variant, ...rest}: InlineMessageProps) {
34-
const icon = size === 'small' ? smallIcons[variant] : icons[variant]
39+
export function InlineMessage({
40+
children,
41+
className,
42+
size = 'medium',
43+
variant,
44+
leadingVisual: LeadingVisual,
45+
...rest
46+
}: InlineMessageProps) {
47+
let icon: React.ReactNode
48+
49+
if (LeadingVisual !== undefined) {
50+
if (typeof LeadingVisual !== 'string' && isValidElementType(LeadingVisual)) {
51+
icon = <LeadingVisual className={classes.InlineMessageIcon} />
52+
} else {
53+
icon = LeadingVisual
54+
}
55+
} else {
56+
// Use default icon based on variant and size
57+
icon = size === 'small' ? smallIcons[variant] : icons[variant]
58+
}
3559

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

0 commit comments

Comments
 (0)