├── .babelrc.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── jsonViewTheme.ts ├── main.ts ├── manager.ts └── preview.ts ├── .travis.yml ├── .yarn └── releases │ └── yarn-3.6.3.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── .eslintrc ├── DataRenderer.test.tsx ├── DataRenderer.tsx ├── DataTypeDetection.test.ts ├── DataTypeDetection.ts ├── index.test.tsx ├── index.tsx ├── react-app-env.d.ts ├── stories │ └── JsonView.stories.tsx ├── styles.module.css └── typings.d.ts ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100, 9 | "safari": 15, 10 | "firefox": 91 11 | } 12 | } 13 | ], 14 | "@babel/preset-typescript", 15 | "@babel/preset-react" 16 | ], 17 | "plugins": [] 18 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | quote_type = single 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | storybook-static/ 6 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:jsx-a11y/recommended" 9 | ], 10 | "env": { 11 | "node": true 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2020, 15 | "ecmaFeatures": { 16 | "legacyDecorators": true, 17 | "jsx": true 18 | } 19 | }, 20 | "settings": { 21 | "react": { 22 | "version": "detect" 23 | } 24 | }, 25 | "rules": { 26 | "space-before-function-paren": 0, 27 | "react/prop-types": 0, 28 | "react/jsx-handler-names": 0, 29 | "react/jsx-fragments": 0, 30 | "react/no-unused-prop-types": 0, 31 | "import/export": 0, 32 | "no-use-before-define": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | coverage 24 | storybook-static 25 | .pnp.* 26 | .yarn/* 27 | !.yarn/patches 28 | !.yarn/plugins 29 | !.yarn/releases 30 | !.yarn/sdks 31 | !.yarn/versions 32 | 33 | *storybook.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | next 2 | .now 3 | dist 4 | public 5 | *.json 6 | *.d.ts 7 | *.yml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": true, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none", 10 | "printWidth": 100 11 | } 12 | -------------------------------------------------------------------------------- /.storybook/jsonViewTheme.ts: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming'; 2 | 3 | export default create({ 4 | base: 'light', 5 | brandTitle: 'JsonView storybook', 6 | brandUrl: 'https://anyroad.github.io/react-json-view-lite', 7 | brandImage: '', 8 | brandTarget: '_self' 9 | }); 10 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | 6 | addons: [ 7 | '@storybook/addon-onboarding', 8 | '@storybook/addon-links', 9 | '@storybook/addon-essentials', 10 | '@chromatic-com/storybook', 11 | '@storybook/addon-interactions' 12 | ], 13 | 14 | framework: { 15 | name: '@storybook/react-vite', 16 | options: {} 17 | }, 18 | 19 | core: { 20 | disableTelemetry: true 21 | }, 22 | 23 | docs: {}, 24 | 25 | typescript: { 26 | reactDocgen: 'react-docgen-typescript' 27 | } 28 | }; 29 | export default config; 30 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | import jsonViewTheme from './jsonViewTheme'; 3 | 4 | addons.setConfig({ 5 | isFullscreen: false, 6 | showNav: true, 7 | showPanel: true, 8 | panelPosition: 'bottom', 9 | enableShortcuts: true, 10 | showToolbar: true, 11 | theme: jsonViewTheme, 12 | selectedPanel: undefined, 13 | initialActive: 'sidebar', 14 | sidebar: { 15 | showRoots: false, 16 | collapsedRoots: ['other'] 17 | }, 18 | toolbar: { 19 | title: { hidden: false }, 20 | zoom: { hidden: false }, 21 | eject: { hidden: false }, 22 | copy: { hidden: false }, 23 | fullscreen: { hidden: false } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i 9 | } 10 | } 11 | }, 12 | 13 | tags: ['autodocs'] 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: 3 | - npm install codecov -g 4 | - corepack enable 5 | - yarn init -2 6 | sudo: false 7 | node_js: 8 | - 18 9 | install: 10 | - yarn install 11 | script: 12 | - yarn test 13 | - codecov -f coverage/*.json -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.3.cjs 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.4.1 2 | 3 | ### Bug Fixes 4 | 5 | - [React warning when rendering empty array or object and then changing the `data` property](https://github.com/AnyRoad/react-json-view-lite/issues/47) 6 | 7 | ## 2.4.0 8 | 9 | ### New Features 10 | 11 | - [adds beforeExpandChange hook property](https://github.com/AnyRoad/react-json-view-lite/issues/39) 12 | - [adds properties for aria-label of the collapse/expand toggle](https://github.com/AnyRoad/react-json-view-lite/issues/46) 13 | - [adds style prop to stringify String values to keep escaped characters](https://github.com/AnyRoad/react-json-view-lite/issues/42) 14 | 15 | ## 2.3.0 16 | 17 | ### Bug Fixes 18 | 19 | - [ noQuotesForStringValues not applied to empty string](https://github.com/AnyRoad/react-json-view-lite/issues/45) 20 | 21 | ## 2.2.0 22 | 23 | ### New Features 24 | 25 | - [Officially adds support for the React 19](https://github.com/AnyRoad/react-json-view-lite/pull/43) 26 | - Adds render for the `function` fields 27 | 28 | ### Bug Fixes 29 | 30 | - [Fixes object type detection](https://github.com/AnyRoad/react-json-view-lite/pull/44) 31 | 32 | ## 2.1.0 33 | 34 | ### New Features 35 | 36 | - Adds separate style for the expandable elements container (`childFieldsContainer`) 37 | 38 | ## 2.0.1 39 | 40 | ### Bug Fixes 41 | 42 | - Fixes margin and padding for the expandable elements (because `div` element changed to the `ul`) 43 | 44 | ## 2.0.0 45 | 46 | Major version upgrade. 47 | 48 | ### Breaking Changes 49 | 50 | - Dropped support of React 16 and React 17. Please use versions 1.x.x if your project uses React 16 or React 17. 51 | - Expanding and collapsing nodes with the "space" button changed to navigation and expanding using arrow keys. Left and Right to collapse/expand, Up and Down to move to previous/next collapsable element. 52 | 53 | ### New Features 54 | 55 | - [Always quote objects property names with quotesForFieldNames property](https://github.com/AnyRoad/react-json-view-lite/pull/31) 56 | - [Improved a11y support](https://github.com/AnyRoad/react-json-view-lite/pull/32) 57 | 58 | ## 1.5.0 59 | 60 | ### Bug Fixes 61 | 62 | - [Fixed](https://github.com/AnyRoad/react-json-view-lite/issues/28): Improves empty objects and empty arrays. Also fixes too wide space between two spans having the `punctuation` class (e.g. `] ,` or `[ ]`) 63 | 64 | ## 1.4.0 65 | 66 | ### New Feature 67 | 68 | - [Click on field name to expand node](https://github.com/AnyRoad/react-json-view-lite/pull/27) 69 | 70 | ## 1.3.1 71 | 72 | ### Bug Fixes 73 | 74 | - [Fixed](https://github.com/AnyRoad/react-json-view-lite/issues/24) pressing the spacebar expands/collapses and "pages down" in the browser 75 | 76 | ## 1.3.0 77 | 78 | ### New Feature 79 | 80 | - [New style parameter for not adding double quotes to rendered strings](https://github.com/AnyRoad/react-json-view-lite/issues/22) 81 | 82 | ## 1.2.1 83 | 84 | ### Bug Fixes 85 | 86 | - [Fixed](https://github.com/AnyRoad/react-json-view-lite/issues/20) component didn't work with React 16 and React 17 87 | 88 | ## 1.2.0 89 | 90 | ### New Feature 91 | 92 | - [Improved accessibility support](https://github.com/AnyRoad/react-json-view-lite/pull/16) 93 | 94 | ## 1.1.0 95 | 96 | ### New Feature 97 | 98 | - [Render Date as an ISO-formatted string](https://github.com/AnyRoad/react-json-view-lite/pull/13) 99 | 100 | ## 1.0.1 101 | 102 | ### Bug Fixes 103 | 104 | - [Fixed](https://github.com/AnyRoad/react-json-view-lite/pull/14) collapse/expand button style 105 | 106 | ## 1.0.0 107 | 108 | ### Breaking changes 109 | 110 | 1. Property `shouldInitiallyExpand` has different name `shouldExpandNode` in order to emphasize that it will be called every time properties change. 111 | 2. If you use custom styles: 112 | - `pointer` and `expander` are no longer used 113 | - component uses `collapseIcon`, `expandIcon`, `collapsedContent` styles in order to customize expand/collapse icon and collpased content placeholder which were previously hardcode to the `▸`, `▾` and `...`. 114 | Default style values use `::after` pseudo-classes to set the content. 115 | 116 | ## 0.9.8 117 | 118 | ### Bug Fixes 119 | 120 | - Fixed [bug when empty object key was not rendered correctly](https://github.com/AnyRoad/react-json-view-lite/issues/9) 121 | 122 | ## 0.9.7 123 | 124 | ### New Features 125 | 126 | - Added [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) rendering support 127 | 128 | ## 0.9.6 129 | 130 | ### Bug Fixes 131 | 132 | - Fixed css style for comma after primitive type value 133 | 134 | ## 0.9.5 135 | 136 | ### New Features 137 | 138 | - Added minimum a11y support 139 | - Added React 18 to peer dependencies 140 | - Updated dev dependencies versions 141 | - Added storybook 142 | 143 | ## 0.9.4 144 | 145 | ### New Features 146 | 147 | - Added ability to expand arrays and nested objects by clicking on the `...` part 148 | - Added `allExpanded` and `collapseAllNested` exported functions which can be used for the `shouldInitiallyExpand` property 149 | - Added new separate style for the "pointer" which applied for `▸`, `▾` and `...` 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AnyRoad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | npm 4 | 5 | 6 | no dependencies 7 | 8 | 9 | size 10 | 11 | 12 | build 13 | 14 | 15 | coverage 16 | 17 | 18 | tree-shakeable 19 | 20 | 21 | types included 22 | 23 | 24 | 25 | downloads per month 26 | 27 | 28 |
29 | 30 |
31 | react-json-view-lite is a tiny component for React allowing to render JSON as a tree. It focused on the balance between performance for large JSON inputs and functionality. It might not have all the rich features (suce as customization, copy, json editinng) but still provides more than just rendering json with highlighting - e.g. ability to collapse/expand nested objects and override css. It is written in TypeScript and has no dependencies. 32 |
33 | 34 | ## Install 35 | 36 | ```bash 37 | npm install --save react-json-view-lite 38 | ``` 39 | 40 | ## Version 2.x.x 41 | 42 | Versions 2.x.x supports only React 18 and later. Please use 1.5.0 if your project uses React 16 or 17. 43 | Also version 2 provides better a11y support, collapsing/expanding and navigation through nested elements using arrow keys ("Space" button does not collapse/expand element anymore), but library size increased about 20%. 44 | If your project uses custom styles you will need to add the css for the `childFieldsContainer` property like below: 45 | 46 | ```css 47 | .child-fields-container { 48 | margin: 0; 49 | padding: 0; 50 | } 51 | ``` 52 | 53 | because implementation uses `ul` element `div` instead of the elemenent according to the [w3.org example](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-1a/). 54 | 55 | ## Migration from the 0.9.x versions 56 | 57 | 1. Property `shouldInitiallyExpand` has different name `shouldExpandNode` in order to emphasize that it will be called every time properties change. 58 | 2. If you use custom styles: 59 | - `pointer` and `expander` are no longer used 60 | - component uses `collapseIcon`, `expandIcon`, `collapsedContent` styles in order to customize expand/collapse icon and collpased content placeholder which were previously hardcode to the `▸`, `▾` and `...`. 61 | Default style values use `::after` pseudo-classes to set the content. 62 | 63 | ## Usage 64 | 65 | ```tsx 66 | import * as React from 'react'; 67 | 68 | import { JsonView, allExpanded, darkStyles, defaultStyles } from 'react-json-view-lite'; 69 | import 'react-json-view-lite/dist/index.css'; 70 | 71 | const json = { 72 | a: 1, 73 | b: 'example' 74 | }; 75 | 76 | const App = () => { 77 | return ( 78 | 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export default App; 86 | ``` 87 | 88 | Please note that in JavaScript, an anonymous function like `function() {}` or `() => {}` always creates a different function every time component is rendered, so you might need to use 89 | [useCallback](https://react.dev/reference/react/useCallback) React Hook for the `shouldExpandNode` parameter or extract the function outside the functional component. 90 | 91 | ### StoryBook 92 | 93 | https://anyroad.github.io/react-json-view-lite/ 94 | 95 | ### Props 96 | 97 | | Name | Type | Default Value | Description | 98 | | ------------------ | -------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 99 | | data | `Object` \| `Array` | | Data which should be rendered | 100 | | style | StyleProps | defaultStyles | Optional. CSS classes for rendering. Library provides two build-in implementations: `darkStyles`, `defaultStyles` (see below) | 101 | | shouldExpandNode | `(level: number, value: any, field?: string) => boolean` | allExpanded | Optional. Function which will be called during initial rendering for each Object and Array of the data in order to calculate should if this node be expanded. **Note** that this function will be called again to update the each node state once the property value changed. `level` startes from `0`, `field` does not have a value for the array element. Library provides two build-in implementations: `allExpanded` and `collapseAllNested` (see below) | 102 | | clickToExpandNode | boolean | false | Optional. Set to true if you want to expand/collapse nodes by clicking on the node itself. | 103 | | beforeExpandChange | (event: NodeExpandingEvent) => boolean | undefined | Optional. Function which will be called before node expanded or collapsed. If the function returns `true` then expand/collapse process goes as usual, if it returns `false` then node state stay the same. For example, you can return `false` to change `shouldExpandNode` property in order to open only desired children nodes. | 104 | 105 | ## interface NodeExpandingEvent 106 | 107 | | Field Name | Type | Description | 108 | | -------------- | ------- | --------------------------------------------------------------- | 109 | | level | number | level of expanded/collapsed node | 110 | | value | any | Field value (object or array) to be expaneded/collapsed | 111 | | field | string? | Field name | 112 | | newExpandValue | boolean | if node is about to be expanded (`true`) or collapsed (`false`) | 113 | 114 | ## interface AriaLabels 115 | 116 | | Field Name | Type | Description | 117 | | ------------ | ------ | --------------------------------------------------------------------------------------- | 118 | | collapseJson | string | `aria-label` property for the "collapse" node button. Default value is "collapse JSON". | 119 | | expandJson | string | `aria-label` property for the "expand" node button. Default value is "expand JSON". | 120 | 121 | ### Extra exported 122 | 123 | | Name | Type | Description | 124 | | ----------------- | ---------------------------- | --------------------------------------------------- | 125 | | defaultStyles | StyleProps | Default styles for light background | 126 | | darkStyles | StyleProps | Default styles for dark background | 127 | | allExpanded | `() => boolean` | Always returns `true` | 128 | | collapseAllNested | `(level: number) => boolean` | Returns `true` only for the first level (`level=0`) | 129 | 130 | ### StyleProps 131 | 132 | | Name | Type | Description | 133 | | ----------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | 134 | | container | string | CSS class name for rendering parent block | 135 | | childFieldsContainer | string | CSS class name for rendering parent block of array or object | 136 | | basicChildStyle | string | CSS class name for property block containing property name and value | 137 | | collapseIcon | string | CSS class name for rendering button collapsing Object and Array nodes. Default content is `▾`. | 138 | | expandIcon | string | CSS class name for rendering button expanding Object and Array nodes. Default content is `▸`. | 139 | | collapsedContent | string | CSS class name for rendering placeholder when Object and Array nodes are collapsed. Default contents is `...`. | 140 | | label | string | CSS class name for rendering property names | 141 | | clickableLabel | string | CSS class name for rendering clickable property names (requires the `clickToExpandNode` prop to be true) | 142 | | nullValue | string | CSS class name for rendering null values | 143 | | undefinedValue | string | CSS class name for rendering undefined values | 144 | | numberValue | string | CSS class name for rendering numeric values | 145 | | stringValue | string | CSS class name for rendering string values | 146 | | booleanValue | string | CSS class name for rendering boolean values | 147 | | otherValue | string | CSS class name for rendering all other values except Object, Arrray, null, undefined, numeric, boolean and string | 148 | | punctuation | string | CSS class name for rendering `,`, `[`, `]`, `{`, `}` | 149 | | noQuotesForStringValues | boolean | whether or not to add double quotes when rendering string values, default value is `false` | 150 | | quotesForFieldNames | boolean | whether or not to add double quotes when rendering field names, default value is `false` | 151 | | ariaLables | AriaLables | Text to use for the `aria-label` properties | 152 | | stringifyStringValues | boolean | whether or not to call `JSON.stringify` for string values in order to preserve escaped string characters like new line, tab or quotes | 153 | 154 | ## Comparison with other libraries 155 | 156 | ### Size and dependencies 157 | 158 | Here is the size benchmark (using [bundlephobia.com](https://bundlephobia.com)) against similar React libraries (found by https://www.npmjs.com/search?q=react%20json&ranking=popularity): 159 | 160 | | Library | Bundle size | Bundle size (gzip) | Dependencies | 161 | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | 162 | | **react-json-view-lite** | [![](https://badgen.net/bundlephobia/min/react-json-view-lite?color=6ead0a&label=)](https://bundlephobia.com/result?p=react-json-view-lite) | [![](https://badgen.net/bundlephobia/minzip/react-json-view-lite?color=6ead0a&label=)](https://bundlephobia.com/result?p=react-json-view-lite) | [![](https://badgen.net/bundlephobia/dependency-count/react-json-view-lite?color=6ead0a&label=)](https://bundlephobia.com/result?p=react-json-view-lite) | 163 | | react-json-pretty | [![](https://badgen.net/bundlephobia/min/react-json-pretty?color=red&label=)](https://bundlephobia.com/result?p=react-json-pretty) | [![](https://badgen.net/bundlephobia/minzip/react-json-pretty?color=red&label=)](https://bundlephobia.com/result?p=react-json-pretty) | [![](https://badgen.net/bundlephobia/dependency-count/react-json-pretty?color=red&label=)](https://bundlephobia.com/result?p=react-json-pretty) | 164 | | react-json-inspector | [![](https://badgen.net/bundlephobia/min/react-json-inspector?color=red&label=)](https://bundlephobia.com/result?p=react-json-inspector) | [![](https://badgen.net/bundlephobia/minzip/react-json-inspector?color=red&label=)](https://bundlephobia.com/result?p=react-json-inspector) | [![](https://badgen.net/bundlephobia/dependency-count/react-json-inspector?color=red&label=)](https://bundlephobia.com/result?p=react-json-inspector) | 165 | | react-json-tree | [![](https://badgen.net/bundlephobia/min/react-json-tree?color=red&label=)](https://bundlephobia.com/result?p=react-json-tree) | [![](https://badgen.net/bundlephobia/minzip/react-json-tree?color=red&label=)](https://bundlephobia.com/result?p=react-json-tree) | [![](https://badgen.net/bundlephobia/dependency-count/react-json-tree?color=red&label=)](https://bundlephobia.com/result?p=react-json-tree) | 166 | | react-json-view | [![](https://badgen.net/bundlephobia/min/react-json-view?color=red&label=)](https://bundlephobia.com/result?p=react-json-view) | [![](https://badgen.net/bundlephobia/minzip/react-json-view?color=red&label=)](https://bundlephobia.com/result?p=react-json-view) | [![](https://badgen.net/bundlephobia/dependency-count/react-json-view?color=red&label=)](https://bundlephobia.com/result?p=react-json-view) | 167 | | react-json-tree-viewer | [![](https://badgen.net/bundlephobia/min/react-json-tree-viewer?color=red&label=)](https://bundlephobia.com/result?p=react-json-tree-viewer) | [![](https://badgen.net/bundlephobia/minzip/react-json-tree-viewer?color=red&label=)](https://bundlephobia.com/result?p=react-json-tree-viewer) | [![](https://badgen.net/bundlephobia/dependency-count/react-json-tree-viewer?color=red&label=)](https://bundlephobia.com/result?p=react-json-tree-viewer) | 168 | 169 | ### Performance 170 | 171 | Performance was mesaured using the [react-component-benchmark](https://github.com/paularmstrong/react-component-benchmark) library. Every component was rendered 50 times using the [300Kb json file](https://github.com/AnyRoad/react-json-view-lite-benchmark/blob/main/src/hugeJson.json) as data source, please refer to source code of the [benchmark project](https://github.com/AnyRoad/react-json-view-lite-benchmark). 172 | All numbers are in milliseconds. Tests were performed on Macbook Air M1 16Gb RAM usging Chrome v96.0.4664.110(official build, arm64). Every component was tested 2 times but there was no significant differences in the results. 173 | 174 | | Library | Min | Max | Average | Median | P90 | 175 | | ------------------------ | ----- | ----- | ------- | ------ | ----- | 176 | | **react-json-view-lite** | 81 | 604 | 195 | 82 | 582 | 177 | | react-json-pretty | 22 | 59 | 32 | 24 | 56 | 178 | | react-json-inspector | 682 | 1 109 | 758 | 711 | 905 | 179 | | react-json-tree | 565 | 1 217 | 658 | 620 | 741 | 180 | | react-json-view | 1 403 | 1 722 | 1529 | 1 540 | 1 631 | 181 | | react-json-tree-viewer | 266 | 663 | 320 | 278 | 455 | 182 | 183 | As you can see `react-json-pretty` renders faster than other libraries but it does not have ability to collapse/expand nested objects so it might be good choice if you need just json syntax highlighting. 184 | 185 | ## License 186 | 187 | MIT © [AnyRoad](https://github.com/AnyRoad) 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-json-view-lite", 3 | "version": "2.4.1", 4 | "description": "JSON viewer component for React focused on performance for large volume input while still providing few customiziation features", 5 | "homepage": "https://github.com/AnyRoad/react-json-view-lite", 6 | "author": "AnyRoad", 7 | "license": "MIT", 8 | "keywords": [ 9 | "react", 10 | "json", 11 | "component", 12 | "view", 13 | "json-view", 14 | "json-tree", 15 | "lite" 16 | ], 17 | "repository": "AnyRoad/react-json-view-lite", 18 | "main": "dist/index.js", 19 | "types": "./dist/index.d.ts", 20 | "module": "dist/index.modern.js", 21 | "source": "src/index.tsx", 22 | "engines": { 23 | "node": ">=18" 24 | }, 25 | "scripts": { 26 | "build": "microbundle-crl --no-compress --format modern,cjs", 27 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 28 | "prepare": "run-s build", 29 | "test": "run-s test:unit test:lint test:build", 30 | "test:build": "run-s build", 31 | "test:lint": "eslint .", 32 | "test:unit": "cross-env CI=1 jest --env=jsdom --coverage", 33 | "predeploy": "npm run build-storybook", 34 | "deploy-storybook": "gh-pages -d storybook-static", 35 | "storybook": "storybook dev -p 6006", 36 | "build-storybook": "storybook build" 37 | }, 38 | "peerDependencies": { 39 | "react": "^18.0.0 || ^19.0.0" 40 | }, 41 | "devDependencies": { 42 | "@babel/preset-typescript": "^7.24.7", 43 | "@chromatic-com/storybook": "^3.2.3", 44 | "@storybook/addon-essentials": "^8.6.4", 45 | "@storybook/addon-interactions": "^8.6.4", 46 | "@storybook/addon-links": "^8.6.4", 47 | "@storybook/blocks": "^8.6.4", 48 | "@storybook/manager-api": "^8.6.4", 49 | "@storybook/react": "^8.6.4", 50 | "@storybook/react-vite": "^8.6.4", 51 | "@storybook/test": "^8.6.4", 52 | "@storybook/theming": "^8.6.4", 53 | "@testing-library/jest-dom": "^6.5.0", 54 | "@testing-library/react": "^16.0.1", 55 | "@testing-library/user-event": "^14.5.0", 56 | "@types/jest": "29.5.13", 57 | "@types/node": "18.19.50", 58 | "@types/react": "18.3.5", 59 | "@types/react-dom": "18.2.7", 60 | "@typescript-eslint/eslint-plugin": "^8.7.0", 61 | "@typescript-eslint/parser": "^8.7.0", 62 | "cross-env": "7.0.3", 63 | "eslint": "8.57.0", 64 | "eslint-config-prettier": "9.1.0", 65 | "eslint-config-standard": "17.1.0", 66 | "eslint-config-standard-react": "13.0.0", 67 | "eslint-plugin-import": "2.30.0", 68 | "eslint-plugin-jsx-a11y": "^6.10.0", 69 | "eslint-plugin-n": "^17.10.0", 70 | "eslint-plugin-node": "11.1.0", 71 | "eslint-plugin-prettier": "^5.2.0", 72 | "eslint-plugin-promise": "^7.1.0", 73 | "eslint-plugin-react": "^7.36.0", 74 | "eslint-plugin-react-hooks": "^4.6.2", 75 | "eslint-plugin-standard": "5.0.0", 76 | "eslint-plugin-storybook": "^0.11.2", 77 | "gh-pages": "6.1.1", 78 | "identity-obj-proxy": "^3.0.0", 79 | "jest": "^29.7.0", 80 | "jest-environment-jsdom": "^29.7.0", 81 | "microbundle-crl": "0.13.11", 82 | "npm-run-all": "4.1.5", 83 | "postcss": "^8.4.47", 84 | "prettier": "3.3.3", 85 | "react": "^18.3.1", 86 | "react-dom": "^18.3.1", 87 | "rollup-jest": "^3.1.0", 88 | "storybook": "^8.6.4", 89 | "ts-jest": "29.1.2", 90 | "typescript": "5.6.3" 91 | }, 92 | "files": [ 93 | "dist" 94 | ], 95 | "jest": { 96 | "collectCoverageFrom": [ 97 | "src/*.{ts,tsx}", 98 | "!/node_modules/" 99 | ], 100 | "moduleNameMapper": { 101 | "^.+\\.(css|less|scss)$": "identity-obj-proxy" 102 | } 103 | }, 104 | "packageManager": "yarn@3.6.3", 105 | "eslintConfig": { 106 | "extends": [ 107 | "plugin:storybook/recommended" 108 | ] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/DataRenderer.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import DataRender, { JsonRenderProps } from './DataRenderer'; 3 | import { allExpanded, collapseAllNested, defaultStyles } from './index'; 4 | import { render, screen, fireEvent } from '@testing-library/react'; 5 | import '@testing-library/jest-dom'; 6 | 7 | const commonProps: Omit, 'outerRef'> = { 8 | lastElement: false, 9 | level: 0, 10 | style: { 11 | container: '', 12 | childFieldsContainer: '', 13 | basicChildStyle: '', 14 | label: '', 15 | clickableLabel: defaultStyles.clickableLabel, 16 | nullValue: '', 17 | undefinedValue: '', 18 | numberValue: '', 19 | stringValue: '', 20 | booleanValue: '', 21 | otherValue: '', 22 | punctuation: '', 23 | expandIcon: defaultStyles.expandIcon, 24 | collapseIcon: defaultStyles.collapseIcon, 25 | collapsedContent: defaultStyles.collapsedContent, 26 | noQuotesForStringValues: false, 27 | ariaLables: { 28 | expandJson: 'expand', 29 | collapseJson: 'collapse' 30 | }, 31 | stringifyStringValues: false 32 | }, 33 | shouldExpandNode: allExpanded, 34 | clickToExpandNode: false, 35 | value: undefined, 36 | field: undefined 37 | }; 38 | 39 | const WrappedDataRenderer = (testProps: Partial>) => { 40 | const ref = React.useRef(null); 41 | return ( 42 |
43 | 44 |
45 | ); 46 | }; 47 | 48 | const NoRefWrappedDataRenderer = (testProps: Partial>) => { 49 | const ref = React.useRef(null); 50 | return ( 51 |
52 | 53 |
54 | ); 55 | }; 56 | 57 | const collapseAll = () => false; 58 | 59 | const testButtonsCollapsed = () => { 60 | const buttons = screen.getAllByRole('button', { hidden: true }); 61 | expect(buttons).toHaveLength(1); 62 | expect(buttons[0]).toHaveClass('expand-icon-light'); 63 | expect(buttons[0]).not.toHaveClass('collapse-icon-light'); 64 | expect(buttons[0]).toHaveAttribute('aria-expanded', 'false'); 65 | return buttons; 66 | }; 67 | 68 | const testButtonsExpanded = () => { 69 | const buttons = screen.getAllByRole('button', { hidden: true }); 70 | expect(buttons).toHaveLength(1); 71 | expect(buttons[0]).toHaveClass('collapse-icon-light'); 72 | expect(buttons[0]).not.toHaveClass('expand-icon-light'); 73 | expect(buttons[0]).toHaveAttribute('aria-expanded', 'true'); 74 | return buttons; 75 | }; 76 | 77 | const testButtonsIfEmpty = () => { 78 | expect(() => { 79 | screen.getAllByRole('button', { hidden: true }); 80 | }).toThrow(); 81 | }; 82 | 83 | describe('DataRender', () => { 84 | it('should render booleans: true', () => { 85 | render(); 86 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 87 | expect(screen.getByText('true')).toBeInTheDocument(); 88 | }); 89 | 90 | it('should render booleans: false', () => { 91 | render(); 92 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 93 | expect(screen.getByText('false')).toBeInTheDocument(); 94 | }); 95 | 96 | it('should render strings', () => { 97 | render(); 98 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 99 | expect(screen.getByText(`"string"`)).toBeInTheDocument(); 100 | }); 101 | 102 | it('should render and stringify strings', () => { 103 | render( 104 | 108 | ); 109 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 110 | expect( 111 | screen.queryByText('"one\\n\'two\'\\tthree.\\r\\n\\"another line\\""') 112 | ).toBeInTheDocument(); 113 | }); 114 | 115 | it('should render strings without quotes', () => { 116 | render( 117 | 121 | ); 122 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 123 | expect(screen.getByText(`string`)).toBeInTheDocument(); 124 | expect(screen.queryByText(`"string"`)).not.toBeInTheDocument(); 125 | expect(screen.getByText(/emtpy:/)).toBeInTheDocument(); 126 | expect(screen.queryByText(`""`)).not.toBeInTheDocument(); 127 | }); 128 | 129 | it('should render strings with quotes if noQuotesForStringValues is undefined', () => { 130 | render( 131 | 135 | ); 136 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 137 | expect(screen.getByText(`"string"`)).toBeInTheDocument(); 138 | }); 139 | 140 | it('should render field names without quotes if quotesForFieldNames is undefined', () => { 141 | render( 142 | 146 | ); 147 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 148 | }); 149 | 150 | it('should render field names with quotes if quotesForFieldNames is true', () => { 151 | render( 152 | 156 | ); 157 | expect(screen.getByText(/"test":/)).toBeInTheDocument(); 158 | }); 159 | 160 | it('should render numbers', () => { 161 | render(); 162 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 163 | expect(screen.getByText('42')).toBeInTheDocument(); 164 | }); 165 | 166 | it('should render bigints', () => { 167 | render(); 168 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 169 | expect(screen.getByText('42n')).toBeInTheDocument(); 170 | }); 171 | 172 | it('should render functions', () => { 173 | render( {} }} />); 174 | expect(screen.getByText(/func:/)).toBeInTheDocument(); 175 | expect(screen.getByText('function() { }')).toBeInTheDocument(); 176 | }); 177 | 178 | it('should render dates', () => { 179 | render(); 180 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 181 | expect(screen.getByText('1970-01-01T00:00:00.000Z')).toBeInTheDocument(); 182 | }); 183 | 184 | it('should render nulls', () => { 185 | render(); 186 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 187 | expect(screen.getByText('null')).toBeInTheDocument(); 188 | }); 189 | 190 | it('should render undefineds', () => { 191 | render(); 192 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 193 | expect(screen.getByText('undefined')).toBeInTheDocument(); 194 | }); 195 | 196 | it('should render unknown types', () => { 197 | render(); 198 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 199 | expect(screen.getByText(/2020/)).toBeInTheDocument(); 200 | }); 201 | 202 | it('should render object with empty key string', () => { 203 | render(); 204 | expect(screen.getByText(/"":/)).toBeInTheDocument(); 205 | expect(screen.getByText(/empty key/)).toBeInTheDocument(); 206 | }); 207 | 208 | it('should render empty objects', () => { 209 | render(); 210 | expect(screen.getByText('{')).toBeInTheDocument(); 211 | expect(screen.getByText('}')).toBeInTheDocument(); 212 | }); 213 | 214 | it('should render nested empty objects', () => { 215 | render(); 216 | expect(screen.getByText('nested:')).toBeInTheDocument(); 217 | expect(screen.getByText('{')).toBeInTheDocument(); 218 | expect(screen.getByText('}')).toBeInTheDocument(); 219 | }); 220 | 221 | it('should not expand empty objects', () => { 222 | render(); 223 | testButtonsIfEmpty(); 224 | }); 225 | 226 | it('should not collapse empty objects', () => { 227 | render(); 228 | testButtonsIfEmpty(); 229 | }); 230 | 231 | it('should render empty arrays', () => { 232 | render(); 233 | expect(screen.getByText('[')).toBeInTheDocument(); 234 | expect(screen.getByText(']')).toBeInTheDocument(); 235 | }); 236 | 237 | it('should render nested empty arrays', () => { 238 | render(); 239 | expect(screen.getByText('nested:')).toBeInTheDocument(); 240 | expect(screen.getByText('[')).toBeInTheDocument(); 241 | expect(screen.getByText(']')).toBeInTheDocument(); 242 | }); 243 | 244 | it('should not expand empty arrays', () => { 245 | render(); 246 | testButtonsIfEmpty(); 247 | }); 248 | 249 | it('should not collapse empty arrays', () => { 250 | render(); 251 | testButtonsIfEmpty(); 252 | }); 253 | 254 | it('should render arrays', () => { 255 | render(); 256 | expect(screen.getByText('1')).toBeInTheDocument(); 257 | expect(screen.getByText('2')).toBeInTheDocument(); 258 | expect(screen.getByText('3')).toBeInTheDocument(); 259 | }); 260 | 261 | it('should render arrays with key', () => { 262 | render(); 263 | expect(screen.getByText('1')).toBeInTheDocument(); 264 | expect(screen.getByText('2')).toBeInTheDocument(); 265 | expect(screen.getByText('3')).toBeInTheDocument(); 266 | }); 267 | 268 | it('should render nested objects', () => { 269 | render(); 270 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 271 | expect(screen.getByText('123')).toBeInTheDocument(); 272 | }); 273 | 274 | it('should render nested objects collapsed', () => { 275 | render( 276 | 277 | ); 278 | expect(screen.getByText(/obj/)).toBeInTheDocument(); 279 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 280 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 281 | }); 282 | 283 | it('should render nested objects collapsed and expand it once property changed', () => { 284 | const { rerender } = render( 285 | 286 | ); 287 | expect(screen.getByText(/obj/)).toBeInTheDocument(); 288 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 289 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 290 | 291 | rerender(); 292 | expect(screen.getByText(/obj/)).toBeInTheDocument(); 293 | expect(screen.queryByText(/test:/)).toBeInTheDocument(); 294 | expect(screen.queryByText('123')).toBeInTheDocument(); 295 | }); 296 | 297 | it('should render nested arrays collapsed', () => { 298 | render(); 299 | expect(screen.queryByText(/test:/)).toBeInTheDocument(); 300 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 301 | }); 302 | 303 | it('should render nested arrays collapsed and expand it once property changed', () => { 304 | const { rerender } = render( 305 | 306 | ); 307 | expect(screen.queryByText(/test:/)).toBeInTheDocument(); 308 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 309 | 310 | rerender(); 311 | expect(screen.queryByText(/test:/)).toBeInTheDocument(); 312 | expect(screen.queryByText('123')).toBeInTheDocument(); 313 | }); 314 | 315 | it('should render top arrays collapsed', () => { 316 | render(); 317 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 318 | }); 319 | 320 | it('should collapse and expand objects by clicking on icon', () => { 321 | render(); 322 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 323 | let buttons = testButtonsExpanded(); 324 | fireEvent.click(buttons[0]); 325 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 326 | buttons = testButtonsCollapsed(); 327 | fireEvent.click(buttons[0]); 328 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 329 | }); 330 | 331 | it('should collapse and expand objects by clicking on node', () => { 332 | const { container } = render( 333 | 338 | ); 339 | 340 | // open the 'test' node by clicking the icon 341 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 342 | expect(screen.queryByText(/child/)).not.toBeInTheDocument(); 343 | let buttons = testButtonsCollapsed(); 344 | fireEvent.click(buttons[0]); 345 | 346 | buttons = screen.getAllByRole('button', { hidden: true }); 347 | expect(buttons.length).toBe(2); 348 | expect(buttons[0]).toHaveClass('collapse-icon-light'); 349 | expect(buttons[1]).toHaveClass('expand-icon-light'); 350 | expect(buttons[0].tabIndex).toEqual(0); 351 | expect(buttons[1].tabIndex).toEqual(-1); 352 | expect(container.getElementsByClassName('clickable-label-light')).toHaveLength(1); 353 | expect(container.getElementsByClassName('collapsed-content-light')).toHaveLength(1); 354 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 355 | expect(screen.queryByText(/child/)).not.toBeInTheDocument(); 356 | fireEvent.click(buttons[0]); 357 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 358 | expect(screen.queryByText(/child/)).not.toBeInTheDocument(); 359 | }); 360 | 361 | it('should expand objects by clicking on collapsed content', () => { 362 | const { container } = render( 363 | 364 | ); 365 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 366 | testButtonsCollapsed(); 367 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 368 | fireEvent.click(collapsedContent[0]); 369 | testButtonsExpanded(); 370 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 371 | }); 372 | 373 | it('should collapse and expand arrays by clicking on icon', () => { 374 | render(); 375 | expect(screen.getByText('1')).toBeInTheDocument(); 376 | let buttons = testButtonsExpanded(); 377 | fireEvent.click(buttons[0]); 378 | expect(screen.queryByText('1')).not.toBeInTheDocument(); 379 | buttons = testButtonsCollapsed(); 380 | fireEvent.click(buttons[0]); 381 | expect(screen.getByText('1')).toBeInTheDocument(); 382 | }); 383 | 384 | it('should expand arrays by clicking on collapsed content', () => { 385 | const { container } = render( 386 | 387 | ); 388 | expect(screen.queryByText('1')).not.toBeInTheDocument(); 389 | testButtonsCollapsed(); 390 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 391 | fireEvent.click(collapsedContent[0]); 392 | testButtonsExpanded(); 393 | expect(screen.getByText('1')).toBeInTheDocument(); 394 | }); 395 | 396 | it('should handle node click event without outerRef value', () => { 397 | const { container } = render( 398 | 399 | ); 400 | expect(screen.queryByText('1')).not.toBeInTheDocument(); 401 | testButtonsCollapsed(); 402 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 403 | fireEvent.click(collapsedContent[0]); 404 | testButtonsExpanded(); 405 | expect(screen.getByText('1')).toBeInTheDocument(); 406 | }); 407 | 408 | it('should handle node click without current tabIndex=0 element', () => { 409 | const { container } = render( 410 | 411 | ); 412 | expect(container.querySelectorAll('[role=button][tabindex="0"]')).toHaveLength(0); 413 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 414 | fireEvent.click(collapsedContent[0]); 415 | testButtonsExpanded(); 416 | expect(screen.getByText('1')).toBeInTheDocument(); 417 | }); 418 | 419 | it('should expand objects by pressing ArrowRight on icon, collapse objects by pressing ArrowLeft on icon', () => { 420 | render(); 421 | 422 | const buttons = testButtonsCollapsed(); 423 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 424 | 425 | fireEvent.keyDown(buttons[0], { key: 'ArrowRight', code: 'ArrowRight' }); 426 | testButtonsExpanded(); 427 | expect(screen.getByText(/test:/)).toBeInTheDocument(); 428 | 429 | fireEvent.keyDown(buttons[0], { key: 'ArrowLeft', code: 'ArrowLeft' }); 430 | testButtonsCollapsed(); 431 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 432 | }); 433 | 434 | it('should not expand objects by pressing other keys on icon', () => { 435 | render(); 436 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 437 | const buttons = testButtonsCollapsed(); 438 | fireEvent.keyDown(buttons[0], { key: 'Enter', code: 'Enter' }); 439 | fireEvent.keyDown(buttons[0], { key: ' ', code: 'Space' }); 440 | testButtonsCollapsed(); 441 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 442 | }); 443 | 444 | it('should expand arrays by pressing ArrowRight on icon, collapse arrays by pressing ArrowLeft on icon', () => { 445 | render(); 446 | 447 | const buttons = testButtonsCollapsed(); 448 | expect(screen.queryByText(/test/)).not.toBeInTheDocument(); 449 | expect(screen.queryByText(/array/)).not.toBeInTheDocument(); 450 | 451 | fireEvent.keyDown(buttons[0], { key: 'ArrowRight', code: 'ArrowRight' }); 452 | testButtonsExpanded(); 453 | expect(screen.getByText(/test/)).toBeInTheDocument(); 454 | expect(screen.getByText(/array/)).toBeInTheDocument(); 455 | 456 | fireEvent.keyDown(buttons[0], { key: 'ArrowLeft', code: 'ArrowLeft' }); 457 | testButtonsCollapsed(); 458 | expect(screen.queryByText(/test/)).not.toBeInTheDocument(); 459 | expect(screen.queryByText(/array/)).not.toBeInTheDocument(); 460 | }); 461 | 462 | it('should not expand arrays by pressing other keys on icon', () => { 463 | render(); 464 | const buttons = testButtonsCollapsed(); 465 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 466 | expect(screen.queryByText(/array/)).not.toBeInTheDocument(); 467 | fireEvent.keyDown(buttons[0], { key: 'Enter', code: 'Enter' }); 468 | fireEvent.keyDown(buttons[0], { key: ' ', code: 'Space' }); 469 | testButtonsCollapsed(); 470 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 471 | expect(screen.queryByText(/array/)).not.toBeInTheDocument(); 472 | }); 473 | 474 | it('only one item with tabindex=0 if level=0, none if level>0', () => { 475 | const data = { test: [1, 2, 3], test2: [1, 2, 3], test3: { a: 'b', c: { d: '1', a: 2 } } }; 476 | 477 | const { container, rerender } = render(); 478 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 479 | 480 | rerender(); 481 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(0); 482 | }); 483 | 484 | it('maintain only one item with tabindex=0 after pressing up and down arrow keys', () => { 485 | const data = { test: [1, 2, 3], test2: [1, 2, 3], test3: { a: 'b', c: { d: '1', a: 2 } } }; 486 | 487 | const { container } = render(); 488 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 489 | 490 | const buttons = screen.getAllByRole('button', { hidden: true }); 491 | fireEvent.keyDown(buttons[0], { key: 'ArrowDown', code: 'ArrowDown' }); 492 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 493 | fireEvent.keyDown(buttons[1], { key: 'ArrowDown', code: 'ArrowDown' }); 494 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 495 | 496 | fireEvent.keyDown(buttons[1], { key: 'ArrowUp', code: 'ArrowUp' }); 497 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 498 | fireEvent.keyDown(buttons[0], { key: 'ArrowUp', code: 'ArrowUp' }); 499 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 500 | }); 501 | 502 | it('maintain only one item with tabindex=0 after pressing up and down arrow keys without current element', () => { 503 | const data = { test: [1, 2, 3], test2: [1, 2, 3], test3: { a: 'b', c: { d: '1', a: 2 } } }; 504 | 505 | const { container } = render(); 506 | 507 | const buttons = screen.getAllByRole('button', { hidden: true }); 508 | fireEvent.keyDown(buttons[0], { key: 'ArrowDown', code: 'ArrowDown' }); 509 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(0); 510 | }); 511 | 512 | it('handle pressing up and down arrow keys without outer ref', () => { 513 | const data = { test: [1, 2, 3], test2: [1, 2, 3], test3: { a: 'b', c: { d: '1', a: 2 } } }; 514 | 515 | const { container } = render(); 516 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 517 | 518 | const buttons = screen.getAllByRole('button', { hidden: true }); 519 | fireEvent.keyDown(buttons[0], { key: 'ArrowDown', code: 'ArrowDown' }); 520 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 521 | fireEvent.keyDown(buttons[1], { key: 'ArrowDown', code: 'ArrowDown' }); 522 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 523 | 524 | fireEvent.keyDown(buttons[1], { key: 'ArrowUp', code: 'ArrowUp' }); 525 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 526 | fireEvent.keyDown(buttons[0], { key: 'ArrowUp', code: 'ArrowUp' }); 527 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 528 | }); 529 | 530 | it('should stop expanding if beforeExpandChange returned false', () => { 531 | const { container } = render( 532 | false} 536 | /> 537 | ); 538 | expect(screen.queryByText(/obj/)).not.toBeInTheDocument(); 539 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 540 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 541 | 542 | testButtonsCollapsed(); 543 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 544 | fireEvent.click(collapsedContent[0]); 545 | testButtonsCollapsed(); 546 | expect(screen.queryByText(/obj/)).not.toBeInTheDocument(); 547 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 548 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 549 | }); 550 | 551 | it('should continue expanding if beforeExpandChange returned true', () => { 552 | const { container } = render( 553 | true} 557 | /> 558 | ); 559 | expect(screen.queryByText(/obj/)).not.toBeInTheDocument(); 560 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 561 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 562 | 563 | testButtonsCollapsed(); 564 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 565 | fireEvent.click(collapsedContent[0]); 566 | expect(screen.getByText(/obj/)).toBeInTheDocument(); 567 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 568 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 569 | }); 570 | 571 | it('should stop expanding if beforeExpandChange returned false and render with new shouldExpandNode value', () => { 572 | let level = null; 573 | let field = null; 574 | let value = null; 575 | let expanded = null; 576 | const inputData = { obj: { test: 123 } }; 577 | 578 | const { container, rerender } = render( 579 | { 583 | level = event.level; 584 | field = event.field; 585 | value = event.value; 586 | expanded = event.newExpandValue; 587 | return false; 588 | }} 589 | /> 590 | ); 591 | expect(screen.queryByText(/obj/)).not.toBeInTheDocument(); 592 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 593 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 594 | 595 | testButtonsCollapsed(); 596 | const collapsedContent = container.getElementsByClassName(commonProps.style.collapsedContent); 597 | fireEvent.click(collapsedContent[0]); 598 | testButtonsCollapsed(); 599 | 600 | expect(level).toBe(0); 601 | expect(expanded).toBe(true); 602 | expect(field).toBeUndefined(); 603 | expect(value).toBe(inputData); 604 | 605 | rerender( 606 | false} 610 | /> 611 | ); 612 | 613 | expect(screen.getByText(/obj/)).toBeInTheDocument(); 614 | expect(screen.queryByText(/test:/)).not.toBeInTheDocument(); 615 | expect(screen.queryByText('123')).not.toBeInTheDocument(); 616 | }); 617 | }); 618 | -------------------------------------------------------------------------------- /src/DataRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as DataTypeDetection from './DataTypeDetection'; 3 | import { AriaLabels, NodeExpandingEvent } from '.'; 4 | 5 | export interface StyleProps { 6 | container: string; 7 | basicChildStyle: string; 8 | label: string; 9 | clickableLabel: string; 10 | nullValue: string; 11 | undefinedValue: string; 12 | numberValue: string; 13 | stringValue: string; 14 | booleanValue: string; 15 | otherValue: string; 16 | punctuation: string; 17 | expandIcon: string; 18 | collapseIcon: string; 19 | collapsedContent: string; 20 | childFieldsContainer: string; 21 | noQuotesForStringValues?: boolean; 22 | quotesForFieldNames?: boolean; 23 | ariaLables: AriaLabels; 24 | stringifyStringValues: boolean; 25 | } 26 | 27 | interface CommonRenderProps { 28 | lastElement: boolean; 29 | /** There should only be one node with `level==0`. */ 30 | level: number; 31 | style: StyleProps; 32 | shouldExpandNode: (level: number, value: any, field?: string) => boolean; 33 | clickToExpandNode: boolean; 34 | outerRef: React.RefObject; 35 | beforeExpandChange?: (event: NodeExpandingEvent) => boolean; 36 | } 37 | 38 | export interface JsonRenderProps extends CommonRenderProps { 39 | field?: string; 40 | value: T; 41 | } 42 | 43 | export interface ExpandableRenderProps extends CommonRenderProps { 44 | field: string | undefined; 45 | value: Array | object; 46 | data: Array<[string | undefined, any]>; 47 | openBracket: string; 48 | closeBracket: string; 49 | } 50 | 51 | // still keep quotes for the field names if it is empty string 52 | // but do not wrap with quotes for the empty string values 53 | // in the quoteStringValue function because it can cause 54 | // double quotes for the custom css styles. 55 | function quoteString(value: string, quoted = false) { 56 | return !value || quoted ? `"${value}"` : value; 57 | } 58 | 59 | function quoteStringValue(value: string, quoted: boolean, stringify: boolean) { 60 | if (stringify) { 61 | return JSON.stringify(value); 62 | } 63 | return quoted ? `"${value}"` : value; 64 | } 65 | 66 | function ExpandableObject({ 67 | field, 68 | value, 69 | data, 70 | lastElement, 71 | openBracket, 72 | closeBracket, 73 | level, 74 | style, 75 | shouldExpandNode, 76 | clickToExpandNode, 77 | outerRef, 78 | beforeExpandChange 79 | }: ExpandableRenderProps) { 80 | // follows tree example for role structure and keypress actions: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-1a/ 81 | 82 | const shouldExpandNodeCalledRef = React.useRef(false); 83 | const [expanded, setExpanded] = React.useState(() => shouldExpandNode(level, value, field)); 84 | const expanderButtonRef = React.useRef(null); 85 | 86 | React.useEffect(() => { 87 | if (!shouldExpandNodeCalledRef.current) { 88 | shouldExpandNodeCalledRef.current = true; 89 | } else { 90 | setExpanded(shouldExpandNode(level, value, field)); 91 | } 92 | // eslint-disable-next-line react-hooks/exhaustive-deps 93 | }, [shouldExpandNode]); 94 | 95 | const contentsId = React.useId(); 96 | 97 | if (data.length === 0) { 98 | return EmptyObject({ field, openBracket, closeBracket, lastElement, style }); 99 | } 100 | 101 | const expanderIconStyle = expanded ? style.collapseIcon : style.expandIcon; 102 | const ariaLabel = expanded ? style.ariaLables.collapseJson : style.ariaLables.expandJson; 103 | const childLevel = level + 1; 104 | const lastIndex = data.length - 1; 105 | 106 | const setExpandWithCallback = (newExpandValue: boolean) => { 107 | if ( 108 | expanded !== newExpandValue && 109 | (!beforeExpandChange || beforeExpandChange({ level, value, field, newExpandValue })) 110 | ) { 111 | setExpanded(newExpandValue); 112 | } 113 | }; 114 | 115 | const onKeyDown = (e: React.KeyboardEvent) => { 116 | if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { 117 | e.preventDefault(); 118 | setExpandWithCallback(e.key === 'ArrowRight'); 119 | } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { 120 | e.preventDefault(); 121 | const direction = e.key === 'ArrowUp' ? -1 : 1; 122 | 123 | if (!outerRef.current) return; 124 | const buttonElements = outerRef.current.querySelectorAll('[role=button]'); 125 | let currentIndex = -1; 126 | 127 | for (let i = 0; i < buttonElements.length; i++) { 128 | if (buttonElements[i].tabIndex === 0) { 129 | currentIndex = i; 130 | break; 131 | } 132 | } 133 | if (currentIndex < 0) { 134 | return; 135 | } 136 | 137 | const nextIndex = (currentIndex + direction + buttonElements.length) % buttonElements.length; // auto-wrap 138 | buttonElements[currentIndex].tabIndex = -1; 139 | buttonElements[nextIndex].tabIndex = 0; 140 | buttonElements[nextIndex].focus(); 141 | } 142 | }; 143 | 144 | const onClick = () => { 145 | setExpandWithCallback(!expanded); 146 | 147 | const buttonElement = expanderButtonRef.current; 148 | if (!buttonElement) return; 149 | const prevButtonElement = outerRef.current?.querySelector( 150 | '[role=button][tabindex="0"]' 151 | ); 152 | if (prevButtonElement) { 153 | prevButtonElement.tabIndex = -1; 154 | } 155 | buttonElement.tabIndex = 0; 156 | buttonElement.focus(); 157 | }; 158 | 159 | return ( 160 |
166 | 178 | {(field || field === '') && 179 | (clickToExpandNode ? ( 180 | // don't apply role="button" or tabIndex even though has onClick, because has same 181 | // function as the +/- expander button (so just expose that button to keyboard and a11y tree) 182 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions 183 | 184 | {quoteString(field, style.quotesForFieldNames)}: 185 | 186 | ) : ( 187 | {quoteString(field, style.quotesForFieldNames)}: 188 | ))} 189 | {openBracket} 190 | 191 | {expanded ? ( 192 |
    193 | {data.map((dataElement, index) => ( 194 | 205 | ))} 206 |
207 | ) : ( 208 | // don't apply role="button" or tabIndex even though has onClick, because has same 209 | // function as the +/- expander button (so just expose that button to keyboard and a11y tree) 210 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions 211 | 212 | )} 213 | 214 | {closeBracket} 215 | {!lastElement && ,} 216 |
217 | ); 218 | } 219 | 220 | export interface EmptyRenderProps { 221 | field: string | undefined; 222 | openBracket: string; 223 | closeBracket: string; 224 | lastElement: boolean; 225 | style: StyleProps; 226 | } 227 | 228 | function EmptyObject({ field, openBracket, closeBracket, lastElement, style }: EmptyRenderProps) { 229 | return ( 230 |
231 | {(field || field === '') && ( 232 | {quoteString(field, style.quotesForFieldNames)}: 233 | )} 234 | {openBracket} 235 | {closeBracket} 236 | {!lastElement && ,} 237 |
238 | ); 239 | } 240 | 241 | function JsonObject({ 242 | field, 243 | value, 244 | style, 245 | lastElement, 246 | shouldExpandNode, 247 | clickToExpandNode, 248 | level, 249 | outerRef, 250 | beforeExpandChange 251 | }: JsonRenderProps) { 252 | return ExpandableObject({ 253 | field, 254 | value, 255 | lastElement: lastElement || false, 256 | level, 257 | openBracket: '{', 258 | closeBracket: '}', 259 | style, 260 | shouldExpandNode, 261 | clickToExpandNode, 262 | data: Object.keys(value).map((key) => [key, value[key as keyof typeof value]]), 263 | outerRef, 264 | beforeExpandChange 265 | }); 266 | } 267 | 268 | function JsonArray({ 269 | field, 270 | value, 271 | style, 272 | lastElement, 273 | level, 274 | shouldExpandNode, 275 | clickToExpandNode, 276 | outerRef, 277 | beforeExpandChange 278 | }: JsonRenderProps>) { 279 | return ExpandableObject({ 280 | field, 281 | value, 282 | lastElement: lastElement || false, 283 | level, 284 | openBracket: '[', 285 | closeBracket: ']', 286 | style, 287 | shouldExpandNode, 288 | clickToExpandNode, 289 | data: value.map((element) => [undefined, element]), 290 | outerRef, 291 | beforeExpandChange 292 | }); 293 | } 294 | 295 | function JsonPrimitiveValue({ 296 | field, 297 | value, 298 | style, 299 | lastElement 300 | }: JsonRenderProps) { 301 | let stringValue; 302 | let valueStyle = style.otherValue; 303 | 304 | if (value === null) { 305 | stringValue = 'null'; 306 | valueStyle = style.nullValue; 307 | } else if (value === undefined) { 308 | stringValue = 'undefined'; 309 | valueStyle = style.undefinedValue; 310 | } else if (DataTypeDetection.isString(value)) { 311 | stringValue = quoteStringValue( 312 | value, 313 | !style.noQuotesForStringValues, 314 | style.stringifyStringValues 315 | ); 316 | valueStyle = style.stringValue; 317 | } else if (DataTypeDetection.isBoolean(value)) { 318 | stringValue = value ? 'true' : 'false'; 319 | valueStyle = style.booleanValue; 320 | } else if (DataTypeDetection.isNumber(value)) { 321 | stringValue = value.toString(); 322 | valueStyle = style.numberValue; 323 | } else if (DataTypeDetection.isBigInt(value)) { 324 | stringValue = `${value.toString()}n`; 325 | valueStyle = style.numberValue; 326 | } else if (DataTypeDetection.isDate(value)) { 327 | stringValue = value.toISOString(); 328 | } else if (DataTypeDetection.isFunction(value)) { 329 | stringValue = 'function() { }'; 330 | } else { 331 | stringValue = (value as any).toString(); 332 | } 333 | 334 | return ( 335 |
336 | {(field || field === '') && ( 337 | {quoteString(field, style.quotesForFieldNames)}: 338 | )} 339 | {stringValue} 340 | {!lastElement && ,} 341 |
342 | ); 343 | } 344 | 345 | export default function DataRender(props: JsonRenderProps) { 346 | const value = props.value; 347 | if (DataTypeDetection.isArray(value)) { 348 | return ; 349 | } 350 | 351 | if ( 352 | DataTypeDetection.isObject(value) && 353 | !DataTypeDetection.isDate(value) && 354 | !DataTypeDetection.isFunction(value) 355 | ) { 356 | return ; 357 | } 358 | 359 | return ; 360 | } 361 | -------------------------------------------------------------------------------- /src/DataTypeDetection.test.ts: -------------------------------------------------------------------------------- 1 | import * as DataTypeDetection from './DataTypeDetection'; 2 | 3 | const trueValue = true; 4 | const falseValue = false; 5 | const stringValue = 'string'; 6 | const numberValue = 42; 7 | const floatNumberValue = 42.42; 8 | const nullValue = null; 9 | const undefinedValue = undefined; 10 | const arrayValue = [numberValue]; 11 | const objectValue = { field: numberValue }; 12 | const dateValue = new Date(); 13 | const errorValue = new Error(); 14 | const regExValue = /test/; 15 | const symbolValue = Symbol('s'); 16 | const bigintValue = BigInt(42); 17 | const func = function (param: number): number { 18 | return param * 2; 19 | }; 20 | 21 | describe('isBoolean', () => { 22 | it('should return `true` for boolean values', () => { 23 | expect(DataTypeDetection.isBoolean(trueValue)).toBe(true); 24 | expect(DataTypeDetection.isBoolean(falseValue)).toBe(true); 25 | expect(DataTypeDetection.isBoolean(Boolean(trueValue))).toBe(true); 26 | expect(DataTypeDetection.isBoolean(Boolean(falseValue))).toBe(true); 27 | }); 28 | 29 | it('should return `false` for non-boolean values', () => { 30 | expect(DataTypeDetection.isBoolean(objectValue)).toBe(false); 31 | expect(DataTypeDetection.isBoolean(arrayValue)).toBe(false); 32 | expect(DataTypeDetection.isBoolean(stringValue)).toBe(false); 33 | expect(DataTypeDetection.isBoolean(numberValue)).toBe(false); 34 | expect(DataTypeDetection.isBoolean(nullValue)).toBe(false); 35 | expect(DataTypeDetection.isBoolean(undefinedValue)).toBe(false); 36 | expect(DataTypeDetection.isBoolean(errorValue)).toBe(false); 37 | expect(DataTypeDetection.isBoolean(dateValue)).toBe(false); 38 | expect(DataTypeDetection.isBoolean(regExValue)).toBe(false); 39 | expect(DataTypeDetection.isBoolean(symbolValue)).toBe(false); 40 | expect(DataTypeDetection.isBoolean(bigintValue)).toBe(false); 41 | expect(DataTypeDetection.isBoolean(func)).toBe(false); 42 | }); 43 | }); 44 | 45 | describe('isNumber', () => { 46 | it('should return `true` for number values', () => { 47 | expect(DataTypeDetection.isNumber(numberValue)).toBe(true); 48 | expect(DataTypeDetection.isNumber(floatNumberValue)).toBe(true); 49 | expect(DataTypeDetection.isNumber(Number(numberValue))).toBe(true); 50 | expect(DataTypeDetection.isNumber(Number(floatNumberValue))).toBe(true); 51 | }); 52 | 53 | it('should return `false` for non-number values', () => { 54 | expect(DataTypeDetection.isNumber(trueValue)).toBe(false); 55 | expect(DataTypeDetection.isNumber(falseValue)).toBe(false); 56 | expect(DataTypeDetection.isNumber(objectValue)).toBe(false); 57 | expect(DataTypeDetection.isNumber(arrayValue)).toBe(false); 58 | expect(DataTypeDetection.isNumber(stringValue)).toBe(false); 59 | expect(DataTypeDetection.isNumber(nullValue)).toBe(false); 60 | expect(DataTypeDetection.isNumber(undefinedValue)).toBe(false); 61 | expect(DataTypeDetection.isNumber(errorValue)).toBe(false); 62 | expect(DataTypeDetection.isNumber(dateValue)).toBe(false); 63 | expect(DataTypeDetection.isNumber(regExValue)).toBe(false); 64 | expect(DataTypeDetection.isNumber(symbolValue)).toBe(false); 65 | expect(DataTypeDetection.isNumber(bigintValue)).toBe(false); 66 | expect(DataTypeDetection.isNumber(func)).toBe(false); 67 | }); 68 | }); 69 | 70 | describe('isString', () => { 71 | it('should return `true` for string values', () => { 72 | expect(DataTypeDetection.isString(stringValue)).toBe(true); 73 | expect(DataTypeDetection.isString(String(stringValue))).toBe(true); 74 | }); 75 | 76 | it('should return `false` for non-string values', () => { 77 | expect(DataTypeDetection.isString(numberValue)).toBe(false); 78 | expect(DataTypeDetection.isString(floatNumberValue)).toBe(false); 79 | expect(DataTypeDetection.isString(trueValue)).toBe(false); 80 | expect(DataTypeDetection.isString(falseValue)).toBe(false); 81 | expect(DataTypeDetection.isString(objectValue)).toBe(false); 82 | expect(DataTypeDetection.isString(arrayValue)).toBe(false); 83 | expect(DataTypeDetection.isString(nullValue)).toBe(false); 84 | expect(DataTypeDetection.isString(undefinedValue)).toBe(false); 85 | expect(DataTypeDetection.isString(errorValue)).toBe(false); 86 | expect(DataTypeDetection.isString(dateValue)).toBe(false); 87 | expect(DataTypeDetection.isString(regExValue)).toBe(false); 88 | expect(DataTypeDetection.isString(symbolValue)).toBe(false); 89 | expect(DataTypeDetection.isString(bigintValue)).toBe(false); 90 | expect(DataTypeDetection.isString(func)).toBe(false); 91 | }); 92 | }); 93 | 94 | describe('isNull', () => { 95 | it('should return `true` for null value', () => { 96 | expect(DataTypeDetection.isNull(nullValue)).toBe(true); 97 | }); 98 | 99 | it('should return `false` for non-null values', () => { 100 | expect(DataTypeDetection.isNull(trueValue)).toBe(false); 101 | expect(DataTypeDetection.isNull(falseValue)).toBe(false); 102 | expect(DataTypeDetection.isNull(objectValue)).toBe(false); 103 | expect(DataTypeDetection.isNull(arrayValue)).toBe(false); 104 | expect(DataTypeDetection.isNull(stringValue)).toBe(false); 105 | expect(DataTypeDetection.isNull(numberValue)).toBe(false); 106 | expect(DataTypeDetection.isNull(undefinedValue)).toBe(false); 107 | expect(DataTypeDetection.isNull(errorValue)).toBe(false); 108 | expect(DataTypeDetection.isNull(dateValue)).toBe(false); 109 | expect(DataTypeDetection.isNull(regExValue)).toBe(false); 110 | expect(DataTypeDetection.isNull(symbolValue)).toBe(false); 111 | expect(DataTypeDetection.isNull(bigintValue)).toBe(false); 112 | expect(DataTypeDetection.isNull(func)).toBe(false); 113 | }); 114 | }); 115 | 116 | describe('isUndefined', () => { 117 | it('should return `true` for null value', () => { 118 | expect(DataTypeDetection.isUndefined(undefinedValue)).toBe(true); 119 | }); 120 | 121 | it('should return `false` for non-null values', () => { 122 | expect(DataTypeDetection.isUndefined(trueValue)).toBe(false); 123 | expect(DataTypeDetection.isUndefined(falseValue)).toBe(false); 124 | expect(DataTypeDetection.isUndefined(objectValue)).toBe(false); 125 | expect(DataTypeDetection.isUndefined(arrayValue)).toBe(false); 126 | expect(DataTypeDetection.isUndefined(stringValue)).toBe(false); 127 | expect(DataTypeDetection.isUndefined(numberValue)).toBe(false); 128 | expect(DataTypeDetection.isUndefined(nullValue)).toBe(false); 129 | expect(DataTypeDetection.isUndefined(errorValue)).toBe(false); 130 | expect(DataTypeDetection.isUndefined(dateValue)).toBe(false); 131 | expect(DataTypeDetection.isUndefined(regExValue)).toBe(false); 132 | expect(DataTypeDetection.isUndefined(symbolValue)).toBe(false); 133 | expect(DataTypeDetection.isUndefined(bigintValue)).toBe(false); 134 | expect(DataTypeDetection.isUndefined(func)).toBe(false); 135 | }); 136 | }); 137 | 138 | describe('isArray', () => { 139 | it('should return `true` for array value', () => { 140 | expect(DataTypeDetection.isArray(arrayValue)).toBe(true); 141 | }); 142 | 143 | it('should return `false` for non-array values', () => { 144 | expect(DataTypeDetection.isArray(trueValue)).toBe(false); 145 | expect(DataTypeDetection.isArray(falseValue)).toBe(false); 146 | expect(DataTypeDetection.isArray(objectValue)).toBe(false); 147 | expect(DataTypeDetection.isArray(stringValue)).toBe(false); 148 | expect(DataTypeDetection.isArray(numberValue)).toBe(false); 149 | expect(DataTypeDetection.isArray(nullValue)).toBe(false); 150 | expect(DataTypeDetection.isArray(undefinedValue)).toBe(false); 151 | expect(DataTypeDetection.isArray(errorValue)).toBe(false); 152 | expect(DataTypeDetection.isArray(dateValue)).toBe(false); 153 | expect(DataTypeDetection.isArray(regExValue)).toBe(false); 154 | expect(DataTypeDetection.isArray(symbolValue)).toBe(false); 155 | expect(DataTypeDetection.isArray(bigintValue)).toBe(false); 156 | expect(DataTypeDetection.isArray(func)).toBe(false); 157 | }); 158 | }); 159 | 160 | describe('isObject', () => { 161 | it('should return `true` for array value', () => { 162 | expect(DataTypeDetection.isObject(objectValue)).toBe(true); 163 | expect(DataTypeDetection.isObject(errorValue)).toBe(true); 164 | expect(DataTypeDetection.isObject(dateValue)).toBe(true); 165 | expect(DataTypeDetection.isObject(regExValue)).toBe(true); 166 | }); 167 | 168 | it('should return `false` for non-array values', () => { 169 | expect(DataTypeDetection.isObject(trueValue)).toBe(false); 170 | expect(DataTypeDetection.isObject(falseValue)).toBe(false); 171 | expect(DataTypeDetection.isObject(arrayValue)).toBe(true); 172 | expect(DataTypeDetection.isObject(stringValue)).toBe(false); 173 | expect(DataTypeDetection.isObject(numberValue)).toBe(false); 174 | expect(DataTypeDetection.isObject(nullValue)).toBe(false); 175 | expect(DataTypeDetection.isObject(undefinedValue)).toBe(false); 176 | expect(DataTypeDetection.isObject(symbolValue)).toBe(false); 177 | expect(DataTypeDetection.isObject(bigintValue)).toBe(false); 178 | }); 179 | }); 180 | 181 | describe('isBigInt', () => { 182 | it('should return `true` for bigint value', () => { 183 | expect(DataTypeDetection.isBigInt(bigintValue)).toBe(true); 184 | }); 185 | 186 | it('should return `false` for non-array values', () => { 187 | expect(DataTypeDetection.isBigInt(objectValue)).toBe(false); 188 | expect(DataTypeDetection.isBigInt(errorValue)).toBe(false); 189 | expect(DataTypeDetection.isBigInt(dateValue)).toBe(false); 190 | expect(DataTypeDetection.isBigInt(regExValue)).toBe(false); 191 | expect(DataTypeDetection.isBigInt(trueValue)).toBe(false); 192 | expect(DataTypeDetection.isBigInt(falseValue)).toBe(false); 193 | expect(DataTypeDetection.isBigInt(arrayValue)).toBe(false); 194 | expect(DataTypeDetection.isBigInt(stringValue)).toBe(false); 195 | expect(DataTypeDetection.isBigInt(numberValue)).toBe(false); 196 | expect(DataTypeDetection.isBigInt(nullValue)).toBe(false); 197 | expect(DataTypeDetection.isBigInt(undefinedValue)).toBe(false); 198 | expect(DataTypeDetection.isBigInt(symbolValue)).toBe(false); 199 | expect(DataTypeDetection.isBigInt(func)).toBe(false); 200 | }); 201 | }); 202 | 203 | describe('isDate', () => { 204 | it('should return `true` for date values', () => { 205 | expect(DataTypeDetection.isDate(dateValue)).toBe(true); 206 | }); 207 | 208 | it('should return `false` for non-date values', () => { 209 | expect(DataTypeDetection.isDate(stringValue)).toBe(false); 210 | expect(DataTypeDetection.isDate(numberValue)).toBe(false); 211 | expect(DataTypeDetection.isDate(floatNumberValue)).toBe(false); 212 | expect(DataTypeDetection.isDate(trueValue)).toBe(false); 213 | expect(DataTypeDetection.isDate(falseValue)).toBe(false); 214 | expect(DataTypeDetection.isDate(objectValue)).toBe(false); 215 | expect(DataTypeDetection.isDate(arrayValue)).toBe(false); 216 | expect(DataTypeDetection.isDate(nullValue)).toBe(false); 217 | expect(DataTypeDetection.isDate(undefinedValue)).toBe(false); 218 | expect(DataTypeDetection.isDate(errorValue)).toBe(false); 219 | expect(DataTypeDetection.isDate(regExValue)).toBe(false); 220 | expect(DataTypeDetection.isDate(symbolValue)).toBe(false); 221 | expect(DataTypeDetection.isDate(bigintValue)).toBe(false); 222 | expect(DataTypeDetection.isDate(func)).toBe(false); 223 | }); 224 | }); 225 | 226 | describe('isFunc', () => { 227 | it('should return `true` for function values', () => { 228 | expect(DataTypeDetection.isFunction(func)).toBe(true); 229 | }); 230 | 231 | it('should return `false` for non-function values', () => { 232 | expect(DataTypeDetection.isFunction(stringValue)).toBe(false); 233 | expect(DataTypeDetection.isFunction(numberValue)).toBe(false); 234 | expect(DataTypeDetection.isFunction(floatNumberValue)).toBe(false); 235 | expect(DataTypeDetection.isFunction(trueValue)).toBe(false); 236 | expect(DataTypeDetection.isFunction(falseValue)).toBe(false); 237 | expect(DataTypeDetection.isFunction(objectValue)).toBe(false); 238 | expect(DataTypeDetection.isFunction(arrayValue)).toBe(false); 239 | expect(DataTypeDetection.isFunction(nullValue)).toBe(false); 240 | expect(DataTypeDetection.isFunction(undefinedValue)).toBe(false); 241 | expect(DataTypeDetection.isFunction(errorValue)).toBe(false); 242 | expect(DataTypeDetection.isFunction(regExValue)).toBe(false); 243 | expect(DataTypeDetection.isFunction(symbolValue)).toBe(false); 244 | expect(DataTypeDetection.isFunction(bigintValue)).toBe(false); 245 | expect(DataTypeDetection.isFunction(dateValue)).toBe(false); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /src/DataTypeDetection.ts: -------------------------------------------------------------------------------- 1 | export const isBoolean = (data: any): data is boolean => { 2 | return typeof data === 'boolean' || data instanceof Boolean; 3 | }; 4 | 5 | export const isNumber = (data: any): data is number => { 6 | return typeof data === 'number' || data instanceof Number; 7 | }; 8 | 9 | export const isBigInt = (data: any): data is BigInt => { 10 | return typeof data === 'bigint' || data instanceof BigInt; 11 | }; 12 | 13 | export const isDate = (data: unknown): data is Date => { 14 | return !!data && data instanceof Date; 15 | }; 16 | 17 | export const isString = (data: any): data is string => { 18 | return typeof data === 'string' || data instanceof String; 19 | }; 20 | 21 | export const isArray = (data: any): data is Array => { 22 | return Array.isArray(data); 23 | }; 24 | 25 | export const isObject = (data: any): data is object => { 26 | return typeof data === 'object' && data !== null; 27 | }; 28 | 29 | export const isNull = (data: any): data is null => { 30 | return data === null; 31 | }; 32 | 33 | export const isUndefined = (data: any): data is undefined => { 34 | return data === undefined; 35 | }; 36 | 37 | export const isFunction = (data: unknown): data is Function => { 38 | return !!data && data instanceof Object && typeof data === 'function'; 39 | }; 40 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { JsonView, defaultStyles, allExpanded, collapseAllNested } from '.'; 3 | import { fireEvent, render, screen } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | import { StyleProps } from './DataRenderer'; 6 | 7 | describe('JsonView', () => { 8 | it('should render object', () => { 9 | render(); 10 | expect(screen.getByText(/test/)).toBeDefined(); 11 | expect(screen.getByText('true')).toBeDefined(); 12 | }); 13 | 14 | it('should render object with default styles', () => { 15 | render(); 16 | expect(screen.getByText(/test/)).toBeInTheDocument(); 17 | expect(screen.getByText('true')).toBeInTheDocument(); 18 | }); 19 | 20 | it('should render object with incomplete style object', () => { 21 | render(); 22 | expect(screen.getByText(/test/)).toBeInTheDocument(); 23 | expect(screen.getByText('true')).toBeInTheDocument(); 24 | }); 25 | 26 | it('should render object and call shouldExpandNode only once', () => { 27 | let invoked = 0; 28 | const shouldExpandNode = () => { 29 | ++invoked; 30 | return true; 31 | }; 32 | render(); 33 | expect(screen.getByText(/test/)).toBeInTheDocument(); 34 | expect(screen.getByText('true')).toBeInTheDocument(); 35 | expect(invoked).toBe(1); 36 | }); 37 | 38 | it('allExpanded should always return true', () => { 39 | expect(allExpanded()).toBeTruthy(); 40 | }); 41 | 42 | it('collapseAllNested should always return true for 0', () => { 43 | expect(collapseAllNested(0)).toBeTruthy(); 44 | expect(collapseAllNested(1)).toBeFalsy(); 45 | expect(collapseAllNested(2)).toBeFalsy(); 46 | expect(collapseAllNested(3)).toBeFalsy(); 47 | }); 48 | 49 | it('should go to next node on ArrowDown, prev node with ArrowUp, tabindex should change', () => { 50 | const { container } = render(); 51 | 52 | expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1); 53 | const buttons = screen.getAllByRole('button', { hidden: true }); 54 | expect(buttons).toHaveLength(3); 55 | 56 | expect(buttons[0].tabIndex).toEqual(0); 57 | expect(buttons[1].tabIndex).toEqual(-1); 58 | expect(buttons[2].tabIndex).toEqual(-1); 59 | 60 | buttons[0].focus(); 61 | expect(buttons[0]).toHaveFocus(); 62 | 63 | fireEvent.keyDown(buttons[0], { key: 'ArrowDown', code: 'ArrowDown' }); 64 | expect(buttons[0]).not.toHaveFocus(); 65 | expect(buttons[1]).toHaveFocus(); 66 | expect(buttons[2]).not.toHaveFocus(); 67 | expect(buttons[0].tabIndex).toEqual(-1); 68 | expect(buttons[1].tabIndex).toEqual(0); 69 | expect(buttons[2].tabIndex).toEqual(-1); 70 | 71 | fireEvent.keyDown(buttons[1], { key: 'ArrowUp', code: 'ArrowUp' }); 72 | expect(buttons[0]).toHaveFocus(); 73 | expect(buttons[1]).not.toHaveFocus(); 74 | expect(buttons[2]).not.toHaveFocus(); 75 | expect(buttons[0].tabIndex).toEqual(0); 76 | expect(buttons[1].tabIndex).toEqual(-1); 77 | expect(buttons[2].tabIndex).toEqual(-1); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import DataRender, { StyleProps } from './DataRenderer'; 3 | import styles from './styles.module.css'; 4 | 5 | export interface NodeExpandingEvent { 6 | level: number; 7 | value: any; 8 | field?: string; 9 | newExpandValue: boolean; 10 | } 11 | 12 | export interface AriaLabels { 13 | collapseJson: string; 14 | expandJson: string; 15 | } 16 | 17 | const defaultAriaLables: AriaLabels = { 18 | collapseJson: 'collapse JSON', 19 | expandJson: 'expand JSON' 20 | }; 21 | 22 | export interface Props extends React.AriaAttributes { 23 | data: Object | Array; 24 | style?: Partial; 25 | shouldExpandNode?: (level: number, value: any, field?: string) => boolean; 26 | clickToExpandNode?: boolean; 27 | beforeExpandChange?: (event: NodeExpandingEvent) => boolean; 28 | } 29 | 30 | export const defaultStyles: StyleProps = { 31 | container: styles['container-light'], 32 | basicChildStyle: styles['basic-element-style'], 33 | childFieldsContainer: styles['child-fields-container'], 34 | label: styles['label-light'], 35 | clickableLabel: styles['clickable-label-light'], 36 | nullValue: styles['value-null-light'], 37 | undefinedValue: styles['value-undefined-light'], 38 | stringValue: styles['value-string-light'], 39 | booleanValue: styles['value-boolean-light'], 40 | numberValue: styles['value-number-light'], 41 | otherValue: styles['value-other-light'], 42 | punctuation: styles['punctuation-light'], 43 | collapseIcon: styles['collapse-icon-light'], 44 | expandIcon: styles['expand-icon-light'], 45 | collapsedContent: styles['collapsed-content-light'], 46 | noQuotesForStringValues: false, 47 | quotesForFieldNames: false, 48 | ariaLables: defaultAriaLables, 49 | stringifyStringValues: false 50 | }; 51 | 52 | export const darkStyles: StyleProps = { 53 | container: styles['container-dark'], 54 | basicChildStyle: styles['basic-element-style'], 55 | childFieldsContainer: styles['child-fields-container'], 56 | label: styles['label-dark'], 57 | clickableLabel: styles['clickable-label-dark'], 58 | nullValue: styles['value-null-dark'], 59 | undefinedValue: styles['value-undefined-dark'], 60 | stringValue: styles['value-string-dark'], 61 | booleanValue: styles['value-boolean-dark'], 62 | numberValue: styles['value-number-dark'], 63 | otherValue: styles['value-other-dark'], 64 | punctuation: styles['punctuation-dark'], 65 | collapseIcon: styles['collapse-icon-dark'], 66 | expandIcon: styles['expand-icon-dark'], 67 | collapsedContent: styles['collapsed-content-dark'], 68 | noQuotesForStringValues: false, 69 | quotesForFieldNames: false, 70 | ariaLables: defaultAriaLables, 71 | stringifyStringValues: false 72 | }; 73 | 74 | export const allExpanded = () => true; 75 | export const collapseAllNested = (level: number) => level < 1; 76 | 77 | export const JsonView = ({ 78 | data, 79 | style = defaultStyles, 80 | shouldExpandNode = allExpanded, 81 | clickToExpandNode = false, 82 | beforeExpandChange, 83 | ...ariaAttrs 84 | }: Props) => { 85 | const outerRef = React.useRef(null); 86 | return ( 87 |
94 | 104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/stories/JsonView.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StoryFn, Meta, StoryObj } from '@storybook/react'; 3 | import { useArgs } from '@storybook/preview-api'; 4 | 5 | import { 6 | JsonView, 7 | defaultStyles, 8 | darkStyles, 9 | allExpanded, 10 | collapseAllNested, 11 | NodeExpandingEvent 12 | } from '../index'; 13 | 14 | export default { 15 | title: 'Json View', 16 | component: JsonView, 17 | argTypes: { 18 | data: { 19 | name: 'data', 20 | description: 'Data to render in the control. Should be an object or array.' 21 | }, 22 | shouldExpandNode: { 23 | name: 'shouldExpandNode', 24 | source: { 25 | type: 'code' 26 | }, 27 | description: 28 | 'Function which will be initially called for each Object and Array of the data in order to calculate should if this node be expanded. `level` startes from `0`, `field` does not have a value for the array element. Library provides two build-in implementations: `allExpanded` and `collapseAllNested`' 29 | }, 30 | style: { 31 | name: 'style', 32 | defaultValue: defaultStyles, 33 | description: 34 | 'Collection of CSS style to use for the component. Library provides two build-in implementations: `darkStyles`, `defaultStyles`' 35 | } 36 | }, 37 | decorators: [ 38 | (Story) => ( 39 |
45 | {Story()} 46 |
47 | ) 48 | ] 49 | } as Meta; 50 | 51 | const Template: StoryFn = (args) => ; 52 | 53 | // @ts-expect-error toJSON does not exist 54 | // eslint-disable-next-line no-extend-native 55 | BigInt.prototype.toJSON = function () { 56 | return this.toString(); 57 | }; 58 | 59 | const jsonData = { 60 | 'string property': 'my string', 61 | '': 'empty name property', 62 | 'bigint property': BigInt('9007199254740991'), 63 | 'number property': 42.42, 64 | 'date property': new Date(0), 65 | 'boolean property': true, 66 | 'null property': null, 67 | 'empty array': [], 68 | 'array propery': [1, 2, 3, 4, 5], 69 | 'empty object': {}, 70 | 'nested object': { 71 | first: true, 72 | second: 'another value', 73 | 'sub nested': { 74 | sub1: [true, true, true], 75 | longText: 76 | ' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam pharetra at dolor eu egestas. Mauris bibendum a sem vel euismod. Proin vitae imperdiet diam. In sed gravida nisi, in convallis felis. Fusce convallis dapibus molestie. In tristique dapibus velit et rutrum. Nam vestibulum sodales tortor. Integer gravida aliquet sollicitudin. Duis at nulla varius, congue risus sit amet, gravida ipsum. Cras placerat pellentesque ipsum, a consequat magna pretium et. Duis placerat dui nisi, eget varius dui egestas eget. Etiam leo mauris, mattis et aliquam hendrerit, dapibus eu massa. Phasellus vitae vestibulum elit. Nulla congue eleifend massa at efficitur. ' 77 | } 78 | }, 79 | func: () => {} 80 | }; 81 | 82 | export const Basic = Template.bind({}); 83 | Basic.args = { 84 | data: jsonData, 85 | style: defaultStyles, 86 | shouldExpandNode: allExpanded 87 | }; 88 | 89 | export const DarkTheme = Template.bind({}); 90 | DarkTheme.args = { 91 | data: jsonData, 92 | style: darkStyles 93 | }; 94 | 95 | export const CollapsedNestedObjects = Template.bind({}); 96 | CollapsedNestedObjects.args = { 97 | data: jsonData, 98 | style: defaultStyles, 99 | shouldExpandNode: collapseAllNested 100 | }; 101 | 102 | export const CollapsedRoot = Template.bind({}); 103 | const collapseAll = () => false; 104 | 105 | CollapsedRoot.args = { 106 | data: jsonData, 107 | style: defaultStyles, 108 | shouldExpandNode: collapseAll 109 | }; 110 | 111 | export const RenderStringsWithoutQuotes = Template.bind({}); 112 | RenderStringsWithoutQuotes.args = { 113 | data: jsonData, 114 | style: { ...defaultStyles, noQuotesForStringValues: true } 115 | }; 116 | 117 | export const RenderFieldNamesWithQuotes = Template.bind({}); 118 | RenderFieldNamesWithQuotes.args = { 119 | data: jsonData, 120 | style: { ...defaultStyles, quotesForFieldNames: true } 121 | }; 122 | 123 | export const StringifyStringValues = Template.bind({}); 124 | StringifyStringValues.args = { 125 | data: { valueWithEscapedCharacters: 'one\n\'two\'\tthree.\r\n"another line"' }, 126 | style: { ...defaultStyles, stringifyStringValues: true } 127 | }; 128 | 129 | export const ClickOnFieldNameToExpand = Template.bind({}); 130 | ClickOnFieldNameToExpand.args = { 131 | data: jsonData, 132 | style: { ...defaultStyles }, 133 | clickToExpandNode: true 134 | }; 135 | 136 | type JsonViewStory = StoryObj; 137 | 138 | export const ExpandOnlyFirstLevelWhenClickOnRoot: JsonViewStory = { 139 | args: { 140 | data: jsonData, 141 | style: { ...defaultStyles }, 142 | shouldExpandNode: collapseAll, 143 | clickToExpandNode: true 144 | }, 145 | argTypes: {}, 146 | name: 'Expand only first level on root click (using beforeExpandChange)', 147 | 148 | render: function Render(args) { 149 | const [{ shouldExpandNode }, updateArgs] = useArgs(); 150 | 151 | const beforeExpandChange = (event: NodeExpandingEvent) => { 152 | if (event.level === 0 && event.newExpandValue) { 153 | updateArgs({ shouldExpandNode: (level: number) => level < 1 }); 154 | return false; 155 | } 156 | return true; 157 | }; 158 | 159 | return ( 160 | 165 | ); 166 | } 167 | }; 168 | -------------------------------------------------------------------------------- /src/styles.module.css: -------------------------------------------------------------------------------- 1 | /* base styles */ 2 | 3 | .container-base { 4 | line-height: 1.2; 5 | white-space: pre-wrap; 6 | white-space: -moz-pre-wrap; 7 | white-space: -pre-wrap; 8 | white-space: -o-pre-wrap; 9 | word-wrap: break-word; 10 | } 11 | 12 | .punctuation-base { 13 | margin-right: 5px; 14 | font-weight: bold; 15 | } 16 | 17 | .punctuation-base + .punctuation-base { 18 | margin-left: -5px; 19 | } 20 | 21 | .pointer { 22 | cursor: pointer; 23 | } 24 | 25 | .expander-base { 26 | composes: pointer; 27 | font-size: 1.2em; 28 | margin-right: 5px; 29 | user-select: none; 30 | } 31 | 32 | .expand-icon::after { 33 | content: '▸'; 34 | } 35 | 36 | .collapse-icon::after { 37 | content: '▾'; 38 | } 39 | 40 | .collapsed-content-base { 41 | composes: pointer; 42 | margin-right: 5px; 43 | } 44 | 45 | .collapsed-content-base::after { 46 | content: '...'; 47 | font-size: 0.8em; 48 | } 49 | 50 | .container-light { 51 | composes: container-base; 52 | background: #eee; 53 | } 54 | 55 | .basic-element-style { 56 | margin: 0; 57 | padding: 0 10px; 58 | } 59 | 60 | .child-fields-container { 61 | margin: 0; 62 | padding: 0; 63 | } 64 | 65 | /* default light style */ 66 | .label-light { 67 | font-weight: 600; 68 | margin-right: 5px; 69 | color: #000000; 70 | } 71 | 72 | .clickable-label-light { 73 | composes: label-light; 74 | composes: pointer; 75 | } 76 | 77 | .punctuation-light { 78 | composes: punctuation-base; 79 | color: #000000; 80 | } 81 | 82 | .value-null-light { 83 | color: #df113a; 84 | } 85 | 86 | .value-undefined-light { 87 | color: #df113a; 88 | } 89 | 90 | .value-string-light { 91 | color: rgb(42, 63, 60); 92 | } 93 | 94 | .value-number-light { 95 | color: #0b75f5; 96 | } 97 | 98 | .value-boolean-light { 99 | color: rgb(70, 144, 56); 100 | } 101 | 102 | .value-other-light { 103 | color: #43413d; 104 | } 105 | 106 | .collapse-icon-light { 107 | composes: expander-base; 108 | composes: collapse-icon; 109 | color: #000000; 110 | } 111 | 112 | .expand-icon-light { 113 | composes: expander-base; 114 | composes: expand-icon; 115 | color: #000000; 116 | } 117 | 118 | .collapsed-content-light { 119 | composes: collapsed-content-base; 120 | color: #000000; 121 | } 122 | 123 | /* default dark style */ 124 | .container-dark { 125 | background: rgb(0, 43, 54); 126 | composes: container-base; 127 | } 128 | 129 | .expand-icon-dark { 130 | composes: expander-base; 131 | composes: expand-icon; 132 | color: rgb(253, 246, 227); 133 | } 134 | 135 | .collapse-icon-dark { 136 | composes: expander-base; 137 | composes: collapse-icon; 138 | color: rgb(253, 246, 227); 139 | } 140 | 141 | .collapsed-content-dark { 142 | composes: collapsed-content-base; 143 | color: rgb(253, 246, 227); 144 | } 145 | 146 | .label-dark { 147 | font-weight: bolder; 148 | margin-right: 5px; 149 | color: rgb(253, 246, 227); 150 | } 151 | 152 | .clickable-label-dark { 153 | composes: label-dark; 154 | composes: pointer; 155 | } 156 | 157 | .punctuation-dark { 158 | composes: punctuation-base; 159 | color: rgb(253, 246, 227); 160 | } 161 | 162 | .value-null-dark { 163 | color: rgb(129, 181, 172); 164 | } 165 | 166 | .value-undefined-dark { 167 | color: rgb(129, 181, 172); 168 | } 169 | 170 | .value-string-dark { 171 | color: rgb(203, 75, 22); 172 | } 173 | 174 | .value-number-dark { 175 | color: rgb(211, 54, 130); 176 | } 177 | 178 | .value-boolean-dark { 179 | color: rgb(174, 129, 255); 180 | } 181 | 182 | .value-other-dark { 183 | color: rgb(38, 139, 210); 184 | } 185 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module '*.css' { 6 | const content: { [className: string]: string }; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "allowSyntheticDefaultImports": true, 21 | "target": "es5", 22 | "allowJs": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true 30 | }, 31 | "include": [ 32 | "src" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "dist", 37 | "example" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } --------------------------------------------------------------------------------