mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GrafanaUI: Add disabled option for menu items (#58980)
This commit is contained in:
parent
400ada1ad0
commit
1ee52e14d2
@ -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…
Reference in New Issue
Block a user