You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Link.tsx 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { css } from '@emotion/react';
  21. import styled from '@emotion/styled';
  22. import React, { HTMLAttributeAnchorTarget } from 'react';
  23. import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router-dom';
  24. import tw, { theme as twTheme } from 'twin.macro';
  25. import { themeBorder, themeColor } from '../helpers/theme';
  26. import OpenNewTabIcon from './icons/OpenNewTabIcon';
  27. import { TooltipWrapperInner } from './Tooltip';
  28. export interface LinkProps extends RouterLinkProps {
  29. blurAfterClick?: boolean;
  30. disabled?: boolean;
  31. forceExternal?: boolean;
  32. icon?: React.ReactNode;
  33. onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
  34. preventDefault?: boolean;
  35. showExternalIcon?: boolean;
  36. stopPropagation?: boolean;
  37. target?: HTMLAttributeAnchorTarget;
  38. }
  39. function BaseLinkWithRef(props: LinkProps, ref: React.ForwardedRef<HTMLAnchorElement>) {
  40. const {
  41. children,
  42. blurAfterClick,
  43. disabled,
  44. icon,
  45. onClick,
  46. preventDefault,
  47. showExternalIcon = !icon,
  48. stopPropagation,
  49. target = '_blank',
  50. to,
  51. ...rest
  52. } = props;
  53. const isExternal = typeof to === 'string' && to.startsWith('http');
  54. const handleClick = React.useCallback(
  55. (event: React.MouseEvent<HTMLAnchorElement>) => {
  56. if (blurAfterClick) {
  57. event.currentTarget.blur();
  58. }
  59. if (preventDefault || disabled) {
  60. event.preventDefault();
  61. }
  62. if (stopPropagation) {
  63. event.stopPropagation();
  64. }
  65. if (onClick && !disabled) {
  66. onClick(event);
  67. }
  68. },
  69. [onClick, blurAfterClick, preventDefault, stopPropagation, disabled]
  70. );
  71. return isExternal ? (
  72. <a
  73. {...rest}
  74. href={to}
  75. onClick={handleClick}
  76. ref={ref}
  77. rel="noopener noreferrer"
  78. target={target}
  79. >
  80. {icon}
  81. {children}
  82. {showExternalIcon && <OpenNewTabIcon className="sw-ml-1" />}
  83. </a>
  84. ) : (
  85. <RouterLink ref={ref} {...rest} onClick={handleClick} to={to}>
  86. {icon}
  87. {children}
  88. </RouterLink>
  89. );
  90. }
  91. export const BaseLink = React.forwardRef(BaseLinkWithRef);
  92. const StyledBaseLink = styled(BaseLink)`
  93. color: var(--color);
  94. border-bottom: ${({ children, icon, theme }) =>
  95. icon && !children ? themeBorder('default', 'transparent')({ theme }) : 'var(--border)'};
  96. &:visited {
  97. color: var(--color);
  98. }
  99. &:hover,
  100. &:focus,
  101. &:active {
  102. color: var(--active);
  103. border-bottom: ${({ children, icon, theme }) =>
  104. icon && !children ? themeBorder('default', 'transparent')({ theme }) : 'var(--borderActive)'};
  105. }
  106. & > svg {
  107. ${tw`sw-align-text-bottom!`}
  108. }
  109. ${({ icon }) =>
  110. icon &&
  111. css`
  112. margin-left: calc(${twTheme('width.icon')} + ${twTheme('spacing.1')});
  113. & > svg,
  114. & > img {
  115. ${tw`sw-mr-1`}
  116. margin-left: calc(-1 * (${twTheme('width.icon')} + ${twTheme('spacing.1')}));
  117. }
  118. `};
  119. `;
  120. export const HoverLink = styled(StyledBaseLink)`
  121. text-decoration: none;
  122. --color: ${themeColor('linkDiscreet')};
  123. --active: ${themeColor('linkActive')};
  124. --border: ${themeBorder('default', 'transparent')};
  125. --borderActive: ${themeBorder('default', 'linkActive')};
  126. ${TooltipWrapperInner} & {
  127. --active: ${themeColor('linkTooltipActive')};
  128. --borderActive: ${themeBorder('default', 'linkTooltipActive')};
  129. }
  130. `;
  131. HoverLink.displayName = 'HoverLink';
  132. export const DiscreetLink = styled(HoverLink)`
  133. --border: ${themeBorder('default', 'linkDiscreet')};
  134. `;
  135. DiscreetLink.displayName = 'DiscreetLink';
  136. const StandoutLink = styled(StyledBaseLink)`
  137. ${tw`sw-font-semibold`}
  138. ${tw`sw-no-underline`}
  139. --color: ${themeColor('linkDefault')};
  140. --active: ${themeColor('linkActive')};
  141. --border: ${themeBorder('default', 'linkDefault')};
  142. --borderActive: ${themeBorder('default', 'linkDefault')};
  143. ${TooltipWrapperInner} & {
  144. --color: ${themeColor('linkTooltipDefault')};
  145. --active: ${themeColor('linkTooltipActive')};
  146. --border: ${themeBorder('default', 'linkTooltipDefault')};
  147. --borderActive: ${themeBorder('default', 'linkTooltipActive')};
  148. }
  149. `;
  150. StandoutLink.displayName = 'StandoutLink';
  151. export default StandoutLink;