Skip to content

Commit 5e2435c

Browse files
author
Mat Brown
committed
Component factory for top bar menus
Builds a component factory for top bar menus, which looks vaguely like a higher-order component but doesn’t really fit the pattern. Specifically: * The `createMenu()` function takes an object with properties `name` and `mapPropsToItems`, which returns a function… * That takes two component classes, a `Label` class and an `Item` class. This will then return a highly decorated component that renders the menu, deals with opening/closing, marking enabled items as enabled, etc. One of the decorations is the `onClickOutside` HOC which properly handles clicks outside the menu (closing it).
1 parent b4a3159 commit 5e2435c

9 files changed

Lines changed: 138 additions & 105 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@
127127
"prop-types": "^15.5.10",
128128
"qs": "^6.1.0",
129129
"react": "^15.4.1",
130-
"react-click-outside": "tj/react-click-outside",
131130
"react-copy-to-clipboard": "^5.0.0",
132131
"react-dom": "^15.4.1",
133132
"react-draggable": "^2.2.6",
134133
"react-ga": "^2.1.2",
134+
"react-onclickoutside": "^6.5.0",
135135
"react-prevent-clickthrough": "^0.0.3",
136136
"react-redux": "^5.0.3",
137137
"reduce-reducers": "^0.1.2",
Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
import ClickOutside from 'react-click-outside';
21
import PropTypes from 'prop-types';
32
import React from 'react';
43
import {t} from 'i18next';
54

