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.

DeferredSpinner.tsx 3.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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 { keyframes } from '@emotion/react';
  21. import styled from '@emotion/styled';
  22. import React from 'react';
  23. import tw, { theme } from 'twin.macro';
  24. import { translate } from '../helpers/l10n';
  25. import { themeColor } from '../helpers/theme';
  26. import { InputSearchWrapper } from './InputSearch';
  27. interface Props {
  28. children?: React.ReactNode;
  29. className?: string;
  30. customSpinner?: JSX.Element;
  31. loading?: boolean;
  32. placeholder?: boolean;
  33. timeout?: number;
  34. }
  35. interface State {
  36. showSpinner: boolean;
  37. }
  38. const DEFAULT_TIMEOUT = 100;
  39. export default class DeferredSpinner extends React.PureComponent<Props, State> {
  40. timer?: number;
  41. state: State = { showSpinner: false };
  42. componentDidMount() {
  43. if (this.props.loading == null || this.props.loading === true) {
  44. this.startTimer();
  45. }
  46. }
  47. componentDidUpdate(prevProps: Props) {
  48. if (prevProps.loading === false && this.props.loading === true) {
  49. this.stopTimer();
  50. this.startTimer();
  51. }
  52. if (prevProps.loading === true && this.props.loading === false) {
  53. this.stopTimer();
  54. this.setState({ showSpinner: false });
  55. }
  56. }
  57. componentWillUnmount() {
  58. this.stopTimer();
  59. }
  60. startTimer = () => {
  61. this.timer = window.setTimeout(
  62. () => this.setState({ showSpinner: true }),
  63. this.props.timeout || DEFAULT_TIMEOUT
  64. );
  65. };
  66. stopTimer = () => {
  67. window.clearTimeout(this.timer);
  68. };
  69. render() {
  70. const { showSpinner } = this.state;
  71. const { customSpinner, className, children, placeholder } = this.props;
  72. if (showSpinner) {
  73. if (customSpinner) {
  74. return customSpinner;
  75. }
  76. return <Spinner className={className} role="status" />;
  77. }
  78. if (children) {
  79. return children;
  80. }
  81. if (placeholder) {
  82. return <Placeholder className={className} />;
  83. }
  84. return null;
  85. }
  86. }
  87. const spinAnimation = keyframes`
  88. from {
  89. transform: rotate(0deg);
  90. }
  91. to {
  92. transform: rotate(-360deg);
  93. }
  94. `;
  95. const Spinner = styled.div`
  96. border: 2px solid transparent;
  97. background: linear-gradient(0deg, ${themeColor('primary')} 50%, transparent 50% 100%) border-box,
  98. linear-gradient(90deg, ${themeColor('primary')} 25%, transparent 75% 100%) border-box;
  99. mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
  100. -webkit-mask-composite: xor;
  101. mask-composite: exclude;
  102. animation: ${spinAnimation} 1s infinite linear;
  103. ${tw`sw-h-4 sw-w-4`};
  104. ${tw`sw-inline-block`};
  105. ${tw`sw-box-border`};
  106. ${tw`sw-rounded-pill`}
  107. ${InputSearchWrapper} & {
  108. top: calc((2.25rem - ${theme('spacing.4')}) / 2);
  109. ${tw`sw-left-3`};
  110. ${tw`sw-absolute`};
  111. }
  112. `;
  113. Spinner.defaultProps = { 'aria-label': translate('loading'), role: 'status' };
  114. const Placeholder = styled.div`
  115. position: relative;
  116. visibility: hidden;
  117. ${tw`sw-inline-flex sw-items-center sw-justify-center`};
  118. ${tw`sw-h-4 sw-w-4`};
  119. `;