├── .gitignore ├── README.md ├── h33pee.gif ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── Hooks └── useOnClickOutside.tsx ├── icons ├── arrow.svg ├── bell.svg ├── bolt.svg ├── caret.svg ├── chevron.svg ├── cog.svg ├── messenger.svg └── plus.svg ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dropdown/ Dropup Menu 2 | 3 | Animated multi-level dropdown menu inspired by Facebook's March 2020 web UI. 4 | 5 | ![](h33pee.gif) 6 | 7 | Watch the full [React dropdown tutorial](https://youtu.be/IF6k0uZuypA) on YouTube by Jeff Delaney. 8 | made by the great Jeff Delaney (https://github.com/codediodeio), modified by me. 9 | 10 | ``` 11 | git clone 12 | 13 | npm i 14 | npm start 15 | ``` 16 | 17 | 18 | I realised after searching a lot, such multilevel advanced animated menu tutorials don't exist. Maybe there are some yeah, but after trying to find a really good one for a long time, I finally landed upon this video that did exactly (actually very closed to) what I wanted, and what I wanted was to re-create youtube player's settings dropup. ( you can see it in any video, there is a cog logo right below, where all settings such as captions customization and playback rate are, just open it and play with it, its satisfying. ), and I just couldn't figure how that was done. This video/code was very close to the one I needed, just that it was limited to 2 levels ( primary and secondary ), so I extended it to 4 levels and found a general way to do so. I am a begineer js developer who loves modern web technologies, so I tried this. thanks to Jeff, I feel much better now. 19 | -------------------------------------------------------------------------------- /h33pee.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facinick/MultiLevelDropUpReact/ff98098f95d3684d650029231e23b4cc3b62dfa5/h33pee.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facebook", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-scripts": "3.4.1", 12 | "react-transition-group": "^4.3.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facinick/MultiLevelDropUpReact/ff98098f95d3684d650029231e23b4cc3b62dfa5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | React App 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facinick/MultiLevelDropUpReact/ff98098f95d3684d650029231e23b4cc3b62dfa5/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facinick/MultiLevelDropUpReact/ff98098f95d3684d650029231e23b4cc3b62dfa5/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import { ReactComponent as CaretIcon } from './icons/caret.svg'; 3 | import { ReactComponent as ChevronIcon } from './icons/chevron.svg'; 4 | import { ReactComponent as ArrowIcon } from './icons/arrow.svg'; 5 | 6 | import React, { useState, useEffect, useRef } from 'react'; 7 | import { CSSTransition } from 'react-transition-group'; 8 | 9 | function App() { 10 | return ( 11 | }> 12 | 13 | 14 | ); 15 | } 16 | 17 | function NavItem(props) { 18 | const [open, setOpen] = useState(false); 19 | 20 | return ( 21 |
22 | setOpen(!open)}> 23 | {props.icon} 24 | 25 | 26 | {open && props.children} 27 |
28 | ); 29 | } 30 | 31 | function getIndex(menu){ 32 | switch(menu){ 33 | case "MAIN" : return 1; 34 | case "PLAYBACK SPEED" : return 2; 35 | case "CLOSED CAPTIONS" : return 2; 36 | case "FONT COLOR" : return 4; 37 | case "FONT STYLE" : return 4; 38 | case "BACKGROUND OPACITY" : return 4; 39 | case "BACKGROUND COLOR" : return 4; 40 | case "CC SETTINGS" : return 3; 41 | default : return 0; 42 | } 43 | } 44 | 45 | function DropdownMenu() { 46 | const [activeMenu, setActiveMenu] = useState('MAIN'); 47 | const [menuHeight, setMenuHeight] = useState(null); 48 | const [activeIndex, setActiveIndex] = useState(1); 49 | const [prevIndex, setPrevIndex] = useState(1); 50 | 51 | const [playbackSpeed, setPlaybackSpeed] = useState(1); 52 | const [fontColor, setFontColor] = useState("WHITE"); 53 | const [fontsize, setFontStyle] = useState("HELVETICA"); 54 | const [backgroundOpacity, setBackgroundOpacity] = useState(0.75); 55 | const [backgroundColor, setbackgroundColor] = useState("BLACK"); 56 | const [closedCaptions, setClosedCaptions] = useState(false); 57 | 58 | const dropdownRef = useRef(null); 59 | 60 | useEffect(() => { 61 | setMenuHeight(dropdownRef.current?.firstChild.offsetHeight) 62 | }, []) 63 | 64 | function calcHeight(el) { 65 | const height = el.offsetHeight; 66 | setMenuHeight(height); 67 | } 68 | 69 | function stateUpdate({event,data}){ 70 | switch(event){ 71 | case "PLAYBACK SPEED": 72 | setPlaybackSpeed(data); 73 | break; 74 | case "FONT COLOR": 75 | setFontColor(data); 76 | break; 77 | case "FONT STYLE": 78 | setFontStyle(data); 79 | break; 80 | case "BACKGROUND OPACITY": 81 | setBackgroundOpacity(data); 82 | break; 83 | case "BACKGROUND COLOR": 84 | setbackgroundColor(data); 85 | break; 86 | case "TOGGLE CC": 87 | setClosedCaptions(data); 88 | break; 89 | } 90 | } 91 | 92 | function DropdownItem(props) { 93 | return ( 94 | { 96 | if(props.goToMenu){ 97 | setActiveMenu(props.goToMenu); 98 | setPrevIndex(activeIndex); 99 | setActiveIndex(getIndex(props.goToMenu)); 100 | } 101 | if(props.event && props.data) stateUpdate({event:props.event,data:props.data}) 102 | }}> 103 | {props.leftIcon && {props.leftIcon}} 104 | {props.children} 105 | {props.rightIcon && {props.rightIcon}} 106 | 107 | ); 108 | } 109 | 110 | return ( 111 |
112 | 113 | 119 |
120 | } 122 | goToMenu="PLAYBACK SPEED"> 123 | PLAYBACK SPEED{`${playbackSpeed}x`} 124 | 125 | } 127 | goToMenu="CLOSED CAPTIONS"> 128 | CLOSED CAPTIONS{closedCaptions? "ON":"OFF"} 129 | 130 | 131 |
132 |
133 | 134 | 140 |
141 | }> 142 |

