Skip to content

Commit b972718

Browse files
authored
Json expand root (#375)
* Json automatically expand root node * Json page limit with show more button * Json date support * Json boolean style
1 parent aff8c24 commit b972718

File tree

6 files changed

+158
-70
lines changed

6 files changed

+158
-70
lines changed

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,33 +56,33 @@
5656
},
5757
"dependencies": {
5858
"hightable": "0.26.3",
59-
"hyparquet": "1.25.0",
59+
"hyparquet": "1.25.1",
6060
"hyparquet-compressors": "1.1.1",
6161
"icebird": "0.3.1",
62-
"squirreling": "0.9.1"
62+
"squirreling": "0.9.4"
6363
},
6464
"devDependencies": {
65-
"@storybook/react-vite": "10.2.10",
65+
"@storybook/react-vite": "10.2.13",
6666
"@testing-library/react": "16.3.2",
67-
"@types/node": "25.2.3",
67+
"@types/node": "25.3.3",
6868
"@types/react": "19.2.14",
6969
"@types/react-dom": "19.2.3",
7070
"@vitejs/plugin-react": "5.1.4",
7171
"@vitest/coverage-v8": "4.0.18",
7272
"eslint": "9.39.2",
7373
"eslint-plugin-react": "7.37.5",
7474
"eslint-plugin-react-hooks": "7.0.1",
75-
"eslint-plugin-react-refresh": "0.5.0",
76-
"eslint-plugin-storybook": "10.2.10",
77-
"globals": "17.3.0",
75+
"eslint-plugin-react-refresh": "0.5.2",
76+
"eslint-plugin-storybook": "10.2.13",
77+
"globals": "17.4.0",
7878
"jsdom": "28.1.0",
79-
"nodemon": "3.1.11",
79+
"nodemon": "3.1.14",
8080
"npm-run-all": "4.1.5",
8181
"react": "19.2.4",
8282
"react-dom": "19.2.4",
83-
"storybook": "10.2.10",
83+
"storybook": "10.2.13",
8484
"typescript": "5.9.3",
85-
"typescript-eslint": "8.56.0",
85+
"typescript-eslint": "8.56.1",
8686
"vite": "7.3.1",
8787
"vitest": "4.0.18"
8888
},

src/components/Json/Json.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,20 @@
6161
.string {
6262
color: #eaa;
6363
}
64+
.boolean {
65+
color: #9ce;
66+
}
6467
.other {
6568
color: #d6d6d6;
6669
}
6770
.comment {
6871
color: #ccd8;
6972
}
73+
.showMore {
74+
cursor: pointer;
75+
color: #ccd8;
76+
background: none;
77+
border: none;
78+
padding: 0;
79+
font: inherit;
80+
}

src/components/Json/Json.stories.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export const Arrays: Story = {
4343
strings2: Array.from({ length: 2 }, (_, i) => `hello ${i}`),
4444
strings8: Array.from({ length: 8 }, (_, i) => `hello ${i}`),
4545
strings100: Array.from({ length: 100 }, (_, i) => `hello ${i}`),
46+
dates1: Array.from({ length: 1 }, (_, i) => new Date(2025, 0, i + 1)),
47+
dates10: Array.from({ length: 10 }, (_, i) => new Date(2025, 0, i + 1)),
4648
misc: Array.from({ length: 8 }, (_, i) => i % 2 ? `hello ${i}` : i),
4749
misc2: Array.from({ length: 8 }, (_, i) => i % 3 === 0 ? i : i % 3 === 1 ? `hello ${i}` : [i, i + 1, i + 2]),
4850
misc3: [1, 'hello', null, undefined],
@@ -53,21 +55,28 @@ export const Arrays: Story = {
5355
render,
5456
}
5557

58+
export const TopLevelArray: Story = {
59+
args: {
60+
json: Array.from({ length: 300 }, (_, i) => [i, i + 1, i + 2]),
61+
},
62+
render,
63+
}
64+
5665
export const Objects: Story = {
5766
args: {
5867
json: {
5968
empty: {},
6069
numbers1: { k0: 1 },
70+
dates1: { d0: new Date('2025-01-01') },
6171
numbers8: Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`k${i}`, i])),
6272
numbers100: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`k${i}`, i])),
6373
strings8: Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`k${i}`, `hello ${i}`])),
6474
strings100: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`k${i}`, `hello ${i}`])),
6575
misc: Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`k${i}`, i % 2 ? `hello ${i}` : i])),
6676
misc2: Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`k${i}`, i % 3 === 0 ? i : i % 3 === 1 ? `hello ${i}` : [i, i + 1, i + 2]])),
6777
misc3: { k0: 1, k1: 'a', k2: null, k3: undefined },
68-
arrays100: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`k${i}`, [i, i + 1, i + 2]])),
78+
arrays200: Object.fromEntries(Array.from({ length: 200 }, (_, i) => [`k${i}`, [i, i + 1, i + 2]])),
6979
},
70-
label: 'json',
7180
},
7281
render,
7382
}

