dropdownControl.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import * as React 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 React.Component<Props> {
  73. static defaultProps: DefaultProps = {
  74. alwaysRenderMenu: true,
  75. menuWidth: '100%',
  76. };
  77. renderButton(isOpen: boolean, getActorProps: GetActorPropsFn) {
  78. const {label, button, buttonProps, buttonTooltipTitle, priority, detached} =
  79. this.props;
  80. if (button) {
  81. return button({isOpen, getActorProps});
  82. }
  83. if (buttonTooltipTitle && !isOpen) {
  84. return (
  85. <Tooltip skipWrapper position="top" title={buttonTooltipTitle}>
  86. <StyledDropdownButton
  87. priority={priority}
  88. {...getActorProps(buttonProps)}
  89. isOpen={isOpen}
  90. data-test-id="dropdown-control-button"
  91. detached={detached}
  92. hideBottomBorder={!detached}
  93. >
  94. {label}
  95. </StyledDropdownButton>
  96. </Tooltip>
  97. );
  98. }
  99. return (
  100. <StyledDropdownButton
  101. priority={priority}
  102. {...getActorProps(buttonProps)}
  103. isOpen={isOpen}
  104. data-test-id="dropdown-control-button"
  105. detached={detached}
  106. hideBottomBorder={!detached}
  107. >
  108. {label}
  109. </StyledDropdownButton>
  110. );
  111. }
  112. renderChildren(isOpen: boolean, getMenuProps: GetMenuPropsFn) {
  113. const {children, alignRight, menuWidth, blendWithActor, priority, detached} =
  114. this.props;
  115. if (typeof children === 'function') {
  116. return children({isOpen, getMenuProps});
  117. }
  118. const alignMenu = alignRight ? 'right' : 'left';
  119. return (
  120. <Content
  121. {...getMenuProps()}
  122. priority={priority}
  123. alignMenu={alignMenu}
  124. width={menuWidth}
  125. isOpen={isOpen}
  126. blendWithActor={blendWithActor}
  127. detached={detached}
  128. blendCorner
  129. data-test-id="dropdown-control"
  130. >
  131. {children}
  132. </Content>
  133. );
  134. }
  135. render() {
  136. const {alwaysRenderMenu, className, fullWidth} = this.props;
  137. return (
  138. <Container className={className} fullWidth={fullWidth ?? false}>
  139. <DropdownMenu alwaysRenderMenu={alwaysRenderMenu}>
  140. {({isOpen, getMenuProps, getActorProps}) => (
  141. <React.Fragment>
  142. {this.renderButton(isOpen, getActorProps)}
  143. {this.renderChildren(isOpen, getMenuProps)}
  144. </React.Fragment>
  145. )}
  146. </DropdownMenu>
  147. </Container>
  148. );
  149. }
  150. }
  151. const Container = styled('div')<{fullWidth: boolean}>`
  152. display: inline-block;
  153. position: relative;
  154. @media (max-width: ${p => p.theme.breakpoints[0]}) {
  155. width: ${p => p.fullWidth && '100%'};
  156. }
  157. `;
  158. const StyledDropdownButton = styled(DropdownButton)`
  159. z-index: ${p => p.theme.zIndex.dropdownAutocomplete.actor};
  160. white-space: nowrap;
  161. `;
  162. const Content = styled(DropdownBubble)<{isOpen: boolean; priority?: ButtonPriority}>`
  163. display: ${p => (p.isOpen ? 'block' : 'none')};
  164. border-color: ${p => p.theme.button[p.priority || 'form'].border};
  165. `;
  166. const DropdownItem = styled(MenuItem)`
  167. font-size: ${p => p.theme.fontSizeMedium};
  168. `;
  169. export default DropdownControl;
  170. export {DropdownItem, Content};