Skip to content

Commit 20e90ea

Browse files
authored
fix: show guideline to right-side props on hover if present (#182)
1 parent 5faeeac commit 20e90ea

File tree

10 files changed

+149
-98
lines changed

10 files changed

+149
-98
lines changed

src/__tests__/__snapshots__/index.spec.tsx.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ exports[`HTML Output given anyOf combiner placed next to allOf given allOf mergi
258258
<div>type</div>
259259
<span>string</span>
260260
</div>
261+
<div></div>
261262
<span>required</span>
262263
</div>
263264
<div>
@@ -425,6 +426,7 @@ exports[`HTML Output given oneOf combiner placed next to allOf given allOf mergi
425426
<div>type</div>
426427
<span>string</span>
427428
</div>
429+
<div></div>
428430
<span>required</span>
429431
</div>
430432
<div>
@@ -548,6 +550,7 @@ exports[`HTML Output given standalone mode, should populate proper nodes 1`] = `
548550
<div>id</div>
549551
<span>string</span>
550552
</div>
553+
<div></div>
551554
<span>read-only</span>
552555
</div>
553556
</div>
@@ -559,6 +562,7 @@ exports[`HTML Output given standalone mode, should populate proper nodes 1`] = `
559562
<div>description</div>
560563
<span>string</span>
561564
</div>
565+
<div></div>
562566
<span>write-only</span>
563567
</div>
564568
</div>

src/components/JsonSchemaViewer.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import * as React from 'react';
1414

1515
import { JSVOptions, JSVOptionsContextProvider } from '../contexts';
1616
import type { JSONSchema } from '../types';
17-
import { PathCrumbs, pathCrumbsAtom } from './PathCrumbs';
17+
import { PathCrumbs } from './PathCrumbs';
1818
import { TopLevelSchemaRow } from './SchemaRow';
19+
import { hoveredNodeAtom } from './SchemaRow/state';
1920

2021
export type JsonSchemaProps = Partial<JSVOptions> & {
2122
schema: JSONSchema;
@@ -74,10 +75,10 @@ const JsonSchemaViewerInner = ({
7475
JsonSchemaProps,
7576
'schema' | 'viewMode' | 'className' | 'resolveRef' | 'emptyText' | 'onTreePopulated' | 'maxHeight' | 'parentCrumbs'
7677
>) => {
77-
const setPathCrumbs = useUpdateAtom(pathCrumbsAtom);
78+
const setHoveredNode = useUpdateAtom(hoveredNodeAtom);
7879
const onMouseLeave = React.useCallback(() => {
79-
setPathCrumbs([]);
80-
}, [setPathCrumbs]);
80+
setHoveredNode(null);
81+
}, [setHoveredNode]);
8182

8283
const { jsonSchemaTreeRoot, nodeCount } = React.useMemo(() => {
8384
const jsonSchemaTree = new JsonSchemaTree(schema, {

src/components/PathCrumbs/index.tsx

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { isRegularNode, isRootNode, SchemaNode } from '@stoplight/json-schema-tree';
21
import { Box, HStack } from '@stoplight/mosaic';
3-
import { atom, useAtom } from 'jotai';
2+
import { useAtom } from 'jotai';
43
import * as React from 'react';
54

65
import { useJSVOptionsContext } from '../../contexts';
7-
8-
export const showPathCrumbsAtom = atom<boolean>(false);
9-
10-
export const pathCrumbsAtom = atom([], (_get, set, node) => {
11-
set(pathCrumbsAtom, propertyPathToObjectPath(node as SchemaNode));
12-
});
6+
import { pathCrumbsAtom, showPathCrumbsAtom } from './state';
137

148
export const PathCrumbs = ({ parentCrumbs = [] }: { parentCrumbs?: string[] }) => {
159
const [showPathCrumbs] = useAtom(showPathCrumbsAtom);
@@ -67,32 +61,3 @@ export const PathCrumbs = ({ parentCrumbs = [] }: { parentCrumbs?: string[] }) =
6761
</HStack>
6862
);
6963
};
70-
71-
function propertyPathToObjectPath(node: SchemaNode) {
72-
const objectPath: string[] = [];
73-
74-
let currentNode: SchemaNode | null = node;
75-
while (currentNode && !isRootNode(currentNode)) {
76-
if (isRegularNode(currentNode)) {
77-
const pathPart = currentNode.subpath[currentNode.subpath.length - 1];
78-
79-
if (currentNode.primaryType === 'array') {
80-
const key = `${pathPart || ''}[]`;
81-
if (objectPath[objectPath.length - 1]) {
82-
objectPath[objectPath.length - 1] = key;
83-
} else {
84-
objectPath.push(key);
85-
}
86-
} else if (
87-
pathPart &&
88-
(currentNode.subpath.length !== 2 || !['allOf', 'oneOf', 'anyOf'].includes(currentNode.subpath[0]))
89-
) {
90-
objectPath.push(currentNode.subpath[currentNode.subpath.length - 1]);
91-
}
92-
}
93-
94-
currentNode = currentNode.parent;
95-
}
96-
97-
return objectPath.reverse();
98-
}

src/components/PathCrumbs/state.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { isRegularNode, isRootNode, SchemaNode } from '@stoplight/json-schema-tree';
2+
import { atom } from 'jotai';
3+
4+
import { hoveredNodeAtom } from '../SchemaRow/state';
5+
6+
export const showPathCrumbsAtom = atom<boolean>(false);
7+
8+
export const pathCrumbsAtom = atom(get => {
9+
const node = get(hoveredNodeAtom);
10+
11+
if (!node) return [];
12+
13+
return propertyPathToObjectPath(node as SchemaNode);
14+
});
15+
16+
function propertyPathToObjectPath(node: SchemaNode) {
17+
const objectPath: string[] = [];
18+
19+
let currentNode: SchemaNode | null = node;
20+
while (currentNode && !isRootNode(currentNode)) {
21+
if (isRegularNode(currentNode)) {
22+
const pathPart = currentNode.subpath[currentNode.subpath.length - 1];
23+
24+
if (currentNode.primaryType === 'array') {
25+
const key = `${pathPart || ''}[]`;
26+
if (objectPath[objectPath.length - 1]) {
27+
objectPath[objectPath.length - 1] = key;
28+
} else {
29+
objectPath.push(key);
30+
}
31+
} else if (
32+
pathPart &&
33+
(currentNode.subpath.length !== 2 || !['allOf', 'oneOf', 'anyOf'].includes(currentNode.subpath[0]))
34+
) {
35+
objectPath.push(currentNode.subpath[currentNode.subpath.length - 1]);
36+
}
37+
}
38+
39+
currentNode = currentNode.parent;
40+
}
41+
42+
return objectPath.reverse();
43+
}

src/components/SchemaRow/SchemaRow.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,32 @@ import {
77
SchemaNode,
88
SchemaNodeKind,
99
} from '@stoplight/json-schema-tree';
10-
import { Box, Flex, Icon, Select, VStack } from '@stoplight/mosaic';
11-
import { useUpdateAtom } from 'jotai/utils';
10+
import { Box, Flex, Icon, Select, SpaceVals, VStack } from '@stoplight/mosaic';
11+
import { useAtomValue, useUpdateAtom } from 'jotai/utils';
1212
import last from 'lodash/last.js';
1313
import * as React from 'react';
1414

1515
import { COMBINER_NAME_MAP } from '../../consts';
1616
import { useJSVOptionsContext } from '../../contexts';
1717
import { calculateChildrenToShow, isFlattenableNode, isPropertyRequired } from '../../tree';
18-
import { pathCrumbsAtom } from '../PathCrumbs';
1918
import { Caret, Description, Format, getValidationsFromSchema, Types, Validations } from '../shared';
2019
import { ChildStack } from '../shared/ChildStack';
21-
import { Properties } from '../shared/Properties';
20+
import { Properties, useHasProperties } from '../shared/Properties';
21+
import { hoveredNodeAtom, isNodeHoveredAtom } from './state';
2222
import { useChoices } from './useChoices';
2323

2424
export interface SchemaRowProps {
2525
schemaNode: SchemaNode;
2626
nestingLevel: number;
27+
pl?: SpaceVals;
2728
}
2829

29-
export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode, nestingLevel }) => {
30+
export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(({ schemaNode, nestingLevel, pl }) => {
3031
const { defaultExpandedDepth, renderRowAddon, onGoToRef, hideExamples, renderRootTreeLines } = useJSVOptionsContext();
3132

32-
const setPathCrumbs = useUpdateAtom(pathCrumbsAtom);
33+
const setHoveredNode = useUpdateAtom(hoveredNodeAtom);
34+
const isHovering = useAtomValue(isNodeHoveredAtom(schemaNode));
35+
3336
const [isExpanded, setExpanded] = React.useState<boolean>(
3437
!isMirroredNode(schemaNode) && nestingLevel <= defaultExpandedDepth,
3538
);
@@ -62,13 +65,20 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
6265
const isCollapsible = childNodes.length > 0;
6366
const isRootLevel = nestingLevel < rootLevel;
6467

68+
const required = isPropertyRequired(schemaNode);
69+
const deprecated = isRegularNode(schemaNode) && schemaNode.deprecated;
70+
const validations = isRegularNode(schemaNode) ? schemaNode.validations : {};
71+
const hasProperties = useHasProperties({ required, deprecated, validations });
72+
6573
return (
6674
<>
6775
<Flex
6876
maxW="full"
77+
pl={pl}
78+
py={2}
6979
onMouseEnter={(e: any) => {
7080
e.stopPropagation();
71-
setPathCrumbs(selectedChoice.type);
81+
setHoveredNode(selectedChoice.type);
7282
}}
7383
>
7484
{!isRootLevel && <Box borderT w={isCollapsible ? 1 : 3} ml={-3} mr={3} mt={2} />}
@@ -82,7 +92,7 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
8292
>
8393
{isCollapsible ? <Caret isExpanded={isExpanded} /> : null}
8494

85-
<Flex alignItems="baseline" fontSize="base" flex={1} pos="sticky" top={0}>
95+
<Flex alignItems="baseline" fontSize="base">
8696
{schemaNode.subpath.length > 0 && shouldShowPropertyName(schemaNode) && (
8797
<Box mr={2} fontFamily="mono" fontWeight="semibold">
8898
{last(schemaNode.subpath)}
@@ -136,11 +146,9 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
136146
)}
137147
</Flex>
138148

139-
<Properties
140-
required={isPropertyRequired(schemaNode)}
141-
deprecated={isRegularNode(schemaNode) && schemaNode.deprecated}
142-
validations={isRegularNode(schemaNode) ? schemaNode.validations : {}}
143-
/>
149+
{hasProperties && <Box bg={isHovering ? 'canvas-200' : undefined} h="px" flex={1} mx={3} />}
150+
151+
<Properties required={required} deprecated={deprecated} validations={validations} />
144152
</Flex>
145153

146154
{typeof description === 'string' && description.length > 0 && <Description value={description} />}
@@ -159,10 +167,12 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
159167
{renderRowAddon ? <Box>{renderRowAddon({ schemaNode, nestingLevel })}</Box> : null}
160168
</Flex>
161169

162-
{isCollapsible && isExpanded ? <ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} /> : null}
170+
{isCollapsible && isExpanded ? (
171+
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
172+
) : null}
163173
</>
164174
);
165-
};
175+
});
166176

167177
function shouldShowPropertyName(schemaNode: SchemaNode) {
168178
return (

src/components/SchemaRow/TopLevelSchemaRow.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as React from 'react';
77
import { COMBINER_NAME_MAP } from '../../consts';
88
import { useIsOnScreen } from '../../hooks/useIsOnScreen';
99
import { calculateChildrenToShow, isComplexArray } from '../../tree';
10-
import { showPathCrumbsAtom } from '../PathCrumbs';
10+
import { showPathCrumbsAtom } from '../PathCrumbs/state';
1111
import { ChildStack } from '../shared/ChildStack';
1212
import { SchemaRow, SchemaRowProps } from './SchemaRow';
1313
import { useChoices } from './useChoices';
@@ -22,7 +22,7 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick<SchemaRowProps, 'schemaNo
2222
return (
2323
<>
2424
<ScrollCheck />
25-
<ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} />
25+
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
2626
</>
2727
);
2828
}
@@ -63,7 +63,9 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick<SchemaRowProps, 'schemaNo
6363
) : null}
6464
</HStack>
6565

66-
{childNodes.length > 0 ? <ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} /> : null}
66+
{childNodes.length > 0 ? (
67+
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
68+
) : null}
6769
</>
6870
);
6971
}
@@ -77,7 +79,9 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick<SchemaRowProps, 'schemaNo
7779
array of:
7880
</Box>
7981

