dropdownControl.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import DropdownBubble from 'sentry/components/dropdownBubble';
  4. import DropdownButton from 'sentry/components/dropdownButton';
  5. import DropdownMenu, {
  6. GetActorPropsFn,
  7. GetMenuPropsFn,
  8. } from 'sentry/components/dropdownMenu';
  9. import MenuItem from 'sentry/components/menuItem';
  10. import Tooltip from 'sentry/components/tooltip';
  11. type ButtonPriority = React.ComponentProps<typeof DropdownButton>['priority'];
  12. type DefaultProps = {
  13. /**
  14. * Should the menu contents always be rendered? Defaults to true.
  15. * Set to false to have menu contents removed from the DOM on close.
  16. */
  17. alwaysRenderMenu: boolean;
  18. /**
  19. * Width of the menu. Defaults to 100% of the button width.
  20. */
  21. menuWidth: string;
  22. };
  23. type ChildrenArgs = {
  24. getMenuProps: GetMenuPropsFn;
  25. isOpen: boolean;
  26. };
  27. type ButtonArgs = {
  28. getActorProps: GetActorPropsFn;
  29. isOpen: boolean;
  30. };
  31. type Props = DefaultProps & {
  32. children:
  33. | ((args: ChildrenArgs) => React.ReactElement)
  34. | React.ReactElement
  35. | Array<React.ReactElement>;
  36. /**
  37. * Align the dropdown menu to the right. (Default aligns to left)
  38. */
  39. alignRight?: boolean;
  40. /**
  41. * This makes the dropdown menu blend (e.g. corners are not rounded) with its
  42. * actor (opener) component
  43. */
  44. blendWithActor?: boolean;
  45. /**
  46. * A closure that returns a styled button. Function will get {isOpen, getActorProps}
  47. * as arguments. Use this if you need to style/replace the dropdown button.
  48. */
  49. button?: (args: ButtonArgs) => React.ReactNode;
  50. /**
  51. * Props to pass to DropdownButton
  52. */
  53. buttonProps?: React.ComponentProps<typeof DropdownButton>;
  54. /**
  55. * Tooltip to show on button when dropdown isn't open
  56. */
  57. buttonTooltipTitle?: string | null;
  58. className?: string;
  59. detached?: boolean;
  60. fullWidth?: boolean;
  61. /**
  62. * String or element for the button contents.
  63. */
  64. label?: NonNullable<React.ReactNode>;
  65. priority?: ButtonPriority;
  66. };
  67. /*
  68. * A higher level dropdown component that helps with building complete dropdowns
  69. * including the button + menu options. Use the `button` or `label` prop to set
  70. * the button content and `children` to provide menu options.
  71. */
  72. class DropdownControl extends Component<Props> {
  73. static defaultProps: DefaultProps = {
  74. alwaysRenderMenu: true,
  75. menuWidth: '100%',
  76. };
  77. renderButton(isOpen: boolean, getActorProps: GetActorPropsFn) {
  78. const {
  79. label,
  80. button,
  81. buttonProps,
  82. buttonTooltipTitle,
  83. priority,
  84. detached,
  85. fullWidth,
  86. } = this.props;
  87. if (button) {
  88. return button({isOpen, getActorProps});
  89. }
  90. if (buttonTooltipTitle && !isOpen) {
  91. return (
  92. <Tooltip skipWrapper position="top" title={buttonTooltipTitle}>
  93. <StyledDropdownButton
  94. priority={priority}
  95. {...getActorProps(buttonProps)}
  96. isOpen={isOpen}
  97. data-test-id="dropdown-control-button"
  98. detached={detached}
  99. hideBottomBorder={!detached}
  100. rightAlignChevron={fullWidth ?? false}
  101. >
  102. {label}
  103. </StyledDropdownButton>
  104. </Tooltip>
  105. );
  106. }
  107. return (
  108. <StyledDropdownButton
  109. priority={priority}
  110. {...getActorProps(buttonProps)}
  111. isOpen={isOpen}
  112. data-test-id="dropdown-control-button"
  113. detached={detached}
  114. hideBottomBorder={!detached}
  115. rightAlignChevron={fullWidth ?? false}
  116. >
  117. {label}
  118. </StyledDropdownButton>
  119. );
  120. }
  121. renderChildren(isOpen: boolean, getMenuProps: GetMenuPropsFn) {
  122. const {children, alignRight, menuWidth, blendWithActor, priority, detached} =
  123. this.props;
  124. if (typeof children === 'function') {
  125. return children({isOpen, getMenuProps});
  126. }
  127. const alignMenu = alignRight ? 'right' : 'left';
  128. return (
  129. <Content
  130. {...getMenuProps()}
  131. priority={priority}
  132. alignMenu={alignMenu}
  133. width={menuWidth}
  134. isOpen={isOpen}
  135. blendWithActor={blendWithActor}
  136. detached={detached}
  137. blendCorner
  138. data-test-id="dropdown-control"
  139. >
  140. {children}
  141. </Content>
  142. );
  143. }
  144. render() {
  145. const {alwaysRenderMenu, className, fullWidth} = this.props;
  146. return (
  147. <Container className={className} fullWidth={fullWidth ?? false}>
  148. <DropdownMenu alwaysRenderMenu={alwaysRenderMenu}>
  149. {({isOpen, getMenuProps, getActorProps}) => (
  150. <Fragment>
  151. {this.renderButton(isOpen, getActorProps)}
  152. {this.renderChildren(isOpen, getMenuProps)}
  153. </Fragment>
  154. )}
  155. </DropdownMenu>
  156. </Container>
  157. );
  158. }
  159. }
  160. const Container = styled('div')<{fullWidth: boolean}>`
  161. display: inline-block;
  162. position: relative;
  163. @media (max-width: ${p => p.theme.breakpoints.small}) {
  164. width: ${p => p.fullWidth && '100%'};
  165. }
  166. `;
  167. const StyledDropdownButton = styled(DropdownButton)`
  168. z-index: ${p => p.theme.zIndex.dropdownAutocomplete.actor};
  169. white-space: nowrap;
  170. `;
  171. const Content = styled(DropdownBubble)<{isOpen: boolean; priority?: ButtonPriority}>`
  172. display: ${p => (p.isOpen ? 'block' : 'none')};
  173. border-color: ${p => p.theme.button[p.priority || 'form'].border};
  174. `;
  175. const DropdownItem = styled(MenuItem)`
  176. font-size: ${p => p.theme.fontSizeMedium};
  177. `;
  178. export default DropdownControl;
  179. export {DropdownItem, Content};