diff --git a/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx b/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx index 874fea03a9..6b8eab67e7 100644 --- a/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx +++ b/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx @@ -13,7 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ReactNode, useMemo } from 'react'; +import React, { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, + useRef, +} from 'react'; import { Schema } from 'jsonschema'; import { Box, @@ -35,6 +42,26 @@ import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import { JsonValue } from '@backstage/config'; import { TreeItem, TreeItemProps, TreeView } from '@material-ui/lab'; +class ScrollForwarder { + private readonly listeners = new Map void>(); + + setScrollListener(id: string, listener: () => void): () => void { + this.listeners.set(id, listener); + + return () => { + if (this.listeners.get(id) === listener) { + this.listeners.delete(id); + } + }; + } + + scrollTo(id: string) { + this.listeners.get(id)?.(); + } +} + +const ScrollContext = createContext(undefined); + interface SchemaViewProps { path: string; depth: number; @@ -205,6 +232,15 @@ export function ChildView({ lastChild?: boolean; }) { const classes = useChildViewStyles(); + const titleRef = useRef(null); + const scroll = useContext(ScrollContext); + + useEffect(() => { + return scroll?.setScrollListener(path, () => { + titleRef.current?.scrollIntoView({ behavior: 'smooth' }); + }); + }, [scroll, path]); + const chips = new Array(); const chipProps = { size: 'small' as const, classes: { root: classes.chip } }; @@ -236,6 +272,7 @@ export function ChildView({ alignItems="center" > @@ -449,7 +486,7 @@ export function createSchemaBrowserItems( } if (matchArr) { return matchArr.map((childSchema, index) => { - const childPath = `${path}/${index}`; + const childPath = `${path}.${index}`; if (depth > 0) expanded.push(childPath); return ( { - const childPath = path ? `${path}/${name}` : name; + const childPath = path ? `${path}.${name}` : name; if (depth > 0) expanded.push(childPath); return ( @@ -511,7 +548,7 @@ export function createSchemaBrowserItems( children.push( ...Object.entries(schema.patternProperties).map( ([name, childSchema]) => { - const childPath = `${path}/<${name}>`; + const childPath = `${path}.<${name}>`; if (depth > 0) expanded.push(childPath); return ( 0) expanded.push(childPath); children.push( @@ -556,6 +593,8 @@ export function createSchemaBrowserItems( } export function SchemaBrowser({ schema }: { schema: Schema }) { + const scroll = useContext(ScrollContext); + const expandedRef = useRef([]); const data = useMemo(() => { const expanded = new Array(); @@ -564,12 +603,27 @@ export function SchemaBrowser({ schema }: { schema: Schema }) { return { items, expanded }; }, [schema]); + if (!scroll) { + throw new Error('No scroll handler available'); + } + + const handleToggle = (_event: unknown, expanded: string[]) => { + expandedRef.current = expanded; + }; + + const handleSelect = (_event: unknown, nodeId: string) => { + if (expandedRef.current.includes(nodeId)) { + scroll.scrollTo(nodeId); + } + }; + return ( } defaultExpandIcon={} + onNodeToggle={handleToggle} + onNodeSelect={handleSelect} > {data.items} @@ -588,13 +642,15 @@ export const SchemaViewer = ({ schema }: SchemaViewerProps) => { maxHeight="100%" > - - - + + + + - - - + + + +