dropdownControl.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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
  80. containerDisplayMode="inline-flex"
  81. position="top"
  82. title={buttonTooltipTitle}
  83. >
  84. <StyledDropdownButton
  85. priority={priority}
  86. {...getActorProps(buttonProps)}
  87. isOpen={isOpen}
  88. >
  89. {label}
  90. </StyledDropdownButton>
  91. </Tooltip>
  92. );
  93. }
  94. return (
  95. <StyledDropdownButton
  96. priority={priority}
  97. {...getActorProps(buttonProps)}
  98. isOpen={isOpen}
  99. >
  100. {label}
  101. </StyledDropdownButton>
  102. );
  103. }
  104. renderChildren(isOpen: boolean, getMenuProps: GetMenuPropsFn) {
  105. const {children, alignRight, menuWidth, blendWithActor, priority} = this.props;
  106. if (typeof children === 'function') {
  107. return children({isOpen, getMenuProps});
  108. }
  109. const alignMenu = alignRight ? 'right' : 'left';
  110. return (
  111. <Content
  112. {...getMenuProps()}
  113. priority={priority}
  114. alignMenu={alignMenu}
  115. width={menuWidth}
  116. isOpen={isOpen}
  117. blendWithActor={blendWithActor}
  118. blendCorner
  119. >
  120. {children}
  121. </Content>
  122. );
  123. }
  124. render() {
  125. const {alwaysRenderMenu, className} = this.props;
  126. return (
  127. <Container className={className}>
  128. <DropdownMenu alwaysRenderMenu={alwaysRenderMenu}>
  129. {({isOpen, getMenuProps, getActorProps}) => (
  130. <React.Fragment>
  131. {this.renderButton(isOpen, getActorProps)}
  132. {this.renderChildren(isOpen, getMenuProps)}
  133. </React.Fragment>
  134. )}
  135. </DropdownMenu>
  136. </Container>
  137. );
  138. }
  139. }
  140. const Container = styled('div')`
  141. display: inline-block;
  142. position: relative;
  143. `;
  144. const StyledDropdownButton = styled(DropdownButton)`
  145. z-index: ${p => p.theme.zIndex.dropdownAutocomplete.actor};
  146. white-space: nowrap;
  147. `;
  148. const Content = styled(DropdownBubble)<{isOpen: boolean; priority?: ButtonPriority}>`
  149. display: ${p => (p.isOpen ? 'block' : 'none')};
  150. border-color: ${p => p.theme.button[p.priority || 'form'].border};
  151. `;
  152. const DropdownItem = styled(MenuItem)`
  153. font-size: ${p => p.theme.fontSizeMedium};
  154. `;
  155. export default DropdownControl;
  156. export {DropdownItem, Content};