Skip to content

Commit 9978ac0

Browse files
committed
Add DropdownMenu component with better CSSTransitionGroup implementation
1 parent b2e3da6 commit 9978ac0

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"dependencies": {
4343
"boron": "^0.2.3",
44+
"classnames": "^2.1.3",
4445
"immutable": "^3.x.x",
4546
"js-yaml": "^3.5.5",
4647
"json-beautify": "^1.0.1",
@@ -51,6 +52,7 @@
5152
"react-dom": "^15.x",
5253
"react-file-download": "^0.3.2",
5354
"react-redux": "^4.x.x",
55+
"react-transition-group": "^1.1.1",
5456
"redux": "^3.x.x",
5557
"swagger-client": "~3.0.10",
5658
"swagger-ui": "^3.0.9",
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Adapted from https://github.com/mlaursen/react-dd-menu/blob/master/src/js/DropdownMenu.js
2+
3+
import React, { PureComponent, PropTypes } from 'react';
4+
import ReactDOM from 'react-dom';
5+
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
6+
import classnames from 'classnames';
7+
8+
const TAB = 9;
9+
const SPACEBAR = 32;
10+
const ALIGNMENTS = ['center', 'right', 'left'];
11+
const MENU_SIZES = ['sm', 'md', 'lg', 'xl'];
12+
13+
14+
export default class DropdownMenu extends PureComponent {
15+
static propTypes = {
16+
isOpen: PropTypes.bool.isRequired,
17+
close: PropTypes.func.isRequired,
18+
toggle: PropTypes.node.isRequired,
19+
children: PropTypes.node,
20+
inverse: PropTypes.bool,
21+
align: PropTypes.oneOf(ALIGNMENTS),
22+
animAlign: PropTypes.oneOf(ALIGNMENTS),
23+
textAlign: PropTypes.oneOf(ALIGNMENTS),
24+
menuAlign: PropTypes.oneOf(ALIGNMENTS),
25+
className: PropTypes.string,
26+
size: PropTypes.oneOf(MENU_SIZES),
27+
upwards: PropTypes.bool,
28+
animate: PropTypes.bool,
29+
enterTimeout: PropTypes.number,
30+
leaveTimeout: PropTypes.number,
31+
closeOnInsideClick: PropTypes.bool,
32+
closeOnOutsideClick: PropTypes.bool,
33+
};
34+
35+
static defaultProps = {
36+
inverse: false,
37+
align: 'center',
38+
animAlign: null,
39+
textAlign: null,
40+
menuAlign: null,
41+
className: null,
42+
size: null,
43+
upwards: false,
44+
animate: true,
45+
enterTimeout: 150,
46+
leaveTimeout: 150,
47+
closeOnInsideClick: true,
48+
closeOnOutsideClick: true,
49+
};
50+
51+
static MENU_SIZES = MENU_SIZES;
52+
static ALIGNMENTS = ALIGNMENTS;
53+
54+
componentDidUpdate(prevProps) {
55+
if(this.props.isOpen === prevProps.isOpen) {
56+
return;
57+
}
58+
59+
const menuItems = ReactDOM.findDOMNode(this).querySelector('.dd-menu > .dd-menu-items');
60+
if(this.props.isOpen && !prevProps.isOpen) {
61+
this.lastWindowClickEvent = this.handleClickOutside;
62+
document.addEventListener('click', this.lastWindowClickEvent);
63+
if(this.props.closeOnInsideClick) {
64+
menuItems.addEventListener('click', this.props.close);
65+
}
66+
menuItems.addEventListener('onkeydown', this.close);
67+
} else if(!this.props.isOpen && prevProps.isOpen) {
68+
document.removeEventListener('click', this.lastWindowClickEvent);
69+
if(prevProps.closeOnInsideClick) {
70+
menuItems.removeEventListener('click', this.props.close);
71+
}
72+
menuItems.removeEventListener('onkeydown', this.close);
73+
74+
this.lastWindowClickEvent = null;
75+
}
76+
}
77+
78+
componentWillUnmount() {
79+
if(this.lastWindowClickEvent) {
80+
document.removeEventListener('click', this.lastWindowClickEvent);
81+
}
82+
}
83+
84+
close = (e) => {
85+
const key = e.which || e.keyCode;
86+
if(key === SPACEBAR) {
87+
this.props.close();
88+
e.preventDefault();
89+
}
90+
};
91+
92+
handleClickOutside = (e) => {
93+
if(!this.props.closeOnOutsideClick) {
94+
return;
95+
}
96+
97+
const node = ReactDOM.findDOMNode(this);
98+
let target = e.target;
99+
100+
while(target.parentNode) {
101+
if(target === node) {
102+
return;
103+
}
104+
105+
target = target.parentNode;
106+
}
107+
108+
this.props.close(e);
109+
};
110+
111+
handleKeyDown = (e) => {
112+
const key = e.which || e.keyCode;
113+
if(key !== TAB) {
114+
return;
115+
}
116+
117+
const items = ReactDOM.findDOMNode(this).querySelectorAll('button,a');
118+
const id = e.shiftKey ? 1 : items.length - 1;
119+
120+
if(e.target === items[id]) {
121+
this.props.close(e);
122+
}
123+
};
124+
125+
126+
render() {
127+
const { menuAlign, align, inverse, size, className } = this.props;
128+
129+
const menuClassName = classnames(
130+
'dd-menu',
131+
`dd-menu-${menuAlign || align}`,
132+
{ 'dd-menu-inverse': inverse },
133+
className,
134+
size ? ('dd-menu-' + size) : null
135+
);
136+
137+
const { textAlign, upwards, animAlign, animate, enterTimeout, leaveTimeout } = this.props;
138+
139+
const listClassName = 'dd-items-' + (textAlign || align);
140+
const transitionProps = {
141+
transitionName: 'grow-from-' + (upwards ? 'up-' : '') + (animAlign || align),
142+
component: 'div',
143+
className: classnames('dd-menu-items', { 'dd-items-upwards': upwards }),
144+
onKeyDown: this.handleKeyDown,
145+
transitionEnter: animate,
146+
transitionLeave: animate,
147+
transitionEnterTimeout: enterTimeout,
148+
transitionLeaveTimeout: leaveTimeout,
149+
};
150+
151+
return (
152+
<div className={menuClassName}>
153+
{this.props.toggle}
154+
<CSSTransitionGroup {...transitionProps}>
155+
{this.props.isOpen &&
156+
<ul key="items" className={listClassName}>{this.props.children}</ul>
157+
}
158+
</CSSTransitionGroup>
159+
</div>
160+
);
161+
}
162+
}

src/standalone/topbar/topbar.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { PropTypes } from "react"
22
import Swagger from "swagger-client"
33
import "whatwg-fetch"
4-
import DropdownMenu from "react-dd-menu"
4+
import DropdownMenu from "./DropdownMenu"
55
import Modal from "boron/DropModal"
66
import downloadFile from "react-file-download"
77
import YAML from "js-yaml"

0 commit comments

Comments
 (0)