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.

InteractiveIcon.tsx 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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 classNames from 'classnames';
  23. import React from 'react';
  24. import tw from 'twin.macro';
  25. import { OPACITY_20_PERCENT } from '../helpers/constants';
  26. import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
  27. import { isDefined } from '../helpers/types';
  28. import { ThemedProps } from '../types/theme';
  29. import { BaseLink, LinkProps } from './Link';
  30. import { IconProps } from './icons/Icon';
  31. export type InteractiveIconSize = 'small' | 'medium';
  32. export interface InteractiveIconProps {
  33. Icon: React.ComponentType<React.PropsWithChildren<IconProps>>;
  34. 'aria-label': string;
  35. children?: React.ReactNode;
  36. className?: string;
  37. currentColor?: boolean;
  38. disabled?: boolean;
  39. iconProps?: IconProps;
  40. id?: string;
  41. innerRef?: React.Ref<HTMLButtonElement>;
  42. onClick?: VoidFunction;
  43. size?: InteractiveIconSize;
  44. stopPropagation?: boolean;
  45. to?: LinkProps['to'];
  46. }
  47. export class InteractiveIconBase extends React.PureComponent<InteractiveIconProps> {
  48. handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
  49. const { disabled, onClick, stopPropagation = true } = this.props;
  50. if (stopPropagation) {
  51. event.stopPropagation();
  52. }
  53. if (onClick && !disabled) {
  54. onClick();
  55. }
  56. };
  57. render() {
  58. const {
  59. Icon,
  60. children,
  61. disabled,
  62. innerRef,
  63. onClick,
  64. size = 'medium',
  65. to,
  66. iconProps = {},
  67. ...htmlProps
  68. } = this.props;
  69. const props = {
  70. ...htmlProps,
  71. 'aria-disabled': disabled,
  72. disabled,
  73. size,
  74. type: 'button' as const,
  75. };
  76. if (to) {
  77. return (
  78. <IconLink {...props} onClick={onClick} showExternalIcon={false} stopPropagation to={to}>
  79. <Icon className={classNames({ 'sw-mr-1': isDefined(children) })} {...iconProps} />
  80. {children}
  81. </IconLink>
  82. );
  83. }
  84. return (
  85. <IconButton {...props} onClick={this.handleClick} ref={innerRef}>
  86. <Icon className={classNames({ 'sw-mr-1': isDefined(children) })} {...iconProps} />
  87. {children}
  88. </IconButton>
  89. );
  90. }
  91. }
  92. const buttonIconStyle = (props: ThemedProps & { size: InteractiveIconSize }) => css`
  93. box-sizing: border-box;
  94. border: none;
  95. outline: none;
  96. text-decoration: none;
  97. color: var(--color);
  98. background-color: var(--background);
  99. transition:
  100. background-color 0.2s ease,
  101. outline 0.2s ease,
  102. color 0.2s ease;
  103. ${tw`sw-inline-flex sw-items-center sw-justify-center`}
  104. ${tw`sw-cursor-pointer`}
  105. ${{
  106. small: tw`sw-h-6 sw-px-1 sw-rounded-1/2`,
  107. medium: tw`sw-h-control sw-px-[0.625rem] sw-rounded-2`,
  108. }[props.size]}
  109. &:hover,
  110. &:focus,
  111. &:active {
  112. color: var(--colorHover);
  113. background-color: var(--backgroundHover);
  114. }
  115. &:focus,
  116. &:active {
  117. outline: ${themeBorder('focus', 'var(--focus)')(props)};
  118. }
  119. &:disabled,
  120. &:disabled:hover {
  121. color: ${themeContrast('buttonDisabled')(props)};
  122. background-color: var(--background);
  123. ${tw`sw-cursor-not-allowed`}
  124. }
  125. `;
  126. const IconLink = styled(BaseLink)`
  127. ${buttonIconStyle}
  128. `;
  129. const IconButton = styled.button`
  130. ${buttonIconStyle}
  131. `;
  132. export const InteractiveIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> = styled(
  133. InteractiveIconBase,
  134. )`
  135. --background: ${themeColor('interactiveIcon')};
  136. --backgroundHover: ${themeColor('interactiveIconHover')};
  137. --color: ${({ currentColor, theme }) =>
  138. currentColor ? 'currentColor' : themeContrast('interactiveIcon')({ theme })};
  139. --colorHover: ${themeContrast('interactiveIconHover')};
  140. --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
  141. `;
  142. export const DiscreetInteractiveIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> =
  143. styled(InteractiveIcon)`
  144. --color: ${themeColor('discreetInteractiveIcon')};
  145. `;
  146. export const DestructiveIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> = styled(
  147. InteractiveIconBase,
  148. )`
  149. --background: ${themeColor('destructiveIcon')};
  150. --backgroundHover: ${themeColor('destructiveIconHover')};
  151. --color: ${themeContrast('destructiveIcon')};
  152. --colorHover: ${themeContrast('destructiveIconHover')};
  153. --focus: ${themeColor('destructiveIconFocus', OPACITY_20_PERCENT)};
  154. `;
  155. export const DismissProductNewsIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> =
  156. styled(InteractiveIcon)`
  157. --background: ${themeColor('productNews')};
  158. --backgroundHover: ${themeColor('productNewsHover')};
  159. --color: ${themeContrast('productNews')};
  160. --colorHover: ${themeContrast('productNewsHover')};
  161. --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
  162. height: 28px;
  163. `;