diff --git a/src/components/Json/Json.module.css b/src/components/Json/Json.module.css
index 1646e5c..524284c 100644
--- a/src/components/Json/Json.module.css
+++ b/src/components/Json/Json.module.css
@@ -25,19 +25,26 @@
.clickable {
cursor: pointer;
-}
-.drill {
- color: #556;
- font-size: 10pt;
- margin-left: -12px;
- margin-right: 4px;
- padding: 2px 0;
- user-select: none;
-}
-.clickable:hover .drill {
- color: #778;
+ /* drill */
+ &::before {
+ content: '▶';
+ color: #556;
+ font-size: 10pt;
+ margin-left: -12px;
+ margin-right: 4px;
+ padding: 2px 0;
+ user-select: none;
+ }
+ &:hover::before {
+ color: #778;
+ }
+ &[aria-expanded="true"]::before {
+ content: '▼';
+ }
}
+
+
.key {
color: #aad;
font-weight: bold;
diff --git a/src/components/Json/Json.test.tsx b/src/components/Json/Json.test.tsx
index 3bde428..e7a1c37 100644
--- a/src/components/Json/Json.test.tsx
+++ b/src/components/Json/Json.test.tsx
@@ -27,9 +27,8 @@ describe('Json Component', () => {
[1, 'foo', null],
Array.from({ length: 101 }, (_, i) => i),
])('collapses any array', (array) => {
- const { queryByText } = render()
- expect(queryByText('▶')).toBeTruthy()
- expect(queryByText('▼')).toBeNull()
+ const { getByRole } = render()
+ expect(getByRole('treeitem').ariaExpanded).toBe('false')
})
it.for([
@@ -40,13 +39,12 @@ describe('Json Component', () => {
])('shows short arrays of primitive items, without trailing comment about length', (array) => {
const { queryByText } = render()
expect(queryByText('...')).toBeNull()
- expect(queryByText('length')).toBeNull()
+ expect(queryByText(/length/)).toBeNull()
})
it.for([
- // [1, 'foo', [1, 2, 3]], // TODO(SL): this one does not collapses, what to do? The text is wrong
Array.from({ length: 101 }, (_, i) => i),
- ])('hides long arrays, and non-primitive items, with trailing comment about length', (array) => {
+ ])('hides long arrays with trailing comment about length', (array) => {
const { getByText } = render()
getByText('...')
getByText(/length/)
@@ -66,20 +64,39 @@ describe('Json Component', () => {
getByText('"42"')
})
+ it.for([
+ [1, 'foo', [1, 2, 3]],
+ [1, 'foo', { nested: true }],
+ ])('expands short arrays with non-primitive values', (arr) => {
+ const { getAllByRole } = render()
+ const treeItems = getAllByRole('treeitem')
+ expect(treeItems.length).toBe(2)
+ expect(treeItems[0]?.getAttribute('aria-expanded')).toBe('true') // the root
+ expect(treeItems[1]?.getAttribute('aria-expanded')).toBe('false') // the non-primitive value (object/array)
+ })
+
it.for([
{ obj: [314, null] },
{ obj: { nested: true } },
])('expands short objects with non-primitive values', (obj) => {
- const { getByText } = render()
- getByText('▼')
+ const { getAllByRole } = render()
+ const treeItems = getAllByRole('treeitem')
+ expect(treeItems.length).toBe(2)
+ expect(treeItems[0]?.getAttribute('aria-expanded')).toBe('true') // the root
+ expect(treeItems[1]?.getAttribute('aria-expanded')).toBe('false') // the non-primitive value (object/array)
})
it.for([
{ obj: [314, null] },
{ obj: { nested: true } },
])('hides the content and append number of entries when objects with non-primitive values are collapsed', (obj) => {
- const { getByText } = render()
- fireEvent.click(getByText('▼'))
+ const { getAllByRole, getByText } = render()
+ const root = getAllByRole('treeitem')[0]
+ if (!root) { /* type assertion, getAllByRole would already have thrown */
+ throw new Error('No root element found')
+ }
+ fireEvent.click(root)
+ expect(root.getAttribute('aria-expanded')).toBe('false')
getByText('...')
getByText(/entries/)
})
@@ -90,9 +107,8 @@ describe('Json Component', () => {
{ a: 1, b: true, c: null, d: undefined },
Object.fromEntries(Array.from({ length: 101 }, (_, i) => [`key${i}`, { nested: true }])),
])('collapses long objects, or objects with only primitive values (included empty object)', (obj) => {
- const { queryByText } = render()
- expect(queryByText('▶')).toBeTruthy()
- expect(queryByText('▼')).toBeNull()
+ const { getByRole } = render()
+ expect(getByRole('treeitem').getAttribute('aria-expanded')).toBe('false')
})
it.for([
@@ -105,21 +121,23 @@ describe('Json Component', () => {
it('toggles array collapse state', () => {
const longArray = Array.from({ length: 101 }, (_, i) => i)
- const { getByText, queryByText } = render()
+ const { getByRole, getByText, queryByText } = render()
+ const treeItem = getByRole('treeitem')
getByText('...')
- fireEvent.click(getByText('▶'))
+ fireEvent.click(treeItem)
expect(queryByText('...')).toBeNull()
- fireEvent.click(getByText('▼'))
+ fireEvent.click(treeItem)
getByText('...')
})
it('toggles object collapse state', () => {
const longObject = Object.fromEntries(Array.from({ length: 101 }, (_, i) => [`key${i}`, { nested: true }]))
- const { getByText, queryByText } = render()
+ const { getByRole, getByText, queryByText } = render()
+ const treeItem = getByRole('treeitem') // only one treeitem because the inner objects are collapsed and not represented as treeitems
getByText('...')
- fireEvent.click(getByText('▶'))
+ fireEvent.click(treeItem)
expect(queryByText('...')).toBeNull()
- fireEvent.click(getByText('▼'))
+ fireEvent.click(treeItem)
getByText('...')
})
})
diff --git a/src/components/Json/Json.tsx b/src/components/Json/Json.tsx
index 20c9657..31f98c6 100644
--- a/src/components/Json/Json.tsx
+++ b/src/components/Json/Json.tsx
@@ -14,7 +14,7 @@ interface JsonProps {
* JSON viewer component with collapsible objects and arrays.
*/
export default function Json({ json, label, className }: JsonProps): ReactNode {
- return
+ return
}
@@ -89,19 +89,17 @@ function JsonArray({ array, label }: { array: unknown[], label?: string }): Reac
const [collapsed, setCollapsed] = useState(shouldObjectCollapse(array))
const key = label ?
{label}: : ''
if (collapsed) {
- return
{ setCollapsed(false) }}>
-
{'\u25B6'}
+ return
{ setCollapsed(false) }}>
{key}
}
return <>
-
{ setCollapsed(true) }}>
-
{'\u25BC'}
+
{ setCollapsed(true) }}>
{key}
{'['}
-