diff --git a/client/common/icons.jsx b/client/common/icons.jsx index 624f9f31..215083a6 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -12,6 +12,7 @@ import Exit from '../images/exit.svg'; import DropdownArrow from '../images/down-filled-triangle.svg'; import Preferences from '../images/preferences.svg'; import Play from '../images/triangle-arrow-right.svg'; +import More from '../images/more.svg'; import Code from '../images/code.svg'; import Terminal from '../images/terminal.svg'; @@ -77,4 +78,6 @@ export const ExitIcon = withLabel(Exit); export const DropdownArrowIcon = withLabel(DropdownArrow); export const PreferencesIcon = withLabel(Preferences); export const PlayIcon = withLabel(Play); +export const MoreIcon = withLabel(More); export const TerminalIcon = withLabel(Terminal); +export const CodeIcon = withLabel(Code); diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx new file mode 100644 index 00000000..48490f9c --- /dev/null +++ b/client/components/Dropdown.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router'; +import styled from 'styled-components'; +import { remSize, prop, common } from '../theme'; +import IconButton from './mobile/IconButton'; +import Button from '../common/Button'; + +const DropdownWrapper = styled.ul` + background-color: ${prop('Modal.background')}; + border: 1px solid ${prop('Modal.border')}; + box-shadow: 0 0 18px 0 ${prop('shadowColor')}; + color: ${prop('primaryTextColor')}; + + position: absolute; + top: ${remSize(64)}; + right: ${remSize(16)}; + + text-align: left; + width: ${remSize(180)}; + display: flex; + flex-direction: column; + height: auto; + z-index: 9999; + border-radius: ${remSize(6)}; + + & li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; } + & li:last-child { border-radius: 0 0 ${remSize(5)} ${remSize(5)}; } + + & li:hover { + + background-color: ${prop('Button.hover.background')}; + color: ${prop('Button.hover.foreground')}; + + & button, & a { + color: ${prop('Button.hover.foreground')}; + } + } + + li { + height: ${remSize(36)}; + cursor: pointer; + display: flex; + align-items: center; + + & button, + & a { + color: ${prop('primaryTextColor')}; + width: 100%; + text-align: left; + padding: ${remSize(8)} ${remSize(16)}; + } + } +`; + +// TODO: Add Icon to the left of the items in the menu +// const MaybeIcon = (Element, label) => Element && ; + +const Dropdown = ({ items }) => ( + + {/* className="nav__items-left" */} + {items && items.map(({ title, icon, href }) => ( +
  • + + {/* {MaybeIcon(icon, `Navigate to ${title}`)} */} + {title} + +
  • + )) + } +
    +); + +Dropdown.propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + action: PropTypes.func, + icon: PropTypes.func, + href: PropTypes.string + })), +}; + +Dropdown.defaultProps = { + items: [], +}; + +export default Dropdown; diff --git a/client/components/OverlayManager.jsx b/client/components/OverlayManager.jsx new file mode 100644 index 00000000..53025a7e --- /dev/null +++ b/client/components/OverlayManager.jsx @@ -0,0 +1,57 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; + +import Dropdown from './Dropdown'; + +import { PreferencesIcon } from '../common/icons'; + +const OverlayManager = ({ overlay, hideOverlay }) => { + const ref = useRef({}); + + const handleClickOutside = ({ target }) => { + if (ref && ref.current && !ref.current.contains(target)) { + hideOverlay(); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [ref]); + + const headerNavOptions = [ + { + icon: PreferencesIcon, + title: 'Preferences', + href: '/mobile/preferences', + }, + { icon: PreferencesIcon, title: 'Examples', href: '/mobile/examples' }, + { + icon: PreferencesIcon, + title: 'Original Editor', + href: '/mobile/preferences', + }, + ]; + + const jsx = ( + +
    { ref.current = r; }} > + {overlay === 'dropdown' && } +
    +
    + ); + + return jsx && createPortal(jsx, document.body); +}; + + +OverlayManager.propTypes = { + overlay: PropTypes.string, + hideOverlay: PropTypes.func.isRequired, +}; + +OverlayManager.defaultProps = { overlay: null }; + +export default OverlayManager; diff --git a/client/images/more.svg b/client/images/more.svg new file mode 100644 index 00000000..ee41de95 --- /dev/null +++ b/client/images/more.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index ddb6d55e..749caaaa 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -88,8 +88,6 @@ const Console = () => { const cm = useRef({}); - useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; }); - const consoleClass = classNames({ 'preview-console': true, 'preview-console--collapsed': !isExpanded diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index 3a5c102a..eb0740e1 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -54,7 +54,7 @@ class NewFileForm extends React.Component { NewFileForm.propTypes = { fields: PropTypes.shape({ - name: PropTypes.object.isRequired, // eslint-disable-line + name: PropTypes.object.isRequired }).isRequired, handleSubmit: PropTypes.func.isRequired, createFile: PropTypes.func.isRequired, diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 2da6ccb1..04c590fe 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -53,7 +53,7 @@ class NewFolderForm extends React.Component { NewFolderForm.propTypes = { fields: PropTypes.shape({ - name: PropTypes.object.isRequired, // eslint-disable-line + name: PropTypes.object.isRequired }).isRequired, handleSubmit: PropTypes.func.isRequired, createFolder: PropTypes.func.isRequired, diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index e024e193..7a174e89 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -7,6 +7,7 @@ import styled from 'styled-components'; // Imports to be Refactored import { bindActionCreators } from 'redux'; + import * as FileActions from '../actions/files'; import * as IDEActions from '../actions/ide'; import * as ProjectActions from '../actions/project'; @@ -19,7 +20,7 @@ import { getHTMLFile } from '../reducers/files'; // Local Imports import Editor from '../components/Editor'; -import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons'; +import { PlayIcon, ExitIcon, MoreIcon } from '../../../common/icons'; import IconButton from '../../../components/mobile/IconButton'; import Header from '../../../components/mobile/Header'; @@ -28,6 +29,7 @@ import Footer from '../../../components/mobile/Footer'; import IDEWrapper from '../../../components/mobile/IDEWrapper'; import Console from '../components/Console'; import { remSize } from '../../../theme'; +import OverlayManager from '../../../components/OverlayManager'; import ActionStrip from '../../../components/mobile/ActionStrip'; const isUserOwner = ({ project, user }) => @@ -37,6 +39,7 @@ const Expander = styled.div` height: ${props => (props.expanded ? remSize(160) : remSize(27))}; `; + const MobileIDEView = (props) => { const { preferences, @@ -64,7 +67,11 @@ const MobileIDEView = (props) => { } = props; const [tmController, setTmController] = useState(null); // eslint-disable-line - const [overlay, setOverlay] = useState(null); // eslint-disable-line + const [overlayName, setOverlay] = useState(null); // eslint-disable-line + + // TODO: Move this to OverlayController (?) + const hideOverlay = () => setOverlay(null); + // const overlayRef = useRef({}); return ( @@ -80,10 +87,9 @@ const MobileIDEView = (props) => { } > setOverlay('preferences')} - icon={PreferencesIcon} - aria-label="Open preferences menu" + onClick={() => setOverlay('dropdown')} + icon={MoreIcon} + aria-label="Options" /> { provideController={setTmController} /> +
    {ide.consoleIsExpanded && ( @@ -139,6 +146,12 @@ const MobileIDEView = (props) => { )}
    + +
    ); }; @@ -160,7 +173,7 @@ MobileIDEView.propTypes = { ide: PropTypes.shape({ isPlaying: PropTypes.bool.isRequired, isAccessibleOutputPlaying: PropTypes.bool.isRequired, - consoleEvent: PropTypes.array, // eslint-disable-line + consoleEvent: PropTypes.array, modalIsVisible: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired, consoleIsExpanded: PropTypes.bool.isRequired, @@ -186,7 +199,7 @@ MobileIDEView.propTypes = { }).isRequired, editorAccessibility: PropTypes.shape({ - lintMessages: PropTypes.array.isRequired, // eslint-disable-line + lintMessages: PropTypes.array.isRequired, }).isRequired, project: PropTypes.shape({ diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index 2b97c2ff..dc48fe73 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -46,7 +46,7 @@ function ResetPasswordForm(props) { ResetPasswordForm.propTypes = { fields: PropTypes.shape({ - email: PropTypes.object.isRequired, // eslint-disable-line + email: PropTypes.object.isRequired }).isRequired, handleSubmit: PropTypes.func.isRequired, initiateResetPassword: PropTypes.func.isRequired, diff --git a/client/theme.js b/client/theme.js index 45f2772e..25688d3a 100644 --- a/client/theme.js +++ b/client/theme.js @@ -41,7 +41,8 @@ export const grays = { }; export const common = { - baseFontSize: 12 + baseFontSize: 12, + shadowColor: 'rgba(0, 0, 0, 0.16)' }; export const remSize = size => `${size / common.baseFontSize}rem`; @@ -97,6 +98,10 @@ export default { border: grays.middleLight, }, }, + Modal: { + background: grays.light, + border: grays.middleLight + }, Separator: grays.middleLight, }, [Theme.dark]: { @@ -138,6 +143,10 @@ export default { border: grays.middleDark, }, }, + Modal: { + background: grays.dark, + border: grays.middleDark + }, Separator: grays.middleDark, }, [Theme.contrast]: { @@ -179,6 +188,10 @@ export default { border: grays.middleDark, }, }, + Modal: { + background: grays.dark, + border: grays.middleDark + }, Separator: grays.middleDark, }, };