accordion.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import {ReactNode} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {IconChevron} from 'sentry/icons';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. interface AccordionItemContent {
  8. content: () => ReactNode;
  9. header: () => ReactNode;
  10. }
  11. interface Props {
  12. expandedIndex: number;
  13. items: AccordionItemContent[];
  14. setExpandedIndex: (index: number) => void;
  15. buttonOnLeft?: boolean;
  16. collapsible?: boolean;
  17. }
  18. export default function Accordion({
  19. expandedIndex,
  20. setExpandedIndex,
  21. items,
  22. buttonOnLeft,
  23. collapsible = true,
  24. }: Props) {
  25. return (
  26. <AccordionContainer>
  27. {items.map((item, index) => (
  28. <AccordionItem
  29. isExpanded={index === expandedIndex}
  30. currentIndex={index}
  31. key={index}
  32. content={item.content()}
  33. setExpandedIndex={setExpandedIndex}
  34. buttonOnLeft={buttonOnLeft}
  35. collapsible={collapsible}
  36. >
  37. {item.header()}
  38. </AccordionItem>
  39. ))}
  40. </AccordionContainer>
  41. );
  42. }
  43. function AccordionItem({
  44. isExpanded,
  45. currentIndex: index,
  46. children,
  47. setExpandedIndex,
  48. content,
  49. buttonOnLeft,
  50. collapsible,
  51. }: {
  52. children: ReactNode;
  53. content: ReactNode;
  54. currentIndex: number;
  55. isExpanded: boolean;
  56. setExpandedIndex: (index: number) => void;
  57. buttonOnLeft?: boolean;
  58. collapsible?: boolean;
  59. }) {
  60. const button = collapsible ? (
  61. <Button
  62. icon={<IconChevron size="xs" direction={isExpanded ? 'up' : 'down'} />}
  63. aria-label={isExpanded ? t('Collapse') : t('Expand')}
  64. aria-expanded={isExpanded}
  65. size="zero"
  66. borderless
  67. onClick={() => {
  68. isExpanded ? setExpandedIndex(-1) : setExpandedIndex(index);
  69. }}
  70. />
  71. ) : (
  72. <Button
  73. icon={<IconChevron size="xs" direction={isExpanded ? 'up' : 'down'} />}
  74. aria-label={t('Expand')}
  75. aria-expanded={isExpanded}
  76. disabled={isExpanded}
  77. size="zero"
  78. borderless
  79. onClick={() => setExpandedIndex(index)}
  80. />
  81. );
  82. return buttonOnLeft ? (
  83. <StyledLineItem>
  84. <ButtonLeftListItemContainer>
  85. {button}
  86. {children}
  87. </ButtonLeftListItemContainer>
  88. <LeftContentContainer>{isExpanded && content}</LeftContentContainer>
  89. </StyledLineItem>
  90. ) : (
  91. <StyledLineItem>
  92. <ListItemContainer>
  93. {children}
  94. {button}
  95. </ListItemContainer>
  96. <StyledContentContainer>{isExpanded && content}</StyledContentContainer>
  97. </StyledLineItem>
  98. );
  99. }
  100. const StyledLineItem = styled('li')`
  101. line-height: ${p => p.theme.text.lineHeightBody};
  102. `;
  103. const AccordionContainer = styled('ul')`
  104. padding: ${space(1)} 0 0 0;
  105. margin: 0;
  106. list-style-type: none;
  107. `;
  108. const ButtonLeftListItemContainer = styled('div')`
  109. display: flex;
  110. border-top: 1px solid ${p => p.theme.border};
  111. padding: ${space(1)} ${space(2)};
  112. font-size: ${p => p.theme.fontSizeMedium};
  113. column-gap: ${space(1.5)};
  114. `;
  115. const ListItemContainer = styled('div')`
  116. display: flex;
  117. border-top: 1px solid ${p => p.theme.border};
  118. padding: ${space(1)} ${space(2)};
  119. font-size: ${p => p.theme.fontSizeMedium};
  120. `;
  121. const StyledContentContainer = styled('div')`
  122. padding: ${space(0)} ${space(2)};
  123. `;
  124. const LeftContentContainer = styled('div')`
  125. padding: ${space(0)} ${space(0.25)};
  126. `;