diff options
12 files changed, 130 insertions, 72 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExternalLink.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExternalLink.tsx index 0acba13623..4f160ad64d 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/ExternalLink.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/ExternalLink.tsx @@ -1,19 +1,13 @@ import { useCallback } from 'react'; -import styled from 'styled-components'; import { Url } from '../../shared/constants'; import { useAppContext } from '../context'; import { Link, LinkProps } from '../lib/components'; -export type ExternalLinkProps = Omit<LinkProps<'a'>, 'href' | 'as'> & { +export type ExternalLinkProps = Omit<LinkProps, 'href' | 'as'> & { to: Url; }; -const StyledLink = styled(Link)` - display: inline-flex; - width: fit-content; -`; - function ExternalLink({ to, onClick, ...props }: ExternalLinkProps) { const { openUrl } = useAppContext(); const navigate = useCallback( @@ -26,10 +20,11 @@ function ExternalLink({ to, onClick, ...props }: ExternalLinkProps) { }, [onClick, openUrl, to], ); - return <StyledLink href="" onClick={navigate} {...props} />; + return <Link href="" onClick={navigate} {...props} />; } const ExternalLinkNamespace = Object.assign(ExternalLink, { + Text: Link.Text, Icon: Link.Icon, }); diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/InternalLink.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/InternalLink.tsx index 70f8b7c496..ba6b32280e 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/InternalLink.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/InternalLink.tsx @@ -4,11 +4,11 @@ import { Link, LinkProps } from '../lib/components'; import { useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; -export type InternalLinkProps = Omit<LinkProps<'a'>, 'href' | 'as'> & { +export type InternalLinkProps = Omit<LinkProps, 'href' | 'as'> & { to: RoutePath; }; -export function InternalLink({ to, onClick, ...props }: InternalLinkProps) { +function InternalLink({ to, onClick, ...props }: InternalLinkProps) { const history = useHistory(); const navigate = useCallback( (e: React.MouseEvent<HTMLAnchorElement>) => { @@ -22,3 +22,10 @@ export function InternalLink({ to, onClick, ...props }: InternalLinkProps) { ); return <Link href="" onClick={navigate} {...props} />; } + +const InternalLinkNamespace = Object.assign(InternalLink, { + Text: Link.Text, + Icon: Link.Icon, +}); + +export { InternalLinkNamespace as InternalLink }; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx index 0a43452513..71045a2bfd 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import styled from 'styled-components'; import { InAppNotificationSubtitle } from '../../shared/notifications'; import { LabelTiny } from '../lib/components'; @@ -11,10 +10,6 @@ export type NotificationSubtitleProps = { subtitle?: string | InAppNotificationSubtitle[]; }; -const StyledExternalLink = styled(ExternalLink)` - display: flex; -`; - const formatSubtitle = (subtitle: InAppNotificationSubtitle) => { const content = formatHtml(subtitle.content); if (subtitle.action) { @@ -22,15 +17,15 @@ const formatSubtitle = (subtitle: InAppNotificationSubtitle) => { case 'navigate-internal': return ( <InternalLink variant="labelTiny" {...subtitle.action.link}> - {content} + <InternalLink.Text>{content}</InternalLink.Text> </InternalLink> ); case 'navigate-external': return ( - <StyledExternalLink variant="labelTiny" {...subtitle.action.link}> - {content} - <ExternalLink.Icon icon="external" size="small" /> - </StyledExternalLink> + <ExternalLink variant="labelTiny" {...subtitle.action.link}> + <ExternalLink.Text>{content}</ExternalLink.Text> + <ExternalLink.Icon icon="external" /> + </ExternalLink> ); default: break; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx index 3de932bc86..9e3acf985e 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx @@ -758,11 +758,13 @@ function TunnelProtocolSetting() { </Cell.CellFooterText> </AriaDescription> <ExternalLink variant="labelTiny" to={urls.removingOpenVpnBlog}> - {sprintf( - // TRANSLATORS: Link in tunnel protocol selector footer to blog post - // TRANSLATORS: about OpenVPN support ending. - messages.pgettext('vpn-settings-view', 'Read more'), - )} + <ExternalLink.Text> + {sprintf( + // TRANSLATORS: Link in tunnel protocol selector footer to blog post + // TRANSLATORS: about OpenVPN support ending. + messages.pgettext('vpn-settings-view', 'Read more'), + )} + </ExternalLink.Text> <ExternalLink.Icon icon="external" size="small" /> </ExternalLink> </Cell.CellFooter> diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx index 40a598b666..5e1bef8d5f 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/Link.tsx @@ -1,62 +1,66 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; -import { Colors, colors, Radius } from '../../foundations'; -import { Text, TextProps } from '../typography'; -import { LinkIcon } from './components'; +import { Colors, colors, Radius, Typography } from '../../foundations'; +import { TransientProps } from '../../types'; +import { LinkIcon, LinkText, StyledIcon as StyledLinkIcon, StyledLinkText } from './components'; +import { useHoverColor } from './hooks'; +import { LinkProvider } from './LinkContext'; -export type LinkProps<T extends React.ElementType = 'a'> = TextProps<T> & { +type LinkBaseProps = { + variant?: Typography; + color?: Colors; onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void; }; -const StyledText = styled(Text)<{ - $hoverColor: Colors | undefined; -}>((props) => ({ - background: colors.transparent, - cursor: 'default', - textDecoration: 'none', - display: 'inline', +export type LinkProps = LinkBaseProps & + Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkBaseProps>; - '&&:hover': { - textDecorationLine: 'underline', - textUnderlineOffset: '2px', - color: props.$hoverColor, - }, - '&&:focus-visible': { - borderRadius: Radius.radius4, - outline: `2px solid ${colors.whiteAlpha60}`, - outlineOffset: '2px', - }, -})); - -const getHoverColor = (color: Colors | undefined) => { - switch (color) { - case 'whiteAlpha60': - return 'white'; - default: - return undefined; +const StyledLink = styled.a< + TransientProps<LinkProps> & { + $hoverColor?: Colors; } -}; +>(({ $hoverColor }) => { + return css` + cursor: default; + text-decoration: none; + display: inline; + width: fit-content; + + &&:hover > ${StyledLinkText} { + text-decoration-line: underline; + text-underline-offset: 2px; + color: ${$hoverColor}; + } + + &&:focus-visible > ${StyledLinkText} { + border-radius: ${Radius.radius4}; + outline: 2px solid ${colors.white}; + outline-offset: 2px; + } + + > ${StyledLinkIcon}:first-child:not(:only-child) { + margin-right: 2px; + } + > ${StyledLinkIcon}:last-child:not(:only-child) { + margin-left: 2px; + } + `; +}); -function Link<T extends React.ElementType = 'a'>({ - as: forwardedAs, - color, - ...props -}: LinkProps<T>) { - // If `as` is provided we need to pass it as `forwardedAs` for it to - // be correctly passed to the `Text` component. - const componentProps = forwardedAs ? { ...props, forwardedAs } : props; +function Link({ color, variant, children, ...props }: LinkProps) { + const hoverColor = useHoverColor(color); return ( - <StyledText - forwardedAs="a" - color={color} - $hoverColor={getHoverColor(color)} - {...componentProps} - /> + <LinkProvider variant={variant} color={color}> + <StyledLink $hoverColor={hoverColor} {...props}> + {children} + </StyledLink> + </LinkProvider> ); } const LinkNamespace = Object.assign(Link, { + Text: LinkText, Icon: LinkIcon, }); diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/LinkContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/LinkContext.tsx new file mode 100644 index 0000000000..128428c081 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/LinkContext.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { LinkProps } from './Link'; + +interface LinkContextProps { + color?: LinkProps['color']; + variant?: LinkProps['variant']; +} + +const LinkContext = React.createContext<LinkContextProps | undefined>(undefined); + +export const useLinkContext = (): LinkContextProps => { + const context = React.useContext(LinkContext); + if (!context) { + throw new Error('useLinkContext must be used within a LinkProvider'); + } + return context; +}; + +interface LinkProviderProps { + color?: LinkContextProps['color']; + variant?: LinkContextProps['variant']; + children: React.ReactNode; +} + +export function LinkProvider({ color, variant, children }: LinkProviderProps) { + return <LinkContext.Provider value={{ color, variant }}>{children}</LinkContext.Provider>; +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx index 7af4b28045..3a82f11a4d 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkIcon.tsx @@ -5,8 +5,9 @@ import { Icon, IconProps } from '../../icon'; type LinkIconProps = IconProps; export const StyledIcon = styled(Icon)` - vertical-align: middle; - display: inline-flex; + vertical-align: text-bottom; + display: inline-block; + flex-shrink: 0; `; export function LinkIcon({ ...props }: LinkIconProps) { diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx new file mode 100644 index 0000000000..c8c1ff8baa --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/LinkText.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +import { Text, TextProps } from '../../typography'; +import { useLinkContext } from '../LinkContext'; + +export type LinkTextProps = TextProps; + +export const StyledLinkText = styled(Text)``; + +export function LinkText(props: LinkTextProps) { + const { variant, color } = useLinkContext(); + return <StyledLinkText variant={variant} color={color} {...props}></StyledLinkText>; +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/index.ts index 1718f57d55..efe33915d2 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/components/index.ts @@ -1 +1,2 @@ export * from './LinkIcon'; +export * from './LinkText'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts new file mode 100644 index 0000000000..4f17d940a4 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-hover-color'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-hover-color.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-hover-color.ts new file mode 100644 index 0000000000..f62f26a6fe --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/hooks/use-hover-color.ts @@ -0,0 +1,10 @@ +import { Colors } from '../../../foundations'; + +export const useHoverColor = (color: Colors | undefined) => { + switch (color) { + case 'whiteAlpha60': + return 'white'; + default: + return undefined; + } +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/index.ts new file mode 100644 index 0000000000..3db78f51f0 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/link/index.ts @@ -0,0 +1 @@ +export * from './Link'; |