6-
export default function CurrentUserMenu({isOpen, onClose, onLogOut}) {
5+
export default function CurrentUserMenu({isOpen, onLogOut}) {
76
if (!isOpen) {
87
return null;
98
}
109

1110
return (
12-
<ClickOutside onClickOutside={onClose}>
13-
<div className="top-bar__menu">
14-
<div className="top-bar__menu-item" onClick={onLogOut}>
15-
{t('top-bar.session.log-out-prompt')}
16-
</div>
11+
<div className="top-bar__menu">
12+
<div className="top-bar__menu-item" onClick={onLogOut}>
13+
{t('top-bar.session.log-out-prompt')}
1714
</div>
18-
</ClickOutside>
15+
</div>
1916
);
2017
}
2118

2219
CurrentUserMenu.propTypes = {
2320
isOpen: PropTypes.bool.isRequired,
24-
onClose: PropTypes.func.isRequired,
2521
onLogOut: PropTypes.func.isRequired,
2622
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import map from 'lodash/map';
2+
import PropTypes from 'prop-types';
3+
import libraries from '../../config/libraries';
4+
import createMenu from './createMenu';
5+
import LibraryPickerButton from './LibraryPickerButton';
6+
import LibraryPickerItem from './LibraryPickerItem';
7+
8+
const LibraryPicker = createMenu({
9+
name: 'libraryPicker',
10+
11+
mapPropsToItems({enabledLibraries}) {
12+
return map(libraries, (library, key) => {
13+
const isEnabled = enabledLibraries.includes(key);
14+
15+
return {
16+
key,
17+
isEnabled,
18+
props: {isEnabled, library},
19+
};
20+
});
21+
},
22+
})(LibraryPickerButton, LibraryPickerItem);
23+
24+
LibraryPicker.propTypes = {
25+
enabledLibraries: PropTypes.arrayOf(PropTypes.string).isRequired,
26+
};
27+
28+
export default LibraryPicker;

src/components/TopBar/LibraryPicker.jsx

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,16 @@
1-
import classnames from 'classnames';
2-
import PropTypes from 'prop-types';
31
import React from 'react';
42
import {t} from 'i18next';
5-
import LibraryPicker from './LibraryPicker';
63

7-
export default function LibraryPickerButton({
8-
enabledLibraries,
9-
isOpen,
10-
onClick,
11-
onLibraryToggled,
12-
}) {
4+
export default function LibraryPickerButton() {
135
return (
14-
<div
15-
className={classnames(
16-
'top-bar__menu-button',
17-
{'top-bar__menu-button_active': isOpen},
18-
)}
19-
onClick={onClick}
20-
>
6+
<span>
217
{t('top-bar.libraries')}
228
{' '}
239
<span className="u__icon">
2410
&#xf0d7;
2511
</span>
26-
<LibraryPicker
27-
enabledLibraries={enabledLibraries}
28-
isOpen={isOpen}
29-
onLibraryToggled={onLibraryToggled}
30-
/>
31-
</div>
12+
</span>
3213
);
3314
}
3415

35-
LibraryPickerButton.propTypes = {
36-
enabledLibraries: PropTypes.arrayOf(PropTypes.string).isRequired,
37-
isOpen: PropTypes.bool.isRequired,
38-
onClick: PropTypes.func.isRequired,
39-
onLibraryToggled: PropTypes.func.isRequired,
40-
};
16+
LibraryPickerButton.propTypes = {};
Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,19 @@
1-
import React from 'react';
2-
import PropTypes from 'prop-types';
31
import classnames from 'classnames';
2+
import PropTypes from 'prop-types';
3+
import React from 'react';
44

5-
export default function LibraryPickerItem({
6-
enabled,
7-
library,
8-
onLibraryToggled,
9-
}) {
5+
export default function LibraryPickerItem({isEnabled, library}) {
106
return (
11-
<div
12-
className={classnames('top-bar__menu-item',
13-
{'top-bar__menu-item_active': enabled},
14-
)}
15-
onClick={onLibraryToggled}
16-
>
17-
<span className={classnames('u__icon', {u__invisible: !enabled})}>
7+
<span>
8+
<span className={classnames('u__icon', {u__invisible: !isEnabled})}>
189
&#xf00c;{' '}
1910
</span>
2011
{library.name}
21-
</div>
12+
</span>
2213
);
2314
}
2415

2516
LibraryPickerItem.propTypes = {
26-
enabled: PropTypes.bool.isRequired,
17+
isEnabled: PropTypes.bool.isRequired,
2718
library: PropTypes.object.isRequired,
28-
onLibraryToggled: PropTypes.func.isRequired,
2919
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import classnames from 'classnames';
2+
import {connect} from 'react-redux';
3+
import onClickOutside from 'react-onclickoutside';
4+
import partial from 'lodash/partial';
5+
import preventClickthrough from 'react-prevent-clickthrough';
6+
import property from 'lodash/property';
7+
import PropTypes from 'prop-types';
8+
import React from 'react';
9+
import {closeTopBarMenu, toggleTopBarMenu} from '../../actions';
10+
import {getOpenTopBarMenu} from '../../selectors';
11+
12+
export default function createMenu({mapPropsToItems, name}) {
13+
function mapStateToProps(state) {
14+
const isOpen = getOpenTopBarMenu(state) === name;
15+
return {
16+
isOpen,
17+
disableOnClickOutside: !isOpen,
18+
};
19+
}
20+
21+
function mapDispatchToProps(dispatch) {
22+
return {
23+
onClose() {
24+
dispatch(closeTopBarMenu(name));
25+
},
26+
27+
onToggle() {
28+
dispatch(toggleTopBarMenu(name));
29+
},
30+
};
31+
}
32+
33+
return function createMenuWithMappedProps(Label, Item) {
34+
function Menu(props) {
35+
const {isOpen, onClickItem, onToggle} = props;
36+
const items = mapPropsToItems(props);
37+
const menu = isOpen ?
38+
(
39+
<div
40+
className="top-bar__menu"
41+
onClick={preventClickthrough}
42+
>
43+
{items.map(({key, enabled, props: itemProps}) => (
44+
<div
45+
className={classnames('top-bar__menu-item',
46+
{'top-bar__menu-item_active': enabled},
47+
)}
48+
key={key}
49+
onClick={partial(onClickItem, key)}
50+
>
51+
<Item {...itemProps} />
52+
</div>
53+
))}
54+
</div>
55+
) : null;
56+
57+
return (
58+
<div
59+
className={classnames(
60+
'top-bar__menu-button',
61+
{'top-bar__menu-button_active': isOpen},
62+
)}
63+
onClick={onToggle}
64+
>
65+
<Label />
66+
{menu}
67+
</div>
68+
);
69+
}
70+
71+
Menu.displayName = `Menu(${name})`;
72+
73+
Menu.propTypes = {
74+
isOpen: PropTypes.bool.isRequired,
75+
onClickItem: PropTypes.func.isRequired,
76+
onToggle: PropTypes.func.isRequired,
77+
};
78+
79+
return connect(mapStateToProps, mapDispatchToProps)(
80+
onClickOutside(
81+
Menu,
82+
{handleClickOutside: property('props.onClose')},
83+
),
84+
);
85+
};
86+
}

src/components/TopBar/index.jsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Wordmark from '../../static/images/wordmark.svg';
66
import Pop from '../Pop';
77
import CurrentUser from './CurrentUser';
88
import HamburgerMenuButton from './HamburgerMenuButton';
9-
import LibraryPickerButton from './LibraryPickerButton';
9+
import LibraryPicker from './LibraryPicker';
1010
import NewProjectButton from './NewProjectButton';
1111
import ProjectsButton from './ProjectsButton';
1212
import SnapshotButton from './SnapshotButton';
@@ -78,11 +78,9 @@ export default function TopBar({
7878
projectKeys={projectKeys}
7979
onClick={partial(onClickMenu, 'projectPicker')}
8080
/>
81-
<LibraryPickerButton
81+
<LibraryPicker
8282
enabledLibraries={enabledLibraries}
83-
isOpen={openMenu === 'libraryPicker'}
84-
onClick={partial(onClickMenu, 'libraryPicker')}
85-
onLibraryToggled={partial(onLibraryToggled, currentProjectKey)}
83+
onClickItem={partial(onLibraryToggled, currentProjectKey)}
8684
/>
8785
<SnapshotButton
8886
isInProgress={isSnapshotInProgress}

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6718,10 +6718,6 @@ react-addons-perf@^15.4.2:
67186718
fbjs "^0.8.4"
67196719
object-assign "^4.1.0"
67206720

6721-
react-click-outside@tj/react-click-outside:
6722-
version "1.1.1"
6723-
resolved "https://codeload.github.com/tj/react-click-outside/tar.gz/32c16f09318dff8701cacc4ea73655e1f393f2c7"
6724-
67256721
react-copy-to-clipboard@^5.0.0:
67266722
version "5.0.0"
67276723
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.0.tgz#23ccdd7c4d9ec2cc763839e2a55b1220aeb4f33d"
@@ -6752,6 +6748,10 @@ react-ga@^2.1.2:
67526748
object-assign "^4.0.1"
67536749
prop-types "^15.5.6"
67546750

6751+
react-onclickoutside@^6.5.0:
6752+
version "6.5.0"
6753+
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.5.0.tgz#c45b39fe0d7f087817a9f5edab8c12349856b640"
6754+
67556755
react-prevent-clickthrough@^0.0.3:
67566756
version "0.0.3"
67576757
resolved "https://registry.yarnpkg.com/react-prevent-clickthrough/-/react-prevent-clickthrough-0.0.3.tgz#42d5cd85d7dd76dbb5862062ee7c70f1056edb80"

0 commit comments

Comments
 (0)