Grafana UI: Add description to Menu component (#77808)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Alexa V 2023-11-10 09:32:05 +01:00 committed by GitHub
parent 641a47c71d
commit 10269cb7f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 74 additions and 18 deletions

View File

@ -49,6 +49,42 @@ export function Examples() {
<Menu.Item label="With destructive prop set" icon="trash-alt" destructive /> <Menu.Item label="With destructive prop set" icon="trash-alt" destructive />
</Menu> </Menu>
</StoryExample> </StoryExample>
<StoryExample name="With item menu description">
<Menu>
<Menu.Item label="item1" icon="history" description="item 1 is an important element" shortcut="q p" />
<Menu.Item
label="Item with a very long title"
icon="apps"
description="long titles can be hard to read"
childItems={[
<Menu.Item key="subitem1" label="subitem1" icon="history" />,
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
<Menu.Item
key="subitem3"
label="subitem3"
icon="search-plus"
childItems={[
<Menu.Item key="subitem1" label="subitem1" icon="history" />,
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
<Menu.Item key="subitem3" label="subitem3" icon="search-plus" />,
]}
/>,
]}
shortcut="p s"
/>
<Menu.Item
label="item3"
icon="filter"
description="item 3 is an important element"
childItems={[
<Menu.Item key="subitem1" label="subitem1" icon="history" description="a subitem with a description" />,
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
<Menu.Item key="subitem3" label="subitem3" icon="search-plus" />,
]}
/>
</Menu>
</StoryExample>
<StoryExample name="With disabled items"> <StoryExample name="With disabled items">
<Menu> <Menu>
<Menu.Item label="Google" icon="search-plus" /> <Menu.Item label="Google" icon="search-plus" />

View File

@ -7,6 +7,7 @@ import { useStyles2 } from '../../themes';
import { getFocusStyles } from '../../themes/mixins'; import { getFocusStyles } from '../../themes/mixins';
import { IconName } from '../../types/icon'; import { IconName } from '../../types/icon';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { Stack } from '../Layout/Stack/Stack';
import { SubMenu } from './SubMenu'; import { SubMenu } from './SubMenu';
@ -17,6 +18,8 @@ export type MenuItemElement = HTMLAnchorElement & HTMLButtonElement & HTMLDivEle
export interface MenuItemProps<T = unknown> { export interface MenuItemProps<T = unknown> {
/** Label of the menu item */ /** Label of the menu item */
label: string; label: string;
/** Description of item */
description?: string;
/** Aria label for accessibility support */ /** Aria label for accessibility support */
ariaLabel?: string; ariaLabel?: string;
/** Aria checked for accessibility support */ /** Aria checked for accessibility support */
@ -57,6 +60,7 @@ export const MenuItem = React.memo(
url, url,
icon, icon,
label, label,
description,
ariaLabel, ariaLabel,
ariaChecked, ariaChecked,
target, target,
@ -104,6 +108,7 @@ export const MenuItem = React.memo(
}, },
className className
); );
const disabledProps = { const disabledProps = {
[ItemElement === 'button' ? 'disabled' : 'aria-disabled']: disabled, [ItemElement === 'button' ? 'disabled' : 'aria-disabled']: disabled,
...(ItemElement === 'a' && disabled && { href: undefined, onClick: undefined }), ...(ItemElement === 'a' && disabled && { href: undefined, onClick: undefined }),
@ -159,10 +164,9 @@ export const MenuItem = React.memo(
tabIndex={tabIndex} tabIndex={tabIndex}
{...disabledProps} {...disabledProps}
> >
<> <Stack direction="row" justifyContent="flex-start" alignItems="center">
{icon && <Icon name={icon} className={styles.icon} aria-hidden />} {icon && <Icon name={icon} className={styles.icon} aria-hidden />}
{label} {label}
<div className={cx(styles.rightWrapper, { [styles.withShortcut]: hasShortcut })}> <div className={cx(styles.rightWrapper, { [styles.withShortcut]: hasShortcut })}>
{hasShortcut && ( {hasShortcut && (
<div className={styles.shortcut}> <div className={styles.shortcut}>
@ -181,7 +185,16 @@ export const MenuItem = React.memo(
/> />
)} )}
</div> </div>
</> </Stack>
{description && (
<div
className={cx(styles.description, {
[styles.descriptionWithIcon]: icon !== undefined,
})}
>
{description}
</div>
)}
</ItemElement> </ItemElement>
); );
}) })
@ -197,7 +210,8 @@ const getStyles = (theme: GrafanaTheme2) => {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
color: theme.colors.text.primary, color: theme.colors.text.primary,
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
alignItems: 'stretch',
padding: theme.spacing(0.5, 2), padding: theme.spacing(0.5, 2),
minHeight: theme.spacing(4), minHeight: theme.spacing(4),
margin: 0, margin: 0,
@ -234,7 +248,7 @@ const getStyles = (theme: GrafanaTheme2) => {
}), }),
disabled: css({ disabled: css({
color: theme.colors.action.disabledText, color: theme.colors.action.disabledText,
label: 'menu-item-disabled',
'&:hover, &:focus, &:focus-visible': { '&:hover, &:focus, &:focus-visible': {
cursor: 'not-allowed', cursor: 'not-allowed',
background: 'none', background: 'none',
@ -243,8 +257,6 @@ const getStyles = (theme: GrafanaTheme2) => {
}), }),
icon: css({ icon: css({
opacity: 0.7, opacity: 0.7,
marginRight: '10px',
marginLeft: '-4px',
color: theme.colors.text.secondary, color: theme.colors.text.secondary,
}), }),
rightWrapper: css({ rightWrapper: css({
@ -252,9 +264,6 @@ const getStyles = (theme: GrafanaTheme2) => {
alignItems: 'center', alignItems: 'center',
marginLeft: 'auto', marginLeft: 'auto',
}), }),
shortcutIcon: css({
marginRight: theme.spacing(1),
}),
withShortcut: css({ withShortcut: css({
minWidth: theme.spacing(10.5), minWidth: theme.spacing(10.5),
}), }),
@ -266,5 +275,13 @@ const getStyles = (theme: GrafanaTheme2) => {
color: theme.colors.text.secondary, color: theme.colors.text.secondary,
opacity: 0.7, opacity: 0.7,
}), }),
description: css({
...theme.typography.bodySmall,
color: theme.colors.text.secondary,
textAlign: 'start',
}),
descriptionWithIcon: css({
marginLeft: theme.spacing(3),
}),
}; };
}; };

