mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana UI: Add description to Menu component (#77808)
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
parent
641a47c71d
commit
10269cb7f5
@ -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" />
|
||||||
|
@ -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),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user