import React, { useContext, useRef, type PropsWithChildren, useEffect, useState, useMemo, useCallback, } from 'react'; import styled from 'styled-components'; import { useLocation } from 'react-router-dom'; import type { WithConditions } from '@redocly/config'; import { Marker } from '@redocly/theme/components/Marker/Marker'; import { CodeWalkthroughControlsStateContext, CodeWalkthroughStepsContext, } from '@redocly/theme/core/contexts'; export type CodeStepProps = WithConditions<{ id: string; heading?: string; }>; export function CodeStep({ id, heading, when, unless, children, }: PropsWithChildren) { const location = useLocation(); const compRef = useRef(null); const markerRef = useRef(null); const { areConditionsMet } = useContext(CodeWalkthroughControlsStateContext); const { activeStep, setActiveStep, markers, registerStep, removeStep, registerMarker, removeMarker, lockObserver, filtersElementRef, } = useContext(CodeWalkthroughStepsContext); const isActive = activeStep === id; const [scrollMarginTop, setScrollMarginTop] = useState(0); const marker = useMemo(() => markers[id], [markers, id]); const isVisible = useMemo( () => areConditionsMet({ when, unless }), [areConditionsMet, when, unless], ); const handleActivateStep = useCallback(() => { if (lockObserver) { lockObserver.current = true; if (markerRef.current) { markerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); } setActiveStep(id); setTimeout(() => { lockObserver.current = false; }, 1000); } }, [setActiveStep, lockObserver, id]); const handleRegisterMarker = useCallback( (element: HTMLElement) => registerMarker(id, element), [registerMarker, id], ); const handleRemoveMarker = useCallback( (element: HTMLElement) => removeMarker(id, element), [removeMarker, id], ); useEffect(() => { // If the step is active during navigation or first render, scroll to it if (!isActive) return; const timer = setTimeout(handleActivateStep, 5); return () => clearTimeout(timer); // Ignore dependency array because we only need to run this once // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]); useEffect(() => { const currentCompRef = compRef.current; if (currentCompRef && isVisible) { currentCompRef .querySelectorAll('a, button, input, textarea, select, [tabindex]') .forEach((el) => { el.setAttribute('tabindex', '-1'); }); registerStep(id, currentCompRef); } const filtersElementHeight = filtersElementRef?.current?.clientHeight || 0; const navbarHeight = document.querySelector('nav')?.clientHeight || 0; setScrollMarginTop(filtersElementHeight + navbarHeight + 10); return () => { removeStep(id); }; }, [filtersElementRef, registerStep, removeStep, id, isVisible]); if (!isVisible) { return null; } return ( <> {marker && ( )} {heading ? {heading} : null} {children} ); } const StepContent = styled.div<{ isActive: boolean }>` margin: var(--spacing-xs) 0px var(--spacing-xs) calc(var(--spacing-unit) * 3.5); padding: var(--spacing-md) var(--spacing-lg); background: ${({ isActive }) => (isActive ? 'var(--layer-color)' : 'none')}; border-radius: var(--border-radius); &:hover { background-color: ${({ isActive }) => isActive ? 'var(--code-step-bg-active-hover)' : 'var(--code-step-bg-hover)'}; } `; const StepHeading = styled.p` font-weight: var(--font-weight-semibold); `; export const StepWrapper = styled.div<{ isActive: boolean; scrollMarginTop: number }>` position: relative; scroll-margin-top: ${({ scrollMarginTop }) => scrollMarginTop}px; &::before { content: ''; position: absolute; width: 6px; height: 100%; background-color: ${({ isActive }) => isActive ? 'var(--code-step-vertical-line-bg-active)' : 'none'}; border-radius: var(--border-radius-lg); } &:hover::before { background-color: ${({ isActive }) => isActive ? 'var(--code-step-vertical-line-bg-active)' : 'var(--code-step-vertical-line-bg-hover)'}; } &:hover::before { width: ${({ isActive }) => (isActive ? '8px' : '6px')}; } &:hover ${StepContent} { background-color: ${({ isActive }) => isActive ? 'var(--code-step-bg-active-hover)' : 'var(--code-step-bg-hover)'}; } `;