├── .env ├── .prettierrc.json ├── src ├── index.js ├── App.js ├── Dropdown.js └── App.scss ├── .gitignore ├── public └── index.html ├── .eslintrc.json └── package.json /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Building a custom dropdown menu component for React 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.scss'; 3 | import Dropdown from './Dropdown'; 4 | 5 | const items = [ 6 | { 7 | id: 1, 8 | value: 'Pulp Fiction', 9 | }, 10 | { 11 | id: 2, 12 | value: 'The Prestige', 13 | }, 14 | { 15 | id: 3, 16 | value: 'Blade Runner 2049', 17 | }, 18 | ]; 19 | 20 | function App() { 21 | return ( 22 |
23 |

24 | Buy Movies{' '} 25 | 26 | 🎥 27 | 28 |

29 | 30 |
31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:prettier/recommended", 5 | "prettier/react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "commonjs": true, 10 | "es6": true, 11 | "jest": true, 12 | "node": true 13 | }, 14 | "rules": { 15 | "jsx-a11y/href-no-hash": ["off"], 16 | "react/prop-types": ["off"], 17 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 18 | "max-len": [ 19 | "warn", 20 | { 21 | "code": 80, 22 | "tabWidth": 2, 23 | "comments": 80, 24 | "ignoreComments": false, 25 | "ignoreTrailingComments": true, 26 | "ignoreUrls": true, 27 | "ignoreStrings": true, 28 | "ignoreTemplateLiterals": true, 29 | "ignoreRegExpLiterals": true 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropdown-menu", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "@testing-library/jest-dom": "^4.2.4", 6 | "@testing-library/react": "^9.3.2", 7 | "@testing-library/user-event": "^7.1.2", 8 | "node-sass": "^4.13.1", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-onclickoutside": "^6.9.0", 12 | "react-scripts": "3.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 | "devDependencies": { 36 | "babel-eslint": "^10.0.3", 37 | "eslint": "^6.8.0", 38 | "eslint-config-airbnb": "^18.0.1", 39 | "eslint-config-prettier": "^6.9.0", 40 | "eslint-plugin-import": "^2.20.0", 41 | "eslint-plugin-jsx-a11y": "^6.2.3", 42 | "eslint-plugin-prettier": "^3.1.2", 43 | "eslint-plugin-react": "^7.18.0", 44 | "prettier": "^1.19.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import onClickOutside from 'react-onclickoutside'; 3 | 4 | function Dropdown({ title, items, multiSelect = false }) { 5 | const [open, setOpen] = useState(false); 6 | const [selection, setSelection] = useState([]); 7 | const toggle = () => setOpen(!open); 8 | Dropdown.handleClickOutside = () => setOpen(false); 9 | 10 | function handleOnClick(item) { 11 | if (!selection.some(current => current.id === item.id)) { 12 | if (!multiSelect) { 13 | setSelection([item]); 14 | } else if (multiSelect) { 15 | setSelection([...selection, item]); 16 | } 17 | } else { 18 | let selectionAfterRemoval = selection; 19 | selectionAfterRemoval = selectionAfterRemoval.filter( 20 | current => current.id !== item.id 21 | ); 22 | setSelection([...selectionAfterRemoval]); 23 | } 24 | } 25 | 26 | function isItemInSelection(item) { 27 | if (selection.some(current => current.id === item.id)) { 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | return ( 34 |
35 |
toggle(!open)} 40 | onClick={() => toggle(!open)} 41 | > 42 |
43 |

{title}

44 |
45 |
46 |

{open ? 'Close' : 'Open'}

47 |
48 |
49 | {open && ( 50 | 60 | )} 61 |
62 | ); 63 | } 64 | 65 | const clickOutsideConfig = { 66 | handleClickOutside: () => Dropdown.handleClickOutside, 67 | }; 68 | 69 | export default onClickOutside(Dropdown, clickOutsideConfig); 70 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Dropdown menu 3 | ========================================================================== */ 4 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,700,900'); 5 | 6 | /* RESETS 7 | ============================================ */ 8 | 9 | html { 10 | -webkit-box-sizing: border-box; 11 | box-sizing: border-box; 12 | } 13 | *, *:before, *:after { 14 | -webkit-box-sizing: inherit; 15 | box-sizing: inherit; 16 | } 17 | 18 | body { 19 | margin: 20px 0; 20 | padding: 0; 21 | line-height: 1; 22 | font-size: 16px; 23 | font-family: 'Roboto', sans-serif; 24 | color: #202020; 25 | background-color: #fbfbfb; 26 | font-smooth: always; 27 | -webkit-font-smoothing: antialiased; 28 | -moz-osx-font-smoothing: grayscale; 29 | } 30 | 31 | .container { 32 | max-width: 1140px; 33 | width: 100%; 34 | margin: auto; 35 | } 36 | 37 | @mixin styling() { 38 | background-color: white; 39 | border-color: #ccc; 40 | border-radius: 4px; 41 | border-style: solid; 42 | border-width: 1px; 43 | box-shadow: 0 .125rem .25rem rgba(0,0,0,.075) !important; 44 | } 45 | 46 | /* DROPDOWN 47 | ============================================ */ 48 | 49 | .dd-wrapper { 50 | display: flex; 51 | min-height: 38px; 52 | flex-wrap: wrap; 53 | 54 | .dd-header { 55 | @include styling(); 56 | display: flex; 57 | justify-content: space-between; 58 | cursor: pointer; 59 | width: 100%; 60 | padding: 0 20px; 61 | 62 | &__title--bold { 63 | font-weight: bold; 64 | } 65 | } 66 | 67 | .dd-list { 68 | box-shadow: 0 .125rem .25rem rgba(0,0,0,.075) !important; 69 | padding: 0; 70 | margin: 0; 71 | width: 100%; 72 | margin-top: 20px; 73 | 74 | li { 75 | list-style-type: none; 76 | 77 | &:first-of-type { 78 | > button { 79 | border-top: 1px solid #ccc; 80 | border-top-left-radius: 4px; 81 | border-top-right-radius: 4px; 82 | } 83 | } 84 | 85 | &:last-of-type > button { 86 | border-bottom-left-radius: 4px; 87 | border-bottom-right-radius: 4px; 88 | } 89 | 90 | button { 91 | display: flex; 92 | justify-content: space-between; 93 | background-color: white; 94 | font-size: 16px; 95 | padding: 15px 20px 15px 20px; 96 | border: 0; 97 | border-bottom: 1px solid #ccc; 98 | width: 100%; 99 | text-align: left; 100 | border-left: 1px solid #ccc; 101 | border-right: 1px solid #ccc; 102 | 103 | &:hover, &:focus { 104 | cursor: pointer; 105 | font-weight: bold; 106 | background-color: #ccc; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | --------------------------------------------------------------------------------