You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

218 lines
6.0 KiB

import React, { ReactElement, useState } from 'react';
import { css, cx } from '@emotion/css';
import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { MenuTriggerProps } from '@react-types/menu';
import { useMenuTriggerState } from '@react-stately/menu';
import { useMenuTrigger } from '@react-aria/menu';
import { useFocusVisible, useFocusWithin, useHover, useKeyboard } from '@react-aria/interactions';
import { useButton } from '@react-aria/button';
import { DismissButton, useOverlay } from '@react-aria/overlays';
import { FocusScope } from '@react-aria/focus';
import { NavBarItemMenuContext } from './context';
export interface NavBarItemMenuTriggerProps extends MenuTriggerProps {
children: ReactElement;
item: NavModelItem;
isActive?: boolean;
label: string;
}
export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactElement {
const { item, isActive, label, children: menu, ...rest } = props;
const [menuHasFocus, setMenuHasFocus] = useState(false);
const theme = useTheme2();
const styles = getStyles(theme, isActive);
// Create state based on the incoming props
const state = useMenuTriggerState({ ...rest });
// Get props for the menu trigger and menu elements
const ref = React.useRef(null);
const { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref);
// style to the focused menu item
let { isFocusVisible } = useFocusVisible({ isTextInput: false });
const { hoverProps } = useHover({
onHoverChange: (isHovering) => {
if (isHovering) {
state.open();
} else {
state.close();
}
},
});
const { focusWithinProps } = useFocusWithin({
onFocusWithinChange: (isFocused) => {
if (isFocused && isFocusVisible) {
state.open();
}
if (!isFocused) {
state.close();
setMenuHasFocus(false);
}
},
});
const { keyboardProps } = useKeyboard({
onKeyDown: (e) => {
switch (e.key) {
case 'ArrowRight':
if (!state.isOpen) {
state.open();
}
setMenuHasFocus(true);
break;
default:
break;
}
},
});
// Get props for the button based on the trigger props from useMenuTrigger
const { buttonProps } = useButton(menuTriggerProps, ref);
let element = (
<button
className={styles.element}
{...buttonProps}
{...keyboardProps}
ref={ref}
onClick={item?.onClick}
aria-label={label}
>
<span className={styles.icon}>
{item?.icon && <Icon name={item.icon as IconName} size="xl" />}
{item?.img && <img src={item.img} alt={`${item.text} logo`} />}
</span>
</button>
);
if (item?.url) {
element =
!item.target && item.url.startsWith('/') ? (
<Link
{...buttonProps}
{...keyboardProps}
ref={ref}
href={item.url}
target={item.target}
onClick={item?.onClick}
className={styles.element}
aria-label={label}
>
<span className={styles.icon}>
{item?.icon && <Icon name={item.icon as IconName} size="xl" />}
{item?.img && <img src={item.img} alt={`${item.text} logo`} />}
</span>
</Link>
) : (
<a
href={item.url}
target={item.target}
onClick={item?.onClick}
{...buttonProps}
{...keyboardProps}
ref={ref}
className={styles.element}
aria-label={label}
>
<span className={styles.icon}>
{item?.icon && <Icon name={item.icon as IconName} size="xl" />}
{item?.img && <img src={item.img} alt={`${item.text} logo`} />}
</span>
</a>
);
}
const overlayRef = React.useRef(null);
const { overlayProps } = useOverlay(
{
onClose: () => state.close(),
shouldCloseOnBlur: true,
isOpen: state.isOpen,
isDismissable: true,
},
overlayRef
);
return (
<div className={cx(styles.element, 'dropdown')} {...focusWithinProps} {...hoverProps}>
{element}
{state.isOpen && (
<NavBarItemMenuContext.Provider value={{ menuProps, menuHasFocus, onClose: () => state.close() }}>
<FocusScope restoreFocus>
<div {...overlayProps} ref={overlayRef}>
<DismissButton onDismiss={() => state.close()} />
{menu}
<DismissButton onDismiss={() => state.close()} />
</div>
</FocusScope>
</NavBarItemMenuContext.Provider>
)}
</div>
);
}
const getStyles = (theme: GrafanaTheme2, isActive?: boolean) => ({
container: css`
position: relative;
color: ${isActive ? theme.colors.text.primary : theme.colors.text.secondary};
list-style: none;
&:hover {
background-color: ${theme.colors.action.hover};
color: ${theme.colors.text.primary};
// TODO don't use a hardcoded class here, use isVisible in NavBarDropdown
.navbar-dropdown {
opacity: 1;
visibility: visible;
}
}
`,
element: css`
background-color: transparent;
border: none;
color: inherit;
display: block;
line-height: ${theme.components.sidemenu.width}px;
padding: 0;
text-align: center;
width: ${theme.components.sidemenu.width}px;
&::before {
display: ${isActive ? 'block' : 'none'};
content: ' ';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
border-radius: 2px;
background-image: ${theme.colors.gradients.brandVertical};
}
&:focus-visible {
background-color: ${theme.colors.action.hover};
box-shadow: none;
color: ${theme.colors.text.primary};
outline: 2px solid ${theme.colors.primary.main};
outline-offset: -2px;
transition: none;
}
`,
icon: css`
height: 100%;
width: 100%;
img {
border-radius: 50%;
height: ${theme.spacing(3)};
width: ${theme.spacing(3)};
}
`,
});