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.

Button.tsx 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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 from 'react';
  23. import tw from 'twin.macro';
  24. import { BaseLink, LinkProps } from '../../../components/Link';
  25. import { themeBorder, themeColor, themeContrast } from '../../../helpers/theme';
  26. import { ThemedProps } from '../../../types/theme';
  27. type AllowedButtonAttributes = Pick<
  28. React.ButtonHTMLAttributes<HTMLButtonElement>,
  29. 'aria-label' | 'autoFocus' | 'id' | 'name' | 'role' | 'style' | 'title' | 'type' | 'form'
  30. >;
  31. export interface ButtonProps extends AllowedButtonAttributes {
  32. children?: React.ReactNode;
  33. className?: string;
  34. disabled?: boolean;
  35. download?: string;
  36. icon?: React.ReactNode;
  37. innerRef?: React.Ref<HTMLButtonElement>;
  38. isExternal?: LinkProps['isExternal'];
  39. onClick?: (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => unknown;
  40. preventDefault?: boolean;
  41. reloadDocument?: LinkProps['reloadDocument'];
  42. showExternalIcon?: boolean;
  43. stopPropagation?: boolean;
  44. target?: LinkProps['target'];
  45. to?: LinkProps['to'];
  46. }
  47. export class Button extends React.PureComponent<ButtonProps> {
  48. handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
  49. const { disabled, onClick, stopPropagation = false, type } = this.props;
  50. const { preventDefault = type !== 'submit' } = this.props;
  51. if (preventDefault || disabled) {
  52. event.preventDefault();
  53. }
  54. if (stopPropagation) {
  55. event.stopPropagation();
  56. }
  57. if (onClick && !disabled) {
  58. onClick(event);
  59. }
  60. };
  61. render() {
  62. const {
  63. children,
  64. disabled,
  65. icon,
  66. innerRef,
  67. onClick,
  68. preventDefault,
  69. stopPropagation,
  70. to,
  71. type = 'button',
  72. ...htmlProps
  73. } = this.props;
  74. const props = {
  75. ...htmlProps,
  76. 'aria-disabled': disabled,
  77. disabled,
  78. type,
  79. };
  80. if (to) {
  81. return (
  82. <BaseButtonLink {...props} onClick={onClick} to={to}>
  83. {icon}
  84. {children}
  85. </BaseButtonLink>
  86. );
  87. }
  88. return (
  89. <BaseButton {...props} onClick={this.handleClick} ref={innerRef}>
  90. {icon}
  91. {children}
  92. </BaseButton>
  93. );
  94. }
  95. }
  96. export const buttonStyle = (props: ThemedProps) => css`
  97. box-sizing: border-box;
  98. text-decoration: none;
  99. outline: none;
  100. border: var(--border);
  101. color: var(--color);
  102. background-color: var(--background);
  103. transition:
  104. background-color 0.2s ease,
  105. outline 0.2s ease;
  106. ${tw`sw-inline-flex sw-items-center`}
  107. ${tw`sw-h-control`}
  108. ${tw`sw-body-sm-highlight`}
  109. ${tw`sw-py-2 sw-px-4`}
  110. ${tw`sw-rounded-2`}
  111. ${tw`sw-cursor-pointer`}
  112. &:hover, &:active {
  113. color: var(--color);
  114. background-color: var(--backgroundHover);
  115. }
  116. &:focus,
  117. &:active {
  118. color: var(--color);
  119. outline: ${themeBorder('focus', 'var(--focus)')(props)};
  120. }
  121. &:disabled,
  122. &:disabled:hover {
  123. color: ${themeContrast('buttonDisabled')(props)};
  124. background-color: ${themeColor('buttonDisabled')(props)};
  125. border: ${themeBorder('default', 'buttonDisabledBorder')(props)};
  126. ${tw`sw-cursor-not-allowed`}
  127. }
  128. & > svg {
  129. ${tw`sw-mr-1`}
  130. }
  131. `;
  132. const BaseButtonLink = styled(BaseLink)`
  133. ${buttonStyle}
  134. /*
  135. Workaround to apply disable style to button-link
  136. as link does not have disabled attribute, using props instead
  137. */
  138. ${({ disabled, theme }) =>
  139. disabled
  140. ? `&, &:hover, &:focus, &:active {
  141. color: ${themeContrast('buttonDisabled')({ theme })};
  142. background-color: ${themeColor('buttonDisabled')({ theme })};
  143. border: ${themeBorder('default', 'buttonDisabledBorder')({ theme })};
  144. cursor: not-allowed;
  145. }`
  146. : undefined};
  147. `;
  148. const BaseButton = styled.button`
  149. ${buttonStyle}
  150. /*
  151. Workaround for tooltips issue with onMouseLeave in disabled buttons:
  152. https://github.com/facebook/react/issues/4251
  153. */
  154. & [disabled] {
  155. ${tw`sw-pointer-events-none`};
  156. }
  157. `;