From 1ee52e14d247968c984783fb878d281cc2a47523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uladzimir=20Dzmitra=C4=8Dko=C5=AD?= <3773351+going-confetti@users.noreply.github.com> Date: Thu, 24 Nov 2022 13:39:42 +0100 Subject: [PATCH] GrafanaUI: Add disabled option for menu items (#58980) --- .../src/components/Menu/Menu.story.tsx | 25 ++++++++++++ .../src/components/Menu/MenuItem.test.tsx | 14 +++++++ .../src/components/Menu/MenuItem.tsx | 38 +++++++++++++++++-- .../src/components/Menu/hooks.test.tsx | 24 +++++++++++- .../grafana-ui/src/components/Menu/hooks.ts | 6 +-- 5 files changed, 99 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.tsx index 80499f0e3b5..2c72671a348 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.story.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.story.tsx @@ -49,6 +49,31 @@ export function Examples() { + + + + + + , + , + ]} + /> + , + , + ]} + /> + + + { expect(subMenuContainer.firstChild?.childNodes.length).toBe(2); }); + it('renders disabled subMenu correctly', async () => { + const childItems = [ + , + , + ]; + + 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 = [ , diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx index 84b8c3f0520..47c9940a6b8 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -35,6 +35,8 @@ export interface MenuItemProps { 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(null); useImperativeHandle(ref, () => localRef.current!); @@ -128,6 +148,7 @@ export const MenuItem = React.memo( aria-label={ariaLabel} aria-checked={ariaChecked} tabIndex={tabIndex} + {...disabledProps} > <> {icon && } @@ -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; diff --git a/packages/grafana-ui/src/components/Menu/hooks.test.tsx b/packages/grafana-ui/src/components/Menu/hooks.test.tsx index a35406e6a35..2c8bfabf886 100644 --- a/packages/grafana-ui/src/components/Menu/hooks.test.tsx +++ b/packages/grafana-ui/src/components/Menu/hooks.test.tsx @@ -18,7 +18,10 @@ describe('useMenuFocus', () => { Item 1 Item 2 - Item 3 + + Item 3 + + Item 4 ); @@ -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', () => { diff --git a/packages/grafana-ui/src/components/Menu/hooks.ts b/packages/grafana-ui/src/components/Menu/hooks.ts index f021986fafc..3ca3d2baf5b 100644 --- a/packages/grafana-ui/src/components/Menu/hooks.ts +++ b/packages/grafana-ui/src/components/Menu/hooks.ts @@ -41,7 +41,7 @@ export const useMenuFocus = ({ useEffect(() => { const menuItems = localRef?.current?.querySelectorAll( - `[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( - `[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( - `[data-role="menuitem"]` + '[data-role="menuitem"]:not([data-disabled])' ); const menuItemsCount = menuItems?.length ?? 0;