dropdownControl.tsx 4.6 KB

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