From b28f2c3f6b56e0631ecd363de8ac55e090147640 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Fri, 24 Oct 2025 13:48:54 +0200 Subject: [PATCH] refactor(client): move to react-scroll (#62921) --- client/package.json | 4 +-- .../src/client-only-routes/show-settings.tsx | 21 ++++++++++- .../components/Donation/donation-modal.tsx | 7 ++-- .../src/components/settings/certification.tsx | 8 ++--- .../Introduction/components/block.tsx | 26 +++++++------- .../Introduction/super-block-intro.tsx | 23 +++++++----- curriculum/i18n-curriculum | 1 - package.json | 6 ---- pnpm-lock.yaml | 35 ++++++++----------- 9 files changed, 72 insertions(+), 59 deletions(-) delete mode 160000 curriculum/i18n-curriculum diff --git a/client/package.json b/client/package.json index e1278433c97..bfb9bf29164 100644 --- a/client/package.json +++ b/client/package.json @@ -112,7 +112,7 @@ "react-redux": "7.2.9", "react-reflex": "4.1.0", "react-responsive": "9.0.2", - "react-scrollable-anchor": "0.6.1", + "react-scroll": "1.9.0", "react-spinkit": "3.0.0", "react-tooltip": "4.5.1", "react-transition-group": "4.4.5", @@ -153,7 +153,7 @@ "@types/react-helmet": "6.1.11", "@types/react-redux": "7.1.33", "@types/react-responsive": "8.0.8", - "@types/react-scrollable-anchor": "0.6.4", + "@types/react-scroll": "1.8.10", "@types/react-spinkit": "3.0.10", "@types/react-test-renderer": "16.9.12", "@types/react-transition-group": "4.4.10", diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx index 1d498b3c6ad..f64d1133389 100644 --- a/client/src/client-only-routes/show-settings.tsx +++ b/client/src/client-only-routes/show-settings.tsx @@ -1,8 +1,9 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import Helmet from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import { scroller } from 'react-scroll'; import { Container, Spacer } from '@freecodecamp/ui'; import { useFeatureIsOn } from '@growthbook/growthbook-react'; @@ -109,6 +110,24 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element { const examTokenFlag = useFeatureIsOn('exam-token-widget'); + const handleHashChange = () => { + const id = window.location.hash.replace('#', ''); + if (id) { + scroller.scrollTo(id, { + smooth: true, + duration: 500, + offset: -100 + }); + } + }; + + useEffect(() => { + handleHashChange(); + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + if (showLoading || !user) { return ; } diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx index 66eddf8dd95..de561ed8b10 100644 --- a/client/src/components/Donation/donation-modal.tsx +++ b/client/src/components/Donation/donation-modal.tsx @@ -1,7 +1,7 @@ import { WindowLocation } from '@gatsbyjs/reach-router'; import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; -import { goToAnchor } from 'react-scrollable-anchor'; +import { scroller } from 'react-scroll'; import { bindActionCreators, Dispatch, AnyAction } from 'redux'; import { createSelector } from 'reselect'; import { Modal } from '@freecodecamp/ui'; @@ -67,7 +67,10 @@ function DonateModal({ const handleModalHide = () => { // If modal is open on a SuperBlock page if (isLocationSuperBlock(location)) { - goToAnchor('claim-cert-block'); + scroller.scrollTo('claim-cert-block', { + duration: 0, + smooth: false + }); } if (isA11yFeatureEnabled && canClose) { diff --git a/client/src/components/settings/certification.tsx b/client/src/components/settings/certification.tsx index 5a498f0188f..5a5c377c194 100644 --- a/client/src/components/settings/certification.tsx +++ b/client/src/components/settings/certification.tsx @@ -2,7 +2,7 @@ import { find } from 'lodash-es'; import React, { MouseEvent, useState } from 'react'; import { withTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; -import ScrollableAnchor, { configureAnchors } from 'react-scrollable-anchor'; +import { Element } from 'react-scroll'; import { connect } from 'react-redux'; import { Table, Button, Spacer } from '@freecodecamp/ui'; @@ -39,8 +39,6 @@ import './certification.css'; const { showUpcomingChanges } = env; -configureAnchors({ offset: -40, scrollDuration: 0 }); - const mapDispatchToProps = { openModal }; @@ -296,7 +294,7 @@ function CertificationSettings(props: CertificationSettingsProps) { t: TFunction; }) => { return ( - +
@@ -319,7 +317,7 @@ function CertificationSettings(props: CertificationSettingsProps) {
-
+ ); }; diff --git a/client/src/templates/Introduction/components/block.tsx b/client/src/templates/Introduction/components/block.tsx index 73498da4bbf..b54ca48dbc7 100644 --- a/client/src/templates/Introduction/components/block.tsx +++ b/client/src/templates/Introduction/components/block.tsx @@ -2,7 +2,7 @@ import React, { Component, ReactNode } from 'react'; import type { TFunction } from 'i18next'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import ScrollableAnchor from 'react-scrollable-anchor'; +import { Element } from 'react-scroll'; import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; import { Spacer } from '@freecodecamp/ui'; @@ -179,7 +179,7 @@ export class Block extends Component { * Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/#basic-javascript */ const LegacyChallengeListBlock = ( - +
{ {isExpanded && }
-
+ ); /** @@ -235,7 +235,7 @@ export class Block extends Component { * Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/#javascript-algorithms-and-data-structures-projects */ const ProjectListBlock = ( - +
{
-
+ ); /** @@ -267,7 +267,7 @@ export class Block extends Component { * Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures-v8/#learn-basic-javascript-by-building-a-role-playing-game */ const LegacyChallengeGridBlock = ( - +
{ )}
-
+ ); /** @@ -318,7 +318,7 @@ export class Block extends Component { * Example: https://www.freecodecamp.org/learn/a2-english-for-developers/#learn-greetings-in-your-first-day-at-the-office */ const TaskGridBlock = ( - +
{ )}
-
+ ); /** @@ -364,7 +364,7 @@ export class Block extends Component { * Example: https://www.freecodecamp.org/learn/2022/responsive-web-design/#build-a-survey-form-project */ const LegacyLinkBlock = ( - +
{
-
+ ); /** @@ -416,9 +416,9 @@ export class Block extends Component { */ const AccordionBlock = ( <> - + - +
) => { return createSelector( currentChallengeIdSelector, @@ -129,6 +127,16 @@ const mapDispatchToProps = (dispatch: Dispatch) => }, dispatch ); +const handleHashChange = () => { + const id = window.location.hash.replace('#', ''); + if (id) { + scroller.scrollTo(id, { + smooth: true, + duration: 500, + offset: -50 + }); + } +}; const SuperBlockIntroductionPage = (props: SuperBlockProps) => { const { t } = useTranslation(); @@ -136,13 +144,10 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => { initializeExpandedState(); props.tryToShowDonationModal(); - setTimeout(() => { - configureAnchors({ offset: -40, scrollDuration: 400 }); - }, 0); + handleHashChange(); - return () => { - configureAnchors({ offset: -40, scrollDuration: 0 }); - }; + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/curriculum/i18n-curriculum b/curriculum/i18n-curriculum deleted file mode 160000 index b8ee7a88d29..00000000000 --- a/curriculum/i18n-curriculum +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b8ee7a88d29fc4757b946c83851ae017936bd142 diff --git a/package.json b/package.json index 6ff1e93056e..d3be489df55 100644 --- a/package.json +++ b/package.json @@ -133,12 +133,6 @@ }, "packageManager": "pnpm@10.18.0", "pnpm": { - "peerDependencyRules": { - "allowedVersions": { - "react-scrollable-anchor>react": "17", - "react-scrollable-anchor>react-dom": "17" - } - }, "onlyBuiltDependencies": [ "@freecodecamp/ui", "@prisma/client", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 649e7065c95..03a1bebc2f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -510,9 +510,9 @@ importers: react-responsive: specifier: 9.0.2 version: 9.0.2(react@17.0.2) - react-scrollable-anchor: - specifier: 0.6.1 - version: 0.6.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-scroll: + specifier: 1.9.0 + version: 1.9.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react-spinkit: specifier: 3.0.0 version: 3.0.0 @@ -628,9 +628,9 @@ importers: '@types/react-responsive': specifier: 8.0.8 version: 8.0.8 - '@types/react-scrollable-anchor': - specifier: 0.6.4 - version: 0.6.4 + '@types/react-scroll': + specifier: 1.8.10 + version: 1.8.10 '@types/react-spinkit': specifier: 3.0.10 version: 3.0.10 @@ -4745,8 +4745,8 @@ packages: '@types/react-responsive@8.0.8': resolution: {integrity: sha512-HDUZtoeFRHrShCGaND23HmXAB9evOOTjkghd2wAasLkuorYYitm5A1XLeKkhXKZppcMBxqB/8V4Snl6hRUTA8g==} - '@types/react-scrollable-anchor@0.6.4': - resolution: {integrity: sha512-Yn81DRAXPdOkqvDC6JO9KsTQv2g3bu45WGDfuI5G08vMntT7EvbBGrOeCuPkMQ8Q2JeZoP3TbKEMzLhJ8yAlGA==} + '@types/react-scroll@1.8.10': + resolution: {integrity: sha512-RD4Z7grbdNGOKwKnUBKar6zNxqaW3n8m9QSrfvljW+gmkj1GArb8AFBomVr6xMOgHPD3v1uV3BrIf01py57daQ==} '@types/react-spinkit@3.0.10': resolution: {integrity: sha512-grNfPdesm/xVJPyohfW752bM8N9kuJUx2yFo0I41mZwF3BuXt4+IV4TwaCPcBtA1V3C5r2NUPyqfEUpNTtWbvA==} @@ -9508,9 +9508,6 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jump.js@1.0.1: - resolution: {integrity: sha512-KXS4lfs3AcGdYl7cXk4X9mHb6PWIdrvBzGJs/QnG2g6niRoORkwisedbdjKu0VyUYmE2x7V+cV9ypSpaGRL0zg==} - just-curry-it@3.2.1: resolution: {integrity: sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==} @@ -11784,11 +11781,11 @@ packages: peerDependencies: react: '>=16.8' - react-scrollable-anchor@0.6.1: - resolution: {integrity: sha512-baaZKLXEmmaJzVXthx57K0jFgZjRiSEkhVTTssX03bzB097lIE1BYPVgtpgpAoETxTbiM+vDRJ2IPtClSQlj0A==} + react-scroll@1.9.0: + resolution: {integrity: sha512-mamNcaX9Ng+JeSbBu97nWwRhYvL2oba+xR2GxvyXsbDeGP+gkYIKZ+aDMMj/n20TbV9SCWm/H7nyuNTSiXA6yA==} peerDependencies: - react: ^15.3.0 || ^16.0.0 - react-dom: ^15.3.0 || ^16.0.0 + react: ^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-shallow-renderer@16.15.0: resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==} @@ -19493,7 +19490,7 @@ snapshots: dependencies: '@types/react': 17.0.83 - '@types/react-scrollable-anchor@0.6.4': + '@types/react-scroll@1.8.10': dependencies: '@types/react': 17.0.83 @@ -25883,8 +25880,6 @@ snapshots: object.assign: 4.1.5 object.values: 1.1.7 - jump.js@1.0.1: {} - just-curry-it@3.2.1: {} jwa@1.4.1: @@ -28658,9 +28653,9 @@ snapshots: '@remix-run/router': 1.11.0 react: 17.0.2 - react-scrollable-anchor@0.6.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + react-scroll@1.9.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: - jump.js: 1.0.1 + lodash.throttle: 4.1.1 prop-types: 15.8.1 react: 17.0.2 react-dom: 17.0.2(react@17.0.2)