├── .eslintrc.js
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .neutrinorc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── scripts
├── copy-files.sh
├── npm-adduser.js
└── test-release.sh
├── src
├── components
│ ├── FontStager
│ │ ├── index.css
│ │ └── index.jsx
│ └── MuiTreeView
│ │ ├── README.md
│ │ └── index.jsx
├── index.d.ts
├── index.jsx
├── styleguide
│ ├── StyleGuideRenderer.jsx
│ └── ThemeWrapper.jsx
├── styles.css
└── theme.js
├── styleguide.config.js
├── test
├── MuiTreeView_test.js
└── __snapshots__
│ └── MuiTreeView_test.js.snap
├── webpack.config.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const neutrino = require('neutrino');
2 |
3 | module.exports = neutrino().eslintrc();
4 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Checklist
4 |
9 | - [ ] Make sure there are no linter errors (run `yarn lint` to see the errors and `yarn lint --fix` to fix them)
10 | - [ ] Update the documentation file `README.md` if required
11 | - [ ] Update the Typescript declaration file `src/index.d.ts` if any exposed properties are removed/added
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | lerna-debug.log
6 |
7 | # Build directories
8 | build
9 | lib
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 | .nyc_output
23 |
24 | # node-waf configuration
25 | .lock-wscript
26 |
27 | # Dependency directories
28 | node_modules
29 |
30 | # Optional npm cache directory
31 | .npm
32 |
33 | # Optional REPL history
34 | .node_repl_history
35 |
36 | # Webstorm project metadata
37 | .idea
38 |
39 | # Mac OS
40 | .DS_Store
41 |
42 | # Gitbook docs
43 | _book
44 | /.vscode
45 |
46 | # build directory
47 | lib
48 | build
49 | /styleguide
50 | es5
51 |
52 | # eslint cache
53 | .eslintcache
54 |
55 |
--------------------------------------------------------------------------------
/.neutrinorc.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path');
2 | const reactLint = require('@mozilla-frontend-infra/react-lint');
3 | const reactComponents = require('@neutrinojs/react-components');
4 | const jest = require('@neutrinojs/jest');
5 |
6 | require('babel-register')({
7 | plugins: [
8 | [require.resolve('babel-plugin-transform-es2015-modules-commonjs'), {
9 | useBuiltIns: true
10 | }],
11 | require.resolve('babel-plugin-transform-object-rest-spread'),
12 | ],
13 | cache: false,
14 | });
15 |
16 | const theme = require('./src/theme').default;
17 |
18 | module.exports = {
19 | use: [
20 | reactLint({
21 | rules: {
22 | 'react/jsx-filename-extension': 'off',
23 | 'react/jsx-props-no-spreading': 'off',
24 | // We use @babel/plugin-proposal-class-properties to allow those
25 | 'react/static-property-placement': 'off',
26 | // We use @babel/plugin-proposal-class-properties to allow those
27 | 'react/state-in-constructor': 'off',
28 | },
29 | }),
30 | reactComponents(),
31 | (neutrino) => {
32 | neutrino.config.resolve.alias
33 | .set('react-dom', '@hot-loader/react-dom');
34 |
35 | neutrino.register('styleguide', () => ({
36 | webpackConfig: neutrino.config.toConfig(),
37 | components: join(
38 | neutrino.options.source,
39 | 'components/**',
40 | `*.{${neutrino.options.extensions.join(',')}}`
41 | ),
42 | usageMode: 'expand',
43 | showSidebar: false,
44 | skipComponentsWithoutExample: true,
45 | theme: theme.styleguide,
46 | styles: {
47 | StyleGuide: theme.styleguide.StyleGuide,
48 | },
49 | styleguideComponents: {
50 | Wrapper: join(__dirname, 'src/styleguide/ThemeWrapper.jsx'),
51 | StyleGuideRenderer: join(__dirname, 'src/styleguide/StyleGuideRenderer.jsx'),
52 | },
53 | }));
54 | },
55 | jest(),
56 | ],
57 | };
58 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Changelog
2 |
3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC.
4 |
5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6 |
7 | ### [v5.0.0](https://github.com/helfi92/material-ui-treeview/compare/v4.2.1...v5.0.0)
8 |
9 | > 29 April 2020
10 |
11 | - Breaking: Update to react-router-v5 [`3de1b2d`](https://github.com/helfi92/material-ui-treeview/commit/3de1b2d884f196180dce144d01e54318ef5a4445)
12 |
13 | #### [v4.2.1](https://github.com/helfi92/material-ui-treeview/compare/v4.2.0...v4.2.1)
14 |
15 | > 29 April 2020
16 |
17 | #### [v4.2.0](https://github.com/helfi92/material-ui-treeview/compare/v4.1.0...v4.2.0)
18 |
19 | > 29 April 2020
20 |
21 | - New: Add onEmptySearch prop [`#50`](https://github.com/helfi92/material-ui-treeview/pull/50)
22 | - Update changelog [`9dfe03b`](https://github.com/helfi92/material-ui-treeview/commit/9dfe03b03af0bddd667288d1eb0cbe35e00ede15)
23 |
24 | ### [v4.1.0](https://github.com/helfi92/material-ui-treeview/compare/v3.5.0...v4.1.0)
25 |
26 | > 2 December 2019
27 |
28 | - Feat: Expose a caseSensitiveSearch prop and default to false [`#48`](https://github.com/helfi92/material-ui-treeview/pull/48)
29 | - Migrate to material-ui v4 [`#46`](https://github.com/helfi92/material-ui-treeview/pull/46)
30 | - feat: Update to latest version of Neutrino [`#45`](https://github.com/helfi92/material-ui-treeview/pull/45)
31 | - Migrate to latest material-ui [`a96c47a`](https://github.com/helfi92/material-ui-treeview/commit/a96c47afce4a9e73d3e322fecf7ae444c5e0ef83)
32 | - add marginRight:0 to expandIcon classes [`0efef0f`](https://github.com/helfi92/material-ui-treeview/commit/0efef0f2398b6daf902112f1c918597101dd1652)
33 | - update changelog [`af22545`](https://github.com/helfi92/material-ui-treeview/commit/af22545515139706b565bb1b30f7e830bd0972fc)
34 |
35 | #### [v3.5.0](https://github.com/helfi92/material-ui-treeview/compare/v3.4.0...v3.5.0)
36 |
37 | > 22 October 2019
38 |
39 | - Create a Pull Request template [`#40`](https://github.com/helfi92/material-ui-treeview/pull/40)
40 | - Mention the TypeScript support [`#41`](https://github.com/helfi92/material-ui-treeview/pull/41)
41 | - Migrate to neutrino v9 [`5db5d3e`](https://github.com/helfi92/material-ui-treeview/commit/5db5d3e545c3ccf2f365be4b2232e63a35891731)
42 | - update verdaccio and add npm [`b425cc3`](https://github.com/helfi92/material-ui-treeview/commit/b425cc3c1d2fbf474c6920373fd98ebb6d4f440e)
43 | - Add scripts directory [`014f0ce`](https://github.com/helfi92/material-ui-treeview/commit/014f0ce2ddc13aed06162d9f1e62134c390eb7c7)
44 |
45 | #### [v3.4.0](https://github.com/helfi92/material-ui-treeview/compare/v3.3.0...v3.4.0)
46 |
47 | > 31 July 2019
48 |
49 | - Added onParentClick to MuiTreeView [`#37`](https://github.com/helfi92/material-ui-treeview/pull/37)
50 |
51 | #### [v3.3.0](https://github.com/helfi92/material-ui-treeview/compare/v3.2.0...v3.3.0)
52 |
53 | > 24 April 2019
54 |
55 | - Feat: Expose all node props when onLeafClick is triggered [`#35`](https://github.com/helfi92/material-ui-treeview/pull/35)
56 |
57 | #### [v3.2.0](https://github.com/helfi92/material-ui-treeview/compare/v3.1.0...v3.2.0)
58 |
59 | > 17 February 2019
60 |
61 | - Add a `softSearch` prop [`#31`](https://github.com/helfi92/material-ui-treeview/pull/31)
62 |
63 | #### [v3.1.0](https://github.com/helfi92/material-ui-treeview/compare/v3.0.4...v3.1.0)
64 |
65 | > 17 February 2019
66 |
67 | - Add an href prop to a leaf value [`#30`](https://github.com/helfi92/material-ui-treeview/pull/30)
68 | - Fix: linting errors [`bf45ef5`](https://github.com/helfi92/material-ui-treeview/commit/bf45ef5a61626a73ade6870531ec93cb14a2d116)
69 |
70 | #### [v3.0.4](https://github.com/helfi92/material-ui-treeview/compare/v3.0.3...v3.0.4)
71 |
72 | > 13 November 2018
73 |
74 | - Fix: Use unique key for duplicate values [`#29`](https://github.com/helfi92/material-ui-treeview/pull/29)
75 |
76 | ### [v3.0.3](https://github.com/helfi92/material-ui-treeview/compare/v2.0.1...v3.0.3)
77 |
78 | > 13 November 2018
79 |
80 | - Update types for #25 [`#28`](https://github.com/helfi92/material-ui-treeview/pull/28)
81 | - Breaking: Add ID field to Node [`#25`](https://github.com/helfi92/material-ui-treeview/pull/25)
82 | - Fix: Add styleguide directory to version control [`#26`](https://github.com/helfi92/material-ui-treeview/pull/26)
83 | - Update changelog [`f923b71`](https://github.com/helfi92/material-ui-treeview/commit/f923b713e2cd3554f66469ef286785367668fdb2)
84 | - Update changelog [`0a38ef6`](https://github.com/helfi92/material-ui-treeview/commit/0a38ef687933c314bdd7a2fdd8ff52b66507ba68)
85 |
86 | ### [v2.0.1](https://github.com/helfi92/material-ui-treeview/compare/v1.2.0...v2.0.1)
87 |
88 | > 21 September 2018
89 |
90 | - Re-render tree when the tree prop is changed [`#19`](https://github.com/helfi92/material-ui-treeview/pull/19)
91 | - 2.0.0 [`#17`](https://github.com/helfi92/material-ui-treeview/pull/17)
92 | - Update changelog [`#16`](https://github.com/helfi92/material-ui-treeview/pull/16)
93 | - Compile to es5 by default [`#15`](https://github.com/helfi92/material-ui-treeview/pull/15)
94 |
95 | #### [v1.2.0](https://github.com/helfi92/material-ui-treeview/compare/v1.1.0...v1.2.0)
96 |
97 | > 17 August 2018
98 |
99 | - Typescript [`#12`](https://github.com/helfi92/material-ui-treeview/pull/12)
100 | - Update changelog [`#8`](https://github.com/helfi92/material-ui-treeview/pull/8)
101 | - Stop overriding default props [`#7`](https://github.com/helfi92/material-ui-treeview/pull/7)
102 | - Add verdaccio for local testing [`#4`](https://github.com/helfi92/material-ui-treeview/pull/4)
103 |
104 | #### v1.1.0
105 |
106 | > 6 June 2018
107 |
108 | - Create MuiTreeView [`f234d6b`](https://github.com/helfi92/material-ui-treeview/commit/f234d6bc1ad7dbbfdb71f9ded768d5da9d2788b1)
109 | - First init [`4c18728`](https://github.com/helfi92/material-ui-treeview/commit/4c187287dcd852c62e51f6636533f76fc99f50da)
110 | - Add LICENSE [`b99aa35`](https://github.com/helfi92/material-ui-treeview/commit/b99aa35cabcf841c9ffef70518672e7785112502)
111 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Hassan Ali
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Material-UI Tree View
2 |
3 | A React tree view for material-ui with TypeScript support.
4 |
5 | See the demo at https://hassanali.me/material-ui-treeview.
6 |
7 | ## Getting started
8 |
9 | ```
10 | # If using Yarn:
11 | yarn add material-ui-treeview @material-ui/core
12 |
13 | # If using npm:
14 | npm install --save material-ui-treeview @material-ui/core
15 | ```
16 |
17 | ### Usage
18 |
19 | After importing the component, it can be rendered with the required `tree` prop:
20 |
21 | #### Import
22 |
23 | ```js
24 | import MuiTreeView from 'material-ui-treeview';
25 |
26 | // using require
27 | const MuiTreeView = require('material-ui-treeview').default;
28 | ```
29 |
30 | #### Example
31 |
32 | ```jsx
33 | import React from 'react';
34 | import { render } from 'react-dom';
35 | import MuiTreeView from 'material-ui-treeview';
36 |
37 | const tree = [
38 | {
39 | value: 'Parent A',
40 | nodes: [{ value: 'Child A' }, { value: 'Child B' }],
41 | },
42 | {
43 | value: 'Parent B',
44 | nodes: [
45 | {
46 | value: 'Child C',
47 | },
48 | {
49 | value: 'Parent C',
50 | nodes: [
51 | { value: 'Child D' },
52 | { value: 'Child E' },
53 | { value: 'Child F' },
54 | ],
55 | },
56 | ],
57 | },
58 | ];
59 |
60 | render((
61 |
62 | ), document.getElementById('root'));
63 | ```
64 |
65 | ### Props
66 |
67 |
68 | | Property | Type | Required? | Description |
69 | | --- | --- | --- | --- |
70 | | tree | object | yes | The data to render as a tree view |
71 | | onLeafClick | function | no | Callback function fired when a tree leaf is clicked. |
72 | | onParentClick | function | no | Callback function fired when a tree parent node is clicked. |
73 | | onEmptySearch | node | no | If `searchTerm` or `softSearch` is provided and the filtered tree is empty then `onEmptySearch` will render. This is used to render something other than an empty tree. |
74 | | searchTerm | string | no | A search term to refine the tree. |
75 | | softSearch | boolean | no | Given a `searchTerm`, a subtree will be shown if any parent node higher up in the tree matches the search term. Defaults to `false`. |
76 | | expansionPanelSummaryProps | object | no | Properties applied to the [ExpansionPanelSummary](https://material-ui.com/api/expansion-panel-summary) element. |
77 | | expansionPanelDetailsProps | object | no | Properties applied to the [ExpansionPanelDetails](https://material-ui.com/api/expansion-panel-details) element. |
78 | | listItemProps | object | no | Properties applied to the [ListItem](https://material-ui.com/api/list-item) element. |
79 | | caseSensitiveSearch | boolean | no | If true, search is case sensitive. Defaults to false. |
80 | | Link | node | no | A React Router Link node to use. _Required_ when a leaf node has an href value. |
81 |
82 | ## Development and Contributing
83 |
84 | * Fork and clone this repo.
85 | * Install the dependencies with yarn.
86 | * Start the
87 | - development server with yarn start. Open a browser to http://localhost:5000.
88 | - styleguide with yarn start:styleguide. Open a browser to http://localhost:6060.
89 |
90 | Feel free to open an issue, submit a pull request, or contribute however you would like.
91 | Understand that this documentation is still a work in progress, so file an issue or submit a PR
92 | to ask questions or make improvements. Thanks!
93 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const neutrino = require('neutrino');
2 |
3 | process.env.NODE_ENV = process.env.NODE_ENV || 'test';
4 |
5 | module.exports = neutrino().jest();
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "material-ui-treeview",
3 | "version": "5.0.0",
4 | "main": "MuiTreeView.js",
5 | "types": "index.d.ts",
6 | "description": "A React tree view for material-ui v1.",
7 | "repository": "helfi92/material-ui-treeview",
8 | "keywords": [
9 | "react",
10 | "material-ui",
11 | "tree",
12 | "view",
13 | "treeview",
14 | "tree-view",
15 | "treenode",
16 | "react-component",
17 | "ui",
18 | "material design"
19 | ],
20 | "license": "MIT",
21 | "author": "Hassan Ali ",
22 | "scripts": {
23 | "changelog": "auto-changelog -p",
24 | "build": "webpack --mode production && scripts/copy-files.sh",
25 | "start:styleguide": "styleguidist server",
26 | "start": "webpack-dev-server --mode development",
27 | "deploy": "styleguidist build && gh-pages --remote origin -d styleguide",
28 | "lint": "eslint --cache --format codeframe --ext js,jsx src test",
29 | "test": "jest",
30 | "verdaccio:release": "scripts/test-release.sh",
31 | "publish:npm": "yarn build && npm publish build"
32 | },
33 | "devDependencies": {
34 | "@hot-loader/react-dom": "^16.10.2",
35 | "@material-ui/core": "^4.5.1",
36 | "@material-ui/styles": "^4.5.0",
37 | "@mozilla-frontend-infra/react-lint": "^2.0.1",
38 | "@neutrinojs/jest": "9.0.0-rc.4",
39 | "@neutrinojs/react-components": "9.0.0-rc.4",
40 | "auto-changelog": "^1.7.1",
41 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
42 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
43 | "babel-register": "^6.26.0",
44 | "eslint": "^5",
45 | "fs-extra": "^7.0.0",
46 | "gh-pages": "^1.1.0",
47 | "jest": "^24.9.0",
48 | "neutrino": "9.0.0-rc.4",
49 | "npm": "~6.4.1",
50 | "prop-types": "^15.7.2",
51 | "raf": "^3.4.0",
52 | "react": "^16.13.1",
53 | "react-dom": "^16.13.1",
54 | "react-fout-stager": "^3.0.0",
55 | "react-helmet": "^5.2.1",
56 | "react-router-dom": "^5.1.2",
57 | "react-styleguidist": "^9.2.0",
58 | "react-test-renderer": "^16.4.0",
59 | "typeface-roboto": "^0.0.54",
60 | "verdaccio": "^4.6.1",
61 | "webpack": "^4.41.2",
62 | "webpack-cli": "^3.3.9",
63 | "webpack-dev-server": "^3.8.2"
64 | },
65 | "dependencies": {
66 | "@material-ui/icons": "^4.5.1",
67 | "classnames": "^2.2.6",
68 | "fast-memoize": "^2.5.1",
69 | "ramda": "^0.25.0"
70 | },
71 | "peerDependencies": {
72 | "@material-ui/core": "^4.0.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/scripts/copy-files.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cp {src/index.d.ts,package.json,LICENSE,README.md} build
4 |
--------------------------------------------------------------------------------
/scripts/npm-adduser.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | // eslint-disable-next-line import/no-unresolved
3 | const npm = require('npm');
4 |
5 | const REGISTRY = 'http://localhost:4873';
6 | const email = 'test@test.org';
7 | const username = 'test';
8 | const password = 'test';
9 |
10 | npm.load({}, err => {
11 | if (err) {
12 | throw err;
13 | }
14 |
15 | const auth = { username, password, email };
16 |
17 | npm.config.set('registry', REGISTRY, 'user');
18 | npm.config.set('email', email, 'user');
19 | npm.registry.adduser(REGISTRY, { auth }, (err, doc) => {
20 | if (err) {
21 | throw err;
22 | }
23 |
24 | if (!doc || !doc.token) {
25 | throw new Error('No auth token');
26 | }
27 |
28 | npm.config.setCredentialsByURI(REGISTRY, { token: doc.token });
29 | npm.config.save('user', err => {
30 | if (err) {
31 | throw err;
32 | }
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/scripts/test-release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | npm config set registry http://localhost:4873/;
6 |
7 | # Add npm user so we can use it to publish
8 | scripts/npm-adduser.js;
9 |
10 | # Delete its corresponding verdaccio storage so that we don't have to change the version in order to publish
11 | rm -rf $HOME/.config/verdaccio/storage/material-ui-treeview/
12 |
13 | npm publish build --registry http://localhost:4873/
14 |
15 | npm config set registry https://registry.npmjs.org/;
16 |
--------------------------------------------------------------------------------
/src/components/FontStager/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Roboto400;
3 | src:
4 | url('~typeface-roboto/files/roboto-latin-400.woff2') format('woff2'),
5 | url('~typeface-roboto/files/roboto-latin-400.woff') format('woff');
6 | font-weight: 400;
7 | font-style: normal;
8 | }
9 | @font-face {
10 | font-family: Roboto300;
11 | src:
12 | url('~typeface-roboto/files/roboto-latin-300.woff2') format('woff2'),
13 | url('~typeface-roboto/files/roboto-latin-300.woff') format('woff');
14 | font-weight: 300;
15 | font-style: normal;
16 | }
17 | @font-face {
18 | font-family: Roboto500;
19 | src:
20 | url('~typeface-roboto/files/roboto-latin-500.woff2') format('woff2'),
21 | url('~typeface-roboto/files/roboto-latin-500.woff') format('woff');
22 | font-weight: 500;
23 | font-style: normal;
24 | }
25 |
26 | /*
27 | The purpose of defining class stages is to
28 | re-render once a stage has been met. We start
29 | with the minimal default stage of sans-serif,
30 | and progressively re-render.
31 | */
32 | html, body {
33 | font-family: sans-serif;
34 | font-weight: 400;
35 | -webkit-font-smoothing: antialiased;
36 | color: rgba(255, 255, 255, 0.7);
37 | }
38 |
39 | /*
40 | The defined stages now modify the display of
41 | elements once they are loaded.
42 | */
43 |
44 | /*
45 | During primary stage we only load the Roboto font.
46 | Once it's loaded, update the body to use it.
47 | */
48 | .font-stage-primary html,
49 | .font-stage-primary body {
50 | font-family: Roboto400, sans-serif;
51 | }
52 |
53 | /* Prevent the secondary fonts from being tree-shaken away */
54 | .font-stage-secondary .roboto300 {
55 | font-family: Roboto300, sans-serif;
56 | }
57 | .font-stage-secondary .roboto500 {
58 | font-family: Roboto500, sans-serif;
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/FontStager/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FoutStager from 'react-fout-stager';
3 | import './index.css';
4 |
5 | /**
6 | * Responsible for loading the application typefaces progressively
7 | * using FOUT stage techniques.
8 | */
9 | function FontStager() {
10 | return (
11 |
28 | );
29 | }
30 |
31 | export default FontStager;
32 |
--------------------------------------------------------------------------------
/src/components/MuiTreeView/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | const tree = [
3 | {
4 | value: 'Parent A',
5 | nodes: [{ value: 'Child A' }, { value: 'Child B' }],
6 | },
7 | {
8 | value: 'Parent B',
9 | nodes: [
10 | {
11 | value: 'Child C',
12 | },
13 | {
14 | value: 'Parent C',
15 | nodes: [
16 | { value: 'Child D', id: 'example-id' },
17 | { value: 'Child E' },
18 | { value: 'Child F' },
19 | ],
20 | },
21 | ],
22 | },
23 | ];
24 |
25 | alert("Leaf clicked: " + JSON.stringify(node))}
28 | onParentClick={node => alert("Parent clicked: " + JSON.stringify(node))}
29 | tree={tree}
30 | />
31 | ```
32 |
--------------------------------------------------------------------------------
/src/components/MuiTreeView/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | arrayOf,
4 | bool,
5 | shape,
6 | number,
7 | string,
8 | func,
9 | oneOfType,
10 | object,
11 | node,
12 | } from 'prop-types';
13 | import classNames from 'classnames';
14 | import { prop } from 'ramda';
15 | import memoize from 'fast-memoize';
16 | import { makeStyles, useTheme, withStyles } from '@material-ui/core/styles';
17 | import ListItem from '@material-ui/core/ListItem';
18 | import ListItemText from '@material-ui/core/ListItemText';
19 | import Typography from '@material-ui/core/Typography';
20 | import MuiExpansionPanel from '@material-ui/core/ExpansionPanel';
21 | import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
22 | import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
23 | import KeyboardArrowDown from '@material-ui/icons/KeyboardArrowDown';
24 |
25 | const pickClassName = prop('className');
26 | /** Prop-type for a recursive data structure */
27 | const tree = {
28 | // The node value.
29 | value: string.isRequired,
30 | /**
31 | * A string representation of the location to link to.
32 | * Only use this property on a leaf node.
33 | * This value will be fed directly to the
34 | * [Link](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/Link.md)
35 | * component of `react-router-dom`.
36 | * */
37 | href: string,
38 | // Optional node ID. Useful for when the node value is not unique.
39 | id: oneOfType([string, number]),
40 | };
41 |
42 | Object.assign(tree, {
43 | nodes: arrayOf(oneOfType([shape(tree), string])),
44 | });
45 |
46 | const ExpansionPanel = withStyles({
47 | root: {
48 | '&:before': {
49 | opacity: 0,
50 | },
51 | '&$expanded': {
52 | margin: 0,
53 | },
54 | },
55 | expanded: {},
56 | })(MuiExpansionPanel);
57 | const useStyles = makeStyles(theme => ({
58 | panel: {
59 | width: '100%',
60 | paddingRight: 0,
61 | paddingLeft: 0,
62 | },
63 | panelSummary: {
64 | padding: 0,
65 | paddingRight: theme.spacing(1),
66 | marginLeft: theme.spacing(1),
67 | },
68 | panelDetails: {
69 | padding: 0,
70 | display: 'block',
71 | },
72 | text: {
73 | overflow: 'hidden',
74 | textOverflow: 'ellipsis',
75 | whiteSpace: 'noWrap',
76 | maxWidth: '75vw',
77 | },
78 | listItemTextDense: {
79 | margin: 0,
80 | },
81 | }));
82 |
83 | /**
84 | * Render a tree view.
85 | */
86 | function MuiTreeView(props) {
87 | const theme = useTheme();
88 | const classes = useStyles();
89 | const unit = theme.spacing(1);
90 | const {
91 | tree,
92 | searchTerm,
93 | softSearch,
94 | caseSensitiveSearch,
95 | onEmptySearch,
96 | } = props;
97 | const handleLeafClick = leaf => {
98 | if (props.onLeafClick) {
99 | props.onLeafClick(leaf);
100 | }
101 | };
102 |
103 | const handleParentClick = parent => {
104 | if (props.onParentClick) {
105 | props.onParentClick(parent);
106 | }
107 | };
108 |
109 | const isLeafNode = node => {
110 | return typeof node === 'string' || !node.nodes || !node.nodes.length;
111 | };
112 |
113 | const getNodeValue = node => {
114 | return typeof node === 'string' ? node : node.value;
115 | };
116 |
117 | const getNodeId = node => {
118 | if (typeof node === 'object') {
119 | return node.id;
120 | }
121 | };
122 |
123 | const getNodeHref = node => {
124 | if (typeof node === 'object') {
125 | return node.href;
126 | }
127 | };
128 |
129 | const filter = tree => {
130 | return tree.filter(node => {
131 | const value = getNodeValue(node);
132 | const isLeaf = isLeafNode(node);
133 | const searchRegExp = caseSensitiveSearch
134 | ? RegExp(searchTerm)
135 | : RegExp(searchTerm, 'i');
136 |
137 | if (searchRegExp.test(value)) {
138 | return true;
139 | }
140 |
141 | if (isLeaf) {
142 | return false;
143 | }
144 |
145 | const subtree = filter(node.nodes);
146 |
147 | return Boolean(subtree.length);
148 | });
149 | };
150 |
151 | const createFilteredTree = memoize(
152 | (tree, searchTerm) => (searchTerm ? filter(tree) : tree),
153 | {
154 | serializer: ([tree, searchTerm, softSearch]) =>
155 | `${JSON.stringify(tree)}-${searchTerm}-${softSearch}`,
156 | }
157 | );
158 | const renderNode = ({ node, parent, depth = 0, haltSearch }) => {
159 | const {
160 | searchTerm,
161 | softSearch,
162 | onLeafClick: _,
163 | onParentClick: __,
164 | onEmptySearch: ___,
165 | Link,
166 | expansionPanelSummaryProps,
167 | expansionPanelDetailsProps,
168 | listItemProps,
169 | caseSensitiveSearch,
170 | ...rest
171 | } = props;
172 | const value = getNodeValue(node);
173 | const id = getNodeId(node);
174 | const isLeaf = isLeafNode(node);
175 | const href = isLeaf ? getNodeHref(node) : null;
176 | const textIndent = isLeaf
177 | ? depth * unit + unit + (parent ? unit : 0)
178 | : unit * depth + unit;
179 | const searchRegExp = caseSensitiveSearch
180 | ? RegExp(searchTerm)
181 | : RegExp(searchTerm, 'i');
182 | const shouldHaltSearch =
183 | softSearch && searchTerm ? searchRegExp.test(value) : false;
184 |
185 | if (!haltSearch && isLeaf && searchTerm && !searchRegExp.test(value)) {
186 | return null;
187 | }
188 |
189 | if (!Link && isLeaf && href) {
190 | throw new Error(
191 | 'A Link prop is required when a leaf node has an href specified.'
192 | );
193 | }
194 |
195 | if (isLeaf) {
196 | return (
197 | handleLeafClick({ ...node, value, parent, id })}
204 | button
205 | {...(href
206 | ? {
207 | component: Link,
208 | to: href,
209 | }
210 | : null)}
211 | {...listItemProps}>
212 |
216 |
217 | );
218 | }
219 |
220 | return (
221 |
227 | }
233 | onClick={() => handleParentClick({ ...node, value, parent, id })}>
234 | {node.value}
235 |
236 |
240 | {node.nodes.map(l =>
241 | renderNode({
242 | node: l,
243 | parent: node,
244 | depth: depth + 1,
245 | haltSearch: shouldHaltSearch,
246 | })
247 | )}
248 |
249 |
250 | );
251 | };
252 |
253 | const graph = createFilteredTree(tree, searchTerm, softSearch);
254 |
255 | if (!graph.length && onEmptySearch) {
256 | return onEmptySearch;
257 | }
258 |
259 | return graph.map(node =>
260 | renderNode({ node, parent: null, haltSearch: false })
261 | );
262 | }
263 |
264 | MuiTreeView.propTypes = {
265 | /** The data to render as a tree view */
266 | tree: arrayOf(shape(tree)).isRequired,
267 | /** Callback function fired when a tree leaf is clicked. */
268 | onLeafClick: func,
269 | /** Callback function fired when a tree node is clicked. */
270 | onParentClick: func,
271 | /** A search term to refine the tree */
272 | searchTerm: string,
273 | /**
274 | * Given a `searchTerm`, a subtree will be shown if any parent node
275 | * higher up in the tree matches the search term. Defaults to false.
276 | * */
277 | softSearch: bool,
278 | /** Properties applied to the ExpansionPanelSummary element. */
279 | expansionPanelSummaryProps: object,
280 | /** Properties applied to the ExpansionPanelDetails element. */
281 | expansionPanelDetailsProps: object,
282 | /** Properties applied to the ListItem element. */
283 | listItemProps: object,
284 | /** If true, search is case sensitive. Defaults to false. */
285 | caseSensitiveSearch: bool,
286 | /** Node to render when searchTerm is provided but the search filter
287 | * returns no result. */
288 | onEmptySearch: node,
289 | /**
290 | * A React Router Link node to use. Required when a leaf node
291 | * has an href value.
292 | * */
293 | Link: node,
294 | };
295 |
296 | MuiTreeView.defaultProps = {
297 | searchTerm: null,
298 | softSearch: false,
299 | onLeafClick: null,
300 | onParentClick: null,
301 | expansionPanelSummaryProps: null,
302 | expansionPanelDetailsProps: null,
303 | listItemProps: null,
304 | caseSensitiveSearch: false,
305 | onEmptySearch: null,
306 | Link: null,
307 | };
308 |
309 | export default MuiTreeView;
310 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ExpansionPanelSummaryProps } from '@material-ui/core/ExpansionPanelSummary';
3 | import { ExpansionPanelDetailsProps } from '@material-ui/core/ExpansionPanelDetails';
4 | import { ListItemProps } from '@material-ui/core/ListItem';
5 |
6 | export interface Tree {
7 | value: string;
8 | href?: string;
9 | nodes?: Array;
10 | id?: string | number;
11 | }
12 |
13 | export interface MuiTreeViewProps {
14 | /**
15 | * The data to render as a tree view
16 | */
17 | tree: Tree[];
18 |
19 | /**
20 | * Callback function fired when a tree leaf is clicked.
21 | */
22 | onLeafClick?: (leaf: {
23 | value: string;
24 | parent: Tree;
25 | id?: string | number;
26 | href?: string;
27 | }) => void;
28 |
29 | /**
30 | * Callback function fired when a tree node is clicked.
31 | */
32 | onParentClick?: (parent: Tree) => void;
33 |
34 | /**
35 | * A search term to refine the tree
36 | */
37 | searchTerm?: string;
38 |
39 | /**
40 | * Given a `searchTerm`, a subtree will be shown if any parent node
41 | * higher up in the tree matches the search term. Defaults to false.
42 | */
43 | softSearch?: boolean;
44 |
45 | /**
46 | * Properties applied to the ExpansionPanelSummary element.
47 | */
48 | expansionPanelSummaryProps?: ExpansionPanelSummaryProps;
49 |
50 | /**
51 | * Properties applied to the ExpansionPanelDetails element.
52 | */
53 | expansionPanelDetailsProps?: ExpansionPanelDetailsProps;
54 |
55 | /**
56 | * Properties applied to the ListItem element.
57 | */
58 | listItemProps?: ListItemProps;
59 |
60 | /**
61 | * Makes search insensitive to case if true.
62 | * Defaults to false.
63 | */
64 | caseInsensitiveSearch?: boolean;
65 |
66 | /** Node to render when searchTerm is provided but the search filter
67 | * returns no result.*/
68 | onEmptySearch?: React.ReactNode;
69 |
70 | /**
71 | * A React Router Link node to use. Required when a leaf node
72 | * has an href value.
73 | * */
74 | Link?: React.ReactNode;
75 | }
76 |
77 | export default class MuiTreeView extends React.Component {}
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { render } from 'react-dom';
3 | import { BrowserRouter as Router, Link } from 'react-router-dom';
4 | import Typography from '@material-ui/core/Typography';
5 | import MuiTreeView from './components/MuiTreeView';
6 | import './styles.css';
7 |
8 | const root = document.getElementById('root');
9 | const tree = [
10 | {
11 | value: 'Parent A',
12 | nodes: [{ value: 'Child A' }, { value: 'Child B' }],
13 | },
14 | {
15 | value: 'Parent B',
16 | nodes: [
17 | {
18 | value: 'Child C',
19 | },
20 | {
21 | value: 'Parent C',
22 | nodes: [
23 | { value: 'Child D' },
24 | { value: 'Child E' },
25 | { value: 'Child F', href: '/f' },
26 | ],
27 | },
28 | ],
29 | },
30 | ];
31 |
32 | class App extends Component {
33 | /* eslint-disable-next-line no-alert */
34 | handleLeafClick = node => alert(`Leaf click: ${JSON.stringify(node)}`);
35 |
36 | /* eslint-disable-next-line no-alert */
37 | handleParentClick = node => alert(`Parent click: ${JSON.stringify(node)}`);
38 |
39 | state = {
40 | search: '',
41 | };
42 |
43 | handleInputChange = ({ currentTarget: { value } }) => {
44 | this.setState({ search: value });
45 | };
46 |
47 | render() {
48 | const { search } = this.state;
49 |
50 | return (
51 |
52 |
53 |
54 | MuiTreeView Demo
55 |
56 |
57 | Yikes...
}
61 | defaultExpanded
62 | onLeafClick={this.handleLeafClick}
63 | onParentClick={this.handleParentClick}
64 | tree={tree}
65 | />
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | render(, root);
73 |
--------------------------------------------------------------------------------
/src/styleguide/StyleGuideRenderer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import StyleGuide from 'react-styleguidist/lib/client/rsg-components/StyleGuide/StyleGuideRenderer';
3 | import FontStager from '../components/FontStager';
4 |
5 | function StyleGuideRenderer(props) {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default StyleGuideRenderer;
15 |
--------------------------------------------------------------------------------
/src/styleguide/ThemeWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ThemeProvider } from '@material-ui/core/styles';
3 | import theme from '../theme';
4 |
5 | function ThemeWrapper(props) {
6 | return {props.children};
7 | }
8 |
9 | export default ThemeWrapper;
10 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-family: "Roboto", "sans-serif";
3 | padding: 8px 16px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles';
2 | import { lighten, darken } from '@material-ui/core/styles/colorManipulator';
3 |
4 | const Roboto300 = { fontFamily: 'Roboto300, sans-serif' };
5 | const Roboto400 = { fontFamily: 'Roboto400, sans-serif' };
6 | const Roboto500 = { fontFamily: 'Roboto500, sans-serif' };
7 | const BACKGROUND = '#12202c';
8 | const PRIMARY = '#1b2a39';
9 | const SECONDARY = '#4177a5';
10 | const theme = createMuiTheme({
11 | palette: {
12 | type: 'dark',
13 | background: BACKGROUND,
14 | primary: {
15 | main: PRIMARY,
16 | light: lighten(PRIMARY, 0.2),
17 | dark: darken(PRIMARY, 0.2),
18 | },
19 | secondary: {
20 | main: SECONDARY,
21 | light: lighten(SECONDARY, 0.2),
22 | dark: darken(SECONDARY, 0.2),
23 | },
24 | text: {
25 | primary: 'rgba(255, 255, 255, 0.9)',
26 | secondary: 'rgba(255, 255, 255, 0.7)',
27 | disabled: 'rgba(255, 255, 255, 0.5)',
28 | hint: 'rgba(255, 255, 255, 0.5)',
29 | icon: 'rgba(255, 255, 255, 0.5)',
30 | active: 'rgba(255, 255, 255, 0.12)',
31 | inactive: 'rgba(255, 255, 255, 0.3)',
32 | },
33 | },
34 | typography: {
35 | ...Roboto400,
36 | display4: Roboto300,
37 | display3: Roboto400,
38 | display2: Roboto400,
39 | display1: Roboto400,
40 | headline: Roboto400,
41 | title: Roboto500,
42 | subheading: Roboto400,
43 | body2: Roboto500,
44 | body1: Roboto400,
45 | caption: Roboto400,
46 | button: Roboto500,
47 | },
48 | overrides: {
49 | MuiListItem: {
50 | root: {
51 | paddingTop: 12,
52 | paddingBottom: 12,
53 | },
54 | },
55 | MuiPaper: {
56 | root: {
57 | backgroundColor: PRIMARY,
58 | color: 'inherit',
59 | },
60 | },
61 | },
62 | });
63 |
64 | export default {
65 | ...theme,
66 | styleguide: {
67 | StyleGuide: {
68 | root: {
69 | overflowY: 'scroll',
70 | minHeight: '100vh',
71 | backgroundColor: BACKGROUND,
72 | },
73 | },
74 | fontFamily: {
75 | base: theme.typography.fontFamily,
76 | },
77 | fontSize: {
78 | base: theme.typography.fontSize - 1,
79 | text: theme.typography.fontSize,
80 | small: theme.typography.fontSize - 2,
81 | },
82 | color: {
83 | base: theme.palette.text.primary,
84 | link: theme.palette.text.primary,
85 | linkHover: theme.palette.text.primary,
86 | border: theme.palette.divider,
87 | baseBackground: BACKGROUND,
88 | sidebarBackground: theme.palette.primary.main,
89 | codeBackground: theme.palette.primary.main,
90 | codeBase: '#80CBAE',
91 | codeString: '#C3E88D',
92 | codeProperty: '#FFCB6B',
93 | },
94 | },
95 | };
96 |
--------------------------------------------------------------------------------
/styleguide.config.js:
--------------------------------------------------------------------------------
1 | const neutrino = require('neutrino');
2 |
3 | module.exports = neutrino().styleguide();
4 |
--------------------------------------------------------------------------------
/test/MuiTreeView_test.js:
--------------------------------------------------------------------------------
1 | import 'raf/polyfill';
2 | import React from 'react';
3 | import renderer from 'react-test-renderer';
4 | import MuiTreeView from '../src/components/MuiTreeView';
5 |
6 | const tree = [
7 | {
8 | value: 'Parent A',
9 | nodes: [{ value: 'Child A' }, { value: 'Child B' }],
10 | },
11 | {
12 | value: 'Parent B',
13 | nodes: [
14 | {
15 | value: 'Child C',
16 | },
17 | {
18 | value: 'Parent C',
19 | nodes: [
20 | { value: 'Child D' },
21 | { value: 'Child E' },
22 | { value: 'Child F' },
23 | ],
24 | },
25 | ],
26 | },
27 | ];
28 |
29 | describe('MuiTreeView', () => {
30 | it('renders correctly', () => {
31 | const jsonTree = renderer.create().toJSON();
32 |
33 | expect(jsonTree).toMatchSnapshot();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/__snapshots__/MuiTreeView_test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MuiTreeView renders correctly 1`] = `
4 | Array [
5 |
50 |
67 |
70 |
73 | Parent A
74 |
75 |
76 |
92 |
95 |
107 |
108 |
111 |
112 |
113 |
122 |
125 |
128 |
131 |
154 |
157 | Child A
158 |
159 |
162 |
163 |
186 |
189 | Child B
190 |
191 |
194 |
195 |
196 |
197 |
198 |
199 |
,
200 |
245 |
262 |
265 |
268 | Parent B
269 |
270 |
271 |
287 |
290 |
302 |
303 |
306 |
307 |
308 |
317 |
320 |
323 |
326 |
349 |
352 | Child C
353 |
354 |
357 |
358 |
403 |
420 |
423 |
426 | Parent C
427 |
428 |
429 |
445 |
448 |
460 |
461 |
464 |
465 |
466 |
475 |
478 |
481 |
484 |
507 |
510 | Child D
511 |
512 |
515 |
516 |
539 |
542 | Child E
543 |
544 |
547 |
548 |
571 |
574 | Child F
575 |
576 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
,
590 | ]
591 | `;
592 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const neutrino = require('neutrino');
2 |
3 | module.exports = neutrino().webpack();
4 |
--------------------------------------------------------------------------------