This commit is contained in:
tisdadd 2025-04-04 11:53:34 +01:00 committed by GitHub
commit 70d4aee5a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 112 additions and 16 deletions

View File

@ -0,0 +1,6 @@
---
'react-json-tree': minor
'react-json-tree-example': minor
---
Changed to using buttons for a11y compatibility, and added ability to override Arrow Component.

View File

@ -137,6 +137,34 @@ Their full signatures are:
- `labelRenderer: function(keyPath, nodeType, expanded, expandable)` - `labelRenderer: function(keyPath, nodeType, expanded, expandable)`
- `valueRenderer: function(valueAsString, value, ...keyPath)` - `valueRenderer: function(valueAsString, value, ...keyPath)`
Additionally, it is possible to override the arrows for expanding, for example with a `+` and `-` button.
```tsx
const ArrowOverride = ({ expanded, ...rest }: JSONArrowProps) => {
if (expanded) {
return <button {...rest}>-</button>
}
return <button {...rest}>+</button>
}
<JSONTree ArrowComponentOverride={ArrowOverride} />
```
The default `JSONArrow` component will literally check if an `ArrowComponentOverride` exists and pass it's props there. The typescript for these props is as follows.
```ts
interface JSONArrowProps {
styling: StylingFunction;
arrowStyle?: 'single' | 'double';
expanded: boolean;
nodeType: string;
onClick: React.MouseEventHandler<HTMLButtonElement>;
ariaControls?: string;
ariaLabel?: string
OverrideComponent?: ComponentType<JSONArrowProps>;
}
```
#### More Options #### More Options
- `shouldExpandNodeInitially: function(keyPath, data, level)` - determines if node should be expanded when it first renders (root is expanded by default) - `shouldExpandNodeInitially: function(keyPath, data, level)` - determines if node should be expanded when it first renders (root is expanded by default)

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Map } from 'immutable'; import { Map } from 'immutable';
import { JSONTree, StylingValue } from 'react-json-tree'; import { JSONArrowProps, JSONTree, StylingValue } from 'react-json-tree';
const getLabelStyle: StylingValue = ({ style }, nodeType, expanded) => ({ const getLabelStyle: StylingValue = ({ style }, nodeType, expanded) => ({
style: { style: {
@ -125,6 +125,13 @@ const theme = {
base0F: '#cc6633', base0F: '#cc6633',
}; };
const ArrowOverride = ({ expanded, ...rest }: JSONArrowProps) => {
if (expanded) {
return <button {...rest}>-</button>
}
return <button {...rest}>+</button>
}
const App = () => ( const App = () => (
<div> <div>
<JSONTree data={data} theme={theme} invertTheme /> <JSONTree data={data} theme={theme} invertTheme />
@ -168,10 +175,12 @@ const App = () => (
<p> <p>
Pass <code>labelRenderer</code> or <code>valueRenderer</code>. Pass <code>labelRenderer</code> or <code>valueRenderer</code>.
</p> </p>
<p>Additionally, you may pass an <code>ArrowComponentOverride</code></p>
<div> <div>
<JSONTree <JSONTree
data={data} data={data}
theme={theme} theme={theme}
ArrowComponentOverride={ArrowOverride}
labelRenderer={([raw]) => <span>(({raw})):</span>} labelRenderer={([raw]) => <span>(({raw})):</span>}
valueRenderer={(raw) => ( valueRenderer={(raw) => (
<em> <em>

View File

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import JSONArrow from './JSONArrow.js'; import JSONArrow from './JSONArrow.js';
import type { CircularCache, CommonInternalProps } from './types.js'; import type { CircularCache, CommonInternalProps, KeyPath } from './types.js';
interface Props extends CommonInternalProps { interface Props extends CommonInternalProps {
data: unknown; data: unknown;
@ -10,6 +10,7 @@ interface Props extends CommonInternalProps {
renderChildNodes: (props: Props, from: number, to: number) => React.ReactNode; renderChildNodes: (props: Props, from: number, to: number) => React.ReactNode;
circularCache: CircularCache; circularCache: CircularCache;
level: number; level: number;
keyPath: KeyPath;
} }
export default function ItemRange(props: Props) { export default function ItemRange(props: Props) {
@ -32,6 +33,8 @@ export default function ItemRange(props: Props) {
expanded={false} expanded={false}
onClick={handleClick} onClick={handleClick}
arrowStyle="double" arrowStyle="double"
ariaLabel={`Expand Array from ${from} to ${to}`}
OverrideComponent={props.ArrowComponentOverride}
/> />
{`${from} ... ${to}`} {`${from} ... ${to}`}
</div> </div>

View File

@ -1,29 +1,39 @@
import React from 'react'; import React, { ComponentType } from 'react';
import type { StylingFunction } from 'react-base16-styling'; import type { StylingFunction } from 'react-base16-styling';
interface Props { export interface JSONArrowProps {
styling: StylingFunction; styling: StylingFunction;
arrowStyle?: 'single' | 'double'; arrowStyle?: 'single' | 'double';
expanded: boolean; expanded: boolean;
nodeType: string; nodeType: string;
onClick: React.MouseEventHandler<HTMLDivElement>; onClick: React.MouseEventHandler<HTMLButtonElement>;
ariaControls?: string;
ariaLabel?: string
OverrideComponent?: ComponentType<JSONArrowProps>;
} }
export default function JSONArrow({ export default function JSONArrow({OverrideComponent, ...props}: JSONArrowProps) {
styling, const {
arrowStyle = 'single', styling,
expanded, arrowStyle = 'single',
nodeType, expanded,
onClick, nodeType,
}: Props) { onClick,
ariaControls,
ariaLabel
} = props
if(OverrideComponent) {
return <OverrideComponent {...props} />
}
return ( return (
<div {...styling('arrowContainer', arrowStyle)} onClick={onClick}> <button {...styling('arrowContainer', arrowStyle)} aria-label={ariaLabel} aria-expanded={expanded} aria-controls={ariaControls} onClick={onClick}>
<div {...styling(['arrow', 'arrowSign'], nodeType, expanded, arrowStyle)}> <div {...styling(['arrow', 'arrowSign'], nodeType, expanded, arrowStyle)}>
{'\u25B6'} {'\u25B6'}
{arrowStyle === 'double' && ( {arrowStyle === 'double' && (
<div {...styling(['arrowSign', 'arrowSignInner'])}>{'\u25B6'}</div> <div {...styling(['arrowSign', 'arrowSignInner'])}>{'\u25B6'}</div>
)} )}
</div> </div>
</div> </button>
); );
} }

View File

@ -4,6 +4,7 @@ import getCollectionEntries from './getCollectionEntries.js';
import JSONNode from './JSONNode.js'; import JSONNode from './JSONNode.js';
import ItemRange from './ItemRange.js'; import ItemRange from './ItemRange.js';
import type { CircularCache, CommonInternalProps } from './types.js'; import type { CircularCache, CommonInternalProps } from './types.js';
import getAriaPropsFromKeyPath from './getAriaPropsFromKeyPath.js';
/** /**
* Renders nested values (eg. objects, arrays, lists, etc.) * Renders nested values (eg. objects, arrays, lists, etc.)
@ -62,6 +63,7 @@ function renderChildNodes(
from={entry.from} from={entry.from}
to={entry.to} to={entry.to}
renderChildNodes={renderChildNodes} renderChildNodes={renderChildNodes}
keyPath={[entry.from, ...keyPath]}
/>, />,
); );
} else { } else {
@ -141,6 +143,8 @@ export default function JSONNestedNode(props: Props) {
); );
const stylingArgs = [keyPath, nodeType, expanded, expandable] as const; const stylingArgs = [keyPath, nodeType, expanded, expandable] as const;
const {ariaControls, ariaLabel} = getAriaPropsFromKeyPath(keyPath)
return hideRoot ? ( return hideRoot ? (
<li {...styling('rootNode', ...stylingArgs)}> <li {...styling('rootNode', ...stylingArgs)}>
<ul {...styling('rootNodeChildren', ...stylingArgs)}> <ul {...styling('rootNodeChildren', ...stylingArgs)}>
@ -155,6 +159,9 @@ export default function JSONNestedNode(props: Props) {
nodeType={nodeType} nodeType={nodeType}
expanded={expanded} expanded={expanded}
onClick={handleClick} onClick={handleClick}
ariaControls={ariaControls}
ariaLabel={ariaLabel}
OverrideComponent={props.ArrowComponentOverride}
/> />
)} )}
<label <label
@ -169,7 +176,7 @@ export default function JSONNestedNode(props: Props) {
> >
{renderedItemString} {renderedItemString}
</span> </span>
<ul {...styling('nestedNodeChildren', ...stylingArgs)}> <ul {...styling('nestedNodeChildren', ...stylingArgs)} id={expandable ? ariaControls : undefined}>
{renderedChildren} {renderedChildren}
</ul> </ul>
</li> </li>

View File

@ -122,6 +122,11 @@ const getDefaultThemeStyling = (theme: Base16Theme): StylingConfig => {
arrowContainer: ({ style }, arrowStyle) => ({ arrowContainer: ({ style }, arrowStyle) => ({
style: { style: {
...style, ...style,
background: 'none',
color: 'inherit',
border: 'none',
padding: 0,
font: 'inherit',
display: 'inline-block', display: 'inline-block',
paddingRight: '0.5em', paddingRight: '0.5em',
paddingLeft: arrowStyle === 'double' ? '1em' : 0, paddingLeft: arrowStyle === 'double' ? '1em' : 0,

View File

@ -0,0 +1,21 @@
import { KeyPath } from "./types.js";
const replaceSpacesRegex = / /g;
const getAriaPropsFromKeyPath = (
keyPath: KeyPath
) => {
let ariaControls = '';
let ariaLabel = 'JSON Tree Node: ';
for(let i = keyPath.length - 1; i >= 0; i--) {
const key = keyPath[i];
ariaControls += `${key}`.replace(replaceSpacesRegex, '-');
ariaLabel += `${key} `;
}
ariaLabel = ariaLabel.trim();
return { ariaControls, ariaLabel };
}
export default getAriaPropsFromKeyPath;

View File

@ -47,6 +47,7 @@ export function JSONTree({
isCustomNode = noCustomNode, isCustomNode = noCustomNode,
collectionLimit = 50, collectionLimit = 50,
sortObjectKeys = false, sortObjectKeys = false,
ArrowComponentOverride
}: Props) { }: Props) {
const styling = useMemo( const styling = useMemo(
() => () =>
@ -69,6 +70,7 @@ export function JSONTree({
postprocessValue={postprocessValue} postprocessValue={postprocessValue}
collectionLimit={collectionLimit} collectionLimit={collectionLimit}
sortObjectKeys={sortObjectKeys} sortObjectKeys={sortObjectKeys}
ArrowComponentOverride={ArrowComponentOverride}
/> />
</ul> </ul>
); );
@ -87,4 +89,7 @@ export type {
Styling, Styling,
CommonExternalProps, CommonExternalProps,
} from './types.js'; } from './types.js';
export type { JSONArrowProps } from './JSONArrow.js';
export type { StylingValue }; export type { StylingValue };

View File

@ -1,5 +1,6 @@
import React from 'react'; import React, { ComponentType } from 'react';
import { StylingFunction } from 'react-base16-styling'; import { StylingFunction } from 'react-base16-styling';
import { JSONArrowProps } from './JSONArrow.js';
export type Key = string | number; export type Key = string | number;
@ -46,6 +47,7 @@ export interface CommonExternalProps {
keyPath: KeyPath; keyPath: KeyPath;
labelRenderer: LabelRenderer; labelRenderer: LabelRenderer;
valueRenderer: ValueRenderer; valueRenderer: ValueRenderer;
ArrowComponentOverride?: ComponentType<JSONArrowProps>;
shouldExpandNodeInitially: ShouldExpandNodeInitially; shouldExpandNodeInitially: ShouldExpandNodeInitially;
hideRoot: boolean; hideRoot: boolean;
getItemString: GetItemString; getItemString: GetItemString;