From e4023e8d8d4574a8e7318ab080daa426e330d2a8 Mon Sep 17 00:00:00 2001 From: Liu Liu Date: Wed, 26 Nov 2025 09:45:25 -0800 Subject: [PATCH 1/6] leadingvisual --- .../InlineMessage/InlineMessage.stories.tsx | 39 ++++++++++++++++++- .../src/InlineMessage/InlineMessage.test.tsx | 29 ++++++++++++++ .../react/src/InlineMessage/InlineMessage.tsx | 34 +++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/packages/react/src/InlineMessage/InlineMessage.stories.tsx b/packages/react/src/InlineMessage/InlineMessage.stories.tsx index 23d190488c1..a75f4bfec08 100644 --- a/packages/react/src/InlineMessage/InlineMessage.stories.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.stories.tsx @@ -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 = { @@ -12,9 +22,27 @@ export const Default = () => { return An example inline message } +const iconMap = { + default: undefined, + InfoIcon, + LockIcon, + RocketIcon, + AlertIcon, + CheckCircleIcon, + XCircleIcon, + HeartIcon, + StarIcon, +} as const + export const Playground: StoryObj = { render(args) { - return An example inline message + const {leadingVisual:leadingVisualOption, ...rest} = args + const leadingVisual = leadingVisualOption ? iconMap[leadingVisualOption as keyof typeof iconMap] : undefined + return ( + + An example inline message + + ) }, argTypes: { size: { @@ -29,9 +57,18 @@ export const Playground: StoryObj = { }, options: ['critical', 'success', 'unavailable', 'warning'], }, + leadingVisual: { + name: 'leadingVisual', + control: { + type: 'select', + }, + options: Object.keys(iconMap), + description: 'Select a custom icon or emoji to override the default variant icon', + }, }, args: { size: 'medium', variant: 'success', + leadingVisual: 'default', }, } diff --git a/packages/react/src/InlineMessage/InlineMessage.test.tsx b/packages/react/src/InlineMessage/InlineMessage.test.tsx index b7448da6b10..0b29e7fefcb 100644 --- a/packages/react/src/InlineMessage/InlineMessage.test.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.test.tsx @@ -1,5 +1,6 @@ import {render, screen} from '@testing-library/react' import {describe, expect, it, test} from 'vitest' +import {InfoIcon} from '@primer/octicons-react' import {InlineMessage} from '../InlineMessage' describe('InlineMessage', () => { @@ -79,4 +80,32 @@ describe('InlineMessage', () => { ) expect(screen.getByTestId('container')).toHaveAttribute('data-variant', 'warning') }) + + it('should render a custom icon when `leadingVisual` is a string', () => { + const {container} = render( + + test with emoji + , + ) + expect(screen.getByText('test with emoji')).toBeInTheDocument() + expect(container.textContent).toContain('🔒') + }) + + it('should render a custom JSX element when `leadingVisual` is a React element', () => { + render( + ⚠️}> + test with JSX element + , + ) + expect(screen.getByText('test with JSX element')).toBeInTheDocument() + expect(screen.getByTestId('custom-element')).toBeInTheDocument() + }) + + it('should use default icon when `leadingVisual` is not provided', () => { + const {container} = render(test with default icon) + expect(screen.getByText('test with default icon')).toBeInTheDocument() + // Default icon should be rendered + const svg = container.querySelector('svg') + expect(svg).toBeInTheDocument() + }) }) diff --git a/packages/react/src/InlineMessage/InlineMessage.tsx b/packages/react/src/InlineMessage/InlineMessage.tsx index 002f0577533..aa7d815e244 100644 --- a/packages/react/src/InlineMessage/InlineMessage.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.tsx @@ -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' @@ -14,6 +15,12 @@ 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. + * Can be a React component, JSX element, or string (e.g., emoji). + */ + leadingVisual?: React.ElementType | React.ReactNode } const icons: Record = { @@ -30,8 +37,31 @@ const smallIcons: Record = { unavailable: , } -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, + ...rest +}: InlineMessageProps) { + let icon: React.ReactNode + + if (leadingVisual !== undefined) { + if (typeof leadingVisual === 'string') { + icon = {leadingVisual} + } else if (isValidElementType(leadingVisual)) { + // Component type + const LeadingVisualComponent = leadingVisual + icon = + } else { + // JSX element - wrap it with the icon styling + icon = leadingVisual + } + } else { + // Use default icon based on variant and size + icon = size === 'small' ? smallIcons[variant] : icons[variant] + } return (
From 14d4def966a8f0d91396f24384ba1d6bb3d5e13d Mon Sep 17 00:00:00 2001 From: Liu Liu Date: Mon, 1 Dec 2025 13:38:42 -0800 Subject: [PATCH 2/6] clean up and add changeset --- .changeset/giant-loops-send.md | 5 +++ .../InlineMessage/InlineMessage.stories.tsx | 2 +- .../src/InlineMessage/InlineMessage.test.tsx | 44 ++++++++++++------- .../react/src/InlineMessage/InlineMessage.tsx | 18 +++----- 4 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 .changeset/giant-loops-send.md diff --git a/.changeset/giant-loops-send.md b/.changeset/giant-loops-send.md new file mode 100644 index 00000000000..9b0a22a6979 --- /dev/null +++ b/.changeset/giant-loops-send.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add leadingVisual to InlineMessage component. diff --git a/packages/react/src/InlineMessage/InlineMessage.stories.tsx b/packages/react/src/InlineMessage/InlineMessage.stories.tsx index a75f4bfec08..4c7d665668d 100644 --- a/packages/react/src/InlineMessage/InlineMessage.stories.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.stories.tsx @@ -36,7 +36,7 @@ const iconMap = { export const Playground: StoryObj = { render(args) { - const {leadingVisual:leadingVisualOption, ...rest} = args + const {leadingVisual: leadingVisualOption, ...rest} = args const leadingVisual = leadingVisualOption ? iconMap[leadingVisualOption as keyof typeof iconMap] : undefined return ( diff --git a/packages/react/src/InlineMessage/InlineMessage.test.tsx b/packages/react/src/InlineMessage/InlineMessage.test.tsx index 0b29e7fefcb..2ae36a9edbb 100644 --- a/packages/react/src/InlineMessage/InlineMessage.test.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.test.tsx @@ -1,7 +1,8 @@ import {render, screen} from '@testing-library/react' import {describe, expect, it, test} from 'vitest' -import {InfoIcon} from '@primer/octicons-react' +import {InfoIcon, SearchIcon} from '@primer/octicons-react' import {InlineMessage} from '../InlineMessage' +import React from 'react' describe('InlineMessage', () => { it('should render content passed as `children`', () => { @@ -81,24 +82,33 @@ describe('InlineMessage', () => { expect(screen.getByTestId('container')).toHaveAttribute('data-variant', 'warning') }) - it('should render a custom icon when `leadingVisual` is a string', () => { - const {container} = render( - - test with emoji - , - ) - expect(screen.getByText('test with emoji')).toBeInTheDocument() - expect(container.textContent).toContain('🔒') - }) - - it('should render a custom JSX element when `leadingVisual` is a React element', () => { + it('should render leading visual', () => { render( - ⚠️}> - test with JSX element - , + <> + }> + test with custom icon + + ( +
leadingVisual
+ ))} + > + test with memo icon +
+ ( +
leadingVisual
+ ))} + > + test with forward ref icon +
+ , ) - expect(screen.getByText('test with JSX element')).toBeInTheDocument() - expect(screen.getByTestId('custom-element')).toBeInTheDocument() + expect(screen.getByTestId('search-icon')).toBeInTheDocument() + expect(screen.getByTestId('memo')).toBeInTheDocument() + expect(screen.getByTestId('forward-ref')).toBeInTheDocument() }) it('should use default icon when `leadingVisual` is not provided', () => { diff --git a/packages/react/src/InlineMessage/InlineMessage.tsx b/packages/react/src/InlineMessage/InlineMessage.tsx index aa7d815e244..5ec43ef7e24 100644 --- a/packages/react/src/InlineMessage/InlineMessage.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.tsx @@ -1,6 +1,6 @@ import {AlertFillIcon, AlertIcon, CheckCircleFillIcon, CheckCircleIcon} from '@primer/octicons-react' import {clsx} from 'clsx' -import type React from 'react' +import React from 'react' import {isValidElementType} from 'react-is' import classes from './InlineMessage.module.css' type MessageVariant = 'critical' | 'success' | 'unavailable' | 'warning' @@ -18,7 +18,6 @@ export type InlineMessageProps = React.ComponentPropsWithoutRef<'div'> & { /** * A custom leading visual (icon or other element) to display instead of the default variant icon. - * Can be a React component, JSX element, or string (e.g., emoji). */ leadingVisual?: React.ElementType | React.ReactNode } @@ -42,21 +41,16 @@ export function InlineMessage({ className, size = 'medium', variant, - leadingVisual, + leadingVisual: LeadingVisual, ...rest }: InlineMessageProps) { let icon: React.ReactNode - if (leadingVisual !== undefined) { - if (typeof leadingVisual === 'string') { - icon = {leadingVisual} - } else if (isValidElementType(leadingVisual)) { - // Component type - const LeadingVisualComponent = leadingVisual - icon = + if (LeadingVisual !== undefined) { + if (isValidElementType(LeadingVisual)) { + icon = } else { - // JSX element - wrap it with the icon styling - icon = leadingVisual + icon = LeadingVisual } } else { // Use default icon based on variant and size From 77dfb934681199760c0d462003ec310b74897899 Mon Sep 17 00:00:00 2001 From: Liu Liu Date: Mon, 1 Dec 2025 13:42:47 -0800 Subject: [PATCH 3/6] fix lint --- packages/react/src/InlineMessage/InlineMessage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/InlineMessage/InlineMessage.test.tsx b/packages/react/src/InlineMessage/InlineMessage.test.tsx index 2ae36a9edbb..807769f9a65 100644 --- a/packages/react/src/InlineMessage/InlineMessage.test.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.test.tsx @@ -1,6 +1,6 @@ import {render, screen} from '@testing-library/react' import {describe, expect, it, test} from 'vitest' -import {InfoIcon, SearchIcon} from '@primer/octicons-react' +import {InfoIcon} from '@primer/octicons-react' import {InlineMessage} from '../InlineMessage' import React from 'react' @@ -85,7 +85,7 @@ describe('InlineMessage', () => { it('should render leading visual', () => { render( <> - }> + }> test with custom icon Date: Mon, 1 Dec 2025 13:48:05 -0800 Subject: [PATCH 4/6] import type --- packages/react/src/InlineMessage/InlineMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/InlineMessage/InlineMessage.tsx b/packages/react/src/InlineMessage/InlineMessage.tsx index 5ec43ef7e24..e590d5c6b82 100644 --- a/packages/react/src/InlineMessage/InlineMessage.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.tsx @@ -1,6 +1,6 @@ import {AlertFillIcon, AlertIcon, CheckCircleFillIcon, CheckCircleIcon} from '@primer/octicons-react' import {clsx} from 'clsx' -import React from 'react' +import type React from 'react' import {isValidElementType} from 'react-is' import classes from './InlineMessage.module.css' type MessageVariant = 'critical' | 'success' | 'unavailable' | 'warning' From 6847cbdd682f1f953fa8c14516db986dae50cb26 Mon Sep 17 00:00:00 2001 From: Liu Liu Date: Mon, 1 Dec 2025 13:52:05 -0800 Subject: [PATCH 5/6] test fix --- packages/react/src/InlineMessage/InlineMessage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/InlineMessage/InlineMessage.test.tsx b/packages/react/src/InlineMessage/InlineMessage.test.tsx index 807769f9a65..4678a2e5ded 100644 --- a/packages/react/src/InlineMessage/InlineMessage.test.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.test.tsx @@ -106,7 +106,7 @@ describe('InlineMessage', () => { , ) - expect(screen.getByTestId('search-icon')).toBeInTheDocument() + expect(screen.getByTestId('info-icon')).toBeInTheDocument() expect(screen.getByTestId('memo')).toBeInTheDocument() expect(screen.getByTestId('forward-ref')).toBeInTheDocument() }) From 07f5eee515c3b5070b77a2956bed9e0af3d17e43 Mon Sep 17 00:00:00 2001 From: Liu Liu Date: Tue, 2 Dec 2025 10:05:35 -0800 Subject: [PATCH 6/6] docs, stories, copilot feedback --- packages/react/src/InlineMessage/InlineMessage.docs.json | 8 +++++++- .../react/src/InlineMessage/InlineMessage.stories.tsx | 2 +- packages/react/src/InlineMessage/InlineMessage.tsx | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react/src/InlineMessage/InlineMessage.docs.json b/packages/react/src/InlineMessage/InlineMessage.docs.json index eaa28ca23dd..27ec3bd3f8e 100644 --- a/packages/react/src/InlineMessage/InlineMessage.docs.json +++ b/packages/react/src/InlineMessage/InlineMessage.docs.json @@ -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 } ] -} +} \ No newline at end of file diff --git a/packages/react/src/InlineMessage/InlineMessage.stories.tsx b/packages/react/src/InlineMessage/InlineMessage.stories.tsx index 4c7d665668d..6dabc253fa6 100644 --- a/packages/react/src/InlineMessage/InlineMessage.stories.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.stories.tsx @@ -63,7 +63,7 @@ export const Playground: StoryObj = { type: 'select', }, options: Object.keys(iconMap), - description: 'Select a custom icon or emoji to override the default variant icon', + description: 'Select a custom icon to override the default variant icon', }, }, args: { diff --git a/packages/react/src/InlineMessage/InlineMessage.tsx b/packages/react/src/InlineMessage/InlineMessage.tsx index e590d5c6b82..307f17e82d7 100644 --- a/packages/react/src/InlineMessage/InlineMessage.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.tsx @@ -47,7 +47,7 @@ export function InlineMessage({ let icon: React.ReactNode if (LeadingVisual !== undefined) { - if (isValidElementType(LeadingVisual)) { + if (typeof LeadingVisual !== 'string' && isValidElementType(LeadingVisual)) { icon = } else { icon = LeadingVisual