dropdownControl.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. /**
  60. * String or element for the button contents.
  61. */
  62. label?: React.ReactNode;
  63. priority?: ButtonPriority;
  64. };
  65. /*
  66. * A higher level dropdown component that helps with building complete dropdowns
  67. * including the button + menu options. Use the `button` or `label` prop to set
  68. * the button content and `children` to provide menu options.
  69. */
  70. class DropdownControl extends React.Component<Props> {
  71. static defaultProps: DefaultProps = {
  72. alwaysRenderMenu: true,
  73. menuWidth: '100%',
  74. };
  75. renderButton(isOpen: boolean, getActorProps: GetActorPropsFn) {
  76. const {label, button, buttonProps, buttonTooltipTitle, priority} = this.props;
  77. if (button) {
  78. return button({isOpen, getActorProps});
  79. }
  80. if (buttonTooltipTitle && !isOpen) {
  81. return (
  82. <Tooltip skipWrapper position="top" title={buttonTooltipTitle}>
  83. <StyledDropdownButton
  84. priority={priority}
  85. {...getActorProps(buttonProps)}
  86. isOpen={isOpen}
  87. >
  88. {label}
  89. </StyledDropdownButton>
  90. </Tooltip>
  91. );
  92. }
  93. return (
  94. <StyledDropdownButton
  95. priority={priority}
  96. {...getActorProps(buttonProps)}
  97. isOpen={isOpen}
  98. >
  99. {label}
  100. </StyledDropdownButton>
  101. );
  102. }
  103. renderChildren(isOpen: boolean, getMenuProps: GetMenuPropsFn) {
  104. const {children, alignRight, menuWidth, blendWithActor, priority} = this.props;
  105. if (typeof children === 'function') {
  106. return children({isOpen, getMenuProps});
  107. }
  108. const alignMenu = alignRight ? 'right' : 'left';
  109. return (
  110. <Content
  111. {...getMenuProps()}
  112. priority={priority}
  113. alignMenu={alignMenu}
  114. width={menuWidth}
  115. isOpen={isOpen}
  116. blendWithActor={blendWithActor}
  117. blendCorner
  118. >
  119. {children}
  120. </Content>
  121. );
  122. }
  123. render() {
  124. const {alwaysRenderMenu, className} = this.props;
  125. return (
  126. <Container className={className}>
  127. <DropdownMenu alwaysRenderMenu={alwaysRenderMenu}>
  128. {({isOpen, getMenuProps, getActorProps}) => (
  129. <React.Fragment>
  130. {this.renderButton(isOpen, getActorProps)}
  131. {this.renderChildren(isOpen, getMenuProps)}
  132. </React.Fragment>
  133. )}
  134. </DropdownMenu>
  135. </Container>
  136. );
  137. }
  138. }
  139. const Container = styled('div')`
  140. display: inline-block;
  141. position: relative;
  142. `;
  143. const StyledDropdownButton = styled(DropdownButton)`
  144. z-index: ${p => p.theme.zIndex.dropdownAutocomplete.actor};
  145. white-space: nowrap;
  146. `;
  147. const Content = styled(DropdownBubble)<{isOpen: boolean; priority?: ButtonPriority}>`
  148. display: ${p => (p.isOpen ? 'block' : 'none')};
  149. border-color: ${p => p.theme.button[p.priority || 'form'].border};
  150. `;
  151. const DropdownItem = styled(MenuItem)`
  152. font-size: ${p => p.theme.fontSizeMedium};
  153. `;
  154. export default DropdownControl;
  155. export {DropdownItem, Content};