-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
ref(ui): Refactor EmptyMessage to use core components and simplify API #103798
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
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,146 @@ | ||
| --- | ||
| title: EmptyMessage | ||
| description: A component for displaying empty states with optional icon, title, description, and action buttons. All text content uses balanced text wrapping for better readability. | ||
| source: 'sentry/components/emptyMessage' | ||
| resources: | ||
| js: https://github.com/getsentry/sentry/blob/master/static/app/components/emptyMessage.tsx | ||
| --- | ||
|
|
||
| import {Button} from 'sentry/components/core/button'; | ||
| import EmptyMessage from 'sentry/components/emptyMessage'; | ||
| import Panel from 'sentry/components/panels/panel'; | ||
| import {IconSearch} from 'sentry/icons'; | ||
| import * as Storybook from 'sentry/stories'; | ||
|
|
||
| To create a basic empty message, wrap your message in an `<EmptyMessage>` component. | ||
|
|
||
| ```jsx | ||
| <Panel> | ||
| <EmptyMessage> | ||
| This is a simple empty message with default styling and balanced text wrapping for | ||
| better readability. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| ``` | ||
|
|
||
| ## Props | ||
|
|
||
| - **title** (`React.ReactNode`) - The title of the empty message | ||
| - **icon** (`React.ReactNode`) - Optional icon element | ||
| - **action** (`React.ReactElement`) - Optional action button | ||
| - **size** (`"md" | "lg"`) - Size of the text (default: md) | ||
| - **children** (`React.ReactNode`) - Main content text (automatically uses balanced text wrapping) | ||
|
|
||
| ## Examples | ||
|
|
||
| ### With Title | ||
|
|
||
| Add a title to provide context for the empty state. | ||
|
|
||
| <Storybook.Demo> | ||
| <Panel> | ||
| <EmptyMessage title="No Results Found"> | ||
| We couldn't find any results matching your search criteria. Try adjusting your | ||
| filters or search terms. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| </Storybook.Demo> | ||
| ```jsx | ||
| <EmptyMessage title="No Results Found"> | ||
| We couldn't find any results matching your search criteria. Try adjusting your filters | ||
| or search terms. | ||
| </EmptyMessage> | ||
| ``` | ||
|
|
||
| ### With Icon | ||
|
|
||
| Add an icon to make the empty state more visually distinctive. Icons are defaulted to `lg` size, so there is typically no need to specify an icon size. | ||
|
|
||
| <Storybook.Demo> | ||
| <Panel> | ||
| <EmptyMessage icon={<IconSearch />} title="No Results"> | ||
| We couldn't find any results matching your search criteria. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| </Storybook.Demo> | ||
| ```jsx | ||
| <EmptyMessage icon={<IconSearch />} title="No Results"> | ||
| We couldn't find any results matching your search criteria. | ||
| </EmptyMessage> | ||
| ``` | ||
|
|
||
| ### With Action | ||
|
|
||
| Add action buttons to guide users toward resolving the empty state. | ||
|
|
||
| <Storybook.Demo> | ||
| <Panel> | ||
| <EmptyMessage | ||
| icon={<IconSearch />} | ||
| title="No Results Found" | ||
| action={<Button priority="primary">Clear Filters</Button>} | ||
| > | ||
| We couldn't find any results matching your search criteria. Try adjusting your | ||
| filters or search terms. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| </Storybook.Demo> | ||
| ```jsx | ||
| <EmptyMessage | ||
| icon={<IconSearch />} | ||
| title="No Results Found" | ||
| action={<Button priority="primary">Clear Filters</Button>} | ||
| > | ||
| We couldn't find any results matching your search criteria. Try adjusting your filters | ||
| or search terms. | ||
| </EmptyMessage> | ||
| ``` | ||
|
|
||
| ### Sizes | ||
|
|
||
| EmptyMessage supports two sizes: `md` (default) and `lg`. | ||
|
|
||
| <Storybook.Demo> | ||
| <div style={{display: 'flex', gap: '1rem'}}> | ||
| <Panel style={{flex: 1}}> | ||
| <EmptyMessage icon={<IconSearch />} title="Medium Size"> | ||
| This is the default medium size empty message. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| <Panel style={{flex: 1}}> | ||
| <EmptyMessage size="lg" icon={<IconSearch />} title="Large Size"> | ||
| This is a large size empty message with bigger text. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| </div> | ||
| </Storybook.Demo> | ||
| ```jsx | ||
| <EmptyMessage size="medium" icon={<IconSearch />} title="Medium Size"> | ||
| This is the default medium size empty message. | ||
| </EmptyMessage> | ||
|
|
||
| <EmptyMessage size="lg" icon={<IconSearch />} title="Large Size"> | ||
| This is a large size empty message with bigger text. | ||
| </EmptyMessage> | ||
| ``` | ||
|
|
||
| ### Text Wrapping | ||
|
|
||
| All text content automatically uses CSS text-wrap balance mode for better readability, especially with longer messages. | ||
|
|
||
| <Storybook.Demo> | ||
| <Panel> | ||
| <EmptyMessage icon={<IconSearch />} title="Text Wrapping"> | ||
| Sure you haven't misspelled? If you're using a lesser-known platform, consider | ||
| choosing a more generic SDK like Browser JavaScript, Python, Node, .NET & Java or | ||
| create a generic project. | ||
| </EmptyMessage> | ||
| </Panel> | ||
| </Storybook.Demo> | ||
| ```jsx | ||
| <EmptyMessage icon={<IconSearch />} title="Text Wrapping"> | ||
| Sure you haven't misspelled? If you're using a lesser-known platform, consider choosing | ||
| a more generic SDK like Browser JavaScript, Python, Node, .NET & Java or create a | ||
| generic project. | ||
| </EmptyMessage> | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,51 @@ | ||
| import {css} from '@emotion/react'; | ||
| import styled from '@emotion/styled'; | ||
| import {useTheme} from '@emotion/react'; | ||
| import {mergeProps} from '@react-aria/utils'; | ||
|
|
||
| import {space} from 'sentry/styles/space'; | ||
| import TextBlock from 'sentry/views/settings/components/text/textBlock'; | ||
| import {Container, Flex} from 'sentry/components/core/layout'; | ||
| import {Text} from 'sentry/components/core/text'; | ||
| import {IconDefaultsProvider} from 'sentry/icons/useIconDefaults'; | ||
|
|
||
| interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> { | ||
| action?: React.ReactElement; | ||
| description?: React.ReactNode; | ||
| action?: React.ReactNode; | ||
| icon?: React.ReactNode; | ||
| leftAligned?: boolean; | ||
| size?: 'large' | 'medium'; | ||
| size?: 'lg' | 'md'; | ||
| title?: React.ReactNode; | ||
| } | ||
|
|
||
| type WrapperProps = Pick<Props, 'size'>; | ||
|
|
||
| const EmptyMessage = styled( | ||
| ({ | ||
| title, | ||
| description, | ||
| icon, | ||
| children, | ||
| action, | ||
| leftAligned: _leftAligned, | ||
| ...props | ||
| }: Props) => ( | ||
| <div data-test-id="empty-message" {...props}> | ||
| {icon && <IconWrapper>{icon}</IconWrapper>} | ||
| {title && <Title noMargin={!description && !children && !action}>{title}</Title>} | ||
| {description && <Description>{description}</Description>} | ||
| {children && <Description noMargin>{children}</Description>} | ||
| {action && <Action>{action}</Action>} | ||
| </div> | ||
| ) | ||
| )<WrapperProps>` | ||
| display: flex; | ||
| ${p => | ||
| p.leftAligned | ||
| ? css` | ||
| max-width: 70%; | ||
| align-items: flex-start; | ||
| padding: ${space(4)}; | ||
| ` | ||
| : css` | ||
| text-align: center; | ||
| align-items: center; | ||
| padding: ${space(4)} 15%; | ||
| `}; | ||
| flex-direction: column; | ||
| color: ${p => p.theme.textColor}; | ||
| font-size: ${p => | ||
| p.size && p.size === 'large' ? p.theme.fontSize.xl : p.theme.fontSize.md}; | ||
| `; | ||
|
|
||
| const IconWrapper = styled('div')` | ||
| color: ${p => (p.theme.isChonk ? p.theme.gray400 : p.theme.gray200)}; | ||
| margin-bottom: ${space(1)}; | ||
| `; | ||
|
|
||
| const Title = styled('strong')<{noMargin: boolean}>` | ||
| font-size: ${p => p.theme.fontSize.xl}; | ||
| ${p => !p.noMargin && `margin-bottom: ${space(1)};`} | ||
| `; | ||
|
|
||
| const Description = styled(TextBlock)` | ||
| margin: 0; | ||
| `; | ||
|
|
||
| const Action = styled('div')` | ||
| margin-top: ${space(2)}; | ||
| `; | ||
| function EmptyMessage({title, icon, children, action, size, ...props}: Props) { | ||
| const theme = useTheme(); | ||
|
|
||
| return ( | ||
| <Flex gap="xl" direction="column" padding="3xl"> | ||
| {stackProps => ( | ||
| <Text | ||
| align="center" | ||
| size={size} | ||
| data-test-id="empty-message" | ||
| {...mergeProps(stackProps, props)} | ||
| > | ||
| {icon && ( | ||
| <IconDefaultsProvider size="xl"> | ||
| <Container color={theme.isChonk ? theme.gray400 : theme.gray200}> | ||
|
Contributor
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. Bug: Icon color prop has no effectThe |
||
| {icon} | ||
| </Container> | ||
| </IconDefaultsProvider> | ||
| )} | ||
| {title && ( | ||
| <Text bold size="xl" density="comfortable"> | ||
| {title} | ||
| </Text> | ||
| )} | ||
| {children && ( | ||
| <Text textWrap="balance" density="comfortable"> | ||
| {children} | ||
| </Text> | ||
| )} | ||
| {action && <Container paddingTop="xl">{action}</Container>} | ||
| </Text> | ||
| )} | ||
|
Contributor
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. Bug: Invalid HTML nesting of div inside spanThe |
||
| </Flex> | ||
evanpurkhiser marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
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. Bug: Flex layout broken by Text display overrideThe |
||
| ); | ||
| } | ||
|
|
||
| export default EmptyMessage; | ||
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.
Bug: Invalid HTML nesting of div inside span
The
EmptyMessagecomponent usesTextas its root element, which defaults to rendering aspan. However, it containsContainerchildren (for the icon and action) that render asdivs. Nesting adivinside aspanis invalid HTML and can cause hydration mismatches or layout issues. TheTextcomponent should use theas="div"prop to render a block element.