├── .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 | 112 |
113 | 199 |
, 200 |
245 |
262 |
265 |
268 | Parent B 269 |
270 |
271 | 307 |
308 | 589 |
, 590 | ] 591 | `; 592 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const neutrino = require('neutrino'); 2 | 3 | module.exports = neutrino().webpack(); 4 | --------------------------------------------------------------------------------