Refactored side menu to a separate component.
This commit is contained in:
@@ -1,5 +1,128 @@
|
||||
describe("SideMenu.cy.tsx", () => {
|
||||
it("playground", () => {
|
||||
// cy.mount()
|
||||
import { useState } from "react";
|
||||
import { LeftMenu, RightMenu } from "../../src/components/SideMenu";
|
||||
|
||||
// Test Fixtures
|
||||
function newTestApp(position: "left" | "right") {
|
||||
const Menu = position === "left" ? LeftMenu : RightMenu;
|
||||
|
||||
return function TestApp() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy="app"
|
||||
className="appContainer"
|
||||
style={{ overflowX: "hidden" }}
|
||||
>
|
||||
<button
|
||||
data-cy="toggle-menu"
|
||||
onClick={() => setIsMenuOpen((prev) => !prev)}
|
||||
>
|
||||
Open Menu
|
||||
</button>
|
||||
<Menu isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)}>
|
||||
Menu Contents
|
||||
</Menu>
|
||||
<div data-cy="main-content">Main Content</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Tests
|
||||
function menuTest(position: "left" | "right") {
|
||||
const isLeft = position === "left";
|
||||
|
||||
// 1. Check initial state
|
||||
cy.dataCy(`${position}-menu`)
|
||||
.as("menu")
|
||||
.should("not.be.visible")
|
||||
|
||||
.contains("Menu Contents")
|
||||
.should("not.be.visible");
|
||||
|
||||
// 2. Open menu
|
||||
cy.dataCy("toggle-menu")
|
||||
.as("toggle")
|
||||
.click()
|
||||
|
||||
.get("@menu")
|
||||
.should("be.visible")
|
||||
|
||||
.contains("Menu Contents")
|
||||
.should("be.visible");
|
||||
|
||||
// 3. Close menu with button
|
||||
cy.dataCy("close-menu")
|
||||
.as("close")
|
||||
.click()
|
||||
|
||||
.get("@menu")
|
||||
.should("not.be.visible")
|
||||
|
||||
.contains("Menu Contents")
|
||||
.should("not.be.visible");
|
||||
|
||||
// 4. Reopen menu, verify interactivity, then close with underlay
|
||||
cy.get("@toggle")
|
||||
.click()
|
||||
|
||||
.get("@menu")
|
||||
.should("be.visible")
|
||||
.click("center")
|
||||
|
||||
.get("@menu")
|
||||
.should("be.visible")
|
||||
|
||||
.dataCy("app")
|
||||
.click(isLeft ? "topRight" : "topLeft")
|
||||
|
||||
.get("@menu")
|
||||
.should("not.be.visible");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.disableTransitions();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.enableTransitions();
|
||||
});
|
||||
|
||||
describe("LeftMenu Tests", () => {
|
||||
beforeEach(() => {
|
||||
const TestApp = newTestApp("left");
|
||||
cy.mount(<TestApp />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.enableTransitions();
|
||||
});
|
||||
|
||||
it("works in portrait", () => {
|
||||
cy.viewport("iphone-6");
|
||||
menuTest("left");
|
||||
});
|
||||
|
||||
it("works in landscape", () => {
|
||||
cy.viewport("iphone-6", "landscape");
|
||||
menuTest("left");
|
||||
});
|
||||
});
|
||||
|
||||
describe("RightMenu Tests", () => {
|
||||
beforeEach(() => {
|
||||
const TestApp = newTestApp("right");
|
||||
cy.mount(<TestApp />);
|
||||
});
|
||||
|
||||
it("works in portrait", () => {
|
||||
cy.viewport("iphone-6");
|
||||
menuTest("right");
|
||||
});
|
||||
|
||||
it("works in landscape", () => {
|
||||
cy.viewport("iphone-6", "landscape");
|
||||
menuTest("right");
|
||||
});
|
||||
});
|
||||
|
||||
+3
-115
@@ -4,53 +4,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leftMobileMenu {
|
||||
height: 100vh;
|
||||
width: 0;
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: white;
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s ease-out;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.leftMenuWrapper {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.leftMobileMenu .topNav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.leftMenuWrapper .closeButton {
|
||||
margin: 10px 10px 10px auto;
|
||||
}
|
||||
|
||||
.rightMobileMenu {
|
||||
height: 100vh;
|
||||
width: 0;
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s ease-out;
|
||||
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.rightMenuWrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Large - Landscape Tablets / Desktops */
|
||||
/* Medium - Portrait Tablets */
|
||||
/* Horizontal layout, vertically scrolling picker and palette content */
|
||||
/* Large */
|
||||
@media (min-width: 992px),
|
||||
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
|
||||
.appContainer {
|
||||
@@ -87,8 +41,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium - Landscape Phones */
|
||||
/* Horizontal layout, side menu, vertical tabbed picker */
|
||||
/* Landscape Phone */
|
||||
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
|
||||
.appContainer {
|
||||
}
|
||||
@@ -116,38 +69,6 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.leftMobileMenu {
|
||||
}
|
||||
|
||||
.leftMobileMenu.open {
|
||||
width: 80vh;
|
||||
}
|
||||
|
||||
.leftMenuWrapper {
|
||||
width: 80vh;
|
||||
}
|
||||
|
||||
.leftMobileMenu .topNav {
|
||||
}
|
||||
|
||||
.leftMenuWrapper .closeButton {
|
||||
}
|
||||
|
||||
.rightMobileMenu {
|
||||
}
|
||||
|
||||
.rightMobileMenu.open {
|
||||
width: 80vh;
|
||||
}
|
||||
|
||||
.rightMenuWrapper {
|
||||
width: 80vh;
|
||||
}
|
||||
|
||||
.rightMobileMenu .closeButton {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tabbedContainer {
|
||||
max-height: 100vh;
|
||||
overflow-y: scroll;
|
||||
@@ -172,8 +93,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Small - Portrait Phones*/
|
||||
/* Vertical layout, side menu, horizontal tabbed picker */
|
||||
/* Portrait Phone */
|
||||
@media (max-width: 567px) {
|
||||
.appContainer {
|
||||
}
|
||||
@@ -205,38 +125,6 @@
|
||||
.mobileBetaZone {
|
||||
}
|
||||
|
||||
.leftMobileMenu {
|
||||
}
|
||||
|
||||
.leftMobileMenu.open {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.leftMenuWrapper {
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.leftMobileMenu .topNav {
|
||||
}
|
||||
|
||||
.leftMenuWrapper .closeButton {
|
||||
}
|
||||
|
||||
.rightMobileMenu {
|
||||
}
|
||||
|
||||
.rightMobileMenu.open {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.rightMenuWrapper {
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.rightMobileMenu .closeButton {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tabbedContainer {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
|
||||
+20
-29
@@ -2,14 +2,15 @@ import { useState } from "react";
|
||||
import styles from "./App.module.css";
|
||||
import ColorPicker from "./components/ColorPicker/ColorPicker";
|
||||
import ColorValues from "./components/ColorValues/ColorValues";
|
||||
import { LeftMenu, RightMenu } from "./components/SideMenu";
|
||||
import clsx from "clsx";
|
||||
|
||||
function App() {
|
||||
const [rightMenuOpen, setRightMenuOpen] = useState(false);
|
||||
const [leftMenuOpen, setLeftMenuOpen] = useState(false);
|
||||
const [isRightMenuOpen, setIsRightMenuOpen] = useState(false);
|
||||
const [isLeftMenuOpen, setIsLeftMenuOpen] = useState(false);
|
||||
|
||||
const toggleRightMenu = () => setRightMenuOpen(!rightMenuOpen);
|
||||
const toggleLeftMenu = () => setLeftMenuOpen(!leftMenuOpen);
|
||||
const toggleRightMenu = () => setIsRightMenuOpen(!isRightMenuOpen);
|
||||
const toggleLeftMenu = () => setIsLeftMenuOpen(!isLeftMenuOpen);
|
||||
|
||||
const RightMenuButton = () => (
|
||||
<button className={styles.rightMenuButton} onClick={toggleRightMenu}>
|
||||
@@ -29,9 +30,11 @@ function App() {
|
||||
<LeftMenuButton />
|
||||
<RightMenuButton />
|
||||
</div>
|
||||
|
||||
<div className={styles.mobileLeftNav}>
|
||||
<LeftMenuButton />
|
||||
</div>
|
||||
|
||||
<div className={styles.mobileAlphaZone}>
|
||||
<div className={styles.tabbedContainer}>
|
||||
<div className={clsx(styles.tab, styles.colorPickerContainer)}>
|
||||
@@ -42,42 +45,28 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.mobileBetaZone}>
|
||||
<div className={styles.paletteEditorContainer}></div>
|
||||
</div>
|
||||
|
||||
<div className={styles.mobileRightNav}>
|
||||
<RightMenuButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(styles.leftMobileMenu, {
|
||||
[styles.open]: leftMenuOpen,
|
||||
})}
|
||||
<LeftMenu
|
||||
isOpen={isLeftMenuOpen}
|
||||
onClose={() => setIsLeftMenuOpen(false)}
|
||||
>
|
||||
<div className={styles.leftMenuWrapper}>
|
||||
<div className={styles.topNav}>
|
||||
<button className={styles.closeButton} onClick={toggleLeftMenu}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.paletteLibraryContainer}>User Info</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>User Info</div>
|
||||
</LeftMenu>
|
||||
|
||||
<div
|
||||
className={clsx(styles.rightMobileMenu, {
|
||||
[styles.open]: rightMenuOpen,
|
||||
})}
|
||||
<RightMenu
|
||||
isOpen={isRightMenuOpen}
|
||||
onClose={() => setIsRightMenuOpen(false)}
|
||||
>
|
||||
<div className={styles.rightMenuWrapper}>
|
||||
<div className={styles.topNav}>
|
||||
<button className={styles.closeButton} onClick={toggleRightMenu}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.paletteLibraryContainer}>Palette Library</div>
|
||||
</div>
|
||||
</RightMenu>
|
||||
</div>
|
||||
|
||||
<div className={styles.mainContent}>
|
||||
@@ -85,10 +74,12 @@ function App() {
|
||||
<div className={styles.colorPickerContainer}>
|
||||
<ColorPicker />
|
||||
</div>
|
||||
|
||||
<div className={styles.colorValuesContainer}>
|
||||
<ColorValues />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.betaZone}>
|
||||
<div className={styles.paletteEditorContainer}></div>
|
||||
<div className={styles.paletteLibraryContainer}></div>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
.sideMenu {
|
||||
height: 100vh;
|
||||
width: 0;
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
background: white;
|
||||
overflow-x: hidden;
|
||||
transition: 0.3s ease-out;
|
||||
}
|
||||
|
||||
.sideMenuUnderlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
transition: background-color 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sideMenuUnderlay.open {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.left.sideMenu {
|
||||
left: 0;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.right.sideMenu {
|
||||
right: 0;
|
||||
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.menuWrapper {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.left.menuWrapper {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.right.menuWrapper {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.topNav {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.right.sideMenu .closeButton {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.left.sideMenu .closeButton {
|
||||
margin: 10px 10px 10px auto;
|
||||
}
|
||||
|
||||
/* Landscape Phone */
|
||||
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
|
||||
.left.sideMenu.open,
|
||||
.left.menuWrapper {
|
||||
width: 80vh;
|
||||
}
|
||||
|
||||
.right.sideMenu.open,
|
||||
.right.menuWrapper {
|
||||
width: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Portrait Phone */
|
||||
@media (max-width: 567px) {
|
||||
.left.sideMenu.open,
|
||||
.left.menuWrapper {
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.right.sideMenu.open,
|
||||
.right.menuWrapper {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./SideMenu.module.css";
|
||||
|
||||
interface SideMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface BaseMenuProps extends SideMenuProps {
|
||||
position: "left" | "right";
|
||||
}
|
||||
|
||||
function BaseMenu({ isOpen, onClose, children, position }: BaseMenuProps) {
|
||||
const isLeftMenu = position === "left";
|
||||
|
||||
const handleUnderlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.sideMenuUnderlay, { [styles.open]: isOpen })}
|
||||
onClick={handleUnderlayClick}
|
||||
>
|
||||
<div
|
||||
data-cy={`${position}-menu`}
|
||||
className={clsx(
|
||||
styles.sideMenu,
|
||||
isLeftMenu ? styles.left : styles.right,
|
||||
{ [styles.open]: isOpen },
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.menuWrapper,
|
||||
isLeftMenu ? styles.left : styles.right,
|
||||
)}
|
||||
>
|
||||
<div className={styles.topNav}>
|
||||
<button
|
||||
data-cy="close-menu"
|
||||
className={styles.closeButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function newSideMenu(position: "left" | "right") {
|
||||
return function SideMenu({ isOpen, onClose, children }: SideMenuProps) {
|
||||
return (
|
||||
<BaseMenu isOpen={isOpen} onClose={onClose} position={position}>
|
||||
{children}
|
||||
</BaseMenu>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const LeftMenu = newSideMenu("left");
|
||||
const RightMenu = newSideMenu("right");
|
||||
|
||||
export { LeftMenu, RightMenu };
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LeftMenu, RightMenu } from "./SideMenu";
|
||||
export { LeftMenu, RightMenu };
|
||||
+6
-7
@@ -1,11 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user