Skip to content

Commit 1c358ae

Browse files
committed
Fixed the dropdown menu bug
1 parent b77041a commit 1c358ae

File tree

12 files changed

+221
-150
lines changed

12 files changed

+221
-150
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class App extends Component {
6363
title: 'Menu item 1-2'
6464
}, '-', {
6565
type: 'link',
66-
title: 'Give me stars!',
66+
title: 'Give me the stars!',
6767
url: 'https://github.com/dkozar/react-data-menu/stargazers',
6868
target: '_blank'
6969
}];

build/App.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ var App = exports.App = function (_Component) {
266266
renderers: renderers,
267267
onOpen: function onOpen() {
268268
self.setState({
269-
openOnMouseOver: true
269+
openOnMouseOver: true // let's have the Mac-like behaviour. Once the first dropdown is open by clicking, consequent open on mouse-over.
270270
});
271271
}
272272
};

build/components/DropdownMenu.js

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ function _inherits(subClass, superClass) { if (typeof superClass !== "function"
3434
var MOUSE_ENTER_DELAY = 500,
3535
MOUSE_LEAVE_DELAY = 100,
3636
ALIGNER = _Aligner.Aligner,
37-
HINTS = function HINTS(level) {
38-
return !level ? ['ss', 'se', 'sm', 'ns', 'ne', 'nm'] : // zero depth
37+
HINTS = function HINTS(depth) {
38+
// default hints. Could be overridden via props
39+
return !depth ? ['ss', 'se', 'sm', 'ns', 'ne', 'nm'] : // zero depth (first menu popup)
3940
['es', 'em', 'ee', 'ws', 'wm', 'we']; // all the others
4041
};
4142

@@ -53,9 +54,7 @@ var DropdownMenu = exports.DropdownMenu = function (_Component) {
5354
_this.onClose = _this.onClose.bind(_this);
5455
_this.setMenuVisibility = _this.setMenuVisibility.bind(_this);
5556
_this.hideMenu = _this.hideMenu.bind(_this);
56-
_this.hideMenuDeferred = function () {
57-
_lodash2.default.defer(this.hideMenu);
58-
};
57+
5958
_this.state = {
6059
isOpen: false
6160
};
@@ -70,31 +69,48 @@ var DropdownMenu = exports.DropdownMenu = function (_Component) {
7069
}, {
7170
key: 'onClose',
7271
value: function onClose() {
73-
this.hideMenuDeferred();
72+
this.hideMenu();
7473
this.props.onClose();
7574
}
7675
}, {
7776
key: 'hideMenu',
7877
value: function hideMenu() {
79-
this.setMenuVisibility(false);
78+
var self = this;
79+
80+
_lodash2.default.defer(function () {
81+
// we're deferring the hiding of the menu, so on close it doesn't go through the open->close->open transition
82+
self.setMenuVisibility(false);
83+
});
8084
}
8185
}, {
8286
key: 'setMenuVisibility',
83-
value: function setMenuVisibility(menuVisible) {
87+
value: function setMenuVisibility(visible) {
8488
this.setState({
85-
isOpen: menuVisible
89+
isOpen: visible
8690
});
8791
}
92+
}, {
93+
key: 'tryOpenMenu',
94+
value: function tryOpenMenu() {
95+
if (!this.state.isOpen) {
96+
// open only if currently closed
97+
this.setMenuVisibility(true);
98+
}
99+
// else do nothing. If the menu is already open, it will close we'were clicking away from it.
100+
}
101+
102+
//<editor-fold desc="Button handlers">
103+
88104
}, {
89105
key: 'onButtonClick',
90106
value: function onButtonClick() {
91-
_lodash2.default.defer(this.setMenuVisibility, !this.state.isOpen);
107+
this.tryOpenMenu();
92108
}
93109
}, {
94110
key: 'onButtonMouseEnter',
95111
value: function onButtonMouseEnter() {
96112
if (this.props.openOnMouseOver) {
97-
this.setMenuVisibility(true);
113+
this.tryOpenMenu();
98114
}
99115
}
100116
}, {
@@ -103,20 +119,44 @@ var DropdownMenu = exports.DropdownMenu = function (_Component) {
103119
e.preventDefault();
104120
e.stopPropagation();
105121
}
122+
//</editor-fold>
123+
124+
//<editor-fold desc="Rendering">
125+
126+
}, {
127+
key: 'renderButton',
128+
value: function renderButton() {
129+
// render a child passed from the outside, or a default button
130+
var children = this.props.children || _react2.default.createElement(
131+
'button',
132+
{ ref: 'button', className: 'menu-button' },
133+
this.props.buttonText
134+
),
135+
self = this;
136+
137+
return _react2.default.Children.map(children, function (child) {
138+
return _react2.default.cloneElement(child, {
139+
ref: 'button',
140+
onClick: self.onButtonClick,
141+
onContextMenu: self.onButtonContextMenu,
142+
onMouseEnter: self.onButtonMouseEnter
143+
});
144+
}.bind(this));
145+
}
106146
}, {
107147
key: 'render',
108148
value: function render() {
109149
var menu = this.state.isOpen ? _react2.default.createElement(_Menu.Menu, {
110-
onClick: this.onMenuClick,
111-
onMouseEnter: this.onMenuMouseEnter,
112-
onMouseLeave: this.onMenuMouseLeave,
113150
onOpen: this.onOpen,
114151
onClose: this.onClose,
152+
onItemMouseEnter: this.props.onItemMouseEnter,
153+
onItemMouseLeave: this.props.onItemMouseLeave,
154+
onItemClick: this.props.onItemClick,
115155
aligner: this.props.aligner,
116156
alignTo: this.buttonElement,
117157
hints: this.props.hints,
118158
items: this.props.items,
119-
autoCloseInstances: this.props.autoCloseInstances,
159+
autoCloseOtherMenuInstances: this.props.autoCloseOtherMenuInstances,
120160
renderers: this.props.renderers,
121161
mouseEnterDelay: this.props.mouseEnterDelay,
122162
mouseLeaveDelay: this.props.mouseLeaveDelay
@@ -129,25 +169,8 @@ var DropdownMenu = exports.DropdownMenu = function (_Component) {
129169
menu
130170
);
131171
}
132-
}, {
133-
key: 'renderButton',
134-
value: function renderButton() {
135-
var children = this.props.children || _react2.default.createElement(
136-
'button',
137-
{ ref: 'button', className: 'menu-button' },
138-
this.props.buttonText
139-
),
140-
self = this;
172+
//</editor-fold>
141173

142-
return _react2.default.Children.map(children, function (child) {
143-
return _react2.default.cloneElement(child, {
144-
ref: 'button',
145-
onClick: self.onButtonClick,
146-
onContextMenu: self.onButtonContextMenu,
147-
onMouseEnter: self.onButtonMouseEnter
148-
});
149-
}.bind(this));
150-
}
151174
}, {
152175
key: 'componentDidMount',
153176
value: function componentDidMount() {
@@ -159,25 +182,31 @@ var DropdownMenu = exports.DropdownMenu = function (_Component) {
159182
}(_react.Component);
160183

161184
DropdownMenu.propTypes = {
162-
buttonText: _react2.default.PropTypes.string,
163-
items: _react2.default.PropTypes.array.isRequired,
164-
openOnMouseOver: _react2.default.PropTypes.bool.isRequired,
165-
autoCloseInstances: _react2.default.PropTypes.bool.isRequired,
185+
buttonText: _react2.default.PropTypes.string, // the text of the default button
186+
openOnMouseOver: _react2.default.PropTypes.bool.isRequired, // should menu be opened on mouse over (Mac menu is opened on first click)
187+
items: _react2.default.PropTypes.array.isRequired, // menu items (data)
188+
autoCloseOtherMenuInstances: _react2.default.PropTypes.bool.isRequired,
166189
mouseEnterDelay: _react2.default.PropTypes.number,
167190
mouseLeaveDelay: _react2.default.PropTypes.number,
168191
hints: _react2.default.PropTypes.func.isRequired,
169-
onOpen: _react2.default.PropTypes.func,
170-
onClose: _react2.default.PropTypes.func
192+
onOpen: _react2.default.PropTypes.func, // custom open handler
193+
onClose: _react2.default.PropTypes.func, // custom close handler
194+
onItemMouseEnter: _react2.default.PropTypes.func, // custom item mouse enter handler
195+
onItemMouseLeave: _react2.default.PropTypes.func, // custom item mouse leave handler
196+
onItemClick: _react2.default.PropTypes.func // custom item click handler
171197
};
172198
DropdownMenu.defaultProps = {
173199
buttonText: '- Menu -',
200+
openOnMouseOver: false,
174201
items: [],
175202
aligner: new ALIGNER(),
176-
autoCloseInstances: true,
177-
openOnMouseOver: false,
203+
autoCloseOtherMenuInstances: true,
178204
mouseEnterDelay: MOUSE_ENTER_DELAY,
179205
mouseLeaveDelay: MOUSE_LEAVE_DELAY,
180206
hints: HINTS,
181207
onOpen: function onOpen() {},
182-
onClose: function onClose() {}
208+
onClose: function onClose() {},
209+
onItemMouseEnter: function onItemMouseEnter() {},
210+
onItemMouseLeave: function onItemMouseLeave() {},
211+
onItemClick: function onItemClick() {}
183212
};

build/components/Menu.js

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function _inherits(subClass, superClass) { if (typeof superClass !== "function"
5252

5353
//</editor-fold>
5454

55-
var DEFAULT_LAYER_ID = '___menu___';
55+
var DEFAULT_LAYER_ID = '___react-data-menu___';
5656

5757
var MOUSE_LEAVE_DELAY = 100,
5858
MOUSE_ENTER_DELAY = 200,
@@ -69,6 +69,8 @@ var MOUSE_LEAVE_DELAY = 100,
6969
return !level ? this.props.alignTo || this.props.position : this.hoverData ? this.hoverData.getElement() : null;
7070
};
7171

72+
// references to all the open menu instances
73+
// usually only the single instance, since by default we're allowing only a single menu on screen (we're auto-closing others)
7274
var instances = [];
7375

7476
var Menu = exports.Menu = function (_Component) {
@@ -84,15 +86,12 @@ var Menu = exports.Menu = function (_Component) {
8486
_this.onItemMouseLeave = _this.onItemMouseLeave.bind(_this);
8587
_this.onItemMouseEnter = _this.onItemMouseEnter.bind(_this);
8688
_this.closeMenu = _this.closeMenu.bind(_this);
87-
_this.closeMenuDeferred = function () {
88-
_lodash2.default.defer(this.closeMenu);
89-
};
9089
_this.onAnywhereClickOrContextMenu = _this.onAnywhereClickOrContextMenu.bind(_this);
9190
_this.createPopup = _this.createPopup.bind(_this);
9291
_this.removeChildPopups = _this.removeChildPopups.bind(_this);
9392
_this.removePopups = _this.removePopups.bind(_this);
9493

95-
// debouncing: we don't want the child popup to open immediately
94+
// debouncing: we don't want the child popup to open/close immediately
9695
_this.processInActionDebounced = _lodash2.default.debounce(_this.processInAction.bind(_this), MOUSE_ENTER_DELAY);
9796
_this.processOutActionDebounced = _lodash2.default.debounce(_this.processOutAction.bind(_this), MOUSE_LEAVE_DELAY);
9897

@@ -107,12 +106,6 @@ var Menu = exports.Menu = function (_Component) {
107106

108107
_this.hoverData = null;
109108

110-
if (props.autoCloseInstances) {
111-
_this.closeOtherMenuInstances();
112-
}
113-
114-
//instances.push(this); // BUG with closeOtherMenuInstances
115-
116109
_this.handlers = {
117110
onAnywhereClick: _this.onAnywhereClickOrContextMenu,
118111
onAnywhereContextMenu: _this.onAnywhereClickOrContextMenu,
@@ -123,7 +116,7 @@ var Menu = exports.Menu = function (_Component) {
123116
}
124117

125118
/**
126-
* Only a single instance menu instance should be visible on screen
119+
* Only a single menu instance should be visible on screen
127120
* Instances do close on window click, however they might get instantiated by other means
128121
* (mouse-over the drop-down button etc.)
129122
*/
@@ -132,16 +125,22 @@ var Menu = exports.Menu = function (_Component) {
132125
_createClass(Menu, [{
133126
key: 'closeOtherMenuInstances',
134127
value: function closeOtherMenuInstances() {
128+
var self = this;
129+
135130
_lodash2.default.forEach(instances, function (instance) {
136-
instance.closeMenuDeferred();
131+
if (instance !== self) {
132+
instance.closeMenu();
133+
}
137134
});
138135
instances = [];
139136
}
140137
}, {
141138
key: 'removeInstance',
142139
value: function removeInstance() {
140+
var self = this;
141+
143142
_lodash2.default.remove(instances, function (instance) {
144-
return this === instance;
143+
return self === instance;
145144
});
146145
}
147146

@@ -209,11 +208,13 @@ var Menu = exports.Menu = function (_Component) {
209208
value: function onItemMouseLeave(hoverData) {
210209
this.hoverData = null;
211210
this.processOutActionDebounced(hoverData);
211+
this.props.onItemMouseLeave(hoverData);
212212
}
213213
}, {
214214
key: 'onItemMouseEnter',
215215
value: function onItemMouseEnter(hoverData) {
216216
this.processInActionDebounced(hoverData, false);
217+
this.props.onItemMouseEnter(hoverData);
217218
}
218219
}, {
219220
key: 'onItemClick',
@@ -223,6 +224,7 @@ var Menu = exports.Menu = function (_Component) {
223224
// leaf node
224225
this.closeMenu();
225226
}
227+
this.props.onItemClick(hoverData);
226228
}
227229
//</editor-fold>
228230

@@ -431,7 +433,11 @@ var Menu = exports.Menu = function (_Component) {
431433
}, {
432434
key: 'componentDidMount',
433435
value: function componentDidMount() {
436+
if (this.props.autoCloseOtherMenuInstances) {
437+
this.closeOtherMenuInstances();
438+
}
434439
this.setMenuVisibility(true);
440+
instances.push(this); // BUG with closeOtherMenuInstances
435441
}
436442
}, {
437443
key: 'componentWillUnmount',
@@ -478,9 +484,7 @@ var Menu = exports.Menu = function (_Component) {
478484
}, {
479485
key: 'getPopup',
480486
value: function getPopup(popupId) {
481-
var self = this;
482-
483-
return _lodash2.default.find(self.state.popups, function (popup) {
487+
return _lodash2.default.find(this.state.popups, function (popup) {
484488
return popup.id === popupId;
485489
});
486490
}
@@ -523,27 +527,33 @@ var Menu = exports.Menu = function (_Component) {
523527

524528

525529
Menu.propTypes = {
526-
items: _react2.default.PropTypes.array.isRequired,
527-
renderers: _react2.default.PropTypes.object,
530+
items: _react2.default.PropTypes.array.isRequired, // menu items (data)
531+
renderers: _react2.default.PropTypes.object, // item renderers
528532
mouseEnterDelay: _react2.default.PropTypes.number,
529533
mouseLeaveDelay: _react2.default.PropTypes.number,
530-
autoCloseInstances: _react2.default.PropTypes.bool,
534+
autoCloseOtherMenuInstances: _react2.default.PropTypes.bool, // should opening of a menu close other, currently open menu instances
531535
onOpen: _react2.default.PropTypes.func,
532536
onClose: _react2.default.PropTypes.func,
537+
onItemMouseEnter: _react2.default.PropTypes.func,
538+
onItemMouseLeave: _react2.default.PropTypes.func,
539+
onItemClick: _react2.default.PropTypes.func,
533540
hints: _react2.default.PropTypes.func,
534541
alignToFunc: _react2.default.PropTypes.func,
535542
layer: _react2.default.PropTypes.node,
536543
layerId: _react2.default.PropTypes.string,
537-
autoCleanup: _react2.default.PropTypes.bool
544+
autoCleanup: _react2.default.PropTypes.bool // Liberator's empty layer auto cleanup
538545
};
539546
Menu.defaultProps = {
540547
items: [],
541548
aligner: new ALIGNER(),
542549
mouseEnterDelay: MOUSE_ENTER_DELAY,
543550
mouseLeaveDelay: MOUSE_LEAVE_DELAY,
544-
autoCloseInstances: true,
551+
autoCloseOtherMenuInstances: true,
545552
onOpen: function onOpen() {},
546553
onClose: function onClose() {},
554+
onItemMouseEnter: function onItemMouseEnter() {},
555+
onItemMouseLeave: function onItemMouseLeave() {},
556+
onItemClick: function onItemClick() {},
547557

548558
hints: HINTS,
549559
alignToFunc: ALIGN_TO,

build/data/items1.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var items1 = exports.items1 = [{
7272
title: 'Leaf item'
7373
}, '-', {
7474
type: 'link',
75-
title: 'Give me stars!',
75+
title: 'Give me the stars!',
7676
url: 'https://github.com/dkozar/react-data-menu/stargazers',
7777
target: '_blank'
7878
}];

demo/bundle.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/bundle.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-data-menu",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "Smart data-driven menu rendered in an overlay",
55
"scripts": {
66
"hot": "node hotServer.js",

0 commit comments

Comments
 (0)