Merge pull request #1513 from ghalestrilo/feature/mobile-header-dropdown-menu

Add Dropdown Menu to the mobile IDE View
This commit is contained in:
ghalestrilo 2020-08-03 16:28:43 -03:00 committed by GitHub
commit 8c5c90ba50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 221 additions and 28 deletions

View file

@ -12,6 +12,7 @@ import Exit from '../images/exit.svg';
import DropdownArrow from '../images/down-filled-triangle.svg'; import DropdownArrow from '../images/down-filled-triangle.svg';
import Preferences from '../images/preferences.svg'; import Preferences from '../images/preferences.svg';
import Play from '../images/triangle-arrow-right.svg'; import Play from '../images/triangle-arrow-right.svg';
import More from '../images/more.svg';
import Code from '../images/code.svg'; import Code from '../images/code.svg';
import Terminal from '../images/terminal.svg'; import Terminal from '../images/terminal.svg';
@ -77,4 +78,6 @@ export const ExitIcon = withLabel(Exit);
export const DropdownArrowIcon = withLabel(DropdownArrow); export const DropdownArrowIcon = withLabel(DropdownArrow);
export const PreferencesIcon = withLabel(Preferences); export const PreferencesIcon = withLabel(Preferences);
export const PlayIcon = withLabel(Play); export const PlayIcon = withLabel(Play);
export const MoreIcon = withLabel(More);
export const TerminalIcon = withLabel(Terminal); export const TerminalIcon = withLabel(Terminal);
export const CodeIcon = withLabel(Code);

View file

@ -0,0 +1,92 @@
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;
right: ${props => (props.right ? 0 : 'initial')};
left: ${props => (props.left ? 0 : 'initial')};
${props => (props.align === 'right' && 'right: 0;')}
${props => (props.align === 'left' && 'left: 0;')}
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 && <Element aria-label={label} />;
const Dropdown = ({ items, align }) => (
<DropdownWrapper align={align} >
{/* className="nav__items-left" */}
{items && items.map(({ title, icon, href }) => (
<li key={`nav-${title && title.toLowerCase()}`}>
<Link to={href}>
{/* {MaybeIcon(icon, `Navigate to ${title}`)} */}
{title}
</Link>
</li>
))
}
</DropdownWrapper>
);
Dropdown.propTypes = {
align: PropTypes.oneOf(['left', 'right']),
items: PropTypes.arrayOf(PropTypes.shape({
action: PropTypes.func,
icon: PropTypes.func,
href: PropTypes.string
})),
};
Dropdown.defaultProps = {
items: [],
align: null
};
export default Dropdown;

View file

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';
const OverlayManager = ({ overlay, hideOverlay }) => {
// const [visible, trigger, setRef] = useModalBehavior();
const jsx = (
<React.Fragment>
{/* <div ref={setRef} >
{visible && <Dropdown items={headerNavOptions} />}
</div> */}
</React.Fragment>
);
return jsx && createPortal(jsx, document.body);
};
OverlayManager.propTypes = {
overlay: PropTypes.string,
hideOverlay: PropTypes.func.isRequired,
};
OverlayManager.defaultProps = { overlay: null };
export default OverlayManager;

View file

