aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-05-09 09:17:16 +0200
committerSonarTech <sonartech@sonarsource.com>2018-05-09 20:20:46 +0200
commit09b3d167fa8f399e18a37d56e7c8cbb61f68f97f (patch)
tree415072b29720bdd0c5293a898eb4ed10b807859e /server/sonar-web/src/main/js/components/controls/Dropdown.tsx
parent302775229e9cc6debd58804446cb98c2ea563bd4 (diff)
downloadsonarqube-09b3d167fa8f399e18a37d56e7c8cbb61f68f97f.tar.gz
sonarqube-09b3d167fa8f399e18a37d56e7c8cbb61f68f97f.zip
SONAR-10664 Improve dropdown UI/UX consistency (#217)
Diffstat (limited to 'server/sonar-web/src/main/js/components/controls/Dropdown.tsx')
-rw-r--r--server/sonar-web/src/main/js/components/controls/Dropdown.tsx147
1 files changed, 106 insertions, 41 deletions
diff --git a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
index 3b82fd1c408..a90414a235c 100644
--- a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx
@@ -18,16 +18,33 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
+import ScreenPositionFixer from './ScreenPositionFixer';
+import Toggler from './Toggler';
+import { Popup, PopupPlacement } from '../ui/popups';
+
+interface OnClickCallback {
+ (event?: React.SyntheticEvent<HTMLElement>): void;
+}
interface RenderProps {
closeDropdown: () => void;
- onToggleClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
+ onToggleClick: OnClickCallback;
open: boolean;
}
interface Props {
- children: (renderProps: RenderProps) => JSX.Element;
+ children:
+ | ((renderProps: RenderProps) => JSX.Element)
+ | React.ReactElement<{ onClick: OnClickCallback }>;
+ className?: string;
+ closeOnClick?: boolean;
+ closeOnClickOutside?: boolean;
onOpen?: () => void;
+ overlay: React.ReactNode;
+ overlayPlacement?: PopupPlacement;
+ noOverlayPadding?: boolean;
+ tagName?: string;
}
interface State {
@@ -35,49 +52,18 @@ interface State {
}
export default class Dropdown extends React.PureComponent<Props, State> {
- toggleNode?: HTMLElement;
-
- constructor(props: Props) {
- super(props);
- this.state = { open: false };
- }
+ state: State = { open: false };
componentDidUpdate(_: Props, prevState: State) {
- if (!prevState.open && this.state.open) {
- this.addClickHandler();
- if (this.props.onOpen) {
- this.props.onOpen();
- }
- }
-
- if (prevState.open && !this.state.open) {
- this.removeClickHandler();
+ if (!prevState.open && this.state.open && this.props.onOpen) {
+ this.props.onOpen();
}
}
- componentWillUnmount() {
- this.removeClickHandler();
- }
-
- addClickHandler = () => {
- window.addEventListener('click', this.handleWindowClick);
- };
-
- removeClickHandler = () => {
- window.removeEventListener('click', this.handleWindowClick);
- };
-
- handleWindowClick = (event: MouseEvent) => {
- if (!this.toggleNode || !this.toggleNode.contains(event.target as Node)) {
- this.closeDropdown();
- }
- };
-
closeDropdown = () => this.setState({ open: false });
handleToggleClick = (event?: React.SyntheticEvent<HTMLElement>) => {
if (event) {
- this.toggleNode = event.currentTarget;
event.preventDefault();
event.currentTarget.blur();
}
@@ -85,10 +71,89 @@ export default class Dropdown extends React.PureComponent<Props, State> {
};
render() {
- return this.props.children({
- closeDropdown: this.closeDropdown,
- onToggleClick: this.handleToggleClick,
- open: this.state.open
- });
+ const a11yAttrs = {
+ 'aria-expanded': String(this.state.open),
+ 'aria-haspopup': 'true'
+ };
+
+ const child = React.isValidElement(this.props.children)
+ ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs })
+ : this.props.children({
+ closeDropdown: this.closeDropdown,
+ onToggleClick: this.handleToggleClick,
+ open: this.state.open
+ });
+
+ const { closeOnClick = true, closeOnClickOutside = false } = this.props;
+
+ const toggler = (
+ <Toggler
+ closeOnClick={closeOnClick}
+ closeOnClickOutside={closeOnClickOutside}
+ onRequestClose={this.closeDropdown}
+ open={this.state.open}
+ overlay={
+ <DropdownOverlay
+ noPadding={this.props.noOverlayPadding}
+ placement={this.props.overlayPlacement}>
+ {this.props.overlay}
+ </DropdownOverlay>
+ }>
+ {child}
+ </Toggler>
+ );
+
+ return React.createElement(
+ this.props.tagName || 'div',
+ { className: classNames('dropdown', this.props.className) },
+ toggler
+ );
+ }
+}
+
+interface OverlayProps {
+ className?: string;
+ children: React.ReactNode;
+ noPadding?: boolean;
+ placement?: PopupPlacement;
+}
+
+// TODO use the same styling for <Select />
+// TODO use the same styling for <DateInput />
+
+export class DropdownOverlay extends React.Component<OverlayProps> {
+ get placement() {
+ return this.props.placement || PopupPlacement.Bottom;
+ }
+
+ renderPopup = (leftFix?: number, topFix?: number) => (
+ <Popup
+ arrowStyle={
+ leftFix !== undefined && topFix !== undefined
+ ? { transform: `translate(${-leftFix}px, ${-topFix}px)` }
+ : undefined
+ }
+ className={this.props.className}
+ noPadding={this.props.noPadding}
+ placement={this.placement}
+ style={
+ leftFix !== undefined && topFix !== undefined
+ ? { marginLeft: `calc(50% + ${leftFix}px)` }
+ : undefined
+ }>
+ {this.props.children}
+ </Popup>
+ );
+
+ render() {
+ if (this.placement === PopupPlacement.Bottom) {
+ return (
+ <ScreenPositionFixer>
+ {({ leftFix, topFix }) => this.renderPopup(leftFix, topFix)}
+ </ScreenPositionFixer>
+ );
+ } else {
+ return this.renderPopup();
+ }
}
}