diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 055f88fd65a..e3b31fd9a30 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -13,6 +13,11 @@ export const Components = { }, }, }, + Menu: { + MenuComponent: (title: string) => `${title} menu`, + MenuGroup: (title: string) => `${title} menu group`, + MenuItem: (title: string) => `${title} menu item`, + }, Panels: { Panel: { title: (title: string) => `Panel header title item ${title}`, diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx index 30eb28b7fab..08714bb0dd3 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx @@ -16,10 +16,18 @@ export default { }, }; -const menuItems = [{ label: 'Test', items: [{ label: 'First' }, { label: 'Second' }] }]; +const menuItems = [ + { + label: 'Test', + items: [ + { label: 'First', ariaLabel: 'First' }, + { label: 'Second', ariaLabel: 'Second' }, + ], + }, +]; export const Basic = () => { - return {}} items={menuItems} />; + return {}} itemsGroup={menuItems} />; }; export const WithState = () => { diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index 9cb7adac2ed..d448e9c8efd 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,7 +1,10 @@ -import React, { useRef, useState, useLayoutEffect } from 'react'; +import React, { useRef, useState, useLayoutEffect, useCallback } from 'react'; +import { selectors } from '@grafana/e2e-selectors'; import { useClickAway } from 'react-use'; import { Portal } from '../Portal/Portal'; -import { Menu, MenuItemsGroup } from '../Menu/Menu'; +import { Menu } from '../Menu/Menu'; +import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; +import { MenuItem } from '../Menu/MenuItem'; export interface ContextMenuProps { /** Starting horizontal position for the menu */ @@ -11,12 +14,12 @@ export interface ContextMenuProps { /** Callback for closing the menu */ onClose?: () => void; /** List of the menu items to display */ - items?: MenuItemsGroup[]; + itemsGroup?: MenuItemsGroup[]; /** A function that returns header element */ renderHeader?: () => React.ReactNode; } -export const ContextMenu: React.FC = React.memo(({ x, y, onClose, items, renderHeader }) => { +export const ContextMenu: React.FC = React.memo(({ x, y, onClose, itemsGroup, renderHeader }) => { const menuRef = useRef(null); const [positionStyles, setPositionStyles] = useState({}); @@ -44,10 +47,38 @@ export const ContextMenu: React.FC = React.memo(({ x, y, onClo } }); + const onClick = useCallback(() => { + if (onClose) { + onClose(); + } + }, [onClose]); + const header = renderHeader && renderHeader(); return ( - + + {itemsGroup?.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + ))} + ); }); diff --git a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx index 683654bf49c..51a14559805 100644 --- a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { ContextMenu } from '../ContextMenu/ContextMenu'; -import { MenuItemsGroup } from '../Menu/Menu'; +import { MenuItemsGroup } from '../Menu/MenuGroup'; interface WithContextMenuProps { /** Menu item trigger that accepts openMenu prop */ @@ -30,7 +30,7 @@ export const WithContextMenu: React.FC = ({ children, getC onClose={() => setIsMenuOpen(false)} x={menuPosition.x} y={menuPosition.y} - items={getContextMenuItems()} + itemsGroup={getContextMenuItems()} /> )} diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index eb65c4bd7ca..15499c62f51 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -5,7 +5,8 @@ import { ToolbarButtonVariant, ToolbarButton } from '../Button'; import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; import { css } from 'emotion'; import { useStyles } from '../../themes/ThemeContext'; -import { Menu, MenuItemsGroup } from '../Menu/Menu'; +import { Menu } from '../Menu/Menu'; +import { MenuItem } from '../Menu/MenuItem'; export interface Props extends HTMLAttributes { className?: string; @@ -41,14 +42,6 @@ export const ButtonSelect = React.memo((props: Props) => { setIsOpen(false); }; - const menuGroup: MenuItemsGroup = { - items: options.map((item) => ({ - label: (item.label || item.value) as string, - onClick: () => onChangeInternal(item), - active: item.value === value?.value, - })), - }; - return ( <> (props: Props) => { {isOpen && (
- + + {options.map((item) => ( + onChangeInternal(item)} + active={item.value === value?.value} + /> + ))} +
)} diff --git a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx index 50fe948ac75..28aa8b385d6 100644 --- a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx @@ -29,7 +29,7 @@ export type GraphContextMenuProps = ContextMenuProps & { export const GraphContextMenu: React.FC = ({ getContextMenuSource, timeZone, - items, + itemsGroup, dimensions, contextDimensions, ...otherProps @@ -37,8 +37,8 @@ export const GraphContextMenu: React.FC = ({ const source = getContextMenuSource(); // Do not render items that do not have label specified - const itemsToRender = items - ? items.map((group) => ({ + const itemsToRender = itemsGroup + ? itemsGroup.map((group) => ({ ...group, items: group.items.filter((item) => item.label), })) @@ -81,7 +81,7 @@ export const GraphContextMenu: React.FC = ({ ); }; - return ; + return ; }; /** @internal */ diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx index c7c057bdd61..19e95499feb 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx @@ -1,14 +1,18 @@ import React from 'react'; import { Story } from '@storybook/react'; import { Menu, MenuProps } from './Menu'; +import { MenuItem } from './MenuItem'; +import { MenuGroup } from './MenuGroup'; import { GraphContextMenuHeader } from '..'; +import { StoryExample } from '../../utils/storybook/StoryExample'; +import { VerticalGroup } from '../Layout/Layout'; export default { title: 'General/Menu', component: Menu, argTypes: { items: { control: { disable: true } }, - header: { control: { disable: true } }, + icon: { control: { type: 'select' } }, }, parameters: { knobs: { @@ -23,39 +27,34 @@ export default { }, }; -export const Simple: Story = (args) => ( -
- -
-); +export const Simple: Story = (args) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; Simple.args = { - items: [ - { - label: 'Group 1', - items: [ - { - label: 'Menu item 1', - icon: 'history', - }, - { - label: 'Menu item 2', - icon: 'filter', - }, - ], - }, - { - label: 'Group 2', - items: [ - { - label: 'Menu item 1', - }, - { - label: 'Menu item 2', - }, - ], - }, - ], header: ( { + it('renders items without error', () => { + expect(() => { + render( + + + + + + + ); + }); + }); + + it('renders correct contents', () => { + render( + + + + + + + ); + expect(screen.getByLabelText(selectors.components.Menu.MenuComponent('Test'))).toBeInTheDocument(); + expect(screen.getByLabelText(selectors.components.Menu.MenuGroup('Test'))).toBeInTheDocument(); + expect(screen.getAllByLabelText(selectors.components.Menu.MenuItem('Test'))).toHaveLength(2); + }); +}); diff --git a/packages/grafana-ui/src/components/Menu/Menu.tsx b/packages/grafana-ui/src/components/Menu/Menu.tsx index 84eb7ea3b66..458f0272df7 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.tsx @@ -1,166 +1,35 @@ -import React, { useCallback } from 'react'; -import { css, cx } from 'emotion'; -import { GrafanaTheme, LinkTarget } from '@grafana/data'; -import { List } from '../List/List'; -import { styleMixins, useStyles } from '../../themes'; -import { Icon } from '../Icon/Icon'; -import { IconName } from '../../types'; - -/** @internal */ -export interface MenuItem { - /** Label of the menu item */ - label: string; - /** Target of the menu item (i.e. new window) */ - target?: LinkTarget; - /** Icon of the menu item */ - icon?: IconName; - /** Url of the menu item */ - url?: string; - /** Handler for the click behaviour */ - onClick?: (event?: React.SyntheticEvent) => void; - /** Handler for the click behaviour */ - group?: string; - /** Active */ - active?: boolean; -} - -/** @internal */ -export interface MenuItemsGroup { - /** Label for the menu items group */ - label?: string; - /** Items of the group */ - items: MenuItem[]; -} +import React from 'react'; +import { css } from 'emotion'; +import { GrafanaTheme } from '@grafana/data'; +import { useStyles } from '../../themes'; /** @internal */ export interface MenuProps extends React.HTMLAttributes { /** React element rendered at the top of the menu */ header?: React.ReactNode; - /** Array of menu items */ - items?: MenuItemsGroup[]; - /** Callback performed when menu is closed */ - onClose?: () => void; + children: React.ReactNode; + ariaLabel?: string; } /** @internal */ -export const Menu = React.forwardRef(({ header, items, onClose, ...otherProps }, ref) => { - const styles = useStyles(getMenuStyles); - const onClick = useCallback(() => { - if (onClose) { - onClose(); - } - }, [onClose]); - - return ( -
- {header &&
{header}
} - { - return ; - }} - /> -
- ); -}); -Menu.displayName = 'Menu'; - -interface MenuGroupProps { - group: MenuItemsGroup; - onClick?: () => void; // Used with 'onClose' -} - -const MenuGroup: React.FC = ({ group, onClick }) => { - const styles = useStyles(getMenuStyles); - - if (group.items.length === 0) { - return null; - } - - return ( -
- {group.label &&
{group.label}
} - { - return ( - ) => { - // We can have both url and onClick and we want to allow user to open the link in new tab/window - const isSpecialKeyPressed = e.ctrlKey || e.metaKey || e.shiftKey; - if (isSpecialKeyPressed && item.url) { - return; - } - - if (item.onClick) { - e.preventDefault(); - item.onClick(e); - } - - // Typically closes the context menu - if (onClick) { - onClick(); - } - }} - /> - ); - }} - /> -
- ); -}; -MenuGroup.displayName = 'MenuGroup'; - -interface MenuItemProps { - label: string; - icon?: IconName; - url?: string; - target?: LinkTarget; - onClick?: (e: React.MouseEvent) => void; - className?: string; - active?: boolean; -} - -const MenuItemComponent: React.FC = React.memo( - ({ url, icon, label, target, onClick, className, active }) => { - const styles = useStyles(getMenuStyles); - const itemStyle = cx( - { - [styles.item]: true, - [styles.activeItem]: active, - }, - className - ); +export const Menu = React.forwardRef( + ({ header, children, ariaLabel, ...otherProps }, ref) => { + const styles = useStyles(getStyles); return ( -
- - {icon && } {label} - +
+ {header &&
{header}
} + {children}
); } ); -MenuItemComponent.displayName = 'MenuItemComponent'; +Menu.displayName = 'Menu'; -const getMenuStyles = (theme: GrafanaTheme) => { - const linkColor = theme.colors.text; - const linkColorHover = theme.colors.linkHover; +/** @internal */ +const getStyles = (theme: GrafanaTheme) => { const wrapperBg = theme.colors.formInputBg; const wrapperShadow = theme.isDark ? theme.palette.black : theme.palette.gray3; - const groupLabelColor = theme.colors.textWeak; - const itemBgHover = styleMixins.hoverColor(theme.colors.bg1, theme); const headerBg = theme.colors.formInputBg; const headerSeparator = theme.colors.border3; @@ -178,42 +47,5 @@ const getMenuStyles = (theme: GrafanaTheme) => { display: inline-block; border-radius: ${theme.border.radius.sm}; `, - link: css` - color: ${linkColor}; - display: flex; - cursor: pointer; - padding: 5px 12px 5px 10px; - - &:hover { - color: ${linkColorHover}; - text-decoration: none; - } - `, - item: css` - background: none; - border-left: 2px solid transparent; - cursor: pointer; - white-space: nowrap; - - &:hover { - background: ${itemBgHover}; - border-image: linear-gradient(#f05a28 30%, #fbca0a 99%); - border-image-slice: 1; - } - `, - activeItem: css` - background: ${theme.colors.bg2}; - `, - groupLabel: css` - color: ${groupLabelColor}; - font-size: ${theme.typography.size.sm}; - line-height: ${theme.typography.lineHeight.md}; - padding: ${theme.spacing.xs} ${theme.spacing.sm}; - `, - icon: css` - opacity: 0.7; - margin-right: 10px; - color: ${theme.colors.linkDisabled}; - `, }; }; diff --git a/packages/grafana-ui/src/components/Menu/MenuGroup.tsx b/packages/grafana-ui/src/components/Menu/MenuGroup.tsx new file mode 100644 index 00000000000..30c4e21990a --- /dev/null +++ b/packages/grafana-ui/src/components/Menu/MenuGroup.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { css } from 'emotion'; +import { GrafanaTheme } from '@grafana/data'; +import { useStyles } from '../../themes'; +import { MenuItemProps } from './MenuItem'; + +/** @internal */ +export interface MenuItemsGroup { + /** Label for the menu items group */ + label?: string; + /** Aria label for accessibility support */ + ariaLabel?: string; + /** Items of the group */ + items: MenuItemProps[]; +} +/** @internal */ +export interface MenuGroupProps extends Partial { + /** special children prop to pass children elements */ + children: React.ReactNode; +} + +/** @internal */ +export const MenuGroup: React.FC = ({ label, children, ariaLabel }) => { + const styles = useStyles(getStyles); + + return ( +
+ {label && ( +
+ {label} +
+ )} + {children} +
+ ); +}; +MenuGroup.displayName = 'MenuGroup'; + +/** @internal */ +const getStyles = (theme: GrafanaTheme) => { + const groupLabelColor = theme.colors.textWeak; + return { + groupLabel: css` + color: ${groupLabelColor}; + font-size: ${theme.typography.size.sm}; + line-height: ${theme.typography.lineHeight.md}; + padding: ${theme.spacing.xs} ${theme.spacing.sm}; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx new file mode 100644 index 00000000000..c132344e073 --- /dev/null +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { css, cx } from 'emotion'; +import { GrafanaTheme, LinkTarget } from '@grafana/data'; +import { styleMixins, useStyles } from '../../themes'; +import { Icon } from '../Icon/Icon'; +import { IconName } from '../../types'; + +/** @internal */ +export interface MenuItemProps { + /** Label of the menu item */ + label: string; + /** Aria label for accessibility support */ + ariaLabel: string; + /** Target of the menu item (i.e. new window) */ + target?: LinkTarget; + /** Icon of the menu item */ + icon?: IconName; + /** Url of the menu item */ + url?: string; + /** Handler for the click behaviour */ + onClick?: (event?: React.SyntheticEvent) => void; + /** Custom MenuItem styles*/ + className?: string; + /** Active */ + active?: boolean; +} + +/** @internal */ +export const MenuItem: React.FC = React.memo( + ({ url, icon, label, ariaLabel, target, onClick, className, active }) => { + const styles = useStyles(getStyles); + const itemStyle = cx( + { + [styles.item]: true, + [styles.activeItem]: active, + }, + className + ); + + return ( + + ); + } +); +MenuItem.displayName = 'MenuItem'; + +/** @internal */ +const getStyles = (theme: GrafanaTheme) => { + const linkColor = theme.colors.text; + const linkColorHover = theme.colors.linkHover; + const itemBgHover = styleMixins.hoverColor(theme.colors.bg1, theme); + + return { + link: css` + color: ${linkColor}; + display: flex; + cursor: pointer; + padding: 5px 12px 5px 10px; + &:hover { + color: ${linkColorHover}; + text-decoration: none; + } + `, + item: css` + background: none; + border-left: 2px solid transparent; + cursor: pointer; + white-space: nowrap; + &:hover { + background: ${itemBgHover}; + border-image: linear-gradient(#f05a28 30%, #fbca0a 99%); + border-image-slice: 1; + } + `, + activeItem: css` + background: ${theme.colors.bg2}; + `, + icon: css` + opacity: 0.7; + margin-right: 10px; + color: ${theme.colors.linkDisabled}; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx b/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx index 75d73e5f0a1..6303376c8e2 100644 --- a/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx +++ b/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx @@ -34,7 +34,7 @@ export function useContextMenu( MenuComponent = ( } - items={items} + itemsGroup={items} onClose={() => setOpenedNode(undefined)} x={openedNode.event.pageX} y={openedNode.event.pageY} @@ -49,7 +49,7 @@ export function useContextMenu( MenuComponent = ( } - items={items} + itemsGroup={items} onClose={() => setOpenedEdge(undefined)} x={openedEdge.event.pageX} y={openedEdge.event.pageY} @@ -82,8 +82,10 @@ function getItems(links: LinkModel[]) { return Object.keys(groups).map((key) => { return { label: key, + ariaLabel: key, items: groups[key].map((link) => ({ label: link.newTitle || link.l.title, + ariaLabel: link.newTitle || link.l.title, url: link.l.href, onClick: link.l.onClick, })), diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index f0b55b42cb8..c62c65e07e0 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -102,7 +102,9 @@ export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper'; export * from './SingleStatShared/index'; export { CallToActionCard } from './CallToActionCard/CallToActionCard'; export { ContextMenu, ContextMenuProps } from './ContextMenu/ContextMenu'; -export { Menu, MenuItem, MenuItemsGroup } from './Menu/Menu'; +export { Menu, MenuProps } from './Menu/Menu'; +export { MenuGroup, MenuItemsGroup, MenuGroupProps } from './Menu/MenuGroup'; +export { MenuItem, MenuItemProps } from './Menu/MenuItem'; export { WithContextMenu } from './ContextMenu/WithContextMenu'; export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; export { DataLinkInput } from './DataLinks/DataLinkInput'; diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts index 529cf232400..b99e50e089d 100644 --- a/packages/grafana-ui/src/utils/dataLinks.ts +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -1,14 +1,15 @@ import { LinkModel } from '@grafana/data'; -import { MenuItem } from '../components/Menu/Menu'; +import { MenuItemProps } from '../components/Menu/MenuItem'; import { IconName } from '../types'; /** * Delays creating links until we need to open the ContextMenu */ -export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem[] = (links) => { +export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItemProps[] = (links) => { return links().map((link) => { return { label: link.title, + ariaLabel: link.title, // TODO: rename to href url: link.href, target: link.target, diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 222be19d297..2dd8ff2ce88 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -141,7 +141,7 @@ export function registerAngularDirectives() { react2AngularDirective('graphContextMenu', GraphContextMenu, [ 'x', 'y', - 'items', + 'itemsGroup', ['onClose', { watchDepth: 'reference', wrapApply: true }], ['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }], ['timeZone', { watchDepth: 'reference', wrapApply: true }], diff --git a/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts b/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts index f7cf0380212..07283ae88c3 100644 --- a/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts +++ b/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts @@ -1,10 +1,10 @@ -import { MenuItem } from '@grafana/ui'; +import { MenuItemProps } from '@grafana/ui'; import { FlotDataPoint } from '@grafana/data'; export class GraphContextMenuCtrl { private source?: FlotDataPoint | null; private scope?: any; - menuItemsSupplier?: () => MenuItem[]; + menuItemsSupplier?: () => MenuItemProps[]; scrollContextElement: HTMLElement | null; position: { x: number; @@ -61,7 +61,7 @@ export class GraphContextMenuCtrl { return this.source; }; - setMenuItemsSupplier = (menuItemsSupplier: () => MenuItem[]) => { + setMenuItemsSupplier = (menuItemsSupplier: () => MenuItemProps[]) => { this.menuItemsSupplier = menuItemsSupplier; }; } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index b415981378b..69c46cf0b02 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -24,7 +24,7 @@ import ReactDOM from 'react-dom'; import { GraphLegendProps, Legend } from './Legend/Legend'; import { GraphCtrl } from './module'; -import { graphTickFormatter, graphTimeFormat, IconName, MenuItem, MenuItemsGroup } from '@grafana/ui'; +import { graphTickFormatter, graphTimeFormat, IconName, MenuItemProps, MenuItemsGroup } from '@grafana/ui'; import { provideTheme } from 'app/core/utils/ConfigProvider'; import { DataFrame, @@ -207,6 +207,7 @@ class GraphElement { items: [ { label: 'Add annotation', + ariaLabel: 'Add annotation', icon: 'comment-alt', onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }), }, @@ -221,9 +222,10 @@ class GraphElement { const dataLinks = [ { - items: linksSupplier.getLinks(this.panel.replaceVariables).map((link) => { + items: linksSupplier.getLinks(this.panel.replaceVariables).map((link) => { return { label: link.title, + ariaLabel: link.title, url: link.href, target: link.target, icon: `${link.target === '_self' ? 'link' : 'external-link-alt'}` as IconName, diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts index 27c41178eab..f737f10fb8d 100644 --- a/public/app/plugins/panel/graph/template.ts +++ b/public/app/plugins/panel/graph/template.ts @@ -8,7 +8,7 @@ const template = `
= ({ if (linksSupplier) { items.push({ - items: linksSupplier.getLinks(replaceVariables).map((link) => { + items: linksSupplier.getLinks(replaceVariables).map((link) => { return { label: link.title, + ariaLabel: link.title, url: link.href, target: link.target, icon: `${link.target === '_self' ? 'link' : 'external-link-alt'}` as IconName, @@ -161,7 +162,7 @@ export const ContextMenuView: React.FC = ({ return (