Updated according to feedback:
This commit is contained in:
@@ -188,7 +188,11 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an
|
||||
export interface PanelMenuItem {
|
||||
type?: 'submenu' | 'divider' | 'group';
|
||||
text: string;
|
||||
prefix?: React.ReactElement;
|
||||
/** A React element or IconName that will be displayed before the title */
|
||||
prefix?: React.ReactElement | IconName;
|
||||
/**
|
||||
* @deprecated Use `prefix` instead. This property will be removed in a future release.
|
||||
*/
|
||||
iconClassName?: IconName;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
shortcut?: string;
|
||||
|
||||
@@ -28,9 +28,12 @@ export type PluginExtensionLink = PluginExtensionBase & {
|
||||
type: PluginExtensionTypes.link;
|
||||
path?: string;
|
||||
onClick?: (event?: React.MouseEvent) => void;
|
||||
/**
|
||||
* @deprecated Use `prefix` instead. This property will be removed in a future release.
|
||||
*/
|
||||
icon?: IconName;
|
||||
suffix?: React.ReactElement;
|
||||
prefix?: React.ReactElement;
|
||||
/** A React element or IconName that will be displayed before the title */
|
||||
prefix?: React.ReactElement | IconName;
|
||||
category?: string;
|
||||
openInNewTab?: boolean;
|
||||
};
|
||||
@@ -108,9 +111,12 @@ export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Re
|
||||
description: string;
|
||||
path: string;
|
||||
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
/**
|
||||
* @deprecated Use `prefix` instead. This property will be removed in a future release.
|
||||
*/
|
||||
icon: IconName;
|
||||
suffix: React.ReactElement;
|
||||
prefix: React.ReactElement;
|
||||
/** A React element or IconName that will be displayed before the title */
|
||||
prefix: React.ReactElement | IconName;
|
||||
category: string;
|
||||
openInNewTab: boolean;
|
||||
}>
|
||||
@@ -139,7 +145,9 @@ export type PluginExtensionAddedLinkConfig<Context extends object = object> = Pl
|
||||
// (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
|
||||
configure?: PluginAddedLinksConfigureFunc<Context>;
|
||||
|
||||
// (Optional) A icon that can be displayed in the ui for the extension option.
|
||||
/**
|
||||
* @deprecated Use `prefix` instead. This property will be removed in a future release.
|
||||
*/
|
||||
icon?: IconName;
|
||||
|
||||
// (Optional) A category to be used when grouping the options in the ui
|
||||
@@ -149,11 +157,8 @@ export type PluginExtensionAddedLinkConfig<Context extends object = object> = Pl
|
||||
// (Important: this is not guaranteed, depends on the extension point if it implements it.)
|
||||
openInNewTab?: boolean;
|
||||
|
||||
// (Optional) A React element that will be displayed after the title
|
||||
suffix?: React.ReactElement;
|
||||
|
||||
// (Optional) A React element that will be displayed before the title
|
||||
prefix?: React.ReactElement;
|
||||
// (Optional) A React element or IconName that will be displayed before the title
|
||||
prefix?: React.ReactElement | IconName;
|
||||
};
|
||||
|
||||
export type PluginExtensionExposedComponentConfig<Props = {}> = PluginExtensionConfigBase & {
|
||||
|
||||
@@ -2,12 +2,11 @@ import { css, cx } from '@emotion/css';
|
||||
import { ReactElement, useCallback, useState, useRef, useImperativeHandle, CSSProperties, AriaRole } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
|
||||
import { GrafanaTheme2, IconName, isIconName, LinkTarget } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFocusStyles, getInternalRadius } from '../../themes/mixins';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Stack } from '../Layout/Stack/Stack';
|
||||
|
||||
@@ -29,10 +28,12 @@ export interface MenuItemProps<T = unknown> {
|
||||
ariaChecked?: boolean;
|
||||
/** Target of the menu item (i.e. new window) */
|
||||
target?: LinkTarget;
|
||||
/** Icon of the menu item */
|
||||
/**
|
||||
* @deprecated Use `prefix` instead. This property will be removed in a future release.
|
||||
*/
|
||||
icon?: IconName;
|
||||
/** Prefix of the menu item if icon is specified prefix will be ignored */
|
||||
prefix?: React.ReactElement;
|
||||
/** A React element or IconName that will be displayed before the title */
|
||||
prefix?: React.ReactElement | IconName;
|
||||
/** Role of the menu item */
|
||||
role?: AriaRole;
|
||||
/** Url of the menu item */
|
||||
@@ -180,7 +181,7 @@ export const MenuItem = React.memo(
|
||||
{...disabledProps}
|
||||
>
|
||||
<Stack direction="row" justifyContent="flex-start" alignItems="center">
|
||||
<MenuItemPrefix icon={icon} prefix={prefix} />
|
||||
<MenuItemPrefix prefix={isIconName(icon) ? icon : prefix} />
|
||||
<span className={cx(styles.ellipsis, styles.label)}>{label}</span>
|
||||
<div className={cx(styles.rightWrapper, { [styles.withShortcut]: hasShortcut })}>
|
||||
{hasShortcut && (
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { MenuItemPrefix } from './MenuItemPrefix';
|
||||
|
||||
describe('MenuItemPrefix', () => {
|
||||
it('renders nothing when prefix is not provided', () => {
|
||||
const { container } = render(<MenuItemPrefix />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders icon when prefix is an IconName string', () => {
|
||||
const { container } = render(<MenuItemPrefix prefix="history" />);
|
||||
const svg = container.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('renders custom element when prefix is a React element', () => {
|
||||
render(<MenuItemPrefix prefix={<span data-testid="custom-icon">Custom</span>} />);
|
||||
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('wraps React element prefix in a div with aria-hidden', () => {
|
||||
render(<MenuItemPrefix prefix={<span data-testid="custom-icon">Custom</span>} />);
|
||||
const wrapper = screen.getByTestId('custom-icon').parentElement;
|
||||
expect(wrapper).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('renders nothing when prefix is an invalid value', () => {
|
||||
// @ts-expect-error - testing invalid input
|
||||
const { container } = render(<MenuItemPrefix prefix="not-a-valid-icon-name" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders nothing when prefix is null', () => {
|
||||
// @ts-expect-error - testing null input
|
||||
const { container } = render(<MenuItemPrefix prefix={null} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,30 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isValidElement } from 'react';
|
||||
|
||||
import { IconName } from '@grafana/data';
|
||||
import { IconName, isIconName } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { getSvgSize } from '../Icon/utils';
|
||||
|
||||
type MenuItemPrefixProps = {
|
||||
prefix?: React.ReactElement;
|
||||
icon?: IconName;
|
||||
prefix?: React.ReactElement | IconName;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export function MenuItemPrefix({ prefix, icon }: MenuItemPrefixProps): React.ReactNode {
|
||||
export function MenuItemPrefix({ prefix }: MenuItemPrefixProps): React.ReactNode {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!icon && !prefix) {
|
||||
if (!prefix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
return <Icon name={icon} className={styles.icon} aria-hidden />;
|
||||
if (isIconName(prefix)) {
|
||||
return <Icon name={prefix} className={styles.icon} aria-hidden />;
|
||||
}
|
||||
|
||||
if (!isValidElement(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,9 +19,12 @@ export type AddedLinkRegistryItem<Context extends object = object> = {
|
||||
path?: string;
|
||||
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
configure?: PluginAddedLinksConfigureFunc<Context>;
|
||||
/**
|
||||
* @deprecated Use `prefix` instead. This property will be removed in a future release.
|
||||
*/
|
||||
icon?: IconName;
|
||||
suffix?: React.ReactElement;
|
||||
prefix?: React.ReactElement;
|
||||
/** A React element or IconName that will be displayed before the title */
|
||||
prefix?: React.ReactElement | IconName;
|
||||
category?: string;
|
||||
openInNewTab?: boolean;
|
||||
};
|
||||
|
||||
@@ -73,6 +73,8 @@ export function usePluginLinks({
|
||||
}
|
||||
|
||||
const path = overrides?.path || addedLink.path;
|
||||
// For backwards compatibility: if prefix is not set, fall back to icon
|
||||
const prefix = overrides?.prefix || addedLink.prefix || overrides?.icon || addedLink.icon;
|
||||
const extension: PluginExtensionLink = {
|
||||
id: generateExtensionId(pluginId, extensionPointId, addedLink.title),
|
||||
type: PluginExtensionTypes.link,
|
||||
@@ -86,8 +88,7 @@ export function usePluginLinks({
|
||||
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined,
|
||||
category: overrides?.category || addedLink.category,
|
||||
openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab,
|
||||
suffix: overrides?.suffix || addedLink.suffix,
|
||||
prefix: overrides?.prefix || addedLink.prefix,
|
||||
prefix,
|
||||
};
|
||||
|
||||
extensions.push(extension);
|
||||
|
||||
@@ -486,7 +486,6 @@ export function getLinkExtensionOverrides(
|
||||
icon = config.icon,
|
||||
category = config.category,
|
||||
openInNewTab = config.openInNewTab,
|
||||
suffix = config.suffix,
|
||||
prefix = config.prefix,
|
||||
...rest
|
||||
} = overrides;
|
||||
@@ -513,7 +512,6 @@ export function getLinkExtensionOverrides(
|
||||
icon,
|
||||
category,
|
||||
openInNewTab,
|
||||
suffix,
|
||||
prefix,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user