From 34e5e477e66f1bede3c3416bd0fa0c7b911927a2 Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Tue, 8 Sep 2026 10:25:19 +0530 Subject: [PATCH 1/6] feat: implement smooth scroll navigation and id fallback for steps list Signed-off-by: Mayank Sharma --- .../Steps-indicator/StepsIndicator.style.js | 56 +++++++++++++++---- .../Steps-indicator/index.js | 50 +++++++++++++---- .../Steps-list/Step/index.js | 4 +- .../Animated-steps-list/Steps-list/index.js | 5 +- src/components/Animated-steps-list/index.js | 49 +++++++++++++--- 5 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/components/Animated-steps-list/Steps-indicator/StepsIndicator.style.js b/src/components/Animated-steps-list/Steps-indicator/StepsIndicator.style.js index 4ced5f8be99c3f..19db22f314bdf4 100644 --- a/src/components/Animated-steps-list/Steps-indicator/StepsIndicator.style.js +++ b/src/components/Animated-steps-list/Steps-indicator/StepsIndicator.style.js @@ -1,21 +1,57 @@ import styled from "styled-components"; -export const StepsIndicatorWrapper = styled.ul` +export const StepsIndicatorWrapper = styled.nav` + padding: 0; + margin: 0; + + & .indicator-list { padding: 0; margin: 0; list-style: none; + } + + & li { + margin-bottom: 1rem; - & li { - font-size: 17px; - line-height: 28px; - color: gray; - margin-bottom: 16px; + & .indicator-link { + display: inline-block; + text-decoration: none; + background: transparent; + border: none; + box-shadow: none; + padding: 0; + margin: 0; + font-family: inherit; + font-size: 17px; + line-height: 28px; + color: ${(props) => props.theme.greyB4B4B4ToGrey505050 || "gray"}; + cursor: pointer; + transition: + color 0.2s ease, + font-weight 0.2s ease; - &.active { - font-weight: 600; - color: ${props => props.theme.text}; + &:hover { + color: ${(props) => props.theme.secondaryColor || "#00b39f"}; + background: transparent; + box-shadow: none; + text-decoration: none; + } + &:focus { + outline: none; + box-shadow: none; + } + + &:focus-visible { + outline: 2px solid ${(props) => props.theme.secondaryColor || "#00b39f"}; + outline-offset: 3px; + border-radius: 4px; + } + } - } + &.active .indicator-link { + font-weight: 600; + color: ${(props) => props.theme.text}; } + } `; diff --git a/src/components/Animated-steps-list/Steps-indicator/index.js b/src/components/Animated-steps-list/Steps-indicator/index.js index 315472acc6d3f5..1d7ebdb43d8e62 100644 --- a/src/components/Animated-steps-list/Steps-indicator/index.js +++ b/src/components/Animated-steps-list/Steps-indicator/index.js @@ -1,17 +1,47 @@ import React from "react"; import { StepsIndicatorWrapper } from "./StepsIndicator.style"; -const StepsIndicator = ({ steps, activeIndex }) => { +const StepsIndicator = ({ steps, activeIndex, onItemClick }) => { return ( - - {steps.map((step, index) => ( -
  • - {step.name} -
  • - ))} + + ); }; diff --git a/src/components/Animated-steps-list/Steps-list/Step/index.js b/src/components/Animated-steps-list/Steps-list/Step/index.js index 9d2ed7e7ffaf76..9576338670ebc5 100644 --- a/src/components/Animated-steps-list/Steps-list/Step/index.js +++ b/src/components/Animated-steps-list/Steps-list/Step/index.js @@ -3,7 +3,7 @@ import { useInView } from "react-intersection-observer"; import LogoList from "../../../Logo-List"; import { StepWrapper } from "./Step.style"; -const Step = ({ name, description, logos, onInViewStatusChanged }) => { +const Step = ({ id, name, description, logos, onInViewStatusChanged }) => { const [ref, inView] = useInView({ threshold: 0.4 }); const [inViewStatus, setInViewStatus] = useState(false); @@ -15,7 +15,7 @@ const Step = ({ name, description, logos, onInViewStatusChanged }) => { }, [inView, inViewStatus]); return ( - +

    {name}

    {description}
    diff --git a/src/components/Animated-steps-list/Steps-list/index.js b/src/components/Animated-steps-list/Steps-list/index.js index 0367a5b201c1f1..d1267380c00195 100644 --- a/src/components/Animated-steps-list/Steps-list/index.js +++ b/src/components/Animated-steps-list/Steps-list/index.js @@ -4,7 +4,7 @@ import Step from "./Step"; const StepsList = ({ steps, className, onFocusedIndexChanged }) => { const [viewportStatus, setViewportStatus] = useState( - new Array(steps.length).fill(false) + new Array(steps.length).fill(false), ); const [focusedStepIndex, setFocusedStepIndex] = useState(0); return ( @@ -13,6 +13,8 @@ const StepsList = ({ steps, className, onFocusedIndexChanged }) => { {steps.map((step, index) => ( { // Determine the new status array of the view status const newStatusArray = [...viewportStatus]; @@ -28,7 +30,6 @@ const StepsList = ({ steps, className, onFocusedIndexChanged }) => { onFocusedIndexChanged(newFocusIndex); } }} - {...step} /> ))} diff --git a/src/components/Animated-steps-list/index.js b/src/components/Animated-steps-list/index.js index 3e2fba23f19d37..c0c19922a0bdad 100644 --- a/src/components/Animated-steps-list/index.js +++ b/src/components/Animated-steps-list/index.js @@ -15,10 +15,10 @@ const animationBottomPadding = [0, 180, 180, 50]; const calculateCurrentFrame = (terminalSteps, currentIndex, scrollPosition) => { const percentage = Math.min( (scrollPosition - breakpoints[currentIndex]) / - (breakpoints[currentIndex + 1] - - breakpoints[currentIndex] - - animationBottomPadding[currentIndex]), - 1 + (breakpoints[currentIndex + 1] - + breakpoints[currentIndex] - + animationBottomPadding[currentIndex]), + 1, ); const currentLines = terminalSteps[currentIndex].lines; let totalFrames = 0; @@ -37,21 +37,54 @@ const AnimatedStepsList = ({ terminalHeroState, steps }) => { const scrollPosition = useScrollPosition(); const [indicatorIndex, setIndicatorIndex] = useState(0); const activeTerminalStateIndex = - scrollPosition <= 300 ? 0 : indicatorIndex + 1; + scrollPosition <= 300 ? 0 : indicatorIndex + 1; const terminalSteps = [terminalHeroState].concat( - steps.map((step) => step.terminal) + steps.map((step) => step.terminal), ); const currentFrame = calculateCurrentFrame( terminalSteps, activeTerminalStateIndex, - scrollPosition + scrollPosition, ); + const handleStepClick = (index) => { + const step = steps[index]; + if (!step) return false; + + const stepId = step.id || step.name.toLowerCase().replace(/\s+/g, "-"); + if (typeof document !== "undefined") { + const element = document.getElementById(stepId); + if (element) { + const prefersReducedMotion = + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + element.scrollIntoView({ + behavior: prefersReducedMotion ? "auto" : "smooth", + block: "start", + }); + setIndicatorIndex(index); + if ( + typeof window !== "undefined" && + window.history && + window.history.pushState + ) { + window.history.pushState(null, "", `#${stepId}`); + } + return true; + } + } + return false; + }; + return (
    - +
    Date: Tue, 8 Sep 2026 10:25:28 +0530 Subject: [PATCH 2/6] style: add scroll-margin-top to step wrappers Signed-off-by: Mayank Sharma --- .../Steps-list/Step/Step.style.js | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js index a46828975a32f4..14edbb15bc770a 100644 --- a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js +++ b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js @@ -1,27 +1,29 @@ import styled from "styled-components"; export const StepWrapper = styled.li` - width: 314px; - - @media (max-width: 850px) { - width: unset; - } + width: 314px; + scroll-margin-top: 9.5rem; + + @media (max-width: 850px) { + width: unset; + scroll-margin-top: 6rem; + } - & h3 { - margin-bottom: 12px; - margin-top: 0; - } + & h3 { + margin-bottom: 12px; + margin-top: 0; + } + + & .description { + & > p { + font-size: 17px; + line-height: 28px; + letter-spacing: 0.0125em; + color: var(--gray-2); + } + } - & .description { - & > p { - font-size: 17px; - line-height: 28px; - letter-spacing: 0.0125em; - color: var(--gray-2); - } - } - - & > .logo-list { - margin-top: 38px; - } + & > .logo-list { + margin-top: 38px; + } `; From bcb555f5cee2ee8e9c9cf7e567992d80e5fa84ec Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Tue, 8 Sep 2026 10:26:36 +0530 Subject: [PATCH 3/6] style: support sticky positioning for nav indicator wrapper Signed-off-by: Mayank Sharma --- src/components/Animated-steps-list/AnimatedStepsList.style.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Animated-steps-list/AnimatedStepsList.style.js b/src/components/Animated-steps-list/AnimatedStepsList.style.js index ff5144ccde9a5f..c32bf5c14759ee 100644 --- a/src/components/Animated-steps-list/AnimatedStepsList.style.js +++ b/src/components/Animated-steps-list/AnimatedStepsList.style.js @@ -4,7 +4,8 @@ export const AnimatedStepsListWrapper = styled.div` .animated-steps-list { display: flex; & .indicator-wrapper { - & > ul { + & > ul, + & > nav { position: sticky; top: 200px; padding-bottom: 395px; From 56940d13b46663b1154214fe7f7005086e4049e5 Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Tue, 8 Sep 2026 12:56:03 +0530 Subject: [PATCH 4/6] fix: support deep-linking, browser history navigation, and heading focus Signed-off-by: Mayank Sharma --- .../Steps-list/Step/Step.style.js | 4 ++ .../Steps-list/Step/index.js | 2 +- src/components/Animated-steps-list/index.js | 51 ++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js index 14edbb15bc770a..3d6c822510ecfb 100644 --- a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js +++ b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js @@ -12,6 +12,10 @@ export const StepWrapper = styled.li` & h3 { margin-bottom: 12px; margin-top: 0; + + &:focus { + outline: none; + } } & .description { diff --git a/src/components/Animated-steps-list/Steps-list/Step/index.js b/src/components/Animated-steps-list/Steps-list/Step/index.js index 9576338670ebc5..9ee699f7d5bba8 100644 --- a/src/components/Animated-steps-list/Steps-list/Step/index.js +++ b/src/components/Animated-steps-list/Steps-list/Step/index.js @@ -16,7 +16,7 @@ const Step = ({ id, name, description, logos, onInViewStatusChanged }) => { return ( -

    {name}

    +

    {name}

    {description}
    diff --git a/src/components/Animated-steps-list/index.js b/src/components/Animated-steps-list/index.js index c0c19922a0bdad..2576c844fcf082 100644 --- a/src/components/Animated-steps-list/index.js +++ b/src/components/Animated-steps-list/index.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import useScrollPosition from "./scroll-position"; import StepsList from "./Steps-list"; import StepsIndicator from "./Steps-indicator"; @@ -70,12 +70,61 @@ const AnimatedStepsList = ({ terminalHeroState, steps }) => { ) { window.history.pushState(null, "", `#${stepId}`); } + const heading = element.querySelector("h3"); + if (heading) { + heading.focus({ preventScroll: true }); + } return true; } } return false; }; + useEffect(() => { + if (typeof window === "undefined") return; + + const handleHashNavigation = () => { + const hash = window.location.hash.replace(/^#/, ""); + if (!hash) return; + + const targetIndex = steps.findIndex( + (step) => + (step.id || step.name.toLowerCase().replace(/\s+/g, "-")) === hash, + ); + + if (targetIndex !== -1) { + const stepId = + steps[targetIndex].id || + steps[targetIndex].name.toLowerCase().replace(/\s+/g, "-"); + const element = document.getElementById(stepId); + if (element) { + const prefersReducedMotion = + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + element.scrollIntoView({ + behavior: prefersReducedMotion ? "auto" : "smooth", + block: "start", + }); + setIndicatorIndex(targetIndex); + const heading = element.querySelector("h3"); + if (heading) { + heading.focus({ preventScroll: true }); + } + } + } + }; + + const timer = setTimeout(handleHashNavigation, 100); + window.addEventListener("popstate", handleHashNavigation); + window.addEventListener("hashchange", handleHashNavigation); + + return () => { + clearTimeout(timer); + window.removeEventListener("popstate", handleHashNavigation); + window.removeEventListener("hashchange", handleHashNavigation); + }; + }, [steps]); + return (
    From 5bd806f8f6c0243f6069095fb82cc2aa67567bdb Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Tue, 8 Sep 2026 14:38:17 +0530 Subject: [PATCH 5/6] fix: resolve Y-axis overflow on backward step navigation and address review feedback Signed-off-by: Mayank Sharma --- .../Steps-list/Step/Step.style.js | 12 +++- src/components/Animated-steps-list/index.js | 56 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js index 3d6c822510ecfb..4500eacc5ecacd 100644 --- a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js +++ b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js @@ -12,9 +12,17 @@ export const StepWrapper = styled.li` & h3 { margin-bottom: 12px; margin-top: 0; + scroll-margin-top: 9.5rem; - &:focus { - outline: none; + @media (max-width: 850px) { + scroll-margin-top: 6rem; + } + + &:focus, + &:focus-visible { + outline: 2px solid ${(props) => props.theme.secondaryColor || "#00b39f"}; + outline-offset: 4px; + border-radius: 2px; } } diff --git a/src/components/Animated-steps-list/index.js b/src/components/Animated-steps-list/index.js index 2576c844fcf082..366ffeb4fbe46f 100644 --- a/src/components/Animated-steps-list/index.js +++ b/src/components/Animated-steps-list/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import useScrollPosition from "./scroll-position"; import StepsList from "./Steps-list"; import StepsIndicator from "./Steps-indicator"; @@ -47,6 +47,19 @@ const AnimatedStepsList = ({ terminalHeroState, steps }) => { scrollPosition, ); + if (typeof window !== "undefined") { + const hash = window.location.hash.replace(/^#/, ""); + if ( + !hash || + steps.some( + (step) => + (step.id || step.name.toLowerCase().replace(/\s+/g, "-")) === hash, + ) + ) { + window.gatsby_scroll_hash = "none"; + } + } + const handleStepClick = (index) => { const step = steps[index]; if (!step) return false; @@ -68,6 +81,7 @@ const AnimatedStepsList = ({ terminalHeroState, steps }) => { window.history && window.history.pushState ) { + window.gatsby_scroll_hash = "none"; window.history.pushState(null, "", `#${stepId}`); } const heading = element.querySelector("h3"); @@ -80,22 +94,46 @@ const AnimatedStepsList = ({ terminalHeroState, steps }) => { return false; }; + const stepsRef = useRef(steps); + stepsRef.current = steps; + + const disarmGatsbyAnchorScroll = () => { + if (typeof window !== "undefined") { + const hash = window.location.hash.replace(/^#/, ""); + if ( + !hash || + stepsRef.current.some( + (step) => + (step.id || step.name.toLowerCase().replace(/\s+/g, "-")) === hash, + ) + ) { + window.gatsby_scroll_hash = "none"; + } + } + }; + useEffect(() => { if (typeof window === "undefined") return; + disarmGatsbyAnchorScroll(); + const handleHashNavigation = () => { + disarmGatsbyAnchorScroll(); const hash = window.location.hash.replace(/^#/, ""); - if (!hash) return; + if (!hash) { + setIndicatorIndex(0); + return; + } - const targetIndex = steps.findIndex( + const targetIndex = stepsRef.current.findIndex( (step) => (step.id || step.name.toLowerCase().replace(/\s+/g, "-")) === hash, ); if (targetIndex !== -1) { const stepId = - steps[targetIndex].id || - steps[targetIndex].name.toLowerCase().replace(/\s+/g, "-"); + stepsRef.current[targetIndex].id || + stepsRef.current[targetIndex].name.toLowerCase().replace(/\s+/g, "-"); const element = document.getElementById(stepId); if (element) { const prefersReducedMotion = @@ -115,15 +153,13 @@ const AnimatedStepsList = ({ terminalHeroState, steps }) => { }; const timer = setTimeout(handleHashNavigation, 100); - window.addEventListener("popstate", handleHashNavigation); - window.addEventListener("hashchange", handleHashNavigation); + window.addEventListener("popstate", handleHashNavigation, true); return () => { clearTimeout(timer); - window.removeEventListener("popstate", handleHashNavigation); - window.removeEventListener("hashchange", handleHashNavigation); + window.removeEventListener("popstate", handleHashNavigation, true); }; - }, [steps]); + }, []); return ( From 6ab351f9f4d3ad89c64526110e07d7e265e8bcdb Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Tue, 8 Sep 2026 14:41:37 +0530 Subject: [PATCH 6/6] fix: remove visual outline on programmatic step heading focus Signed-off-by: Mayank Sharma --- .../Animated-steps-list/Steps-list/Step/Step.style.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js index 4500eacc5ecacd..cdb22f05494e8d 100644 --- a/src/components/Animated-steps-list/Steps-list/Step/Step.style.js +++ b/src/components/Animated-steps-list/Steps-list/Step/Step.style.js @@ -18,11 +18,8 @@ export const StepWrapper = styled.li` scroll-margin-top: 6rem; } - &:focus, - &:focus-visible { - outline: 2px solid ${(props) => props.theme.secondaryColor || "#00b39f"}; - outline-offset: 4px; - border-radius: 2px; + &:focus { + outline: none; } }