diff --git a/src/components/BrowserMenu/BrowserMenu.react.js b/src/components/BrowserMenu/BrowserMenu.react.js index ee5f9c0b41..ca3b97fd6e 100644 --- a/src/components/BrowserMenu/BrowserMenu.react.js +++ b/src/components/BrowserMenu/BrowserMenu.react.js @@ -5,23 +5,24 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ -import Popover from 'components/Popover/Popover.react'; +import styles from 'components/BrowserMenu/BrowserMenu.scss'; import Icon from 'components/Icon/Icon.react'; +import Popover from 'components/Popover/Popover.react'; import Position from 'lib/Position'; import PropTypes from 'lib/PropTypes'; import React from 'react'; -import styles from 'components/BrowserMenu/BrowserMenu.scss'; export default class BrowserMenu extends React.Component { constructor() { super(); - this.state = { open: false }; + this.state = { open: false, openToLeft: false }; this.wrapRef = React.createRef(); } render() { let menu = null; + const isSubmenu = !!this.props.parentClose; if (this.state.open) { const position = Position.inDocument(this.wrapRef.current); const titleStyle = [styles.title]; @@ -35,20 +36,50 @@ export default class BrowserMenu extends React.Component { onExternalClick={() => this.setState({ open: false })} >
-
this.setState({ open: false })}> - - {this.props.title} -
-
- {React.Children.map(this.props.children, child => - React.cloneElement(child, { - ...child.props, - onClick: () => { - this.setState({ open: false }); - child.props.onClick(); - }, - }) - )} + {!isSubmenu && ( +
this.setState({ open: false })} + > + {this.props.icon && } + {this.props.title} +
+ )} +
+ {React.Children.map(this.props.children, (child) => { + if (React.isValidElement(child) && child.type === BrowserMenu) { + return React.cloneElement(child, { + ...child.props, + parentClose: () => { + this.setState({ open: false }); + this.props.parentClose?.(); + }, + }); + } + return child; + })}
@@ -61,18 +92,37 @@ export default class BrowserMenu extends React.Component { if (this.props.disabled) { classes.push(styles.disabled); } - let onClick = null; + const entryEvents = {}; if (!this.props.disabled) { - onClick = () => { - this.setState({ open: true }); - this.props.setCurrent(null); - }; + if (isSubmenu) { + entryEvents.onMouseEnter = () => { + const rect = this.wrapRef.current.getBoundingClientRect(); + const width = this.wrapRef.current.clientWidth; + const openToLeft = rect.right + width > window.innerWidth; + this.setState({ open: true, openToLeft }); + this.props.setCurrent?.(null); + }; + } else { + entryEvents.onClick = () => { + this.setState({ open: true, openToLeft: false }); + this.props.setCurrent(null); + }; + } } return (
-
- +
+ {this.props.icon && } {this.props.title} + {isSubmenu && + React.Children.toArray(this.props.children).some(c => React.isValidElement(c) && c.type === BrowserMenu) && ( + + )}
{menu}
@@ -81,12 +131,12 @@ export default class BrowserMenu extends React.Component { } BrowserMenu.propTypes = { - icon: PropTypes.string.isRequired.describe('The name of the icon to place in the menu.'), + icon: PropTypes.string.describe('The name of the icon to place in the menu.'), title: PropTypes.string.isRequired.describe('The title text of the menu.'), - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]).describe( + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).describe( 'The contents of the menu when open. It should be a set of MenuItem and Separator components.' ), + parentClose: PropTypes.func.describe( + 'Closes the parent menu when a nested menu item is selected.' + ), }; diff --git a/src/components/BrowserMenu/BrowserMenu.scss b/src/components/BrowserMenu/BrowserMenu.scss index 9c73ca3341..1cbf889244 100644 --- a/src/components/BrowserMenu/BrowserMenu.scss +++ b/src/components/BrowserMenu/BrowserMenu.scss @@ -89,6 +89,38 @@ font-size: 14px; } + +.subMenuBody { + position: absolute; + top: 0; + border-radius: 0 5px 5px 5px; + background: #797592; + padding: 8px 0; + font-size: 14px; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); +} + +.subMenuBodyLeft { + position: absolute; + top: 0; + border-radius: 5px 0 5px 5px; + background: #797592; + padding: 8px 0; + font-size: 14px; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); +} + +.submenuArrow { + margin-left: 4px; + fill: #66637A; +} + +.entry:hover .submenuArrow { + fill: white; +} + .item { padding: 4px 14px; white-space: nowrap; diff --git a/src/components/BrowserMenu/MenuItem.react.js b/src/components/BrowserMenu/MenuItem.react.js index 5b8e72c51a..84eedf1ad0 100644 --- a/src/components/BrowserMenu/MenuItem.react.js +++ b/src/components/BrowserMenu/MenuItem.react.js @@ -19,8 +19,24 @@ const MenuItem = ({ text, disabled, active, greenActive, onClick }) => { if (greenActive) { classes.push(styles.greenActive); } + + const handleClick = (e) => { + if (!disabled && onClick) { + onClick(e); + } + }; + return ( -
+
{text}
); diff --git a/src/components/Icon/Icon.react.js b/src/components/Icon/Icon.react.js index 750e80806b..072ae9edf6 100644 --- a/src/components/Icon/Icon.react.js +++ b/src/components/Icon/Icon.react.js @@ -7,12 +7,17 @@ */ import PropTypes from 'lib/PropTypes'; import React from 'react'; +import styles from 'components/Icon/Icon.scss'; -const Icon = ({ name, fill, width, height }) => { +const Icon = ({ name, fill, width, height, style, className }) => { const props = { - width: width, - height: height, + width, + height, + style, }; + if (className) { + props.className = styles[className] || className; + } if (fill) { props.fill = fill; } @@ -30,4 +35,5 @@ Icon.propTypes = { width: PropTypes.number.isRequired.describe('The icon width, in pixels.'), height: PropTypes.number.isRequired.describe('The icon height, in pixels.'), fill: PropTypes.string.describe('A valid color, used as the fill property for the SVG.'), + style: PropTypes.object.describe('An object containing CSS styles to apply to the icon.'), }; diff --git a/src/components/Icon/Icon.scss b/src/components/Icon/Icon.scss new file mode 100644 index 0000000000..0bd22ea857 --- /dev/null +++ b/src/components/Icon/Icon.scss @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +@import 'stylesheets/globals.scss'; + +.menuCheck { + margin-right: 6px; + margin-bottom: -1px; + padding-top: 0; +} diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index cfd1c51083..84621a84b9 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -7,18 +7,18 @@ */ import BrowserFilter from 'components/BrowserFilter/BrowserFilter.react'; import BrowserMenu from 'components/BrowserMenu/BrowserMenu.react'; -import Icon from 'components/Icon/Icon.react'; import MenuItem from 'components/BrowserMenu/MenuItem.react'; -import prettyNumber from 'lib/prettyNumber'; -import React, { useRef } from 'react'; import Separator from 'components/BrowserMenu/Separator.react'; -import styles from 'dashboard/Data/Browser/Browser.scss'; -import Toolbar from 'components/Toolbar/Toolbar.react'; -import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react'; import ColumnsConfiguration from 'components/ColumnsConfiguration/ColumnsConfiguration.react'; -import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react'; -import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react'; +import Icon from 'components/Icon/Icon.react'; import Toggle from 'components/Toggle/Toggle.react'; +import Toolbar from 'components/Toolbar/Toolbar.react'; +import styles from 'dashboard/Data/Browser/Browser.scss'; +import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react'; +import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react'; +import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react'; +import prettyNumber from 'lib/prettyNumber'; +import React, { useRef } from 'react'; const BrowserToolbar = ({ className, @@ -82,6 +82,8 @@ const BrowserToolbar = ({ classwiseCloudFunctions, appId, appName, + scrollToTop, + toggleScrollToTop, }) => { const selectionLength = Object.keys(selection).length; const isPendingEditCloneRows = editCloneRows && editCloneRows.length > 0; @@ -424,7 +426,7 @@ const BrowserToolbar = ({