From 559d586797dfd4b2b3b1645146d7ab71124ad09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Wed, 25 Feb 2026 17:08:49 +0000 Subject: [PATCH 01/18] sticky nav on scroll up and not enabled when user uses keyboard navigation --- .../components/Navigation/index.canonical.tsx | 138 +++++++++++++++++- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 027774497fc..c1704958b9c 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -1,4 +1,4 @@ -import React, { useState, use } from 'react'; +import React, { useState, useRef, useEffect, use } from 'react'; import Navigation from '#psammead/psammead-navigation/src'; import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; import { @@ -36,6 +36,11 @@ const CanonicalNavigationContainer: React.FC< const { isLite } = use(RequestContext); const { enabled: topBarOJsEnabled } = useToggle('topBarOJs'); const [isOpen, setIsOpen] = useState(false); + const [showSticky, setShowSticky] = useState(false); + const [lastScrollY, setLastScrollY] = useState(0); + const [isKeyboardNav, setIsKeyboardNav] = useState(false); + const navRef = useRef(null); + const stickyNavRef = useRef(null); useMediaQuery(`(max-width: ${GROUP_2_MAX_WIDTH_BP}rem)`, event => { if (!event.matches) { @@ -43,8 +48,67 @@ const CanonicalNavigationContainer: React.FC< } }); - return ( - + // Sticky nav scroll logic + useEffect(() => { + let ticking = false; + const handleScroll = () => { + if (!navRef.current) return; + if (!ticking) { + window.requestAnimationFrame(() => { + const navElement = navRef.current; + if (!navElement) return; + const navRect = navElement.getBoundingClientRect(); + const { scrollY } = window; + const scrollingUp = scrollY < lastScrollY; + setLastScrollY(scrollY); + // Only show sticky nav if nav is fully out of view and user is scrolling up + // Hide sticky nav before original nav is visible (with threshold) + const threshold = 48; // px, adjust for seamless merge + if (navRect.bottom < -threshold && scrollingUp && !isKeyboardNav) { + setShowSticky(true); + } else { + setShowSticky(false); + } + ticking = false; + }); + ticking = true; + } + }; + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastScrollY, isKeyboardNav]); + + // Keyboard navigation detection + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Tab') { + setIsKeyboardNav(true); + } + }; + const handlePointer = () => { + setIsKeyboardNav(false); + }; + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('mousedown', handlePointer); + window.addEventListener('touchstart', handlePointer); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('mousedown', handlePointer); + window.removeEventListener('touchstart', handlePointer); + }; + }, []); + + // Main nav (normal) + const mainNav = ( +
@@ -83,6 +147,74 @@ const CanonicalNavigationContainer: React.FC< {topBarOJsEnabled && } ); + + // Sticky nav (always rendered, animates in/out) + const stickyNav = + !isKeyboardNav && showSticky ? ( +
+ +
+
+
+ + {topScrollableListItems} + + {!isLite && ( + setIsOpen(!isOpen)} + dir={dir} + /> + )} +
+ + {dropdownListItems} + +
+
+ + {bottomScrollableListItems} + +
+
+
+ {topBarOJsEnabled && } + +
+ ) : null; + + return ( + <> + {mainNav} + {stickyNav} + + ); }; export default CanonicalNavigationContainer; From 57062b02a6a19764e7415e65e1c157016dfe5ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Wed, 25 Feb 2026 17:09:35 +0000 Subject: [PATCH 02/18] higher threshold --- src/app/components/Navigation/index.canonical.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index c1704958b9c..6f0523e55ff 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -63,7 +63,7 @@ const CanonicalNavigationContainer: React.FC< setLastScrollY(scrollY); // Only show sticky nav if nav is fully out of view and user is scrolling up // Hide sticky nav before original nav is visible (with threshold) - const threshold = 48; // px, adjust for seamless merge + const threshold = 65; // px, adjust for seamless merge if (navRect.bottom < -threshold && scrollingUp && !isKeyboardNav) { setShowSticky(true); } else { From ced2845ac9c9edb3135547821a9639641bf63652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Wed, 25 Feb 2026 17:48:15 +0000 Subject: [PATCH 03/18] comments and remove unnecessary code --- .../components/Navigation/index.canonical.tsx | 125 +++++++++--------- 1 file changed, 60 insertions(+), 65 deletions(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 6f0523e55ff..8fb1eb83088 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -37,8 +37,9 @@ const CanonicalNavigationContainer: React.FC< const { enabled: topBarOJsEnabled } = useToggle('topBarOJs'); const [isOpen, setIsOpen] = useState(false); const [showSticky, setShowSticky] = useState(false); + // Refs and state for sticky nav const [lastScrollY, setLastScrollY] = useState(0); - const [isKeyboardNav, setIsKeyboardNav] = useState(false); + const [isKeyboardNav, setIsKeyboardNav] = useState(false); // this is to prevent showing sticky nav when keyboard navigation is detected const navRef = useRef(null); const stickyNavRef = useRef(null); @@ -59,17 +60,18 @@ const CanonicalNavigationContainer: React.FC< if (!navElement) return; const navRect = navElement.getBoundingClientRect(); const { scrollY } = window; - const scrollingUp = scrollY < lastScrollY; + const scrollingUp = scrollY < lastScrollY; // detects scroll direction setLastScrollY(scrollY); // Only show sticky nav if nav is fully out of view and user is scrolling up // Hide sticky nav before original nav is visible (with threshold) - const threshold = 65; // px, adjust for seamless merge + const threshold = 65; // px, adjust for not seeing both original and sticky nav at the same time if (navRect.bottom < -threshold && scrollingUp && !isKeyboardNav) { + // do not show sticky nav if keyboard navigation is detected setShowSticky(true); } else { setShowSticky(false); } - ticking = false; + ticking = false; // the ticking flag is used to prevent multiple requestAnimationFrame calls from stacking up and causing performance issues during fast scrolling. }); ticking = true; } @@ -101,14 +103,7 @@ const CanonicalNavigationContainer: React.FC< // Main nav (normal) const mainNav = ( - +
@@ -149,65 +144,65 @@ const CanonicalNavigationContainer: React.FC< ); // Sticky nav (always rendered, animates in/out) - const stickyNav = - !isKeyboardNav && showSticky ? ( -
- -
-
-
- - {topScrollableListItems} - - {!isLite && ( - setIsOpen(!isOpen)} - dir={dir} - /> - )} -
- - {dropdownListItems} - -
-
+ const stickyNav = !isKeyboardNav ? ( + @@ -200,6 +201,7 @@ const CanonicalNavigationContainer: React.FC< dir={dir} css={styles.bottomRowItems} navPosition="secondary" + isSticky > {bottomScrollableListItems} diff --git a/src/app/legacy/psammead/psammead-navigation/src/DropdownNavigation/index.jsx b/src/app/legacy/psammead/psammead-navigation/src/DropdownNavigation/index.jsx index 0a011bc20e9..76b83d1608c 100644 --- a/src/app/legacy/psammead/psammead-navigation/src/DropdownNavigation/index.jsx +++ b/src/app/legacy/psammead/psammead-navigation/src/DropdownNavigation/index.jsx @@ -45,11 +45,16 @@ const StyledDropdown = styled.div` } `; -export const CanonicalDropdown = ({ isOpen, children, className = '' }) => { +export const CanonicalDropdown = ({ + isOpen, + children, + className = '', + isSticky = false, +}) => { const heightRef = useRef(null); return ( ( From 12f76d761257f5e29729de79e1a605bcc698b5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Thu, 26 Feb 2026 14:46:22 +0000 Subject: [PATCH 10/18] try to only target the visible nav button in the cypress tests --- .../e2e/specialFeatures/atiAnalytics/assertions/navigation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/cypress/e2e/specialFeatures/atiAnalytics/assertions/navigation.ts b/ws-nextjs-app/cypress/e2e/specialFeatures/atiAnalytics/assertions/navigation.ts index bde41f5f456..c332021b9e8 100644 --- a/ws-nextjs-app/cypress/e2e/specialFeatures/atiAnalytics/assertions/navigation.ts +++ b/ws-nextjs-app/cypress/e2e/specialFeatures/atiAnalytics/assertions/navigation.ts @@ -73,7 +73,7 @@ export const assertDropdownNavigationComponentView = ({ cy.visit(path); cy.viewport(320, 480); - cy.get('nav button').click(); + cy.get('nav button:visible').click(); assertATIComponentViewEvent({ component: DROPDOWN_NAVIGATION, @@ -95,7 +95,7 @@ export const assertDropdownNavigationComponentClick = ({ cy.visit(path); cy.viewport(320, 480); - cy.get('nav button').click(); + cy.get('nav button:visible').click(); // Click on first item, then return to the original page cy.get('[data-e2e="dropdown-nav"]').find('a').first().click(); From 6b379df41c10ddfe77d7725d2fab9f9b26ee9646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Thu, 26 Feb 2026 14:59:02 +0000 Subject: [PATCH 11/18] variable name change --- src/app/components/Navigation/index.canonical.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 7df863e846d..a3c1663758b 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -59,14 +59,14 @@ const CanonicalNavigationContainer: React.FC< window.requestAnimationFrame(() => { const navElement = navRef.current; if (!navElement) return; - const navRect = navElement.getBoundingClientRect(); + const mainNavBar = navElement.getBoundingClientRect(); const { scrollY } = window; const scrollingUp = scrollY < lastScrollY; // detects scroll direction setLastScrollY(scrollY); // Only show sticky nav if nav is fully out of view and user is scrolling up // Hide sticky nav before original nav is visible (with threshold) const threshold = 65; // px, adjust for not seeing both original and sticky nav at the same time - if (navRect.bottom < -threshold && scrollingUp && !isKeyboardNav) { + if (mainNavBar.bottom < -threshold && scrollingUp && !isKeyboardNav) { // do not show sticky nav if keyboard navigation is detected setShowSticky(true); } else { From 6bba1d9ca3c893ac01f181249573ded29935678b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Thu, 26 Feb 2026 15:02:11 +0000 Subject: [PATCH 12/18] close dropdown nav when hiding sticky nav or it overlaps --- src/app/components/Navigation/index.canonical.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index a3c1663758b..8af192f8195 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -71,6 +71,7 @@ const CanonicalNavigationContainer: React.FC< setShowSticky(true); } else { setShowSticky(false); + setIsOpen(false); // Close dropdown when sticky nav hides } ticking = false; // the ticking flag is used to prevent multiple requestAnimationFrame calls from stacking up and causing performance issues during fast scrolling. }); From b033e742eb72eea961156d402b7ae587e8ca099e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Thu, 26 Feb 2026 15:30:35 +0000 Subject: [PATCH 13/18] do not show sticky nav on lite site --- .../components/Navigation/index.canonical.tsx | 122 +++++++++--------- 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 8af192f8195..62dd48ff58d 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -84,6 +84,7 @@ const CanonicalNavigationContainer: React.FC< }, [lastScrollY, isKeyboardNav]); // Keyboard navigation detection + // do we need to add other keys? useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Tab') { @@ -145,73 +146,74 @@ const CanonicalNavigationContainer: React.FC< ); - // Sticky nav (always rendered, animates in/out) - const stickyNav = !isKeyboardNav ? ( -