@ -31,7 +31,9 @@ const HeaderDiv = styled.div`
const IconContainer = styled.div` const IconContainer = styled.div`
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))}; margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
list-style: none;
display: flex; display: flex;
flex-direction: horizontal;
`; `;

View file

@ -0,0 +1,10 @@
import React from 'react';
import { useModalBehavior } from '../utils/custom-hooks';
export default (component) => {
const [visible, trigger, setRef] = useModalBehavior();
const wrapper = () => <div ref={setRef}> {visible && component} </div>; // eslint-disable-line
return [trigger, wrapper];
};

5
client/images/more.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.28004 13.76C10.5207 13.76 11.52 14.7324 11.52 16.0174C11.52 17.2676 10.5207 18.24 9.28004 18.24C8.03942 18.24 7.04004 17.2676 7.04004 16.0174C7.04004 14.7324 8.03942 13.76 9.28004 13.76Z" fill="#333333"/>
<path d="M18.24 16C18.24 14.7629 17.2371 13.76 16 13.76C14.7629 13.76 13.76 14.7629 13.76 16C13.76 17.2371 14.7629 18.24 16 18.24C17.2371 18.24 18.24 17.2371 18.24 16Z" fill="#333333"/>
<path d="M22.72 13.76C23.9606 13.76 24.96 14.705 24.96 15.965C24.96 17.295 23.9606 18.24 22.72 18.24C21.4794 18.24 20.48 17.295 20.48 15.965C20.48 14.74 21.4794 13.76 22.72 13.76Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View file

@ -34,7 +34,8 @@ class App extends React.Component {
render() { render() {
return ( return (
<div className="app"> <div className="app">
{this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />} {/* FIXME: Remove false */}
{false && this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />}
{this.props.children} {this.props.children}
</div> </div>
); );

View file

@ -88,8 +88,6 @@ const Console = () => {
const cm = useRef({}); const cm = useRef({});
useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; });
const consoleClass = classNames({ const consoleClass = classNames({
'preview-console': true, 'preview-console': true,
'preview-console--collapsed': !isExpanded 'preview-console--collapsed': !isExpanded

View file

@ -7,6 +7,7 @@ import styled from 'styled-components';
// Imports to be Refactored // Imports to be Refactored
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as FileActions from '../actions/files'; import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
@ -19,7 +20,7 @@ import { getHTMLFile } from '../reducers/files';
// Local Imports // Local Imports
import Editor from '../components/Editor'; 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 IconButton from '../../../components/mobile/IconButton';
import Header from '../../../components/mobile/Header'; import Header from '../../../components/mobile/Header';
@ -28,7 +29,11 @@ import Footer from '../../../components/mobile/Footer';
import IDEWrapper from '../../../components/mobile/IDEWrapper'; import IDEWrapper from '../../../components/mobile/IDEWrapper';
import Console from '../components/Console'; import Console from '../components/Console';
import { remSize } from '../../../theme'; import { remSize } from '../../../theme';
// import OverlayManager from '../../../components/OverlayManager';
import ActionStrip from '../../../components/mobile/ActionStrip'; import ActionStrip from '../../../components/mobile/ActionStrip';
import useAsModal from '../../../components/useAsModal';
import { PreferencesIcon } from '../../../common/icons';
import Dropdown from '../../../components/Dropdown';
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id); const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
@ -37,17 +42,30 @@ const Expander = styled.div`
height: ${props => (props.expanded ? remSize(160) : remSize(27))}; height: ${props => (props.expanded ? remSize(160) : remSize(27))};
`; `;
const NavItem = styled.li`
position: relative;
`;
const headerNavOptions = [
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/examples' },
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', },
];
const MobileIDEView = (props) => { const MobileIDEView = (props) => {
const { const {
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage, preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
selectedFile, updateFileContent, files, selectedFile, updateFileContent, files,
closeEditorOptions, showEditorOptions, showKeyboardShortcutModal, setUnsavedChanges, closeEditorOptions, showEditorOptions,
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console, startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
} = props; } = props;
const [tmController, setTmController] = useState(null); // eslint-disable-line const [tmController, setTmController] = useState(null); // eslint-disable-line
const [overlay, setOverlay] = useState(null); // eslint-disable-line
const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown align="right" items={headerNavOptions} />);
return ( return (
<Screen fullscreen> <Screen fullscreen>
@ -58,13 +76,17 @@ const MobileIDEView = (props) => {
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" /> <IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
} }
> >
<NavItem>
<IconButton <IconButton
to="/mobile/preferences" onClick={triggerNavDropdown}
onClick={() => setOverlay('preferences')} icon={MoreIcon}
icon={PreferencesIcon} aria-label="Options"
aria-label="Open preferences menu"
/> />
<NavDropDown />
</NavItem>
<li>
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" /> <IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
</li>
</Header> </Header>
<IDEWrapper> <IDEWrapper>
@ -82,9 +104,7 @@ const MobileIDEView = (props) => {
editorOptionsVisible={ide.editorOptionsVisible} editorOptionsVisible={ide.editorOptionsVisible}
showEditorOptions={showEditorOptions} showEditorOptions={showEditorOptions}
closeEditorOptions={closeEditorOptions} closeEditorOptions={closeEditorOptions}
showKeyboardShortcutModal={showKeyboardShortcutModal} showKeyboard={ide.isPlaying}
setUnsavedChanges={setUnsavedChanges}
isPlaying={ide.isPlaying}
theme={preferences.theme} theme={preferences.theme}
startRefreshSketch={startRefreshSketch} startRefreshSketch={startRefreshSketch}
stopSketch={stopSketch} stopSketch={stopSketch}
@ -103,6 +123,7 @@ const MobileIDEView = (props) => {
provideController={setTmController} provideController={setTmController}
/> />
</IDEWrapper> </IDEWrapper>
<Footer> <Footer>
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>} {ide.consoleIsExpanded && <Expander expanded><Console /></Expander>}
<ActionStrip /> <ActionStrip />
@ -111,7 +132,6 @@ const MobileIDEView = (props) => {
); );
}; };
MobileIDEView.propTypes = { MobileIDEView.propTypes = {
preferences: PropTypes.shape({ preferences: PropTypes.shape({
@ -130,7 +150,7 @@ MobileIDEView.propTypes = {
ide: PropTypes.shape({ ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
isAccessibleOutputPlaying: PropTypes.bool.isRequired, isAccessibleOutputPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.array, consoleEvent: PropTypes.arrayOf(PropTypes.shape({})),
modalIsVisible: PropTypes.bool.isRequired, modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired, consoleIsExpanded: PropTypes.bool.isRequired,
@ -156,7 +176,7 @@ MobileIDEView.propTypes = {
}).isRequired, }).isRequired,
editorAccessibility: PropTypes.shape({ editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.array.isRequired, lintMessages: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
}).isRequired, }).isRequired,
project: PropTypes.shape({ project: PropTypes.shape({
@ -193,10 +213,6 @@ MobileIDEView.propTypes = {
showEditorOptions: PropTypes.func.isRequired, showEditorOptions: PropTypes.func.isRequired,
showKeyboardShortcutModal: PropTypes.func.isRequired,
setUnsavedChanges: PropTypes.func.isRequired,
startRefreshSketch: PropTypes.func.isRequired, startRefreshSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect, useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import Header from '../../components/mobile/Header'; import Header from '../../components/mobile/Header';
import IconButton from '../../components/mobile/IconButton'; import IconButton from '../../components/mobile/IconButton';
@ -25,7 +24,7 @@ const Content = styled.div`
margin-top: ${remSize(68)}; margin-top: ${remSize(68)};
`; `;
const MobileSketchView = (props) => { const MobileSketchView = () => {
const { files, ide, preferences } = useSelector(state => state); const { files, ide, preferences } = useSelector(state => state);
const htmlFile = useSelector(state => getHTMLFile(state.files)); const htmlFile = useSelector(state => getHTMLFile(state.files));

View file

@ -41,7 +41,8 @@ export const grays = {
}; };
export const common = { export const common = {
baseFontSize: 12 baseFontSize: 12,
shadowColor: 'rgba(0, 0, 0, 0.16)'
}; };
export const remSize = size => `${size / common.baseFontSize}rem`; export const remSize = size => `${size / common.baseFontSize}rem`;
@ -97,6 +98,10 @@ export default {
border: grays.middleLight, border: grays.middleLight,
}, },
}, },
Modal: {
background: grays.light,
border: grays.middleLight
},
Separator: grays.middleLight, Separator: grays.middleLight,
}, },
[Theme.dark]: { [Theme.dark]: {
@ -138,6 +143,10 @@ export default {
border: grays.middleDark, border: grays.middleDark,
}, },
}, },
Modal: {
background: grays.dark,
border: grays.middleDark
},
Separator: grays.middleDark, Separator: grays.middleDark,
}, },
[Theme.contrast]: { [Theme.contrast]: {
@ -179,6 +188,10 @@ export default {
border: grays.middleDark, border: grays.middleDark,
}, },
}, },
Modal: {
background: grays.dark,
border: grays.middleDark
},
Separator: grays.middleDark, Separator: grays.middleDark,
}, },
}; };

View file

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
export const noop = () => {}; export const noop = () => {};
@ -13,3 +13,30 @@ export const useDidUpdate = (callback, deps) => {
} }
}, deps); }, deps);
}; };
// Usage: const ref = useModalBehavior(() => setSomeState(false))
// place this ref on a component
export const useModalBehavior = (hideOverlay) => {
const ref = useRef({});
// Return values
const setRef = (r) => { ref.current = r; };
const [visible, setVisible] = useState(false);
const trigger = () => setVisible(!visible);
const hide = () => setVisible(false);
const handleClickOutside = ({ target }) => {
if (ref && ref.current && !ref.current.contains(target)) {
hide();
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [ref]);
return [visible, trigger, setRef];
};