diff --git a/packages/grafana-ui/src/components/Link/TextLink.mdx b/packages/grafana-ui/src/components/Link/TextLink.mdx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/grafana-ui/src/components/Link/TextLink.story.tsx b/packages/grafana-ui/src/components/Link/TextLink.story.tsx new file mode 100644 index 00000000000..8ef60faa363 --- /dev/null +++ b/packages/grafana-ui/src/components/Link/TextLink.story.tsx @@ -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 ( + + +

+ To get started with a forever free Grafana Cloud account, sign up at   + + grafana.com + + . +

+
+ + + Learn how in the docs + + +
+

*The examples cannot contemplate an internal link due to conflicts between Storybook and React Router

+
+ ); +}; + +Example.parameters = { + controls: { exclude: ['href', 'external', 'variant', 'weight', 'color', 'inline', 'icon'] }, +}; + +export const Basic: StoryFn = (args) => { + return ( +
+ + Go to Google + +
+ ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Link/TextLink.tsx b/packages/grafana-ui/src/components/Link/TextLink.tsx new file mode 100644 index 00000000000..2398e5dd11c --- /dev/null +++ b/packages/grafana-ui/src/components/Link/TextLink.tsx @@ -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, '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( + ( + { 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 ? ( + + {children} + + + ) : ( + + {children} + {icon && } + + ); + } +); + +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', + }, + }, + ]); +}; diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx index fa191317f90..d8e7d460c4f 100644 --- a/packages/grafana-ui/src/components/Text/Text.tsx +++ b/packages/grafana-ui/src/components/Text/Text.tsx @@ -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; - } -}; diff --git a/packages/grafana-ui/src/components/Text/utils.ts b/packages/grafana-ui/src/components/Text/utils.ts new file mode 100644 index 00000000000..650e6ecae74 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/utils.ts @@ -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; + } +}; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 65ca761d285..98969e5e40e 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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';