Skip to content

Commit 52dc783

Browse files
author
Mat Brown
committed
Give menus more control over how items are rendered
Initial implementation had `createMenu` take a `mapPropsToItems`, which returned an array of objects containing properties `key`, `isEnabled`, and `props`. The higher-order component was then passed a component class which actually rendered items. As well as setting up a structure that’s both rigid and not particularly discoverable, this meant that menus were assumed to consist of a homogeneous collection of items, each of which is rendered in the same way and behaves in the same way when clicked. This is not true, however, of things like the hamburger menu. So, instead, `createMenu` exports a `<MenuItem>` component, which is used to wrap the actual contents of a menu item and takes `key`, `isEnabled`, and `onClick` props. The factory now takes a `renderItems` function, which can return any valid JSX node. This also improves the interface of generated menu components, since the click handler can now be called whatever feels natural.
1 parent 9640f18 commit 52dc783

7 files changed

Lines changed: 81 additions & 76 deletions

File tree

src/components/TopBar/LibraryPicker.js

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import classnames from 'classnames';
2+
import map from 'lodash/map';
3+
import partial from 'lodash/partial';
4+
import PropTypes from 'prop-types';
5+
import React from 'react';
6+
import libraries from '../../config/libraries';
7+
import createMenu, {MenuItem} from './createMenu';
8+
import LibraryPickerButton from './LibraryPickerButton';
9+
10+
const LibraryPicker = createMenu({
11+
name: 'libraryPicker',
12+
13+
renderItems({enabledLibraries, onToggleLibrary}) {
14+
return map(libraries, (library, key) => {
15+
const isEnabled = enabledLibraries.includes(key);
16+
17+
return (
18+
<MenuItem
19+
isEnabled={isEnabled}
20+
key={key}
21+
onClick={partial(onToggleLibrary, key)}
22+
>
23+
<span className={classnames('u__icon', {u__invisible: !isEnabled})}>
24+
&#xf00c;{' '}
25+
</span>
26+
{library.name}
27+
</MenuItem>
28+
);
29+
});
30+
},
31+
})(LibraryPickerButton);
32+
33+
LibraryPicker.propTypes = {
34+
enabledLibraries: PropTypes.arrayOf(PropTypes.string).isRequired,
35+
onToggleLibrary: PropTypes.func.isRequired,
36+
};
37+
38+
export default LibraryPicker;

src/components/TopBar/LibraryPickerItem.jsx

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/components/TopBar/ProjectPicker.jsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import map from 'lodash/map';
2+
import partial from 'lodash/partial';
23
import PropTypes from 'prop-types';
4+
import React from 'react';
35
import ProjectPreview from '../../containers/ProjectPreview';
46
import ProjectPickerButton from './ProjectPickerButton';
5-
import createMenu from './createMenu';
7+
import createMenu, {MenuItem} from './createMenu';
68

