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.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 23d190488c1..6dabc253fa6 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 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..4678a2e5ded 100644 --- a/packages/react/src/InlineMessage/InlineMessage.test.tsx +++ b/packages/react/src/InlineMessage/InlineMessage.test.tsx @@ -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`', () => { @@ -79,4 +81,41 @@ describe('InlineMessage', () => { ) expect(screen.getByTestId('container')).toHaveAttribute('data-variant', 'warning') }) + + it('should render leading visual', () => { + render( + <> + }> + test with custom icon + + ( +
leadingVisual
+ ))} + > + test with memo icon +
+ ( +
leadingVisual
+ ))} + > + test with forward ref icon +
+ , + ) + 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(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..307f17e82d7 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,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 = { @@ -30,8 +36,26 @@ 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: LeadingVisual, + ...rest +}: InlineMessageProps) { + let icon: React.ReactNode + + if (LeadingVisual !== undefined) { + if (typeof LeadingVisual !== 'string' && isValidElementType(LeadingVisual)) { + icon = + } else { + icon = LeadingVisual + } + } else { + // Use default icon based on variant and size + icon = size === 'small' ? smallIcons[variant] : icons[variant] + } return (