{subEntryPages.map((entries, index) => (
-
+
{expandedPages.includes(index) ? (
- {entries.map(entry => handleEntry(entry))}
+ {entries.map(entry => (
+
+ ))}
) : null}
@@ -117,36 +164,43 @@ const DefaultRenderer = ({
)
}
+type HandleEntryComponent = (props: { entry: Entry }) => JSX.Element
+
+type ExplorerProps = Partial & {
+ renderer?: Renderer
+ defaultExpanded?: true | Record
+}
+
+type Property = {
+ defaultExpanded?: boolean | Record
+ label: string
+ value: unknown
+}
+
+function isIterable(x: any): x is Iterable {
+ return Symbol.iterator in x
+}
+
export default function Explorer({
value,
defaultExpanded,
renderer = DefaultRenderer,
pageSize = 100,
- depth = 0,
...rest
-}) {
- const [expanded, setExpanded] = React.useState(defaultExpanded)
+}: ExplorerProps) {
+ const [expanded, setExpanded] = React.useState(Boolean(defaultExpanded))
+ const toggleExpanded = React.useCallback(() => setExpanded(old => !old), [])
- const toggle = set => {
- setExpanded(old => (typeof set !== 'undefined' ? set : !old))
- }
-
- const path = []
+ let type: string = typeof value
+ let subEntries: Property[] = []
- let type = typeof value
- let subEntries
- const subEntryPages = []
-
- const makeProperty = sub => {
- const newPath = path.concat(sub.label)
+ const makeProperty = (sub: { label: string; value: unknown }): Property => {
const subDefaultExpanded =
defaultExpanded === true
? { [sub.label]: true }
: defaultExpanded?.[sub.label]
return {
...sub,
- path: newPath,
- depth: depth + 1,
defaultExpanded: subDefaultExpanded,
}
}
@@ -155,54 +209,45 @@ export default function Explorer({
type = 'array'
subEntries = value.map((d, i) =>
makeProperty({
- label: i,
+ label: i.toString(),
value: d,
})
)
} else if (
value !== null &&
typeof value === 'object' &&
+ isIterable(value) &&
typeof value[Symbol.iterator] === 'function'
) {
type = 'Iterable'
subEntries = Array.from(value, (val, i) =>
makeProperty({
- label: i,
+ label: i.toString(),
value: val,
})
)
} else if (typeof value === 'object' && value !== null) {
type = 'object'
- // eslint-disable-next-line no-shadow
- subEntries = Object.entries(value).map(([label, value]) =>
+ subEntries = Object.entries(value).map(([key, val]) =>
makeProperty({
- label,
- value,
+ label: key,
+ value: val,
})
)
}
- if (subEntries) {
- let i = 0
-
- while (i < subEntries.length) {
- subEntryPages.push(subEntries.slice(i, i + pageSize))
- i = i + pageSize
- }
- }
+ const subEntryPages = chunkArray(subEntries, pageSize)
return renderer({
- handleEntry: entry => (
-
+ HandleEntry: ({ entry }) => (
+
),
type,
subEntries,
subEntryPages,
- depth,
value,
- path,
expanded,
- toggle,
+ toggleExpanded,
pageSize,
...rest,
})
diff --git a/src/devtools/tests/Explorer.test.tsx b/src/devtools/tests/Explorer.test.tsx
new file mode 100644
index 0000000000..7a14ad404c
--- /dev/null
+++ b/src/devtools/tests/Explorer.test.tsx
@@ -0,0 +1,57 @@
+import React from 'react'
+import { fireEvent, render, screen } from '@testing-library/react'
+
+import { chunkArray, DefaultRenderer } from '../Explorer'
+
+describe('Explorer', () => {
+ describe('chunkArray', () => {
+ it('when the size is less than one return an empty array', () => {
+ expect(chunkArray([1, 2, 3], 0)).toStrictEqual([])
+ })
+
+ it('when the array is empty return an empty array', () => {
+ expect(chunkArray([], 2)).toStrictEqual([])
+ })
+
+ it('when the array is evenly chunked return full chunks ', () => {
+ expect(chunkArray([1, 2, 3, 4], 2)).toStrictEqual([
+ [1, 2],
+ [3, 4],
+ ])
+ })
+
+ it('when the array is not evenly chunkable by size the last item is the remaining elements ', () => {
+ const chunks = chunkArray([1, 2, 3, 4, 5], 2)
+ const lastChunk = chunks[chunks.length - 1]
+ expect(lastChunk).toStrictEqual([5])
+ })
+ })
+
+ describe('DefaultRenderer', () => {
+ it('when the entry label is clicked, toggle expanded', async () => {
+ const toggleExpanded = jest.fn()
+
+ render(
+ <>>}
+ value={undefined}
+ subEntries={[]}
+ type="string"
+ />
+ )
+
+ const expandButton = screen.getByRole('button', {
+ name: /▶ the top level label 0 item/i,
+ })
+
+ fireEvent.click(expandButton)
+
+ expect(toggleExpanded).toHaveBeenCalledTimes(1)
+ })
+ })
+})