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.Item label="With destructive prop set" icon="trash-alt" destructive />
|
||||||
</Menu>
|
</Menu>
|
||||||
</StoryExample>
|
</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">
|
<StoryExample name="With header & groups">
|
||||||
<Menu
|
<Menu
|
||||||
header={
|
header={
|
||||||
|
@ -51,6 +51,20 @@ describe('MenuItem', () => {
|
|||||||
expect(subMenuContainer.firstChild?.childNodes.length).toBe(2);
|
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 () => {
|
it('opens subMenu on ArrowRight', async () => {
|
||||||
const childItems = [
|
const childItems = [
|
||||||
<MenuItem key="subitem1" label="subitem1" icon="history" />,
|
<MenuItem key="subitem1" label="subitem1" icon="history" />,
|
||||||
|
@ -35,6 +35,8 @@ export interface MenuItemProps<T = any> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** Active */
|
/** Active */
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
/** Disabled */
|
||||||
|
disabled?: boolean;
|
||||||
/** Show in destructive style (error color) */
|
/** Show in destructive style (error color) */
|
||||||
destructive?: boolean;
|
destructive?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
@ -57,6 +59,7 @@ export const MenuItem = React.memo(
|
|||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
active,
|
active,
|
||||||
|
disabled,
|
||||||
destructive,
|
destructive,
|
||||||
childItems,
|
childItems,
|
||||||
role = 'menuitem',
|
role = 'menuitem',
|
||||||
@ -68,13 +71,21 @@ export const MenuItem = React.memo(
|
|||||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||||
const [openedWithArrow, setOpenedWithArrow] = useState(false);
|
const [openedWithArrow, setOpenedWithArrow] = useState(false);
|
||||||
const onMouseEnter = useCallback(() => {
|
const onMouseEnter = useCallback(() => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSubMenuOpen(true);
|
setIsSubMenuOpen(true);
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
}, []);
|
}, [disabled]);
|
||||||
const onMouseLeave = useCallback(() => {
|
const onMouseLeave = useCallback(() => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSubMenuOpen(false);
|
setIsSubMenuOpen(false);
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
}, []);
|
}, [disabled]);
|
||||||
|
|
||||||
const hasSubMenu = childItems && childItems.length > 0;
|
const hasSubMenu = childItems && childItems.length > 0;
|
||||||
const ItemElement = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a';
|
const ItemElement = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a';
|
||||||
@ -82,10 +93,19 @@ export const MenuItem = React.memo(
|
|||||||
{
|
{
|
||||||
[styles.item]: true,
|
[styles.item]: true,
|
||||||
[styles.active]: isActive,
|
[styles.active]: isActive,
|
||||||
[styles.destructive]: destructive,
|
[styles.disabled]: disabled,
|
||||||
|
[styles.destructive]: destructive && !disabled,
|
||||||
},
|
},
|
||||||
className
|
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);
|
const localRef = useRef<MenuItemElement>(null);
|
||||||
useImperativeHandle(ref, () => localRef.current!);
|
useImperativeHandle(ref, () => localRef.current!);
|
||||||
@ -128,6 +148,7 @@ export const MenuItem = React.memo(
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-checked={ariaChecked}
|
aria-checked={ariaChecked}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
{...disabledProps}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{icon && <Icon name={icon} className={styles.icon} aria-hidden />}
|
{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`
|
icon: css`
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -18,7 +18,10 @@ describe('useMenuFocus', () => {
|
|||||||
Item 1
|
Item 1
|
||||||
</span>
|
</span>
|
||||||
<span data-role="menuitem">Item 2</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -31,6 +34,7 @@ describe('useMenuFocus', () => {
|
|||||||
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
|
||||||
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
|
||||||
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
||||||
|
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
|
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 1').tabIndex).toBe(0);
|
||||||
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
|
||||||
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
||||||
|
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
|
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 1').tabIndex).toBe(-1);
|
||||||
expect(screen.getByText('Item 2').tabIndex).toBe(0);
|
expect(screen.getByText('Item 2').tabIndex).toBe(0);
|
||||||
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
||||||
|
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
|
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 1').tabIndex).toBe(0);
|
||||||
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
|
||||||
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
|
||||||
|
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
|
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 1').tabIndex).toBe(-1);
|
||||||
expect(screen.getByText('Item 2').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', () => {
|
it('calls close on ArrowLeft and unfocuses all items', () => {
|
||||||
|
@ -41,7 +41,7 @@ export const useMenuFocus = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
|
const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
|
||||||
`[data-role="menuitem"]`
|
'[data-role="menuitem"]:not([data-disabled])'
|
||||||
);
|
);
|
||||||
menuItems?.[focusedItem]?.focus();
|
menuItems?.[focusedItem]?.focus();
|
||||||
menuItems?.forEach((menuItem, i) => {
|
menuItems?.forEach((menuItem, i) => {
|
||||||
@ -51,7 +51,7 @@ export const useMenuFocus = ({
|
|||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
const firstMenuItem = localRef?.current?.querySelector<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
|
const firstMenuItem = localRef?.current?.querySelector<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
|
||||||
`[data-role="menuitem"]`
|
'[data-role="menuitem"]:not([data-disabled])'
|
||||||
);
|
);
|
||||||
if (firstMenuItem) {
|
if (firstMenuItem) {
|
||||||
firstMenuItem.tabIndex = 0;
|
firstMenuItem.tabIndex = 0;
|
||||||
@ -61,7 +61,7 @@ export const useMenuFocus = ({
|
|||||||
|
|
||||||
const handleKeys = (event: React.KeyboardEvent) => {
|
const handleKeys = (event: React.KeyboardEvent) => {
|
||||||
const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
|
const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>(
|
||||||
`[data-role="menuitem"]`
|
'[data-role="menuitem"]:not([data-disabled])'
|
||||||
);
|
);
|
||||||
const menuItemsCount = menuItems?.length ?? 0;
|
const menuItemsCount = menuItems?.length ?? 0;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user