clippedBox.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import {PureComponent} from 'react';
  2. import {findDOMNode} from 'react-dom';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import color from 'color';
  6. import Button from 'sentry/components/button';
  7. import {t} from 'sentry/locale';
  8. import space from 'sentry/styles/space';
  9. type DefaultProps = {
  10. btnText?: string;
  11. /**
  12. * The "show more" button is 28px tall.
  13. * Do not clip if there is only a few more pixels
  14. */
  15. clipFlex?: number;
  16. clipHeight?: number;
  17. defaultClipped?: boolean;
  18. };
  19. type Props = {
  20. clipFlex: number;
  21. clipHeight: number;
  22. className?: string;
  23. /**
  24. * When available replaces the default clipFade component
  25. */
  26. clipFade?: ({showMoreButton}: {showMoreButton: React.ReactNode}) => React.ReactNode;
  27. /**
  28. * Triggered when user clicks on the show more button
  29. */
  30. onReveal?: () => void;
  31. /**
  32. * Its trigged when the component is mounted and its height available
  33. */
  34. onSetRenderedHeight?: (renderedHeight: number) => void;
  35. renderedHeight?: number;
  36. title?: string;
  37. } & DefaultProps;
  38. type State = {
  39. isClipped: boolean;
  40. isRevealed: boolean;
  41. renderedHeight?: number;
  42. };
  43. class ClippedBox extends PureComponent<Props, State> {
  44. static defaultProps: DefaultProps = {
  45. defaultClipped: false,
  46. clipHeight: 200,
  47. clipFlex: 28,
  48. btnText: t('Show More'),
  49. };
  50. state: State = {
  51. isClipped: !!this.props.defaultClipped,
  52. isRevealed: false, // True once user has clicked "Show More" button
  53. renderedHeight: this.props.renderedHeight,
  54. };
  55. componentDidMount() {
  56. // eslint-disable-next-line react/no-find-dom-node
  57. const renderedHeight = (findDOMNode(this) as HTMLElement).offsetHeight;
  58. this.props.onSetRenderedHeight?.(renderedHeight);
  59. this.calcHeight(renderedHeight);
  60. }
  61. componentDidUpdate(_prevProps: Props, prevState: State) {
  62. if (prevState.renderedHeight !== this.props.renderedHeight) {
  63. this.setRenderedHeight();
  64. }
  65. if (prevState.renderedHeight !== this.state.renderedHeight) {
  66. this.calcHeight(this.state.renderedHeight);
  67. }
  68. if (this.state.isRevealed || !this.state.isClipped) {
  69. return;
  70. }
  71. if (!this.props.renderedHeight) {
  72. // eslint-disable-next-line react/no-find-dom-node
  73. const renderedHeight = (findDOMNode(this) as HTMLElement).offsetHeight;
  74. if (renderedHeight < this.props.clipHeight) {
  75. this.reveal();
  76. }
  77. }
  78. }
  79. setRenderedHeight() {
  80. this.setState({
  81. renderedHeight: this.props.renderedHeight,
  82. });
  83. }
  84. calcHeight(renderedHeight?: number) {
  85. if (!renderedHeight) {
  86. return;
  87. }
  88. if (
  89. !this.state.isClipped &&
  90. renderedHeight > this.props.clipHeight + this.props.clipFlex
  91. ) {
  92. /* eslint react/no-did-mount-set-state:0 */
  93. // okay if this causes re-render; cannot determine until
  94. // rendered first anyways
  95. this.setState({
  96. isClipped: true,
  97. });
  98. }
  99. }
  100. reveal = () => {
  101. const {onReveal} = this.props;
  102. this.setState({
  103. isClipped: false,
  104. isRevealed: true,
  105. });
  106. if (onReveal) {
  107. onReveal();
  108. }
  109. };
  110. handleClickReveal = (event: React.MouseEvent) => {
  111. event.stopPropagation();
  112. this.reveal();
  113. };
  114. render() {
  115. const {isClipped, isRevealed} = this.state;
  116. const {title, children, clipHeight, btnText, className, clipFade} = this.props;
  117. const showMoreButton = (
  118. <Button
  119. onClick={this.reveal}
  120. priority="primary"
  121. size="xs"
  122. aria-label={btnText ?? t('Show More')}
  123. >
  124. {btnText}
  125. </Button>
  126. );
  127. return (
  128. <Wrapper
  129. clipHeight={clipHeight}
  130. isClipped={isClipped}
  131. isRevealed={isRevealed}
  132. className={className}
  133. >
  134. {title && <Title>{title}</Title>}
  135. {children}
  136. {isClipped &&
  137. (clipFade?.({showMoreButton}) ?? <ClipFade>{showMoreButton}</ClipFade>)}
  138. </Wrapper>
  139. );
  140. }
  141. }
  142. export default ClippedBox;
  143. const Wrapper = styled('div', {
  144. shouldForwardProp: prop =>
  145. prop !== 'clipHeight' && prop !== 'isClipped' && prop !== 'isRevealed',
  146. })<State & {clipHeight: number}>`
  147. position: relative;
  148. padding: ${space(1.5)} 0;
  149. /* For "Show More" animation */
  150. ${p =>
  151. p.isRevealed &&
  152. css`
  153. transition: all 5s ease-in-out;
  154. max-height: 50000px;
  155. `};
  156. ${p =>
  157. p.isClipped &&
  158. css`
  159. max-height: ${p.clipHeight}px;
  160. overflow: hidden;
  161. `};
  162. `;
  163. const Title = styled('h5')`
  164. margin-bottom: ${space(1)};
  165. `;
  166. const ClipFade = styled('div')`
  167. position: absolute;
  168. left: 0;
  169. right: 0;
  170. bottom: 0;
  171. padding: 40px 0 0;
  172. background-image: linear-gradient(
  173. 180deg,
  174. ${p => color(p.theme.background).alpha(0.15).string()},
  175. ${p => p.theme.background}
  176. );
  177. text-align: center;
  178. border-bottom: ${space(1.5)} solid ${p => p.theme.background};
  179. /* Let pointer-events pass through ClipFade to visible elements underneath it */
  180. pointer-events: none;
  181. /* Ensure pointer-events trigger event listeners on "Expand" button */
  182. > * {
  183. pointer-events: auto;
  184. }
  185. `;