80-
{childNodes.length > 0 ? <ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} /> : null}
82+
{childNodes.length > 0 ? (
83+
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
84+
) : null}
8185
</>
8286
);
8387
}

src/components/SchemaRow/state.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { SchemaNode } from '@stoplight/json-schema-tree';
2+
import { atom } from 'jotai';
3+
import { atomFamily } from 'jotai/utils';
4+
5+
export const hoveredNodeAtom = atom<SchemaNode | null>(null);
6+
export const isNodeHoveredAtom = atomFamily((node: SchemaNode) => atom(get => node === get(hoveredNodeAtom)));
7+
export const isChildNodeHoveredAtom = atomFamily((parent: SchemaNode) =>
8+
atom(get => {
9+
const hoveredNode = get(hoveredNodeAtom);
10+
11+
if (!hoveredNode || hoveredNode === parent) return false;
12+
13+
return hoveredNode.parent === parent;
14+
}),
15+
);
Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,47 @@
11
import { SchemaNode } from '@stoplight/json-schema-tree';
2-
import { SpaceVals, VStack } from '@stoplight/mosaic';
2+
import { Box, SpaceVals } from '@stoplight/mosaic';
33
import * as React from 'react';
44

55
import { NESTING_OFFSET } from '../../consts';
66
import { useJSVOptionsContext } from '../../contexts';
77
import { SchemaRow, SchemaRowProps } from '../SchemaRow';
88

