Skip to content

Commit 81493e6

Browse files
committed
Json page limit with show more button
1 parent 0fd683c commit 81493e6

5 files changed

Lines changed: 84 additions & 20 deletions

File tree

src/components/Json/Json.module.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,11 @@
6767
.comment {
6868
color: #ccd8;
6969
}
70+
.showMore {
71+
cursor: pointer;
72+
color: #ccd8;
73+
background: none;
74+
border: none;
75+
padding: 0;
76+
font: inherit;
77+
}

src/components/Json/Json.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const Arrays: Story = {
5555

5656
export const TopLevelArray: Story = {
5757
args: {
58-
json: Array.from({ length: 100 }, (_, i) => [i, i + 1, i + 2]),
58+
json: Array.from({ length: 300 }, (_, i) => [i, i + 1, i + 2]),
5959
},
6060
render,
6161
}

src/components/Json/Json.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,44 @@ describe('Json Component', () => {
126126
getByText(/entries/)
127127
})
128128

129+
it('paginates large arrays showing only first 100 items', () => {
130+
const largeArray = Array.from({ length: 150 }, (_, i) => i)
131+
const { getByText, queryByText } = render(<Json json={largeArray} />)
132+
getByText('0')
133+
getByText('99')
134+
expect(queryByText('100')).toBeNull()
135+
getByText('Show more...')
136+
})
137+
138+
it('shows more array items when clicking Show more...', async () => {
139+
const largeArray = Array.from({ length: 150 }, (_, i) => i)
140+
const { getByText, queryByText } = render(<Json json={largeArray} />)
141+
const user = userEvent.setup()
142+
await user.click(getByText('Show more...'))
143+
getByText('100')
144+
getByText('149')
145+
expect(queryByText('Show more...')).toBeNull()
146+
})
147+
148+
it('paginates large objects showing only first 100 entries', () => {
149+
const largeObj = Object.fromEntries(Array.from({ length: 150 }, (_, i) => [`key${i}`, i]))
150+
const { getByText, queryByText } = render(<Json json={largeObj} />)
151+
getByText('key0:')
152+
getByText('key99:')
153+
expect(queryByText('key100:')).toBeNull()
154+
getByText('Show more...')
155+
})
156+
157+
it('shows more object entries when clicking Show more...', async () => {
158+
const largeObj = Object.fromEntries(Array.from({ length: 150 }, (_, i) => [`key${i}`, i]))
159+
const { getByText, queryByText } = render(<Json json={largeObj} />)
160+
const user = userEvent.setup()
161+
await user.click(getByText('Show more...'))
162+
getByText('key100:')
163+
getByText('key149:')
164+
expect(queryByText('Show more...')).toBeNull()
165+
})
166+
129167
it('toggles array collapse state', async () => {
130168
const longArray = Array.from({ length: 101 }, (_, i) => i)
131169
const { getByRole, getByText, queryByText } = render(<Json json={longArray} />)

src/components/Json/Json.tsx

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
import { ReactNode, useState } from 'react'
22
import { cn } from '../../lib'
33
import styles from './Json.module.css'
4-
import { isPrimitive, shouldObjectCollapse } from './helpers.js'
4+
import { isPrimitive, shouldObjectCollapse, stringifyPrimitive } from './helpers.js'
55
import { useWidth } from './useWidth.js'
66

7+
const defaultPageLimit = 100
8+
79
interface JsonProps {
810
json: unknown
911
label?: string
1012
className?: string
1113
expandRoot?: boolean // Expand the top-level object/array by default
14+
pageLimit?: number // Max items to render before showing "Show more..."
1215
}
1316

1417
/**
1518
* JSON viewer component with collapsible objects and arrays.
1619
*/
17-
export default function Json({ json, label, className, expandRoot = true }: JsonProps): ReactNode {
20+
export default function Json({ json, label, className, expandRoot = true, pageLimit }: JsonProps): ReactNode {
1821
return <div className={cn(styles.json, className)} role="tree">
19-
<JsonContent json={json} label={label} expandRoot={expandRoot} />
22+
<JsonContent json={json} label={label} expandRoot={expandRoot} pageLimit={pageLimit} />
2023
</div>
2124
}
2225

23-
function JsonContent({ json, label, expandRoot }: JsonProps): ReactNode {
26+
function JsonContent({ json, label, expandRoot, pageLimit }: JsonProps): ReactNode {
2427
let div
2528
if (Array.isArray(json)) {
26-
div = <JsonArray array={json} label={label} expandRoot={expandRoot} />
29+
div = <JsonArray array={json} label={label} expandRoot={expandRoot} pageLimit={pageLimit} />
2730
} else if (typeof json === 'object' && json !== null) {
28-
div = <JsonObject label={label} obj={json} expandRoot={expandRoot} />
31+
div = <JsonObject label={label} obj={json} expandRoot={expandRoot} pageLimit={pageLimit} />
2932
} else {
3033
// primitive
3134
const key = label ? <span className={styles.key}>{label}: </span> : ''
@@ -52,7 +55,7 @@ function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
5255
const separator = ', '
5356

5457
const children: ReactNode[] = []
55-
let suffix: string | undefined = undefined
58+
let suffix: string | undefined
5659

5760
let characterCount = 0
5861
for (const [index, value] of array.entries()) {
@@ -62,9 +65,7 @@ function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
6265
}
6366
// should we continue?
6467
if (isPrimitive(value)) {
65-
const asString = typeof value === 'bigint' ? value.toString() :
66-
value === undefined ? 'undefined' /* see JsonContent - even if JSON.stringify([undefined]) === '[null]' */:
67-
JSON.stringify(value)
68+
const asString = stringifyPrimitive(value)
6869
characterCount += asString.length
6970
if (characterCount < maxCharacterCount) {
7071
children.push(<JsonContent json={value} key={`value-${index}`} />)
@@ -86,13 +87,14 @@ function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
8687
)
8788
}
8889

89-
function JsonArray({ array, label, expandRoot }: { array: unknown[], label?: string, expandRoot?: boolean }): ReactNode {
90+
function JsonArray({ array, label, expandRoot, pageLimit = defaultPageLimit }: { array: unknown[], label?: string, expandRoot?: boolean, pageLimit?: number }): ReactNode {
9091
const [collapsed, setCollapsed] = useState(!expandRoot && shouldObjectCollapse(array))
92+
const [limit, setLimit] = useState(pageLimit)
9193
const key = label ? <span className={styles.key}>{label}: </span> : ''
9294
if (collapsed) {
9395
return <div role="treeitem" className={styles.clickable} aria-expanded="false" onClick={() => { setCollapsed(false) }}>
9496
{key}
95-
<CollapsedArray array={array}></CollapsedArray>
97+
<CollapsedArray array={array} />
9698
</div>
9799
}
98100
return <>
@@ -101,7 +103,10 @@ function JsonArray({ array, label, expandRoot }: { array: unknown[], label?: str
101103
<span className={styles.array}>{'['}</span>
102104
</div>
103105
<ul role="group">
104-
{array.map((item, index) => <li key={index}><JsonContent json={item} /></li>)}
106+
{array.slice(0, limit).map((item, index) => <li key={index}><JsonContent json={item} /></li>)}
107+
{array.length > limit && <li>
108+
<button className={styles.showMore} onClick={() => { setLimit(limit + pageLimit) }}>Show more...</button>
109+
</li>}
105110
</ul>
106111
<div className={styles.array}>{']'}</div>
107112
</>
@@ -114,7 +119,7 @@ function CollapsedObject({ obj }: { obj: object }): ReactNode {
114119
const kvSeparator = ': '
115120

116121
const children: ReactNode[] = []
117-
let suffix: string | undefined = undefined
122+
let suffix: string | undefined
118123

119124
const entries = Object.entries(obj)
120125
let characterCount = 0
@@ -125,9 +130,7 @@ function CollapsedObject({ obj }: { obj: object }): ReactNode {
125130
}
126131
// should we continue?
127132
if (isPrimitive(value)) {
128-
const asString = typeof value === 'bigint' ? value.toString() :
129-
value === undefined ? 'undefined' /* see JsonContent - even if JSON.stringify([undefined]) === '[null]' */:
130-
JSON.stringify(value)
133+
const asString = stringifyPrimitive(value)
131134
characterCount += key.length + kvSeparator.length + asString.length
132135
if (characterCount < maxCharacterCount) {
133136
children.push(<JsonContent json={value as unknown} label={key} key={`value-${index}`} />)
@@ -149,26 +152,31 @@ function CollapsedObject({ obj }: { obj: object }): ReactNode {
149152
)
150153
}
151154

152-
function JsonObject({ obj, label, expandRoot }: { obj: object, label?: string, expandRoot?: boolean }): ReactNode {
155+
function JsonObject({ obj, label, expandRoot, pageLimit = defaultPageLimit }: { obj: object, label?: string, expandRoot?: boolean, pageLimit?: number }): ReactNode {
153156
const [collapsed, setCollapsed] = useState(!expandRoot && shouldObjectCollapse(obj))
157+
const [limit, setLimit] = useState(pageLimit)
154158
const key = label ? <span className={styles.key}>{label}: </span> : ''
155159
if (collapsed) {
156160
return <div role="treeitem" className={styles.clickable} aria-expanded="false" onClick={() => { setCollapsed(false) }}>
157161
{key}
158162
<CollapsedObject obj={obj} />
159163
</div>
160164
}
165+
const entries = Object.entries(obj)
161166
return <>
162167
<div role="treeitem" className={styles.clickable} aria-expanded="true" onClick={() => { setCollapsed(true) }}>
163168
{key}
164169
<span className={styles.object}>{'{'}</span>
165170
</div>
166171
<ul role="group">
167-
{Object.entries(obj).map(([key, value]) =>
172+
{entries.slice(0, limit).map(([key, value]) =>
168173
<li key={key}>
169174
<JsonContent json={value as unknown} label={key} />
170175
</li>
171176
)}
177+
{entries.length > limit && <li>
178+
<button className={styles.showMore} onClick={() => { setLimit(limit + pageLimit) }}>Show more...</button>
179+
</li>}
172180
</ul>
173181
<div className={styles.object}>{'}'}</div>
174182
</>

src/components/Json/helpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ export function isPrimitive(value: unknown): boolean {
99
)
1010
}
1111

12+
export function stringifyPrimitive(value: unknown): string {
13+
if (typeof value === 'bigint') {
14+
return value.toString()
15+
} else if (value === undefined) {
16+
return 'undefined'
17+
} else {
18+
return JSON.stringify(value)
19+
}
20+
}
21+
1222
export function shouldObjectCollapse(obj: object): boolean {
1323
const values = Object.values(obj)
1424
if (

0 commit comments

Comments
 (0)