79
const ProjectPicker = createMenu({
810
name: 'projectPicker',
@@ -11,12 +13,16 @@ const ProjectPicker = createMenu({
1113
return currentProjectKey && projectKeys.length > 1;
1214
},
1315

14-
mapPropsToItems({currentProjectKey, projectKeys}) {
15-
return map(projectKeys, projectKey => ({
16-
key: projectKey,
17-
isEnabled: projectKey === currentProjectKey,
18-
props: {projectKey},
19-
}));
16+
renderItems({currentProjectKey, projectKeys, onChangeCurrentProject}) {
17+
return map(projectKeys, projectKey => (
18+
<MenuItem
19+
isEnabled={projectKey === currentProjectKey}
20+
key={projectKey}
21+
onClick={partial(onChangeCurrentProject, projectKey)}
22+
>
23+
<ProjectPreview projectKey={projectKey} />
24+
</MenuItem>
25+
));
2026
},
2127
})(ProjectPickerButton, ProjectPreview);
2228

src/components/TopBar/createMenu.jsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
1+
/* eslint-disable react/no-multi-comp */
2+
13
import classnames from 'classnames';
24
import {connect} from 'react-redux';
35
import constant from 'lodash/constant';
46
import onClickOutside from 'react-onclickoutside';
5-
import partial from 'lodash/partial';
67
import preventClickthrough from 'react-prevent-clickthrough';
78
import property from 'lodash/property';
89
import PropTypes from 'prop-types';
910
import React from 'react';
1011
import {closeTopBarMenu, toggleTopBarMenu} from '../../actions';
1112
import {getOpenTopBarMenu} from '../../selectors';
1213

14+
export function MenuItem({children, isEnabled, onClick}) {
15+
return (
16+
<div
17+
className={classnames('top-bar__menu-item',
18+
{'top-bar__menu-item_active': isEnabled},
19+
)}
20+
onClick={onClick}
21+
>
22+
{children}
23+
</div>
24+
);
25+
}
26+
27+
MenuItem.propTypes = {
28+
children: PropTypes.node.isRequired,
29+
isEnabled: PropTypes.bool.isRequired,
30+
onClick: PropTypes.func.isRequired,
31+
};
32+
1333
export default function createMenu({
1434
isVisible = constant(true),
15-
mapPropsToItems,
35+
renderItems,
1636
name,
1737
}) {
1838
function mapStateToProps(state) {
@@ -35,31 +55,20 @@ export default function createMenu({
3555
};
3656
}
3757

38-
return function createMenuWithMappedProps(Label, Item) {
58+
return function createMenuWithMappedProps(Label) {
3959
function Menu(props) {
4060
if (!isVisible(props)) {
4161
return null;
4262
}
4363

44-
const {isOpen, onClickItem, onToggle} = props;
45-
const items = mapPropsToItems(props);
64+
const {isOpen, onToggle} = props;
4665
const menu = isOpen ?
4766
(
4867
<div
4968
className="top-bar__menu"
5069
onClick={preventClickthrough}
5170
>
52-
{items.map(({key, enabled, props: itemProps}) => (
53-
<div
54-
className={classnames('top-bar__menu-item',
55-
{'top-bar__menu-item_active': enabled},
56-
)}
57-
key={key}
58-
onClick={partial(onClickItem, key)}
59-
>
60-
<Item {...itemProps} />
61-
</div>
62-
))}
71+
{renderItems(props)}
6372
</div>
6473
) : null;
6574

@@ -81,7 +90,6 @@ export default function createMenu({
8190

8291
Menu.propTypes = {
8392
isOpen: PropTypes.bool.isRequired,
84-
onClickItem: PropTypes.func.isRequired,
8593
onToggle: PropTypes.func.isRequired,
8694
};
8795

src/components/TopBar/index.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export default function TopBar({
4545
onCreateSnapshot,
4646
onExportGist,
4747
onExportRepo,
48-
onLibraryToggled,
4948
onLogOut,
5049
onStartLogIn,
50+
onToggleLibrary,
5151
onToggleTextSize,
5252
}) {
5353
const {popVariant, modifier} = uiVariants({validationState, isUserTyping});
@@ -77,11 +77,11 @@ export default function TopBar({
7777
<ProjectPicker
7878
currentProjectKey={currentProjectKey}
7979
projectKeys={projectKeys}
80-
onClickItem={onChangeCurrentProject}
80+
onChangeCurrentProject={onChangeCurrentProject}
8181
/>
8282
<LibraryPicker
8383
enabledLibraries={enabledLibraries}
84-
onClickItem={partial(onLibraryToggled, currentProjectKey)}
84+
onToggleLibrary={partial(onToggleLibrary, currentProjectKey)}
8585
/>
8686
<SnapshotButton
8787
isInProgress={isSnapshotInProgress}
@@ -120,9 +120,9 @@ TopBar.propTypes = {
120120
onCreateSnapshot: PropTypes.func.isRequired,
121121
onExportGist: PropTypes.func.isRequired,
122122
onExportRepo: PropTypes.func.isRequired,
123-
onLibraryToggled: PropTypes.func.isRequired,
124123
onLogOut: PropTypes.func.isRequired,
125124
onStartLogIn: PropTypes.func.isRequired,
125+
onToggleLibrary: PropTypes.func.isRequired,
126126
onToggleTextSize: PropTypes.func.isRequired,
127127
};
128128

src/containers/TopBar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function mapDispatchToProps(dispatch) {
8181
dispatch(exportRepo());
8282
},
8383

84-
onLibraryToggled(projectKey, libraryKey) {
84+
onToggleLibrary(projectKey, libraryKey) {
8585
dispatch(toggleLibrary(projectKey, libraryKey));
8686
},
8787

0 commit comments

Comments
 (0)