accordionRow.tsx 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import type React from 'react';
  2. import {useEffect, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import {motion} from 'framer-motion';
  5. import {IconChevron} from 'sentry/icons';
  6. import {space} from 'sentry/styles/space';
  7. type AccordionRowProps = {
  8. /**
  9. * The body of the accordion that is shown & hidden
  10. */
  11. body: React.ReactNode;
  12. /**
  13. * The header of the accordion that is always shown
  14. */
  15. title: React.ReactNode;
  16. /**
  17. * Whether to render the body
  18. */
  19. disableBody?: boolean;
  20. /**
  21. * Whether the accordion is disabled (cannot be opened)
  22. */
  23. disabled?: boolean;
  24. /**
  25. * Action to execute upon opening accordion
  26. */
  27. onOpen?: () => Promise<void>;
  28. };
  29. function AccordionRow({
  30. disabled = false,
  31. disableBody,
  32. body,
  33. title,
  34. onOpen = async () => {},
  35. }: AccordionRowProps) {
  36. const [isExpanded, setIsExpanded] = useState(false);
  37. const duration = 0.2; // how long it takes the accordion to open/close
  38. const animationState = isExpanded ? 'open' : 'collapsed';
  39. useEffect(() => {
  40. if (disabled) {
  41. setIsExpanded(false);
  42. }
  43. }, [disabled]);
  44. const toggleDetails = () => {
  45. setIsExpanded(!isExpanded);
  46. };
  47. return (
  48. <AccordionContent>
  49. <Title
  50. onClick={async () => {
  51. if (!isExpanded) {
  52. await onOpen();
  53. }
  54. toggleDetails();
  55. }}
  56. disabled={disabled}
  57. data-test-id="accordion-title"
  58. >
  59. {title}
  60. {!disabled && <IconChevron direction={isExpanded ? 'up' : 'down'} size="sm" />}
  61. </Title>
  62. <motion.div
  63. initial="collapsed"
  64. animate={disabled || disableBody ? undefined : animationState}
  65. variants={{
  66. open: {
  67. height: 'auto',
  68. overflow: 'visible',
  69. transition: {duration, overflow: {delay: duration}},
  70. },
  71. collapsed: {height: 0, overflow: 'hidden', transition: {duration}},
  72. }}
  73. >
  74. {body}
  75. </motion.div>
  76. </AccordionContent>
  77. );
  78. }
  79. const AccordionContent = styled('div')`
  80. display: flex;
  81. flex-direction: column;
  82. padding-left: ${space(2)};
  83. padding-right: ${space(2)};
  84. width: 100%;
  85. `;
  86. const Title = styled('div')<{disabled: boolean}>`
  87. width: 100%;
  88. display: flex;
  89. justify-content: space-between;
  90. align-items: center;
  91. cursor: ${props => (props.disabled ? 'auto' : 'pointer')};
  92. `;
  93. export default AccordionRow;