diff --git a/src/__tests__/__snapshots__/index.spec.tsx.snap b/src/__tests__/__snapshots__/index.spec.tsx.snap
index cec9e77e..6837c649 100644
--- a/src/__tests__/__snapshots__/index.spec.tsx.snap
+++ b/src/__tests__/__snapshots__/index.spec.tsx.snap
@@ -258,6 +258,7 @@ exports[`HTML Output given anyOf combiner placed next to allOf given allOf mergi
type
string
+
required
@@ -425,6 +426,7 @@ exports[`HTML Output given oneOf combiner placed next to allOf given allOf mergi
type
string
+
required
@@ -548,6 +550,7 @@ exports[`HTML Output given standalone mode, should populate proper nodes 1`] = `
id
string
+
read-only
@@ -559,6 +562,7 @@ exports[`HTML Output given standalone mode, should populate proper nodes 1`] = `
description
string
+
write-only
diff --git a/src/components/JsonSchemaViewer.tsx b/src/components/JsonSchemaViewer.tsx
index 9247d425..b35409e6 100644
--- a/src/components/JsonSchemaViewer.tsx
+++ b/src/components/JsonSchemaViewer.tsx
@@ -14,8 +14,9 @@ import * as React from 'react';
import { JSVOptions, JSVOptionsContextProvider } from '../contexts';
import type { JSONSchema } from '../types';
-import { PathCrumbs, pathCrumbsAtom } from './PathCrumbs';
+import { PathCrumbs } from './PathCrumbs';
import { TopLevelSchemaRow } from './SchemaRow';
+import { hoveredNodeAtom } from './SchemaRow/state';
export type JsonSchemaProps = Partial & {
schema: JSONSchema;
@@ -74,10 +75,10 @@ const JsonSchemaViewerInner = ({
JsonSchemaProps,
'schema' | 'viewMode' | 'className' | 'resolveRef' | 'emptyText' | 'onTreePopulated' | 'maxHeight' | 'parentCrumbs'
>) => {
- const setPathCrumbs = useUpdateAtom(pathCrumbsAtom);
+ const setHoveredNode = useUpdateAtom(hoveredNodeAtom);
const onMouseLeave = React.useCallback(() => {
- setPathCrumbs([]);
- }, [setPathCrumbs]);
+ setHoveredNode(null);
+ }, [setHoveredNode]);
const { jsonSchemaTreeRoot, nodeCount } = React.useMemo(() => {
const jsonSchemaTree = new JsonSchemaTree(schema, {
diff --git a/src/components/PathCrumbs/index.tsx b/src/components/PathCrumbs/index.tsx
index 8d5f2590..6930ddf3 100644
--- a/src/components/PathCrumbs/index.tsx
+++ b/src/components/PathCrumbs/index.tsx
@@ -1,15 +1,9 @@
-import { isRegularNode, isRootNode, SchemaNode } from '@stoplight/json-schema-tree';
import { Box, HStack } from '@stoplight/mosaic';
-import { atom, useAtom } from 'jotai';
+import { useAtom } from 'jotai';
import * as React from 'react';
import { useJSVOptionsContext } from '../../contexts';
-
-export const showPathCrumbsAtom = atom(false);
-
-export const pathCrumbsAtom = atom([], (_get, set, node) => {
- set(pathCrumbsAtom, propertyPathToObjectPath(node as SchemaNode));
-});
+import { pathCrumbsAtom, showPathCrumbsAtom } from './state';
export const PathCrumbs = ({ parentCrumbs = [] }: { parentCrumbs?: string[] }) => {
const [showPathCrumbs] = useAtom(showPathCrumbsAtom);
@@ -67,32 +61,3 @@ export const PathCrumbs = ({ parentCrumbs = [] }: { parentCrumbs?: string[] }) =
);
};
-
-function propertyPathToObjectPath(node: SchemaNode) {
- const objectPath: string[] = [];
-
- let currentNode: SchemaNode | null = node;
- while (currentNode && !isRootNode(currentNode)) {
- if (isRegularNode(currentNode)) {
- const pathPart = currentNode.subpath[currentNode.subpath.length - 1];
-
- if (currentNode.primaryType === 'array') {
- const key = `${pathPart || ''}[]`;
- if (objectPath[objectPath.length - 1]) {
- objectPath[objectPath.length - 1] = key;
- } else {
- objectPath.push(key);
- }
- } else if (
- pathPart &&
- (currentNode.subpath.length !== 2 || !['allOf', 'oneOf', 'anyOf'].includes(currentNode.subpath[0]))
- ) {
- objectPath.push(currentNode.subpath[currentNode.subpath.length - 1]);
- }
- }
-
- currentNode = currentNode.parent;
- }
-
- return objectPath.reverse();
-}
diff --git a/src/components/PathCrumbs/state.ts b/src/components/PathCrumbs/state.ts
new file mode 100644
index 00000000..8545b9e5
--- /dev/null
+++ b/src/components/PathCrumbs/state.ts
@@ -0,0 +1,43 @@
+import { isRegularNode, isRootNode, SchemaNode } from '@stoplight/json-schema-tree';
+import { atom } from 'jotai';
+
+import { hoveredNodeAtom } from '../SchemaRow/state';
+
+export const showPathCrumbsAtom = atom(false);
+
+export const pathCrumbsAtom = atom(get => {
+ const node = get(hoveredNodeAtom);
+
+ if (!node) return [];
+
+ return propertyPathToObjectPath(node as SchemaNode);
+});
+
+function propertyPathToObjectPath(node: SchemaNode) {
+ const objectPath: string[] = [];
+
+ let currentNode: SchemaNode | null = node;
+ while (currentNode && !isRootNode(currentNode)) {
+ if (isRegularNode(currentNode)) {
+ const pathPart = currentNode.subpath[currentNode.subpath.length - 1];
+
+ if (currentNode.primaryType === 'array') {
+ const key = `${pathPart || ''}[]`;
+ if (objectPath[objectPath.length - 1]) {
+ objectPath[objectPath.length - 1] = key;
+ } else {
+ objectPath.push(key);
+ }
+ } else if (
+ pathPart &&
+ (currentNode.subpath.length !== 2 || !['allOf', 'oneOf', 'anyOf'].includes(currentNode.subpath[0]))
+ ) {
+ objectPath.push(currentNode.subpath[currentNode.subpath.length - 1]);
+ }
+ }
+
+ currentNode = currentNode.parent;
+ }
+
+ return objectPath.reverse();
+}
diff --git a/src/components/SchemaRow/SchemaRow.tsx b/src/components/SchemaRow/SchemaRow.tsx
index 770545d2..180ed8d2 100644
--- a/src/components/SchemaRow/SchemaRow.tsx
+++ b/src/components/SchemaRow/SchemaRow.tsx
@@ -7,29 +7,32 @@ import {
SchemaNode,
SchemaNodeKind,
} from '@stoplight/json-schema-tree';
-import { Box, Flex, Icon, Select, VStack } from '@stoplight/mosaic';
-import { useUpdateAtom } from 'jotai/utils';
+import { Box, Flex, Icon, Select, SpaceVals, VStack } from '@stoplight/mosaic';
+import { useAtomValue, useUpdateAtom } from 'jotai/utils';
import last from 'lodash/last.js';
import * as React from 'react';
import { COMBINER_NAME_MAP } from '../../consts';
import { useJSVOptionsContext } from '../../contexts';
import { calculateChildrenToShow, isFlattenableNode, isPropertyRequired } from '../../tree';
-import { pathCrumbsAtom } from '../PathCrumbs';
import { Caret, Description, Format, getValidationsFromSchema, Types, Validations } from '../shared';
import { ChildStack } from '../shared/ChildStack';
-import { Properties } from '../shared/Properties';
+import { Properties, useHasProperties } from '../shared/Properties';
+import { hoveredNodeAtom, isNodeHoveredAtom } from './state';
import { useChoices } from './useChoices';
export interface SchemaRowProps {
schemaNode: SchemaNode;
nestingLevel: number;
+ pl?: SpaceVals;
}
-export const SchemaRow: React.FunctionComponent = ({ schemaNode, nestingLevel }) => {
+export const SchemaRow: React.FunctionComponent = React.memo(({ schemaNode, nestingLevel, pl }) => {
const { defaultExpandedDepth, renderRowAddon, onGoToRef, hideExamples, renderRootTreeLines } = useJSVOptionsContext();
- const setPathCrumbs = useUpdateAtom(pathCrumbsAtom);
+ const setHoveredNode = useUpdateAtom(hoveredNodeAtom);
+ const isHovering = useAtomValue(isNodeHoveredAtom(schemaNode));
+
const [isExpanded, setExpanded] = React.useState(
!isMirroredNode(schemaNode) && nestingLevel <= defaultExpandedDepth,
);
@@ -62,13 +65,20 @@ export const SchemaRow: React.FunctionComponent = ({ schemaNode,
const isCollapsible = childNodes.length > 0;
const isRootLevel = nestingLevel < rootLevel;
+ const required = isPropertyRequired(schemaNode);
+ const deprecated = isRegularNode(schemaNode) && schemaNode.deprecated;
+ const validations = isRegularNode(schemaNode) ? schemaNode.validations : {};
+ const hasProperties = useHasProperties({ required, deprecated, validations });
+
return (
<>
{
e.stopPropagation();
- setPathCrumbs(selectedChoice.type);
+ setHoveredNode(selectedChoice.type);
}}
>
{!isRootLevel && }
@@ -82,7 +92,7 @@ export const SchemaRow: React.FunctionComponent = ({ schemaNode,
>
{isCollapsible ? : null}
-
+
{schemaNode.subpath.length > 0 && shouldShowPropertyName(schemaNode) && (
{last(schemaNode.subpath)}
@@ -136,11 +146,9 @@ export const SchemaRow: React.FunctionComponent = ({ schemaNode,
)}
-
+ {hasProperties && }
+
+
{typeof description === 'string' && description.length > 0 && }
@@ -159,10 +167,12 @@ export const SchemaRow: React.FunctionComponent = ({ schemaNode,
{renderRowAddon ? {renderRowAddon({ schemaNode, nestingLevel })} : null}
- {isCollapsible && isExpanded ? : null}
+ {isCollapsible && isExpanded ? (
+
+ ) : null}
>
);
-};
+});
function shouldShowPropertyName(schemaNode: SchemaNode) {
return (
diff --git a/src/components/SchemaRow/TopLevelSchemaRow.tsx b/src/components/SchemaRow/TopLevelSchemaRow.tsx
index 29455c65..ef1b2a1a 100644
--- a/src/components/SchemaRow/TopLevelSchemaRow.tsx
+++ b/src/components/SchemaRow/TopLevelSchemaRow.tsx
@@ -7,7 +7,7 @@ import * as React from 'react';
import { COMBINER_NAME_MAP } from '../../consts';
import { useIsOnScreen } from '../../hooks/useIsOnScreen';
import { calculateChildrenToShow, isComplexArray } from '../../tree';
-import { showPathCrumbsAtom } from '../PathCrumbs';
+import { showPathCrumbsAtom } from '../PathCrumbs/state';
import { ChildStack } from '../shared/ChildStack';
import { SchemaRow, SchemaRowProps } from './SchemaRow';
import { useChoices } from './useChoices';
@@ -22,7 +22,7 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick
-
+
>
);
}
@@ -63,7 +63,9 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick
- {childNodes.length > 0 ? : null}
+ {childNodes.length > 0 ? (
+
+ ) : null}
>
);
}
@@ -77,7 +79,9 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick
- {childNodes.length > 0 ? : null}
+ {childNodes.length > 0 ? (
+
+ ) : null}
>
);
}
diff --git a/src/components/SchemaRow/state.ts b/src/components/SchemaRow/state.ts
new file mode 100644
index 00000000..6a389649
--- /dev/null
+++ b/src/components/SchemaRow/state.ts
@@ -0,0 +1,15 @@
+import { SchemaNode } from '@stoplight/json-schema-tree';
+import { atom } from 'jotai';
+import { atomFamily } from 'jotai/utils';
+
+export const hoveredNodeAtom = atom(null);
+export const isNodeHoveredAtom = atomFamily((node: SchemaNode) => atom(get => node === get(hoveredNodeAtom)));
+export const isChildNodeHoveredAtom = atomFamily((parent: SchemaNode) =>
+ atom(get => {
+ const hoveredNode = get(hoveredNodeAtom);
+
+ if (!hoveredNode || hoveredNode === parent) return false;
+
+ return hoveredNode.parent === parent;
+ }),
+);
diff --git a/src/components/shared/ChildStack.tsx b/src/components/shared/ChildStack.tsx
index e72403f0..bc186b8c 100644
--- a/src/components/shared/ChildStack.tsx
+++ b/src/components/shared/ChildStack.tsx
@@ -1,5 +1,5 @@
import { SchemaNode } from '@stoplight/json-schema-tree';
-import { SpaceVals, VStack } from '@stoplight/mosaic';
+import { Box, SpaceVals } from '@stoplight/mosaic';
import * as React from 'react';
import { NESTING_OFFSET } from '../../consts';
@@ -7,40 +7,41 @@ import { useJSVOptionsContext } from '../../contexts';
import { SchemaRow, SchemaRowProps } from '../SchemaRow';
type ChildStackProps = {
+ schemaNode: SchemaNode;
childNodes: readonly SchemaNode[];
currentNestingLevel: number;
className?: string;
RowComponent?: React.FC;
};
-export const ChildStack = ({
- childNodes,
- currentNestingLevel,
- className,
- RowComponent = SchemaRow,
-}: ChildStackProps) => {
- const { renderRootTreeLines } = useJSVOptionsContext();
- const rootLevel = renderRootTreeLines ? 0 : 1;
- const isRootLevel = currentNestingLevel < rootLevel;
+export const ChildStack = React.memo(
+ ({ childNodes, currentNestingLevel, className, RowComponent = SchemaRow }: ChildStackProps) => {
+ const { renderRootTreeLines } = useJSVOptionsContext();
+ const rootLevel = renderRootTreeLines ? 0 : 1;
+ const isRootLevel = currentNestingLevel < rootLevel;
- let ml: SpaceVals | undefined;
- if (!isRootLevel) {
- ml = currentNestingLevel === rootLevel ? 'px' : 4;
- }
+ let ml: SpaceVals | undefined;
+ if (!isRootLevel) {
+ ml = currentNestingLevel === rootLevel ? 'px' : 7;
+ }
- return (
-
- {childNodes.map((childNode: SchemaNode) => (
-
- ))}
-
- );
-};
+ return (
+
+ {childNodes.map((childNode: SchemaNode) => (
+
+ ))}
+
+ );
+ },
+);
diff --git a/src/components/shared/Properties.tsx b/src/components/shared/Properties.tsx
index 6e594eb5..e012e97a 100644
--- a/src/components/shared/Properties.tsx
+++ b/src/components/shared/Properties.tsx
@@ -10,6 +10,14 @@ export interface IProperties {
validations: Dictionary;
}
+export const useHasProperties = ({ required, deprecated, validations: { readOnly, writeOnly } }: IProperties) => {
+ const { viewMode } = useJSVOptionsContext();
+
+ const showVisibilityValidations = viewMode === 'standalone' && !!readOnly !== !!writeOnly;
+
+ return deprecated || showVisibilityValidations || required;
+};
+
export const Properties: React.FunctionComponent = ({
required,
deprecated,
diff --git a/src/components/shared/__tests__/Property.spec.tsx b/src/components/shared/__tests__/Property.spec.tsx
index cc652d21..1cf7860b 100644
--- a/src/components/shared/__tests__/Property.spec.tsx
+++ b/src/components/shared/__tests__/Property.spec.tsx
@@ -83,7 +83,7 @@ describe('Property component', () => {
const wrapper = render(schema, ['properties', 'foo']);
expect(wrapper.find(SchemaRow).html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
});
@@ -102,7 +102,7 @@ describe('Property component', () => {
const wrapper = render(schema, ['properties', 'foo']);
expect(wrapper.find(SchemaRow).html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
});
@@ -120,7 +120,7 @@ describe('Property component', () => {
const wrapper = render(schema, ['items', 'properties', 'foo']);
expect(wrapper.html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
});
@@ -141,7 +141,7 @@ describe('Property component', () => {
const wrapper = render(schema);
expect(wrapper.html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
});
@@ -164,7 +164,7 @@ describe('Property component', () => {
const wrapper = render(schema);
expect(wrapper.html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
});
@@ -199,12 +199,12 @@ describe('Property component', () => {
let wrapper = render(schema, ['properties', 'array-all-objects', 'items', 'properties', 'foo']);
expect(wrapper.html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
wrapper = render(schema, ['properties', 'array-all-objects', 'items', 'properties', 'bar']);
expect(wrapper.html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
});
@@ -224,7 +224,7 @@ describe('Property component', () => {
const wrapper = mount();
expect(wrapper.html()).toMatchInlineSnapshot(
- `""`,
+ `""`,
);
wrapper.unmount();
});