Skip to content

Commit 793c393

Browse files
Merge branch 'add-support-for-external-links-in-link-component-des-1826'
2 parents e145159 + 8bc21e0 commit 793c393

File tree

22 files changed

+154
-142
lines changed

22 files changed

+154
-142
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useCallback } from 'react';
2+
3+
import { Url } from '../../shared/constants';
4+
import { useAppContext } from '../context';
5+
import { Link, LinkProps } from '../lib/components';
6+
7+
export type ExternalLinkProps = Omit<LinkProps<'a'>, 'href' | 'as'> & {
8+
to: Url;
9+
};
10+
11+
export const ExternalLink = ({ to, onClick, ...props }: ExternalLinkProps) => {
12+
const { openUrl } = useAppContext();
13+
const navigate = useCallback(
14+
(e: React.MouseEvent<HTMLAnchorElement>) => {
15+
e.preventDefault();
16+
if (onClick) {
17+
onClick(e);
18+
}
19+
return openUrl(to);
20+
},
21+
[onClick, openUrl, to],
22+
);
23+
return <Link href="" onClick={navigate} {...props} />;
24+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useCallback } from 'react';
2+
3+
import { Link, LinkProps } from '../lib/components';
4+
import { useHistory } from '../lib/history';
5+
import { RoutePath } from '../lib/routes';
6+
7+
export type InternalLinkProps = Omit<LinkProps<'a'>, 'href' | 'as'> & {
8+
to: RoutePath;
9+
};
10+
11+
export const InternalLink = ({ to, onClick, ...props }: InternalLinkProps) => {
12+
const history = useHistory();
13+
const navigate = useCallback(
14+
(e: React.MouseEvent<HTMLAnchorElement>) => {
15+
e.preventDefault();
16+
if (onClick) {
17+
onClick(e);
18+
}
19+
return history.push(to);
20+
},
21+
[history, to, onClick],
22+
);
23+
return <Link href="" onClick={navigate} {...props} />;
24+
};

desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
} from '../../shared/notifications';
1717
import { useAppContext } from '../context';
1818
import useActions from '../lib/actionsHook';
19-
import { Link } from '../lib/components';
2019
import { Colors } from '../lib/foundations';
2120
import { transitions, useHistory } from '../lib/history';
2221
import { formatHtml } from '../lib/html-formatter';
@@ -28,6 +27,7 @@ import { RoutePath } from '../lib/routes';
2827
import accountActions from '../redux/account/actions';
2928
import { IReduxState, useSelector } from '../redux/store';
3029
import * as AppButton from './AppButton';
30+
import { InternalLink } from './InternalLink';
3131
import { ModalAlert, ModalAlertType, ModalMessage, ModalMessageList } from './Modal';
3232
import {
3333
NotificationActions,
@@ -141,13 +141,13 @@ export default function NotificationArea(props: IProps) {
141141
{notification.title}
142142
</NotificationTitle>
143143
<NotificationSubtitle data-testid="notificationSubTitle">
144-
{notification.subtitleAction?.type === 'navigate' ? (
145-
<Link
144+
{notification.subtitleAction?.type === 'navigate-internal' ? (
145+
<InternalLink
146146
variant="labelTiny"
147147
color={Colors.white60}
148148
{...notification.subtitleAction.link}>
149149
{formatHtml(notification.subtitle ?? '')}
150-
</Link>
150+
</InternalLink>
151151
) : (
152152
formatHtml(notification.subtitle ?? '')
153153
)}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import React from 'react';
12
import styled from 'styled-components';
23

34
import { Colors } from '../../../foundations';
45
import { BodySmallSemiBold, BodySmallSemiBoldProps } from '../../typography';
56
import { useButtonContext } from '../ButtonContext';
67

7-
export type ButtonTextProps = Omit<BodySmallSemiBoldProps, 'color'>;
8+
export type ButtonTextProps<T extends React.ElementType = 'span'> = BodySmallSemiBoldProps<T>;
89
export const StyledText = styled(BodySmallSemiBold)``;
910

10-
export const ButtonText = (props: ButtonTextProps) => {
11+
export const ButtonText = <T extends React.ElementType = 'span'>(props: ButtonTextProps<T>) => {
1112
const { disabled } = useButtonContext();
1213
return <StyledText color={disabled ? Colors.white40 : Colors.white} {...props} />;
1314
};

desktop/packages/mullvad-vpn/src/renderer/lib/components/filter-chip/components/FilterChipText.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { Colors } from '../../../foundations';
44
import { BodySmallSemiBoldProps, LabelTiny } from '../../typography';
55
import { useFilterChipContext } from '../FilterChipContext';
66

7-
export type FilterChipTextProps = Omit<BodySmallSemiBoldProps, 'color'>;
7+
export type FilterChipTextProps<T extends React.ElementType = 'span'> = BodySmallSemiBoldProps<T>;
88

99
export const StyledText = styled(LabelTiny)``;
1010

11-
export const FilterChipText = (props: FilterChipTextProps) => {
11+
export const FilterChipText = <T extends React.ElementType = 'span'>(
12+
props: FilterChipTextProps<T>,
13+
) => {
1214
const { disabled } = useFilterChipContext();
1315
return <StyledText color={disabled ? Colors.white40 : Colors.white} {...props} />;
1416
};

desktop/packages/mullvad-vpn/src/renderer/lib/components/navigation-header/components/NavigationHeaderTitle.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export const StyledText = styled(TitleMedium)<{ $visible?: boolean }>(({ $visibl
1515
textOverflow: 'ellipsis',
1616
}));
1717

18-
export const NavigationHeaderTitle = ({ children }: NavigationHeaderTitleProps) => {
18+
export const NavigationHeaderTitle = ({ children, ...props }: NavigationHeaderTitleProps) => {
1919
const { titleVisible } = useNavigationHeader();
2020
return (
21-
<StyledText tag="h1" $visible={titleVisible}>
21+
<StyledText forwardedAs="h1" $visible={titleVisible} {...props}>
2222
{children}
2323
</StyledText>
2424
);

desktop/packages/mullvad-vpn/src/renderer/lib/components/progress/components/ProgressPercent.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { Colors } from '../../../foundations';
44
import { LabelTiny, LabelTinyProps } from '../../typography';
55
import { useProgress } from '../ProgressContext';
66

7-
export type ProgressPercentProps = Omit<LabelTinyProps, 'children'>;
7+
export type ProgressPercentProps<T extends React.ElementType = 'span'> = LabelTinyProps<T>;
88

99
const StyledText = styled(LabelTiny)`
1010
min-width: 26px;
1111
`;
1212

13-
export const ProgressPercent = (props: ProgressPercentProps) => {
13+
export const ProgressPercent = <T extends React.ElementType = 'span'>(
14+
props: ProgressPercentProps<T>,
15+
) => {
1416
const { percent, disabled } = useProgress();
1517
return (
1618
<StyledText color={disabled ? Colors.white40 : Colors.white} {...props}>

desktop/packages/mullvad-vpn/src/renderer/lib/components/progress/components/ProgressText.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@ import { Colors } from '../../../foundations';
22
import { LabelTiny, LabelTinyProps } from '../../typography';
33
import { useProgress } from '../ProgressContext';
44

5-
export type ProgressTextProps = LabelTinyProps;
5+
export type ProgressTextProps<T extends React.ElementType = 'span'> = LabelTinyProps<T>;
66

7-
export const ProgressText = ({ children, ...props }: ProgressTextProps) => {
7+
export const ProgressText = <T extends React.ElementType = 'span'>(props: ProgressTextProps<T>) => {
88
const { disabled } = useProgress();
9-
return (
10-
<LabelTiny color={disabled ? Colors.white40 : Colors.white60} {...props}>
11-
{children}
12-
</LabelTiny>
13-
);
9+
return <LabelTiny color={disabled ? Colors.white40 : Colors.white60} {...props} />;
1410
};
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Text, TextProps } from './Text';
22

3-
export type BodySmallProps = Omit<TextProps, 'variant'>;
3+
export type BodySmallProps<E extends React.ElementType = 'span'> = TextProps<E>;
44

5-
export const BodySmall = ({ children, ...props }: BodySmallProps) => (
6-
<Text variant="bodySmall" {...props}>
7-
{children}
8-
</Text>
5+
export const BodySmall = <T extends React.ElementType = 'span'>(props: BodySmallProps<T>) => (
6+
<Text variant="bodySmall" {...props} />
97
);
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Text, TextProps } from './Text';
22

3-
export type BodySmallSemiBoldProps = Omit<TextProps, 'variant'>;
3+
export type BodySmallSemiBoldProps<E extends React.ElementType = 'span'> = TextProps<E>;
44

5-
export const BodySmallSemiBold = ({ children, ...props }: BodySmallSemiBoldProps) => (
6-
<Text variant="bodySmallSemibold" {...props}>
7-
{children}
8-
</Text>
9-
);
5+
export const BodySmallSemiBold = <T extends React.ElementType = 'span'>(
6+
props: BodySmallSemiBoldProps<T>,
7+
) => <Text variant="bodySmallSemibold" {...props} />;
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Text, TextProps } from './Text';
22

3-
export type FoonoteMiniProps = Omit<TextProps, 'variant'>;
3+
export type FootnoteMiniProps<E extends React.ElementType = 'span'> = TextProps<E>;
44

5-
export const FootnoteMini = ({ children, ...props }: FoonoteMiniProps) => (
6-
<Text variant="footnoteMini" {...props}>
7-
{children}
8-
</Text>
5+
export const FootnoteMini = <T extends React.ElementType = 'span'>(props: FootnoteMiniProps<T>) => (
6+
<Text variant="footnoteMini" {...props} />
97
);
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { Text } from './Text';
22
import { TextProps } from './Text';
33

4-
export type LabelProps = TextProps & React.LabelHTMLAttributes<HTMLLabelElement>;
4+
export type LabelProps<T extends React.ElementType = 'label'> = TextProps<T>;
55

6-
export const Label = ({ children, ...props }: LabelProps) => {
7-
return (
8-
<Text as={'label'} {...props}>
9-
{children}
10-
</Text>
11-
);
6+
export const Label = <T extends React.ElementType = 'label'>(props: LabelProps<T>) => {
7+
return <Text as="label" {...props} />;
128
};
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { Text, TextProps } from './Text';
2-
export type LabelTinyProps = Omit<TextProps, 'variant'>;
32

4-
export const LabelTiny = ({ children, ...props }: LabelTinyProps) => (
5-
<Text variant="labelTiny" {...props}>
6-
{children}
7-
</Text>
3+
export type LabelTinyProps<E extends React.ElementType = 'span'> = TextProps<E>;
4+
5+
export const LabelTiny = <T extends React.ElementType = 'span'>(props: LabelTinyProps<T>) => (
6+
<Text variant="labelTiny" {...props} />
87
);
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
import React, { useCallback } from 'react';
1+
import React from 'react';
22
import styled from 'styled-components';
33

44
import { Colors, Radius } from '../../foundations';
5-
import { useHistory } from '../../history';
6-
import { RoutePath } from '../../routes';
7-
import { buttonReset } from '../../styles';
85
import { Text, TextProps } from './Text';
96

10-
export interface LinkProps extends TextProps, Omit<React.HtmlHTMLAttributes<'button'>, 'color'> {
11-
to: RoutePath;
12-
}
7+
export type LinkProps<T extends React.ElementType = 'a'> = TextProps<T> & {
8+
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
9+
};
1310

1411
const StyledText = styled(Text)<{
1512
$hoverColor: Colors | undefined;
1613
}>((props) => ({
17-
...buttonReset,
1814
background: 'transparent',
15+
cursor: 'default',
16+
textDecoration: 'none',
17+
display: 'inline-flex',
18+
alignItems: 'center',
1919

20-
'&:hover': {
20+
'&&:hover': {
2121
textDecorationLine: 'underline',
2222
textUnderlineOffset: '2px',
2323
color: props.$hoverColor,
2424
},
25-
'&:focus-visible': {
25+
'&&:focus-visible': {
2626
borderRadius: Radius.radius4,
2727
outline: `2px solid ${Colors.white}`,
2828
outlineOffset: '2px',
@@ -38,25 +38,20 @@ const getHoverColor = (color: Colors | undefined) => {
3838
}
3939
};
4040

41-
export const Link = ({ to, children, color, onClick, ...props }: LinkProps) => {
42-
const history = useHistory();
43-
const navigate = useCallback(
44-
(e: React.MouseEvent<'button'>) => {
45-
if (onClick) {
46-
onClick(e);
47-
}
48-
return history.push(to);
49-
},
50-
[history, to, onClick],
51-
);
41+
export const Link = <T extends React.ElementType = 'a'>({
42+
as: forwardedAs,
43+
color,
44+
...props
45+
}: LinkProps<T>) => {
46+
// If `as` is provided we need to pass it as `forwardedAs` for it to
47+
// be correctly passed to the `Text` component.
48+
const componentProps = forwardedAs ? { ...props, forwardedAs } : props;
5249
return (
5350
<StyledText
54-
onClick={navigate}
55-
as={'button'}
51+
forwardedAs="a"
5652
color={color}
5753
$hoverColor={getHoverColor(color)}
58-
{...props}>
59-
{children}
60-
</StyledText>
54+
{...componentProps}
55+
/>
6156
);
6257
};
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,37 @@
1-
import { createElement, forwardRef } from 'react';
2-
import styled, { WebTarget } from 'styled-components';
1+
import React from 'react';
2+
import styled from 'styled-components';
33

4-
import { Colors, Typography, typography, TypographyProperties } from '../../foundations';
5-
import { TransientProps } from '../../types';
4+
import { Colors, Typography, typography } from '../../foundations';
5+
import { PolymorphicProps, TransientProps } from '../../types';
66

7-
export type TextProps = React.PropsWithChildren<{
7+
type TextBaseProps = {
88
variant?: Typography;
99
color?: Colors;
10-
tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
11-
as?: WebTarget;
12-
style?: React.CSSProperties;
13-
}>;
10+
};
1411

15-
const StyledText = styled(
16-
({ tag = 'span', ...props }: { tag: TextProps['tag'] } & TransientProps<TypographyProperties>) =>
17-
createElement(tag, props),
18-
)((props) => ({
19-
color: 'var(--color)',
20-
fontFamily: props.$fontFamily,
21-
fontWeight: props.$fontWeight,
22-
fontSize: props.$fontSize,
23-
lineHeight: props.$lineHeight,
24-
}));
12+
export type TextProps<T extends React.ElementType = 'span'> = PolymorphicProps<T, TextBaseProps>;
2513

26-
export const Text = forwardRef(
27-
(
28-
{
29-
tag = 'span',
30-
variant = 'bodySmall',
31-
color = Colors.white,
32-
children,
33-
style,
34-
...props
35-
}: TextProps,
36-
ref,
37-
) => {
38-
const { fontFamily, fontSize, fontWeight, lineHeight } = typography[variant];
39-
return (
40-
<StyledText
41-
ref={ref}
42-
tag={tag}
43-
style={
44-
{
45-
'--color': color,
46-
...style,
47-
} as React.CSSProperties
48-
}
49-
$fontFamily={fontFamily}
50-
$fontWeight={fontWeight}
51-
$fontSize={fontSize}
52-
$lineHeight={lineHeight}
53-
{...props}>
54-
{children}
55-
</StyledText>
56-
);
14+
const StyledText = styled.span<TransientProps<TextBaseProps>>(
15+
({ $variant = 'bodySmall', $color = Colors.white }) => {
16+
const { fontFamily, fontSize, fontWeight, lineHeight } = typography[$variant];
17+
return `
18+
--color: ${$color};
19+
20+
color: var(--color);
21+
font-family: ${fontFamily};
22+
font-size: ${fontSize};
23+
font-weight: ${fontWeight};
24+
line-height: ${lineHeight};
25+
`;
5726
},
5827
);
5928

29+
export const Text = <T extends React.ElementType = 'span'>({
30+
variant,
31+
color,
32+
...props
33+
}: TextProps<T>) => {
34+
return <StyledText $variant={variant} $color={color} {...props} />;
35+
};
36+
6037
Text.displayName = 'Text';

0 commit comments

Comments
 (0)