🔀 merge from feature/mobile-header-dropdown-menu

This commit is contained in:
ghalestrilo 2020-07-29 18:19:18 -03:00
commit c019fbcf4c
8 changed files with 85 additions and 80 deletions

View File

@ -13,8 +13,9 @@ const DropdownWrapper = styled.ul`
color: ${prop('primaryTextColor')}; color: ${prop('primaryTextColor')};
position: absolute; position: absolute;
top: ${remSize(64)}; right: ${props => (props.right ? 0 : 'initial')};
right: ${remSize(16)}; left: ${props => (props.left ? 0 : 'initial')};
text-align: left; text-align: left;
width: ${remSize(180)}; width: ${remSize(180)};
@ -56,8 +57,8 @@ const DropdownWrapper = styled.ul`
// TODO: Add Icon to the left of the items in the menu // TODO: Add Icon to the left of the items in the menu
// const MaybeIcon = (Element, label) => Element && <Element aria-label={label} />; // const MaybeIcon = (Element, label) => Element && <Element aria-label={label} />;
const Dropdown = ({ items }) => ( const Dropdown = ({ items, right, left }) => (
<DropdownWrapper> <DropdownWrapper right={right} left={left}>
{/* className="nav__items-left" */} {/* className="nav__items-left" */}
{items && items.map(({ title, icon, href }) => ( {items && items.map(({ title, icon, href }) => (
<li key={`nav-${title && title.toLowerCase()}`}> <li key={`nav-${title && title.toLowerCase()}`}>
@ -72,6 +73,8 @@ const Dropdown = ({ items }) => (
); );
Dropdown.propTypes = { Dropdown.propTypes = {
right: PropTypes.bool,
left: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.shape({ items: PropTypes.arrayOf(PropTypes.shape({
action: PropTypes.func, action: PropTypes.func,
icon: PropTypes.func, icon: PropTypes.func,
@ -81,6 +84,8 @@ Dropdown.propTypes = {
Dropdown.defaultProps = { Dropdown.defaultProps = {
items: [], items: [],
right: false,
left: false,
}; };
export default Dropdown; export default Dropdown;

View File

@ -1,45 +1,15 @@
import React, { useRef, useEffect } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import Dropdown from './Dropdown';
import { PreferencesIcon } from '../common/icons';
const OverlayManager = ({ overlay, hideOverlay }) => { const OverlayManager = ({ overlay, hideOverlay }) => {
const ref = useRef({}); // const [visible, trigger, setRef] = useModalBehavior();
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 = ( const jsx = (
<React.Fragment> <React.Fragment>
<div ref={(r) => { ref.current = r; }} > {/* <div ref={setRef} >
{overlay === 'dropdown' && <Dropdown items={headerNavOptions} />} {visible && <Dropdown items={headerNavOptions} />}
</div> </div> */}
</React.Fragment> </React.Fragment>
); );

View File

@ -37,7 +37,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

@ -23,20 +23,13 @@ class Searchbar extends React.Component {
}); });
} }
handleSearchEnter = (e) => {
if (e.key === 'Enter') {
this.searchChange();
}
}
searchChange = () => { searchChange = () => {
if (this.state.searchValue.trim().length === 0) return;
this.props.setSearchTerm(this.state.searchValue.trim()); this.props.setSearchTerm(this.state.searchValue.trim());
}; };
handleSearchChange = (e) => { handleSearchChange = (e) => {
this.setState({ searchValue: e.target.value }, () => { this.setState({ searchValue: e.target.value }, () => {
this.throttledSearchChange(this.state.searchValue); this.throttledSearchChange(this.state.searchValue.trim());
}); });
} }
@ -53,7 +46,6 @@ class Searchbar extends React.Component {
value={searchValue} value={searchValue}
placeholder={this.props.searchLabel} placeholder={this.props.searchLabel}
onChange={this.handleSearchChange} onChange={this.handleSearchChange}
onKeyUp={this.handleSearchEnter}
/> />
<button <button
className="searchbar__clear-button" className="searchbar__clear-button"

View File

@ -29,8 +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 OverlayManager from '../../../components/OverlayManager';
import ActionStrip from '../../../components/mobile/ActionStrip'; import ActionStrip from '../../../components/mobile/ActionStrip';
import { useAsModal } from '../../../utils/custom-hooks';
import { PreferencesIcon } from '../../../common/icons';
import Dropdown from '../../../components/Dropdown';
const isUserOwner = ({ project, user }) => const isUserOwner = ({ project, user }) =>
project.owner && project.owner.id === user.id; project.owner && project.owner.id === user.id;
@ -39,6 +42,16 @@ 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 {
@ -67,11 +80,9 @@ const MobileIDEView = (props) => {
} = props; } = props;
const [tmController, setTmController] = useState(null); // eslint-disable-line const [tmController, setTmController] = useState(null); // eslint-disable-line
const [overlayName, setOverlay] = useState(null); // eslint-disable-line
// TODO: Move this to OverlayController (?)
const hideOverlay = () => setOverlay(null); const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown right items={headerNavOptions} />);
// const overlayRef = useRef({});
return ( return (
<Screen fullscreen> <Screen fullscreen>
@ -86,19 +97,17 @@ const MobileIDEView = (props) => {
/> />
} }
> >
<IconButton <NavItem>
onClick={() => setOverlay('dropdown')} <IconButton
icon={MoreIcon} onClick={triggerNavDropdown}
aria-label="Options" icon={MoreIcon}
/> aria-label="Options"
<IconButton />
to="/mobile/preview" <NavDropDown />
onClick={() => { </NavItem>
startSketch(); <li>
}} <IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
icon={PlayIcon} </li>
aria-label="Run sketch"
/>
</Header> </Header>
<IDEWrapper> <IDEWrapper>
@ -116,9 +125,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}
@ -146,12 +153,6 @@ const MobileIDEView = (props) => {
)} )}
<ActionStrip /> <ActionStrip />
</Footer> </Footer>
<OverlayManager
// ref={overlayRef}
overlay={overlayName}
hideOverlay={hideOverlay}
/>
</Screen> </Screen>
); );
}; };

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';
@ -21,7 +20,7 @@ import { remSize } from '../../theme';
import Footer from '../../components/mobile/Footer'; import Footer from '../../components/mobile/Footer';
import Content from './MobileViewContent'; import Content from './MobileViewContent';
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

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useMemo, useState } from 'react';
export const noop = () => {}; export const noop = () => {};
@ -13,3 +13,39 @@ 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(true);
const trigger = () => setVisible(true);
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];
};
// TODO: This is HOC, not a hook. Where do we put it?
export const useAsModal = (component) => {
const [visible, trigger, setRef] = useModalBehavior();
const wrapper = () => (<div ref={setRef}> {visible && component} </div>); // eslint-disable-line
return [trigger, wrapper];
};

View File

@ -25,7 +25,7 @@ _Note_: The installation steps assume you are using a Unix-like shell. If you ar
* For Mac OSX with [homebrew](http://brew.sh/): `brew tap mongodb/brew` then `brew install mongodb-community` and finally start the server with `brew services start mongodb-community` or you can visit the installation guide here [Installation Guide For MacOS](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/) * For Mac OSX with [homebrew](http://brew.sh/): `brew tap mongodb/brew` then `brew install mongodb-community` and finally start the server with `brew services start mongodb-community` or you can visit the installation guide here [Installation Guide For MacOS](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/)
* For Windows and Linux: [MongoDB Installation](https://docs.mongodb.com/manual/installation/) * For Windows and Linux: [MongoDB Installation](https://docs.mongodb.com/manual/installation/)
7. `$ cp .env.example .env` 7. `$ cp .env.example .env`
8. (Optional) Update `.env` with necessary keys to enable certain app behavoirs, i.e. add Github ID and Github Secret if you want to be able to log in with Github. 8. (Optional) Update `.env` with necessary keys to enable certain app behaviors, i.e. add Github ID and Github Secret if you want to be able to log in with Github.
9. `$ npm run fetch-examples` - this downloads the example sketches into a user called 'p5' 9. `$ npm run fetch-examples` - this downloads the example sketches into a user called 'p5'
10. `$ npm start` 10. `$ npm start`
11. Navigate to [http://localhost:8000](http://localhost:8000) in your browser 11. Navigate to [http://localhost:8000](http://localhost:8000) in your browser