src/components/Json/Json.test.tsx

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ describe('Json Component', () => {
2121
getByText('"bar"')
2222
})
2323

24-
it.for([
25-
[],
26-
[1, 2, 3],
27-
Array.from({ length: 101 }, (_, i) => i),
28-
])('collapses any array with primitives', (array) => {
29-
const { getByRole } = render(<Json json={array} />)
24+
it('expands root array by default', () => {
25+
const { getByRole } = render(<Json json={[1, 2, 3]} />)
26+
expect(getByRole('treeitem').ariaExpanded).toBe('true')
27+
})
28+
29+
it('collapses array with primitives when expandRoot is false', () => {
30+
const { getByRole } = render(<Json json={[1, 2, 3]} expandRoot={false} />)
3031
expect(getByRole('treeitem').ariaExpanded).toBe('false')
3132
})
3233

@@ -41,10 +42,9 @@ describe('Json Component', () => {
4142
expect(queryByText(/length/)).toBeNull()
4243
})
4344

44-
it.for([
45-
Array.from({ length: 101 }, (_, i) => i),
46-
])('hides long arrays with trailing comment about length', (array) => {
47-
const { getByText } = render(<Json json={array} />)
45+
it('hides long arrays with trailing comment about length when collapsed', () => {
46+
const longArray = Array.from({ length: 101 }, (_, i) => i)
47+
const { getByText } = render(<Json json={longArray} expandRoot={false} />)
4848
getByText('...')
4949
getByText(/length/)
5050
})
@@ -55,6 +55,11 @@ describe('Json Component', () => {
5555
getByText('"value"')
5656
})
5757

58+
it('renders a Date as its ISO string', () => {
59+
const { getByText } = render(<Json json={new Date('2025-01-01')} />)
60+
getByText('"2025-01-01T00:00:00.000Z"')
61+
})
62+
5863
it('renders nested objects', () => {
5964
const { getByText } = render(<Json json={{ obj: { arr: [314, '42'] } }} />)
6065
getByText('obj:')
@@ -109,46 +114,85 @@ describe('Json Component', () => {
109114
getByText(/entries/)
110115
})
111116

112-
it.for([
113-
{},
114-
{ a: 1, b: 2 },
115-
{ a: 1, b: true, c: null, d: undefined },
116-
Object.fromEntries(Array.from({ length: 101 }, (_, i) => [`key${i}`, { nested: true }])),
117-
])('collapses long objects, or objects with only primitive values (included empty object)', (obj) => {
118-
const { getByRole } = render(<Json json={obj} />)
117+
it('expands root object by default', () => {
118+
const { getByRole } = render(<Json json={{ a: 1, b: 2 }} />)
119+
expect(getByRole('treeitem').getAttribute('aria-expanded')).toBe('true')
120+
})
121+
122+
it('collapses objects with only primitive values when expandRoot is false', () => {
123+
const { getByRole } = render(<Json json={{ a: 1, b: 2 }} expandRoot={false} />)
119124
expect(getByRole('treeitem').getAttribute('aria-expanded')).toBe('false')
120125
})
121126

122-
it.for([
123-
Object.fromEntries(Array.from({ length: 101 }, (_, i) => [`key${i}`, { nested: true }])),
124-
])('hides the content and append number of entries when objects has many entries', (obj) => {
125-
const { getByText } = render(<Json json={obj} />)
127+
it('hides the content and append number of entries when objects has many entries when collapsed', () => {
128+
const longObject = Object.fromEntries(Array.from({ length: 101 }, (_, i) => [`key${i}`, { nested: true }]))
129+
const { getByText } = render(<Json json={longObject} expandRoot={false} />)
126130
getByText('...')
127131
getByText(/entries/)
128132
})
129133

134+
it('paginates large arrays showing only first 100 items', () => {
135+
const largeArray = Array.from({ length: 150 }, (_, i) => i)
136+
const { getByText, queryByText } = render(<Json json={largeArray} />)
137+
getByText('0')
138+
getByText('99')
139+
expect(queryByText('100')).toBeNull()
140+
getByText('Show more...')
141+
})
142+
143+
it('shows more array items when clicking Show more...', async () => {
144+
const largeArray = Array.from({ length: 150 }, (_, i) => i)
145+
const { getByText, queryByText } = render(<Json json={largeArray} />)
146+
const user = userEvent.setup()
147+
await user.click(getByText('Show more...'))
148+
getByText('100')
149+
getByText('149')
150+
expect(queryByText('Show more...')).toBeNull()
151+
})
152+
153+
it('paginates large objects showing only first 100 entries', () => {
154+
const largeObj = Object.fromEntries(Array.from({ length: 150 }, (_, i) => [`key${i}`, i]))
155+
const { getByText, queryByText } = render(<Json json={largeObj} />)
156+
getByText('key0:')
157+
getByText('key99:')
158+
expect(queryByText('key100:')).toBeNull()
159+
getByText('Show more...')
160+
})
161+
162+
it('shows more object entries when clicking Show more...', async () => {
163+
const largeObj = Object.fromEntries(Array.from({ length: 150 }, (_, i) => [`key${i}`, i]))
164+
const { getByText, queryByText } = render(<Json json={largeObj} />)
165+
const user = userEvent.setup()
166+
await user.click(getByText('Show more...'))
167+
getByText('key100:')
168+
getByText('key149:')
169+
expect(queryByText('Show more...')).toBeNull()
170+
})
171+
130172
it('toggles array collapse state', async () => {
131173
const longArray = Array.from({ length: 101 }, (_, i) => i)
132174
const { getByRole, getByText, queryByText } = render(<Json json={longArray} />)
133175
const treeItem = getByRole('treeitem')
134-
getByText('...')
176+
expect(queryByText('...')).toBeNull() // expanded by default
135177
const user = userEvent.setup()
136-
await user.click(treeItem)
137-
expect(queryByText('...')).toBeNull()
138-
await user.click(treeItem)
178+
await user.click(treeItem) // collapse
139179
getByText('...')
180+
await user.click(treeItem) // expand again
181+
expect(queryByText('...')).toBeNull()
140182
})
141183

142184
it('toggles object collapse state', async () => {
143185
const longObject = Object.fromEntries(Array.from({ length: 101 }, (_, i) => [`key${i}`, { nested: true }]))
144-
const { getByRole, getByText, queryByText } = render(<Json json={longObject} />)
145-
const treeItem = getByRole('treeitem') // only one treeitem because the inner objects are collapsed and not represented as treeitems
146-
getByText('...')
147-
const user = userEvent.setup()
148-
await user.click(treeItem)
186+
const { getAllByRole, getByRole, getByText, queryByText } = render(<Json json={longObject} />)
187+
const treeItem = getAllByRole('treeitem')[0] // expanded by default due to expandRoot
188+
if (!treeItem) throw new Error('No root element found')
149189
expect(queryByText('...')).toBeNull()
150-
await user.click(treeItem)
190+
const user = userEvent.setup()
191+
await user.click(treeItem) // collapse
192+
getByRole('treeitem') // now only one treeitem
151193
getByText('...')
194+
await user.click(treeItem) // expand again
195+
expect(queryByText('...')).toBeNull()
152196
})
153197
})
154198

0 commit comments

Comments
 (0)