Skip to content

Commit f339cb8

Browse files
authored
feat: Add Settings menu to scroll info panel to top when browsing through rows (#2937)
1 parent 6e0bf33 commit f339cb8

File tree

10 files changed

+241
-44
lines changed

10 files changed

+241
-44
lines changed

src/components/BrowserMenu/BrowserMenu.react.js

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,24 @@
55
* This source code is licensed under the license found in the LICENSE file in
66
* the root directory of this source tree.
77
*/
8-
import Popover from 'components/Popover/Popover.react';
8+
import styles from 'components/BrowserMenu/BrowserMenu.scss';
99
import Icon from 'components/Icon/Icon.react';
10+
import Popover from 'components/Popover/Popover.react';
1011
import Position from 'lib/Position';
1112
import PropTypes from 'lib/PropTypes';
1213
import React from 'react';
13-
import styles from 'components/BrowserMenu/BrowserMenu.scss';
1414

1515
export default class BrowserMenu extends React.Component {
1616
constructor() {
1717
super();
1818

19-
this.state = { open: false };
19+
this.state = { open: false, openToLeft: false };
2020
this.wrapRef = React.createRef();
2121
}
2222

2323
render() {
2424
let menu = null;
25+
const isSubmenu = !!this.props.parentClose;
2526
if (this.state.open) {
2627
const position = Position.inDocument(this.wrapRef.current);
2728
const titleStyle = [styles.title];
@@ -35,20 +36,50 @@ export default class BrowserMenu extends React.Component {
3536
onExternalClick={() => this.setState({ open: false })}
3637
>
3738
<div className={styles.menu}>
38-
<div className={titleStyle.join(' ')} onClick={() => this.setState({ open: false })}>
39-
<Icon name={this.props.icon} width={14} height={14} />
40-
<span>{this.props.title}</span>
41-
</div>
42-
<div className={styles.body} style={{ minWidth: this.wrapRef.current.clientWidth }}>
43-
{React.Children.map(this.props.children, child =>
44-
React.cloneElement(child, {
45-
...child.props,
46-
onClick: () => {
47-
this.setState({ open: false });
48-
child.props.onClick();
49-
},
50-
})
51-
)}
39+
{!isSubmenu && (
40+
<div
41+
className={titleStyle.join(' ')}
42+
onClick={() => this.setState({ open: false })}
43+
>
44+
{this.props.icon && <Icon name={this.props.icon} width={14} height={14} />}
45+
<span>{this.props.title}</span>
46+
</div>
47+
)}
48+
<div
49+
className={
50+
isSubmenu
51+
? this.state.openToLeft
52+
? styles.subMenuBodyLeft
53+
: styles.subMenuBody
54+
: styles.body
55+
}
56+
style={{
57+
minWidth: this.wrapRef.current.clientWidth,
58+
...(isSubmenu
59+
? {
60+
top: 0,
61+
left: this.state.openToLeft
62+
? 0
63+
: `${this.wrapRef.current.clientWidth - 3}px`,
64+
transform: this.state.openToLeft
65+
? 'translateX(calc(-100% + 3px))'
66+
: undefined,
67+
}
68+
: {}),
69+
}}
70+
>
71+
{React.Children.map(this.props.children, (child) => {
72+
if (React.isValidElement(child) && child.type === BrowserMenu) {
73+
return React.cloneElement(child, {
74+
...child.props,
75+
parentClose: () => {
76+
this.setState({ open: false });
77+
this.props.parentClose?.();
78+
},
79+
});
80+
}
81+
return child;
82+
})}
5283
</div>
5384
</div>
5485
</Popover>
@@ -61,18 +92,37 @@ export default class BrowserMenu extends React.Component {
6192
if (this.props.disabled) {
6293
classes.push(styles.disabled);
6394
}
64-
let onClick = null;
95+
const entryEvents = {};
6596
if (!this.props.disabled) {
66-
onClick = () => {
67-
this.setState({ open: true });
68-
this.props.setCurrent(null);
69-
};
97+
if (isSubmenu) {
98+
entryEvents.onMouseEnter = () => {
99+
const rect = this.wrapRef.current.getBoundingClientRect();
100+
const width = this.wrapRef.current.clientWidth;
101+
const openToLeft = rect.right + width > window.innerWidth;
102+
this.setState({ open: true, openToLeft });
103+
this.props.setCurrent?.(null);
104+
};
105+
} else {
106+
entryEvents.onClick = () => {
107+
this.setState({ open: true, openToLeft: false });
108+
this.props.setCurrent(null);
109+
};
110+
}
70111
}
71112
return (
72113
<div className={styles.wrap} ref={this.wrapRef}>
73-
<div className={classes.join(' ')} onClick={onClick}>
74-
<Icon name={this.props.icon} width={14} height={14} />
114+
<div className={classes.join(' ')} {...entryEvents}>
115+
{this.props.icon && <Icon name={this.props.icon} width={14} height={14} />}
75116
<span>{this.props.title}</span>
117+
{isSubmenu &&
118+
React.Children.toArray(this.props.children).some(c => React.isValidElement(c) && c.type === BrowserMenu) && (
119+
<Icon
120+
name="right-outline"
121+
width={12}
122+
height={12}
123+
className={styles.submenuArrow}
124+
/>
125+
)}
76126
</div>
77127
{menu}
78128
</div>
@@ -81,12 +131,12 @@ export default class BrowserMenu extends React.Component {
81131
}
82132

83133
BrowserMenu.propTypes = {
84-
icon: PropTypes.string.isRequired.describe('The name of the icon to place in the menu.'),
134+
icon: PropTypes.string.describe('The name of the icon to place in the menu.'),
85135
title: PropTypes.string.isRequired.describe('The title text of the menu.'),
86-
children: PropTypes.oneOfType([
87-
PropTypes.arrayOf(PropTypes.node),
88-
PropTypes.node,
89-
]).describe(
136+
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).describe(
90137
'The contents of the menu when open. It should be a set of MenuItem and Separator components.'
91138
),
139+
parentClose: PropTypes.func.describe(
140+
'Closes the parent menu when a nested menu item is selected.'
141+
),
92142
};

src/components/BrowserMenu/BrowserMenu.scss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,38 @@
8989
font-size: 14px;
9090
}
9191

92+
93+
.subMenuBody {
94+
position: absolute;
95+
top: 0;
96+
border-radius: 0 5px 5px 5px;
97+
background: #797592;
98+
padding: 8px 0;
99+
font-size: 14px;
100+
border: 1px solid rgba(0, 0, 0, 0.1);
101+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
102+
}
103+
104+
.subMenuBodyLeft {
105+
position: absolute;
106+
top: 0;
107+
border-radius: 5px 0 5px 5px;
108+
background: #797592;
109+
padding: 8px 0;
110+
font-size: 14px;
111+
border: 1px solid rgba(0, 0, 0, 0.1);
112+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
113+
}
114+
115+
.submenuArrow {
116+
margin-left: 4px;
117+
fill: #66637A;
118+
}
119+
120+
.entry:hover .submenuArrow {
121+
fill: white;
122+
}
123+
92124
.item {
93125
padding: 4px 14px;
94126
white-space: nowrap;

src/components/BrowserMenu/MenuItem.react.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,24 @@ const MenuItem = ({ text, disabled, active, greenActive, onClick }) => {
1919
if (greenActive) {
2020
classes.push(styles.greenActive);
2121
}
22+
23+
const handleClick = (e) => {
24+
if (!disabled && onClick) {
25+
onClick(e);
26+
}
27+
};
28+
2229
return (
23-
<div className={classes.join(' ')} onClick={disabled ? undefined : onClick}>
30+
<div
31+
className={classes.join(' ')}
32+
onClick={handleClick}
33+
onMouseDown={handleClick} // This is needed - onClick alone doesn't work in this context
34+
style={{
35+
position: 'relative',
36+
zIndex: 9999,
37+
cursor: 'pointer'
38+
}}
39+
>
2440
{text}
2541
</div>
2642
);

src/components/Icon/Icon.react.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
*/
88
import PropTypes from 'lib/PropTypes';
99
import React from 'react';
10+
import styles from 'components/Icon/Icon.scss';
1011

11-
const Icon = ({ name, fill, width, height }) => {
12+
const Icon = ({ name, fill, width, height, style, className }) => {
1213
const props = {
13-
width: width,
14-
height: height,
14+
width,
15+
height,
16+
style,
1517
};
18+
if (className) {
19+
props.className = styles[className] || className;
20+
}
1621
if (fill) {
1722
props.fill = fill;
1823
}
@@ -30,4 +35,5 @@ Icon.propTypes = {
3035
width: PropTypes.number.isRequired.describe('The icon width, in pixels.'),
3136
height: PropTypes.number.isRequired.describe('The icon height, in pixels.'),
3237
fill: PropTypes.string.describe('A valid color, used as the fill property for the SVG.'),
38+
style: PropTypes.object.describe('An object containing CSS styles to apply to the icon.'),
3339
};

src/components/Icon/Icon.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
@import 'stylesheets/globals.scss';
9+
10+
.menuCheck {
11+
margin-right: 6px;
12+
margin-bottom: -1px;
13+
padding-top: 0;
14+
}

src/dashboard/Data/Browser/BrowserToolbar.react.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
*/
88
import BrowserFilter from 'components/BrowserFilter/BrowserFilter.react';
99
import BrowserMenu from 'components/BrowserMenu/BrowserMenu.react';
10-
import Icon from 'components/Icon/Icon.react';
1110
import MenuItem from 'components/BrowserMenu/MenuItem.react';
12-
import prettyNumber from 'lib/prettyNumber';
13-
import React, { useRef } from 'react';
1411
import Separator from 'components/BrowserMenu/Separator.react';
15-
import styles from 'dashboard/Data/Browser/Browser.scss';
16-
import Toolbar from 'components/Toolbar/Toolbar.react';
17-
import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react';
1812
import ColumnsConfiguration from 'components/ColumnsConfiguration/ColumnsConfiguration.react';
19-
import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react';
20-
import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react';
13+
import Icon from 'components/Icon/Icon.react';
2114
import Toggle from 'components/Toggle/Toggle.react';
15+
import Toolbar from 'components/Toolbar/Toolbar.react';
16+
import styles from 'dashboard/Data/Browser/Browser.scss';
17+
import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react';
18+
import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react';
19+
import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react';
20+
import prettyNumber from 'lib/prettyNumber';
21+
import React, { useRef } from 'react';
2222

2323
const BrowserToolbar = ({
2424
className,
@@ -82,6 +82,8 @@ const BrowserToolbar = ({
8282
classwiseCloudFunctions,
8383
appId,
8484
appName,
85+
scrollToTop,
86+
toggleScrollToTop,
8587
}) => {
8688
const selectionLength = Object.keys(selection).length;
8789
const isPendingEditCloneRows = editCloneRows && editCloneRows.length > 0;
@@ -424,7 +426,7 @@ const BrowserToolbar = ({
424426
<noscript />
425427
)}
426428
{enableSecurityDialog ? <div className={styles.toolbarSeparator} /> : <noscript />}
427-
<BrowserMenu setCurrent={setCurrent} title="Script" icon="gear-solid">
429+
<BrowserMenu setCurrent={setCurrent} title="Script" icon="script-solid">
428430
<MenuItem
429431
disabled={selectionLength === 0}
430432
text={
@@ -443,6 +445,30 @@ const BrowserToolbar = ({
443445
<MenuItem text={'Cancel all pending rows'} onClick={onCancelPendingEditRows} />
444446
</BrowserMenu>
445447
)}
448+
<div className={styles.toolbarSeparator} />
449+
<BrowserMenu setCurrent={setCurrent} title="Settings" icon="gear-solid">
450+
<BrowserMenu title="Info Panel" setCurrent={setCurrent}>
451+
<MenuItem
452+
text={
453+
<span>
454+
{scrollToTop && (
455+
<Icon
456+
name="check"
457+
width={12}
458+
height={12}
459+
fill="#ffffffff"
460+
className="menuCheck"
461+
/>
462+
)}
463+
Scroll to top
464+
</span>
465+
}
466+
onClick={() => {
467+
toggleScrollToTop();
468+
}}
469+
/>
470+
</BrowserMenu>
471+
</BrowserMenu>
446472
</Toolbar>
447473
);
448474
};

0 commit comments

Comments
 (0)