├── .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 |
45 |
46 |
{open ? 'Close' : 'Open'}
47 |
48 |
49 | {open && (
50 |
51 | {items.map(item => (
52 | -
53 |
57 |
58 | ))}
59 |
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 |
--------------------------------------------------------------------------------