GrafanaUI: Add disabled option for menu items (#58980)

pull/59298/head^2
Uladzimir Dzmitračkoŭ 3 years ago committed by GitHub
parent 400ada1ad0
commit 1ee52e14d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      packages/grafana-ui/src/components/Menu/Menu.story.tsx
  2. 14
      packages/grafana-ui/src/components/Menu/MenuItem.test.tsx
  3. 38
      packages/grafana-ui/src/components/Menu/MenuItem.tsx
  4. 24
      packages/grafana-ui/src/components/Menu/hooks.test.tsx
  5. 6
      packages/grafana-ui/src/components/Menu/hooks.ts

@ -49,6 +49,31 @@ export function Examples() {
<Menu.Item label="With destructive prop set" icon="trash-alt" destructive />
</Menu>
</StoryExample>
<StoryExample name="With disabled items">
<Menu>
<Menu.Item label="Google" icon="search-plus" />
<Menu.Item label="Disabled action" icon="history" disabled />
<Menu.Item label="Disabled link" icon="external-link-alt" url="http://google.com" target="_blank" disabled />
<Menu.Item
label="Submenu"
icon="apps"
childItems={[
<Menu.Item key="subitem1" label="subitem1" icon="history" disabled />,
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
]}
/>
<Menu.Item
label="Disabled submenu"
icon="apps"
disabled
childItems={[
<Menu.Item key="subitem1" label="subitem1" icon="history" />,
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
]}
/>
<Menu.Item label="Disabled destructive action" icon="trash-alt" destructive disabled />
</Menu>
</StoryExample>
<StoryExample name="With header & groups">
<Menu
header={

@ -51,6 +51,20 @@ describe('MenuItem', () => {
expect(subMenuContainer.firstChild?.childNodes.length).toBe(2);
});
it('renders disabled subMenu correctly', async () => {
const childItems = [
<MenuItem key="subitem1" label="subitem1" icon="history" />,
<MenuItem key="subitem2" label="subitem2" icon="apps" />,
];
render(getMenuItem({ childItems, disabled: true }));
fireEvent.mouseOver(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')));
const subMenuContainer = screen.queryByLabelText(selectors.components.Menu.SubMenu.container);
expect(subMenuContainer).toBe(null);
});
it('opens subMenu on ArrowRight', async () => {
const childItems = [
<MenuItem key="subitem1" label="subitem1" icon="history" />,

@ -35,6 +35,8 @@ export interface MenuItemProps<T = any> {
className?: string;
/** Active */
active?: boolean;
/** Disabled */
disabled?: boolean;
/** Show in destructive style (error color) */
destructive?: boolean;
tabIndex?: number;
@ -57,6 +59,7 @@ export const MenuItem = React.memo(
onClick,
className,
active,
disabled,
destructive,
childItems,
role = 'menuitem',
@ -68,13 +71,21 @@ export const MenuItem = React.memo(
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const [openedWithArrow, setOpenedWithArrow] = useState(false);
const onMouseEnter = useCallback(() => {
if (disabled) {
return;
}
setIsSubMenuOpen(true);
setIsActive(true);
}, []);
}, [disabled]);
const onMouseLeave = useCallback(() => {
if (disabled) {
return;
}
setIsSubMenuOpen(false);
setIsActive(false);
}, []);
}, [disabled]);
const hasSubMenu = childItems && childItems.length > 0;
const ItemElement = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a';
@ -82,10 +93,19 @@ export const MenuItem = React.memo(
{
[styles.item]: true,
[styles.active]: isActive,
[styles.destructive]: destructive,
[styles.disabled]: disabled,
[styles.destructive]: destructive && !disabled,
},
className
);
const disabledProps = {
[ItemElement === 'button' ? 'disabled' : 'aria-disabled']: disabled,
...(ItemElement === 'a' && disabled && { href: undefined, onClick: undefined }),
...(disabled && {
tabIndex: -1,
['data-disabled']: disabled, // used to identify disabled items in Menu.tsx
}),
};
const localRef = useRef<MenuItemElement>(null);
useImperativeHandle(ref, () => localRef.current!);
@ -128,6 +148,7 @@ export const MenuItem = React.memo(
aria-label={ariaLabel}
aria-checked={ariaChecked}
tabIndex={tabIndex}
{...disabledProps}
>
<>
{icon && <Icon name={icon} className={styles.icon} aria-hidden />}
@ -200,6 +221,17 @@ const getStyles = (theme: GrafanaTheme2) => {
}
}
`,
disabled: css`
color: ${theme.colors.action.disabledText};
&:hover,
&:focus,
&:focus-visible {
cursor: not-allowed;
background: none;
color: ${theme.colors.action.disabledText};
}
`,
icon: css`
opacity: 0.7;
margin-right: 10px;

@ -18,7 +18,10 @@ describe('useMenuFocus', () => {
Item 1
</span>
<span data-role="menuitem">Item 2</span>
<span data-role="menuitem">Item 3</span>
<span data-role="menuitem" data-disabled>
Item 3
</span>
<span data-role="menuitem">Item 4</span>
</div>
);
@ -31,6 +34,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
@ -42,6 +46,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 1').tabIndex).toBe(0);
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
@ -53,6 +58,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
expect(screen.getByText('Item 2').tabIndex).toBe(0);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
@ -64,6 +70,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 1').tabIndex).toBe(0);
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
@ -74,7 +81,20 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
expect(screen.getByText('Item 3').tabIndex).toBe(0);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(0);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys6] = result.current;
rerender(getMenuElement(ref, handleKeys6));
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
expect(screen.getByText('Item 2').tabIndex).toBe(0);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
});
it('calls close on ArrowLeft and unfocuses all items', () => {

@ -41,7 +41,7 @@ export const useMenuFocus = ({
useEffect(() => {
const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
`[data-role="menuitem"]`
'[data-role="menuitem"]:not([data-disabled])'
);
menuItems?.[focusedItem]?.focus();
menuItems?.forEach((menuItem, i) => {
@ -51,7 +51,7 @@ export const useMenuFocus = ({
useEffectOnce(() => {
const firstMenuItem = localRef?.current?.querySelector<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
`[data-role="menuitem"]`
'[data-role="menuitem"]:not([data-disabled])'
);
if (firstMenuItem) {
firstMenuItem.tabIndex = 0;
@ -61,7 +61,7 @@ export const useMenuFocus = ({
const handleKeys = (event: React.KeyboardEvent) => {
const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
`[data-role="menuitem"]`
'[data-role="menuitem"]:not([data-disabled])'
);
const menuItemsCount = menuItems?.length ?? 0;

Loading…
Cancel
Save