Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some accessibility to the react-json-tree for keyboard naviagation, also allow Arrow Override #1747

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/purple-timers-whisper.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 28 additions & 0 deletions packages/react-json-tree/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,34 @@ Their full signatures are:
- `labelRenderer: function(keyPath, nodeType, expanded, expandable)`
- `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

- `shouldExpandNodeInitially: function(keyPath, data, level)` - determines if node should be expanded when it first renders (root is expanded by default)
Expand Down
11 changes: 10 additions & 1 deletion packages/react-json-tree/examples/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
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) => ({
style: {
Expand Down Expand Up @@ -125,6 +125,13 @@ const theme = {
base0F: '#cc6633',
};

const ArrowOverride = ({ expanded, ...rest }: JSONArrowProps) => {
if (expanded) {
return <button {...rest}>-</button>
}
return <button {...rest}>+</button>
}

const App = () => (
<div>
<JSONTree data={data} theme={theme} invertTheme />
Expand Down Expand Up @@ -168,10 +175,12 @@ const App = () => (
<p>
Pass <code>labelRenderer</code> or <code>valueRenderer</code>.
</p>
<p>Additionally, you may pass an <code>ArrowComponentOverride</code></p>
<div>
<JSONTree
data={data}
theme={theme}
ArrowComponentOverride={ArrowOverride}
labelRenderer={([raw]) => <span>(({raw})):</span>}
valueRenderer={(raw) => (
<em>
Expand Down
5 changes: 4 additions & 1 deletion packages/react-json-tree/src/ItemRange.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import JSONArrow from './JSONArrow.js';
import type { CircularCache, CommonInternalProps } from './types.js';
import type { CircularCache, CommonInternalProps, KeyPath } from './types.js';

interface Props extends CommonInternalProps {
data: unknown;
Expand All @@ -10,6 +10,7 @@ interface Props extends CommonInternalProps {
renderChildNodes: (props: Props, from: number, to: number) => React.ReactNode;
circularCache: CircularCache;
level: number;
keyPath: KeyPath;
}

export default function ItemRange(props: Props) {
Expand All @@ -32,6 +33,8 @@ export default function ItemRange(props: Props) {
expanded={false}
onClick={handleClick}
arrowStyle="double"
ariaLabel={`Expand Array from ${from} to ${to}`}
OverrideComponent={props.ArrowComponentOverride}
/>
{`${from} ... ${to}`}
</div>
Expand Down
34 changes: 22 additions & 12 deletions packages/react-json-tree/src/JSONArrow.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
import React from 'react';
import React, { ComponentType } from 'react';
import type { StylingFunction } from 'react-base16-styling';

interface Props {
export interface JSONArrowProps {
styling: StylingFunction;
arrowStyle?: 'single' | 'double';
expanded: boolean;
nodeType: string;
onClick: React.MouseEventHandler<HTMLDivElement>;
onClick: React.MouseEventHandler<HTMLButtonElement>;
ariaControls?: string;
ariaLabel?: string
OverrideComponent?: ComponentType<JSONArrowProps>;
}

export default function JSONArrow({
styling,
arrowStyle = 'single',
expanded,
nodeType,
onClick,
}: Props) {
export default function JSONArrow({OverrideComponent, ...props}: JSONArrowProps) {
const {
styling,
arrowStyle = 'single',
expanded,
nodeType,
onClick,
ariaControls,
ariaLabel
} = props
if(OverrideComponent) {
return <OverrideComponent {...props} />
}

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)}>
{'\u25B6'}
{arrowStyle === 'double' && (
<div {...styling(['arrowSign', 'arrowSignInner'])}>{'\u25B6'}</div>
)}
</div>
</div>
</button>
);
}
9 changes: 8 additions & 1 deletion packages/react-json-tree/src/JSONNestedNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import getCollectionEntries from './getCollectionEntries.js';
import JSONNode from './JSONNode.js';
import ItemRange from './ItemRange.js';
import type { CircularCache, CommonInternalProps } from './types.js';
import getAriaPropsFromKeyPath from './getAriaPropsFromKeyPath.js';

/**
* Renders nested values (eg. objects, arrays, lists, etc.)
Expand Down Expand Up @@ -62,6 +63,7 @@ function renderChildNodes(
from={entry.from}
to={entry.to}
renderChildNodes={renderChildNodes}
keyPath={[entry.from, ...keyPath]}
/>,
);
} else {
Expand Down Expand Up @@ -141,6 +143,8 @@ export default function JSONNestedNode(props: Props) {
);
const stylingArgs = [keyPath, nodeType, expanded, expandable] as const;

const {ariaControls, ariaLabel} = getAriaPropsFromKeyPath(keyPath)

return hideRoot ? (
<li {...styling('rootNode', ...stylingArgs)}>
<ul {...styling('rootNodeChildren', ...stylingArgs)}>
Expand All @@ -155,6 +159,9 @@ export default function JSONNestedNode(props: Props) {
nodeType={nodeType}
expanded={expanded}
onClick={handleClick}
ariaControls={ariaControls}
ariaLabel={ariaLabel}
OverrideComponent={props.ArrowComponentOverride}
/>
)}
<label
Expand All @@ -169,7 +176,7 @@ export default function JSONNestedNode(props: Props) {
>
{renderedItemString}
</span>
<ul {...styling('nestedNodeChildren', ...stylingArgs)}>
<ul {...styling('nestedNodeChildren', ...stylingArgs)} id={expandable ? ariaControls : undefined}>
{renderedChildren}
</ul>
</li>
Expand Down
5 changes: 5 additions & 0 deletions packages/react-json-tree/src/createStylingFromTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ const getDefaultThemeStyling = (theme: Base16Theme): StylingConfig => {
arrowContainer: ({ style }, arrowStyle) => ({
style: {
...style,
background: 'none',
color: 'inherit',
border: 'none',
padding: 0,
font: 'inherit',
display: 'inline-block',
paddingRight: '0.5em',
paddingLeft: arrowStyle === 'double' ? '1em' : 0,
Expand Down
21 changes: 21 additions & 0 deletions packages/react-json-tree/src/getAriaPropsFromKeyPath.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions packages/react-json-tree/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function JSONTree({
isCustomNode = noCustomNode,
collectionLimit = 50,
sortObjectKeys = false,
ArrowComponentOverride
}: Props) {
const styling = useMemo(
() =>
Expand All @@ -69,6 +70,7 @@ export function JSONTree({
postprocessValue={postprocessValue}
collectionLimit={collectionLimit}
sortObjectKeys={sortObjectKeys}
ArrowComponentOverride={ArrowComponentOverride}
/>
</ul>
);
Expand All @@ -87,4 +89,7 @@ export type {
Styling,
CommonExternalProps,
} from './types.js';

export type { JSONArrowProps } from './JSONArrow.js';

export type { StylingValue };
4 changes: 3 additions & 1 deletion packages/react-json-tree/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import React, { ComponentType } from 'react';
import { StylingFunction } from 'react-base16-styling';
import { JSONArrowProps } from './JSONArrow.js';

export type Key = string | number;

Expand Down Expand Up @@ -46,6 +47,7 @@ export interface CommonExternalProps {
keyPath: KeyPath;
labelRenderer: LabelRenderer;
valueRenderer: ValueRenderer;
ArrowComponentOverride?: ComponentType<JSONArrowProps>;
shouldExpandNodeInitially: ShouldExpandNodeInitially;
hideRoot: boolean;
getItemString: GetItemString;
Expand Down