PLAYBACK SPEED

143 |
144 | 0.75x 145 | 1x (Default) 146 | 1.5x 147 | 2x 148 |
149 |
150 | 151 | prevIndex?"slideLeft":"slideRight"}`} 155 | unmountOnExit 156 | onEnter={calcHeight}> 157 |
158 | }> 159 |

CLOSED CAPTIONS

160 |
161 | 162 | TURN OFF 163 | 164 | ENGLISH 165 | } 167 | goToMenu="CC SETTINGS"> 168 | CC SETTINGS 169 |
170 |
171 | 172 | prevIndex?"slideLeft":"slideRight"}`} 176 | unmountOnExit 177 | onEnter={calcHeight}> 178 |
179 | }> 180 |

CC SETTINGS

181 |
182 | } 184 | goToMenu="FONT COLOR"> 185 | FONT COLOR 186 | } 188 | goToMenu="FONT STYLE"> 189 | FONT SIZE 190 | } 192 | goToMenu="BACKGROUND COLOR"> 193 | BACKGROUND COLOR 194 | } 196 | goToMenu="BACKGROUND OPACITY"> 197 | BACKGROUND OPACITY 198 |
199 |
200 | 201 | 207 |
208 | }> 209 |

FONT COLORS

210 |
211 | RED 212 | BLUE 213 | GREEN 214 | YELLOW 215 | BLACK 216 | WHITE (Default) 217 |
218 |
219 | 220 | 226 |
227 | }> 228 |

FONT STYLES

229 |
230 | HELVETICA 231 | SANS-SERIF (Default) 232 | CURSIVE 233 |
234 |
235 | 236 | 242 |
243 | }> 244 |

BACKGROUND COLOR

245 |
246 | WHITE 247 | BLACK (Default) 248 | RED 249 | GREEN 250 | BLUE 251 | YELLOW 252 |
253 |
254 | 255 | 261 |
262 | }> 263 |

BACKGROUND OPACITY

264 |
265 | 0 266 | 25 267 | 50 268 | 75 (Default) 269 | 100 270 |
271 |
272 | 273 |
274 | ); 275 | } 276 | 277 | export default App; 278 | -------------------------------------------------------------------------------- /src/Hooks/useOnClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | export function useOnClickOutside(ref, handler) { 4 | useEffect( 5 | () => { 6 | const listener = event => { 7 | // Do nothing if clicking ref's element or descendent elements 8 | if (!ref.current || ref.current.contains(event.target)) { 9 | return; 10 | } 11 | 12 | handler(event); 13 | }; 14 | 15 | document.addEventListener('mousedown', listener); 16 | document.addEventListener('touchstart', listener); 17 | 18 | return () => { 19 | document.removeEventListener('mousedown', listener); 20 | document.removeEventListener('touchstart', listener); 21 | }; 22 | }, 23 | [ref, handler] 24 | ); 25 | } -------------------------------------------------------------------------------- /src/icons/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/bolt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/caret.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/chevron.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/cog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/messenger.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: white; 4 | font-family: roboto; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 11 | monospace; 12 | } 13 | 14 | :root { 15 | --bg: #rgba(0, 0, 0, 0.85); 16 | --bg-accent: #484a4d; 17 | --text-color: #dadce1; 18 | --nav-size: 60px; 19 | --border: 0px solid #474a4d; 20 | --border-radius: 2px; 21 | --speed: 500ms; 22 | } 23 | 24 | ul { 25 | list-style: none; 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | a { 31 | color: var(--text-color); 32 | text-decoration: none;; 33 | } 34 | 35 | /* Top Navigation Bar */ 36 | 37 | /*