GrafanaUI: Create TextLink component (#69330)

This commit is contained in:
Laura Fernández
2023-07-14 14:35:27 +02:00
committed by GitHub
parent dcf26564db
commit 6ad8009106
6 changed files with 220 additions and 29 deletions
@@ -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 &#160;
<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';