GrafanaUI: Create TextLink component (#69330)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { StoryExample } from '../../utils/storybook/StoryExample';
|
||||
import { VerticalGroup } from '../Layout/Layout';
|
||||
import { P } from '../Text/TextElements';
|
||||
|
||||
import { TextLink } from './TextLink';
|
||||
import mdx from './TextLink.mdx';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'General/TextLink',
|
||||
component: TextLink,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: { exclude: ['href', 'external'] },
|
||||
},
|
||||
argTypes: {
|
||||
variant: { control: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined] },
|
||||
weight: {
|
||||
control: 'select',
|
||||
options: ['bold', 'medium', 'light', 'regular', undefined],
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'disabled', 'link', 'maxContrast', undefined],
|
||||
},
|
||||
inline: { control: 'boolean' },
|
||||
},
|
||||
args: {
|
||||
variant: 'body',
|
||||
weight: 'light',
|
||||
color: undefined,
|
||||
inline: false,
|
||||
href: 'https://www.google.com',
|
||||
external: true,
|
||||
icon: 'external-link-alt',
|
||||
},
|
||||
};
|
||||
|
||||
export const Example: StoryFn = (args) => {
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<StoryExample name="This is a 'inline + external' link with the default behaviour">
|
||||
<P>
|
||||
To get started with a forever free Grafana Cloud account, sign up at  
|
||||
<TextLink href="https://grafana.com/" external {...args}>
|
||||
grafana.com
|
||||
</TextLink>
|
||||
.
|
||||
</P>
|
||||
</StoryExample>
|
||||
<StoryExample name="This is a 'standalone + external' link with the default behaviour">
|
||||
<TextLink href="https://grafana.com/docs/grafana/latest/" inline={false} external {...args}>
|
||||
Learn how in the docs
|
||||
</TextLink>
|
||||
</StoryExample>
|
||||
<hr />
|
||||
<P>*The examples cannot contemplate an internal link due to conflicts between Storybook and React Router</P>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
Example.parameters = {
|
||||
controls: { exclude: ['href', 'external', 'variant', 'weight', 'color', 'inline', 'icon'] },
|
||||
};
|
||||
|
||||
export const Basic: StoryFn = (args) => {
|
||||
return (
|
||||
<div>
|
||||
<TextLink href={args.href} {...args}>
|
||||
Go to Google
|
||||
</TextLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { AnchorHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2, locationUtil, textUtil, ThemeTypographyVariantTypes } from '@grafana/data';
|
||||
|
||||
import { useTheme2 } from '../../themes';
|
||||
import { IconName, IconSize } from '../../types';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { customWeight } from '../Text/utils';
|
||||
|
||||
import { Link } from './Link';
|
||||
|
||||
interface TextLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'target' | 'rel'> {
|
||||
/** url to which redirect the user, external or internal */
|
||||
href: string;
|
||||
/** Color to use for text */
|
||||
color?: keyof GrafanaTheme2['colors']['text'];
|
||||
/** Specify if the link will redirect users to a page in or out Grafana */
|
||||
external?: boolean;
|
||||
/** True when the link will be displayed inline with surrounding text, false if it will be displayed as a block. Depending on this prop correspondant default styles will be applied */
|
||||
inline?: boolean;
|
||||
/** The default variant is 'body'. To fit another styles set the correspondent variant as it is necessary also to adjust the icon size */
|
||||
variant?: keyof ThemeTypographyVariantTypes;
|
||||
/** Override the default weight for the used variant */
|
||||
weight?: 'light' | 'regular' | 'medium' | 'bold';
|
||||
/** Set the icon to be shown. An external link will show the 'external-link-alt' icon as default.*/
|
||||
icon?: IconName;
|
||||
children: string;
|
||||
}
|
||||
|
||||
const svgSizes: {
|
||||
[key in keyof ThemeTypographyVariantTypes]: IconSize;
|
||||
} = {
|
||||
h1: 'xl',
|
||||
h2: 'xl',
|
||||
h3: 'lg',
|
||||
h4: 'lg',
|
||||
h5: 'md',
|
||||
h6: 'md',
|
||||
body: 'md',
|
||||
bodySmall: 'xs',
|
||||
};
|
||||
|
||||
export const TextLink = forwardRef<HTMLAnchorElement, TextLinkProps>(
|
||||
(
|
||||
{ href, color = 'link', external = false, inline = true, variant = 'body', weight, icon, children, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const validUrl = locationUtil.stripBaseFromUrl(textUtil.sanitizeUrl(href ?? ''));
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getLinkStyles(theme, inline, variant, weight, color);
|
||||
const externalIcon = icon || 'external-link-alt';
|
||||
|
||||
return external ? (
|
||||
<a href={validUrl} ref={ref} target="_blank" rel="noreferrer" {...rest} className={styles}>
|
||||
{children}
|
||||
<Icon size={svgSizes[variant] || 'md'} name={externalIcon} />
|
||||
</a>
|
||||
) : (
|
||||
<Link ref={ref} href={validUrl} {...rest} className={styles}>
|
||||
{children}
|
||||
{icon && <Icon name={icon} size={svgSizes[variant] || 'md'} />}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextLink.displayName = 'TextLink';
|
||||
|
||||
export const getLinkStyles = (
|
||||
theme: GrafanaTheme2,
|
||||
inline: boolean,
|
||||
variant?: keyof ThemeTypographyVariantTypes,
|
||||
weight?: TextLinkProps['weight'],
|
||||
color?: TextLinkProps['color']
|
||||
) => {
|
||||
return css([
|
||||
variant && {
|
||||
...theme.typography[variant],
|
||||
},
|
||||
weight && {
|
||||
fontWeight: customWeight(weight, theme),
|
||||
},
|
||||
color && {
|
||||
color: theme.colors.text[color],
|
||||
},
|
||||
{
|
||||
alignItems: 'center',
|
||||
gap: '0.25em',
|
||||
display: 'inline-flex',
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
},
|
||||
inline && {
|
||||
textDecoration: 'underline',
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -5,6 +5,8 @@ import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
|
||||
import { customWeight, customColor } from './utils';
|
||||
|
||||
export interface TextProps {
|
||||
/** Defines what HTML element is defined underneath */
|
||||
as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p' | 'legend';
|
||||
@@ -75,32 +77,3 @@ const getTextStyles = (
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => {
|
||||
switch (weight) {
|
||||
case 'bold':
|
||||
return theme.typography.fontWeightBold;
|
||||
case 'medium':
|
||||
return theme.typography.fontWeightMedium;
|
||||
case 'light':
|
||||
return theme.typography.fontWeightLight;
|
||||
case 'regular':
|
||||
case undefined:
|
||||
return theme.typography.fontWeightRegular;
|
||||
}
|
||||
};
|
||||
|
||||
const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => {
|
||||
switch (color) {
|
||||
case 'error':
|
||||
return theme.colors.error.text;
|
||||
case 'success':
|
||||
return theme.colors.success.text;
|
||||
case 'info':
|
||||
return theme.colors.info.text;
|
||||
case 'warning':
|
||||
return theme.colors.warning.text;
|
||||
default:
|
||||
return color ? theme.colors.text[color] : undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { TextProps } from './Text';
|
||||
|
||||
export const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => {
|
||||
switch (weight) {
|
||||
case 'bold':
|
||||
return theme.typography.fontWeightBold;
|
||||
case 'medium':
|
||||
return theme.typography.fontWeightMedium;
|
||||
case 'light':
|
||||
return theme.typography.fontWeightLight;
|
||||
case 'regular':
|
||||
case undefined:
|
||||
return theme.typography.fontWeightRegular;
|
||||
}
|
||||
};
|
||||
|
||||
export const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => {
|
||||
switch (color) {
|
||||
case 'error':
|
||||
return theme.colors.error.text;
|
||||
case 'success':
|
||||
return theme.colors.success.text;
|
||||
case 'info':
|
||||
return theme.colors.info.text;
|
||||
case 'warning':
|
||||
return theme.colors.warning.text;
|
||||
default:
|
||||
return color ? theme.colors.text[color] : undefined;
|
||||
}
|
||||
};
|
||||
@@ -204,6 +204,7 @@ export { ToolbarButton, ToolbarButtonRow } from './ToolbarButton';
|
||||
export { ValuePicker } from './ValuePicker/ValuePicker';
|
||||
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
|
||||
export { Link } from './Link/Link';
|
||||
export { TextLink } from './Link/TextLink';
|
||||
|
||||
export { Label } from './Forms/Label';
|
||||
export { Field, type FieldProps } from './Forms/Field';
|
||||
|
||||
Reference in New Issue
Block a user