diff --git a/cypress/component/SideMenu.cy.tsx b/cypress/component/SideMenu.cy.tsx index 396382b..fa173ed 100644 --- a/cypress/component/SideMenu.cy.tsx +++ b/cypress/component/SideMenu.cy.tsx @@ -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 ( +
+ + setIsMenuOpen(false)}> + Menu Contents + +
Main Content
+
+ ); + }; +} + +// 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(); + }); + + 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(); + }); + + it("works in portrait", () => { + cy.viewport("iphone-6"); + menuTest("right"); + }); + + it("works in landscape", () => { + cy.viewport("iphone-6", "landscape"); + menuTest("right"); }); }); diff --git a/src/App.module.css b/src/App.module.css index 5e5331a..ce4b11a 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -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; diff --git a/src/App.tsx b/src/App.tsx index a6e6f3e..f8df026 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( - -
User Info
- - + setIsLeftMenuOpen(false)} + > +
User Info
+
-
-
-
- -
+ setIsRightMenuOpen(false)} + >
Palette Library
-
+
@@ -85,10 +74,12 @@ function App() {
+
+
diff --git a/src/components/SideMenu/SideMenu.module.css b/src/components/SideMenu/SideMenu.module.css new file mode 100644 index 0000000..bb0cb1d --- /dev/null +++ b/src/components/SideMenu/SideMenu.module.css @@ -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; + } +} diff --git a/src/components/SideMenu/SideMenu.tsx b/src/components/SideMenu/SideMenu.tsx new file mode 100644 index 0000000..7b0802d --- /dev/null +++ b/src/components/SideMenu/SideMenu.tsx @@ -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) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + return ( +
+
e.stopPropagation()} + > +
+
+ +
+ {children} +
+
+
+ ); +} + +function newSideMenu(position: "left" | "right") { + return function SideMenu({ isOpen, onClose, children }: SideMenuProps) { + return ( + + {children} + + ); + }; +} + +const LeftMenu = newSideMenu("left"); +const RightMenu = newSideMenu("right"); + +export { LeftMenu, RightMenu }; diff --git a/src/components/SideMenu/index.ts b/src/components/SideMenu/index.ts new file mode 100644 index 0000000..2be693a --- /dev/null +++ b/src/components/SideMenu/index.ts @@ -0,0 +1,2 @@ +import { LeftMenu, RightMenu } from "./SideMenu"; +export { LeftMenu, RightMenu }; diff --git a/src/main.tsx b/src/main.tsx index 360a3c3..eff7ccc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( , -) - +);