diff options
Diffstat (limited to 'server/sonar-web/design-system/src/components/Dropdown.tsx')
-rw-r--r-- | server/sonar-web/design-system/src/components/Dropdown.tsx | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx new file mode 100644 index 00000000000..f04b595decd --- /dev/null +++ b/server/sonar-web/design-system/src/components/Dropdown.tsx @@ -0,0 +1,140 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../helpers/l10n'; +import { PopupPlacement, PopupZLevel } from '../helpers/positioning'; +import { InputSizeKeys } from '../types/theme'; +import { DropdownMenu } from './DropdownMenu'; +import DropdownToggler from './DropdownToggler'; +import MenuIcon from './icons/MenuIcon'; +import { InteractiveIcon } from './InteractiveIcon'; + +type OnClickCallback = (event?: React.MouseEvent<HTMLElement>) => void; +type A11yAttrs = Pick<React.AriaAttributes, 'aria-controls' | 'aria-expanded' | 'aria-haspopup'> & { + id: string; + role: React.AriaRole; +}; +interface RenderProps { + a11yAttrs: A11yAttrs; + closeDropdown: VoidFunction; + onToggleClick: OnClickCallback; + open: boolean; +} + +interface Props { + allowResizing?: boolean; + children: + | ((renderProps: RenderProps) => JSX.Element) + | React.ReactElement<{ onClick: OnClickCallback }>; + className?: string; + closeOnClick?: boolean; + id: string; + onOpen?: VoidFunction; + overlay: React.ReactNode; + placement?: PopupPlacement; + size?: InputSizeKeys; + zLevel?: PopupZLevel; +} + +interface State { + open: boolean; +} + +export default class Dropdown extends React.PureComponent<Props, State> { + state: State = { open: false }; + + componentDidUpdate(_: Props, prevState: State) { + if (!prevState.open && this.state.open && this.props.onOpen) { + this.props.onOpen(); + } + } + + handleClose = () => { + this.setState({ open: false }); + }; + + handleToggleClick: OnClickCallback = (event) => { + if (event) { + event.preventDefault(); + event.currentTarget.blur(); + } + this.setState((state) => ({ open: !state.open })); + }; + + render() { + const { open } = this.state; + const { allowResizing, className, closeOnClick = true, id, size = 'full', zLevel } = this.props; + const a11yAttrs: A11yAttrs = { + 'aria-controls': `${id}-dropdown`, + 'aria-expanded': open, + 'aria-haspopup': 'menu', + id: `${id}-trigger`, + role: 'button', + }; + + const children = React.isValidElement(this.props.children) + ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs }) + : this.props.children({ + a11yAttrs, + closeDropdown: this.handleClose, + onToggleClick: this.handleToggleClick, + open, + }); + + return ( + <DropdownToggler + allowResizing={allowResizing} + aria-labelledby={`${id}-trigger`} + className={className} + id={`${id}-dropdown`} + onRequestClose={this.handleClose} + open={open} + overlay={ + <DropdownMenu onClick={closeOnClick ? this.handleClose : undefined} size={size}> + {this.props.overlay} + </DropdownMenu> + } + placement={this.props.placement} + zLevel={zLevel} + > + {children} + </DropdownToggler> + ); + } +} + +interface ActionsDropdownProps extends Omit<Props, 'children' | 'overlay'> { + buttonSize?: 'small' | 'medium'; + children: React.ReactNode; +} + +export function ActionsDropdown(props: ActionsDropdownProps) { + const { children, buttonSize, ...dropdownProps } = props; + return ( + <Dropdown overlay={children} {...dropdownProps}> + <InteractiveIcon + Icon={MenuIcon} + aria-label={translate('menu')} + size={buttonSize} + stopPropagation={false} + /> + </Dropdown> + ); +} |