You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Dropdown.tsx 4.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import React from 'react';
  21. import { translate } from '../helpers/l10n';
  22. import { PopupPlacement, PopupZLevel } from '../helpers/positioning';
  23. import { InputSizeKeys } from '../types/theme';
  24. import { DropdownMenu } from './DropdownMenu';
  25. import DropdownToggler from './DropdownToggler';
  26. import MenuIcon from './icons/MenuIcon';
  27. import { InteractiveIcon } from './InteractiveIcon';
  28. type OnClickCallback = (event?: React.MouseEvent<HTMLElement>) => void;
  29. type A11yAttrs = Pick<React.AriaAttributes, 'aria-controls' | 'aria-expanded' | 'aria-haspopup'> & {
  30. id: string;
  31. role: React.AriaRole;
  32. };
  33. interface RenderProps {
  34. a11yAttrs: A11yAttrs;
  35. closeDropdown: VoidFunction;
  36. onToggleClick: OnClickCallback;
  37. open: boolean;
  38. }
  39. interface Props {
  40. allowResizing?: boolean;
  41. children:
  42. | ((renderProps: RenderProps) => JSX.Element)
  43. | React.ReactElement<{ onClick: OnClickCallback }>;
  44. className?: string;
  45. closeOnClick?: boolean;
  46. id: string;
  47. onOpen?: VoidFunction;
  48. overlay: React.ReactNode;
  49. placement?: PopupPlacement;
  50. size?: InputSizeKeys;
  51. zLevel?: PopupZLevel;
  52. }
  53. interface State {
  54. open: boolean;
  55. }
  56. export default class Dropdown extends React.PureComponent<Props, State> {
  57. state: State = { open: false };
  58. componentDidUpdate(_: Props, prevState: State) {
  59. if (!prevState.open && this.state.open && this.props.onOpen) {
  60. this.props.onOpen();
  61. }
  62. }
  63. handleClose = () => {
  64. this.setState({ open: false });
  65. };
  66. handleToggleClick: OnClickCallback = (event) => {
  67. if (event) {
  68. event.preventDefault();
  69. event.currentTarget.blur();
  70. }
  71. this.setState((state) => ({ open: !state.open }));
  72. };
  73. render() {
  74. const { open } = this.state;
  75. const { allowResizing, className, closeOnClick = true, id, size = 'full', zLevel } = this.props;
  76. const a11yAttrs: A11yAttrs = {
  77. 'aria-controls': `${id}-dropdown`,
  78. 'aria-expanded': open,
  79. 'aria-haspopup': 'menu',
  80. id: `${id}-trigger`,
  81. role: 'button',
  82. };
  83. const children = React.isValidElement(this.props.children)
  84. ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs })
  85. : this.props.children({
  86. a11yAttrs,
  87. closeDropdown: this.handleClose,
  88. onToggleClick: this.handleToggleClick,
  89. open,
  90. });
  91. return (
  92. <DropdownToggler
  93. allowResizing={allowResizing}
  94. aria-labelledby={`${id}-trigger`}
  95. className={className}
  96. id={`${id}-dropdown`}
  97. onRequestClose={this.handleClose}
  98. open={open}
  99. overlay={
  100. <DropdownMenu onClick={closeOnClick ? this.handleClose : undefined} size={size}>
  101. {this.props.overlay}
  102. </DropdownMenu>
  103. }
  104. placement={this.props.placement}
  105. zLevel={zLevel}
  106. >
  107. {children}
  108. </DropdownToggler>
  109. );
  110. }
  111. }
  112. interface ActionsDropdownProps extends Omit<Props, 'children' | 'overlay'> {
  113. buttonSize?: 'small' | 'medium';
  114. children: React.ReactNode;
  115. }
  116. export function ActionsDropdown(props: ActionsDropdownProps) {
  117. const { children, buttonSize, ...dropdownProps } = props;
  118. return (
  119. <Dropdown overlay={children} {...dropdownProps}>
  120. <InteractiveIcon
  121. Icon={MenuIcon}
  122. aria-label={translate('menu')}
  123. size={buttonSize}
  124. stopPropagation={false}
  125. />
  126. </Dropdown>
  127. );
  128. }