99
type ChildStackProps = {
10+
schemaNode: SchemaNode;
1011
childNodes: readonly SchemaNode[];
1112
currentNestingLevel: number;
1213
className?: string;
1314
RowComponent?: React.FC<SchemaRowProps>;
1415
};
1516

16-
export const ChildStack = ({
17-
childNodes,
18-
currentNestingLevel,
19-
className,
20-
RowComponent = SchemaRow,
21-
}: ChildStackProps) => {
22-
const { renderRootTreeLines } = useJSVOptionsContext();
23-
const rootLevel = renderRootTreeLines ? 0 : 1;
24-
const isRootLevel = currentNestingLevel < rootLevel;
17+
export const ChildStack = React.memo(
18+
({ childNodes, currentNestingLevel, className, RowComponent = SchemaRow }: ChildStackProps) => {
19+
const { renderRootTreeLines } = useJSVOptionsContext();
20+
const rootLevel = renderRootTreeLines ? 0 : 1;
21+
const isRootLevel = currentNestingLevel < rootLevel;
2522

26-
let ml: SpaceVals | undefined;
27-
if (!isRootLevel) {
28-
ml = currentNestingLevel === rootLevel ? 'px' : 4;
29-
}
23+
let ml: SpaceVals | undefined;
24+
if (!isRootLevel) {
25+
ml = currentNestingLevel === rootLevel ? 'px' : 7;
26+
}
3027

31-
return (
32-
<VStack
33-
className={className}
34-
pl={isRootLevel ? undefined : NESTING_OFFSET}
35-
ml={ml}
36-
spacing={4}
37-
fontSize="sm"
38-
borderL={isRootLevel ? undefined : true}
39-
data-level={currentNestingLevel}
40-
>
41-
{childNodes.map((childNode: SchemaNode) => (
42-
<RowComponent key={childNode.id} schemaNode={childNode} nestingLevel={currentNestingLevel + 1} />
43-
))}
44-
</VStack>
45-
);
46-
};
28+
return (
29+
<Box
30+
className={className}
31+
ml={ml}
32+
fontSize="sm"
33+
borderL={isRootLevel ? undefined : true}
34+
data-level={currentNestingLevel}
35+
>
36+
{childNodes.map((childNode: SchemaNode) => (
37+
<RowComponent
38+
key={childNode.id}
39+
schemaNode={childNode}
40+
nestingLevel={currentNestingLevel + 1}
41+
pl={isRootLevel ? undefined : NESTING_OFFSET}
42+
/>
43+
))}
44+
</Box>
45+
);
46+
},
47+
);

0 commit comments

Comments
 (0)