diff --git a/src/components/ComposableTutorial/ComposableTutorial.tsx b/src/components/ComposableTutorial/ComposableTutorial.tsx index 32980a19f..ce624a90a 100644 --- a/src/components/ComposableTutorial/ComposableTutorial.tsx +++ b/src/components/ComposableTutorial/ComposableTutorial.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect, useMemo } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { useLocation } from '@gatsbyjs/reach-router'; import { parse, ParsedQuery, stringify } from 'query-string'; import { navigate } from 'gatsby'; @@ -12,6 +12,7 @@ import { isOfflineDocsBuild } from '../../utils/is-offline-docs-build'; import { OFFLINE_COMPOSABLE_CLASSNAME } from '../../utils/head-scripts/offline-ui/composable-tutorials'; import ComponentFactory from '../ComponentFactory'; import { findAllKeyValuePairs } from '../../utils/find-all-key-value-pairs'; +import { findAllNestedAttribute } from '../../utils/find-all-nested-attribute'; import ConfigurableOption from './ConfigurableOption'; import ComposableContext, { ComposableContextProvider } from './ComposableContext'; @@ -169,6 +170,7 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => { const { currentSelections, setCurrentSelections } = useContext(ComposableContext); const location = useLocation(); const { composable_options: composableOptions, children } = nodeData; + const isNavigatingRef = useRef(false); const validSelections = useMemo(() => { const res: Set = new Set(); @@ -182,6 +184,25 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => { return res; }, [children]); + const refToSelection = useMemo(() => { + const res: Record> = {}; + const composableContents: ComposableNode[] = findAllKeyValuePairs(children, 'name', 'selected-content'); + for (const composableContent of composableContents) { + const ids = findAllNestedAttribute(composableContent.children, 'id'); + const html_ids = findAllNestedAttribute(composableContent.children, 'html_id'); + const selection = composableContent.selections; + for (const id of [...ids, ...html_ids]) { + // if multiple composable contents have the same id, this is a content issue. + // use the first composable content that has this id + if (res[id]) { + continue; + } + res[id] = selection; + } + } + return res; + }, [children]); + const externalQueryParamsString = useMemo(() => { const queryParams = parse(location.search); const composableOptionsKeys = composableOptions.map((option) => option.value); @@ -195,11 +216,11 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => { }, [composableOptions, location.search]); const navigatePreservingExternalQueryParams = useCallback( - (queryString: string, preserveScroll = false) => { + (queryString: string, preserveScroll = false, hash = '') => { navigate( `${queryString.startsWith('?') ? '' : '?'}${queryString}${ queryString.length > 0 && externalQueryParamsString.length > 0 ? '&' : '' - }${externalQueryParamsString}`, + }${externalQueryParamsString}${hash ? `#${hash}` : ''}`, { state: { preserveScroll } } ); }, @@ -214,6 +235,26 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => { return; } + // Skip if this useEffect was triggered by our own navigation + if (isNavigatingRef.current) { + isNavigatingRef.current = false; + return; + } + + // first verify if there is a hash + // if there is a hash and it belongs to a composable option, + // set the current selections that composable option to show the content with hash id + const hash = location.hash?.slice(1); + if (hash) { + const selection = refToSelection[hash]; + if (selection) { + setCurrentSelections(selection); + const queryString = new URLSearchParams(selection).toString(); + isNavigatingRef.current = true; + return navigatePreservingExternalQueryParams(`?${queryString}`, false, hash); + } + } + // read query params const queryParams = parse(location.search); @@ -239,6 +280,8 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => { composableOptions, location.pathname, location.search, + location.hash, + refToSelection, validSelections, setCurrentSelections, navigatePreservingExternalQueryParams,