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>
</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">
<Menu>
<Menu.Item label="Google" icon="search-plus" />

View File

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

View File

@ -25,18 +25,21 @@ describe('NewActionsButton', () => {
it('should display the correct urls with a given parent folder', async () => {
await renderAndOpen(mockParentFolder);
expect(screen.getByText('New dashboard')).toHaveAttribute(
expect(screen.getByRole('link', { name: 'New dashboard' })).toHaveAttribute(
'href',
`/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 () => {
await renderAndOpen();
expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import');
expect(screen.getByRole('link', { name: 'New dashboard' })).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByRole('link', { name: 'Import' })).toHaveAttribute('href', '/dashboard/import');
});
it('clicking the "New folder" button opens the drawer', async () => {
@ -57,7 +60,7 @@ describe('NewActionsButton', () => {
const newButton = screen.getByText('New');
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.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('row', { 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', () => {