View File

@ -49,8 +49,8 @@ export const SubMenu = React.memo(
return ( return (
<> <>
<div className={styles.iconWrapper} aria-label={selectors.components.Menu.SubMenu.icon}> <div className={styles.iconWrapper} aria-hidden aria-label={selectors.components.Menu.SubMenu.icon}>
<Icon name="angle-right" className={styles.icon} aria-hidden /> <Icon name="angle-right" className={styles.icon} />
</div> </div>
{isOpen && ( {isOpen && (
<div <div

View File

@ -25,18 +25,21 @@ describe('NewActionsButton', () => {
it('should display the correct urls with a given parent folder', async () => { it('should display the correct urls with a given parent folder', async () => {
await renderAndOpen(mockParentFolder); await renderAndOpen(mockParentFolder);
expect(screen.getByText('New dashboard')).toHaveAttribute( expect(screen.getByRole('link', { name: 'New dashboard' })).toHaveAttribute(
'href', 'href',
`/dashboard/new?folderUid=${mockParentFolder.uid}` `/dashboard/new?folderUid=${mockParentFolder.uid}`
); );
expect(screen.getByText('Import')).toHaveAttribute('href', `/dashboard/import?folderUid=${mockParentFolder.uid}`); expect(screen.getByRole('link', { name: 'Import' })).toHaveAttribute(
'href',
`/dashboard/import?folderUid=${mockParentFolder.uid}`
);
}); });
it('should display urls without params when there is no parent folder', async () => { it('should display urls without params when there is no parent folder', async () => {
await renderAndOpen(); await renderAndOpen();
expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new'); expect(screen.getByRole('link', { name: 'New dashboard' })).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import'); expect(screen.getByRole('link', { name: 'Import' })).toHaveAttribute('href', '/dashboard/import');
}); });
it('clicking the "New folder" button opens the drawer', async () => { it('clicking the "New folder" button opens the drawer', async () => {
@ -57,7 +60,7 @@ describe('NewActionsButton', () => {
const newButton = screen.getByText('New'); const newButton = screen.getByText('New');
await userEvent.click(newButton); await userEvent.click(newButton);
expect(screen.getByText('New dashboard')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'New dashboard' })).toBeInTheDocument();
expect(screen.getByText('Import')).toBeInTheDocument(); expect(screen.getByText('Import')).toBeInTheDocument();
expect(screen.queryByText('New folder')).not.toBeInTheDocument(); expect(screen.queryByText('New folder')).not.toBeInTheDocument();
}); });

View File

@ -68,7 +68,7 @@ it('renders with all buttons enabled except paste a panel', () => {
expect(screen.getByText('visualization', { exact: false })).not.toBeDisabled(); expect(screen.getByText('visualization', { exact: false })).not.toBeDisabled();
expect(screen.getByText('row', { exact: false })).not.toBeDisabled(); expect(screen.getByText('row', { exact: false })).not.toBeDisabled();
expect(screen.getByText('library', { exact: false })).not.toBeDisabled(); expect(screen.getByText('library', { exact: false })).not.toBeDisabled();
expect(screen.getByText('paste panel', { exact: false })).toBeDisabled(); expect(screen.getByRole('menuitem', { name: 'Paste panel' })).toBeDisabled();
}); });
it('renders with all buttons enabled', () => { it('renders with all buttons enabled', () => {