From 105bf248ea2ce78b8f598e542dad2c3eff606a4c Mon Sep 17 00:00:00 2001 From: Torrelle Steven Date: Wed, 26 Nov 2025 15:36:55 +0100 Subject: [PATCH 1/3] Select toc item based on scroll direction in TOC --- Radzen.Blazor/wwwroot/Radzen.Blazor.js | 106 ++++++++++++++++--------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/Radzen.Blazor/wwwroot/Radzen.Blazor.js b/Radzen.Blazor/wwwroot/Radzen.Blazor.js index 127435245c2..52b37f5ad90 100644 --- a/Radzen.Blazor/wwwroot/Radzen.Blazor.js +++ b/Radzen.Blazor/wwwroot/Radzen.Blazor.js @@ -19,6 +19,7 @@ if (!Element.prototype.closest) { var resolveCallbacks = []; var rejectCallbacks = []; var radzenRecognition; +var selectedNavigationSelector; window.Radzen = { isRTL: function (el) { @@ -2563,52 +2564,81 @@ window.Radzen = { if (scroll) { const target = document.querySelector(selector); if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); + selectedNavigationSelector = selector; + target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); } } }, registerScrollListener: function (element, ref, selectors, selector) { - let currentSelector; - const container = selector ? document.querySelector(selector) : document.documentElement; - const elements = selectors.map(document.querySelector, document); - - this.unregisterScrollListener(element); - element.scrollHandler = () => { - const center = (container.tagName === 'HTML' ? 0 : container.getBoundingClientRect().top) + container.clientHeight / 2; - - let min = Number.MAX_SAFE_INTEGER; - let match; - - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - if (!element) continue; - - const rect = element.getBoundingClientRect(); - const diff = Math.abs(rect.top - center); - - if (!match && rect.top < center) { - match = selectors[i]; - min = diff; - continue; - } + let currentSelector; + const container = selector ? document.querySelector(selector) : document.documentElement; + const elements = selectors.map(document.querySelector, document); + + this.unregisterScrollListener(element); + + // helper to get current scroll position of container + const getScrollPosition = () => container && container.tagName === 'HTML' ? (window.scrollY || document.documentElement.scrollTop) : (container ? container.scrollTop : 0); + // store last scroll position on the element so we can determine direction + let lastScrollPosition = getScrollPosition(); + + let timeoutId = null; + const debounce = (callback, wait) => { + return () => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback(); + }, wait); + }; + } - if (match && rect.top >= center) continue; + element.scrollHandler = () => { + const containerRect = container && container.tagName === 'HTML' + ? { top: 0, bottom: window.innerHeight, height: window.innerHeight, clientHeight: window.innerHeight } + : container.getBoundingClientRect(); + + const scrollTop = getScrollPosition(); + const isDown = scrollTop > lastScrollPosition; + lastScrollPosition = scrollTop; + let min = Number.MAX_SAFE_INTEGER; + let match; + for (let i = 0; i < elements.length; i++) { + const elm = elements[i]; + if (!elm) continue; + + const rect = elm.getBoundingClientRect(); + // determine threshold based on scroll direction + const threshold = isDown ? containerRect.bottom : containerRect.top; + const diff = Math.abs(rect.top - threshold); + + if (!match && rect.top < threshold) { + match = selectors[i]; + min = diff; + continue; + } - if (diff < min) { - match = selectors[i]; - min = diff; - } - } + if (match && rect.top >= threshold) continue; - if (match !== currentSelector) { - currentSelector = match; - this.navigateTo(currentSelector, false); - ref.invokeMethodAsync('ScrollIntoView', currentSelector); - } - }; + if (diff < min) { + match = selectors[i]; + min = diff; + } + } - document.addEventListener('scroll', element.scrollHandler, true); - window.addEventListener('resize', element.scrollHandler, true); + if (match && match !== currentSelector) { + currentSelector = match; + if (!selectedNavigationSelector || (match === selectedNavigationSelector)) { + this.navigateTo(currentSelector, false); + ref.invokeMethodAsync('ScrollIntoView', currentSelector); + } + } + // clear selected navigation selector after scroll completes + if (selectedNavigationSelector && match === selectedNavigationSelector) { + debounce(() => { selectedNavigationSelector = undefined; }, 100)(); + } + }; + + document.addEventListener('scroll', element.scrollHandler, true); + window.addEventListener('resize', element.scrollHandler, true); }, unregisterScrollListener: function (element) { document.removeEventListener('scroll', element.scrollHandler, true); From 9508a55de270ad0de6fc1d5a92f4c633d428f3d5 Mon Sep 17 00:00:00 2001 From: Torrelle Steven Date: Thu, 27 Nov 2025 10:56:41 +0100 Subject: [PATCH 2/3] Select correct RadzenTocItem when reloading a page with anchor cleanup code --- Radzen.Blazor/wwwroot/Radzen.Blazor.js | 33 +++++++------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/Radzen.Blazor/wwwroot/Radzen.Blazor.js b/Radzen.Blazor/wwwroot/Radzen.Blazor.js index 52b37f5ad90..c763b7f1af3 100644 --- a/Radzen.Blazor/wwwroot/Radzen.Blazor.js +++ b/Radzen.Blazor/wwwroot/Radzen.Blazor.js @@ -19,7 +19,6 @@ if (!Element.prototype.closest) { var resolveCallbacks = []; var rejectCallbacks = []; var radzenRecognition; -var selectedNavigationSelector; window.Radzen = { isRTL: function (el) { @@ -2564,8 +2563,7 @@ window.Radzen = { if (scroll) { const target = document.querySelector(selector); if (target) { - selectedNavigationSelector = selector; - target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); + target.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }); } } }, @@ -2581,23 +2579,14 @@ window.Radzen = { // store last scroll position on the element so we can determine direction let lastScrollPosition = getScrollPosition(); - let timeoutId = null; - const debounce = (callback, wait) => { - return () => { - window.clearTimeout(timeoutId); - timeoutId = window.setTimeout(() => { - callback(); - }, wait); - }; - } - - element.scrollHandler = () => { + element.scrollHandler = () => { const containerRect = container && container.tagName === 'HTML' ? { top: 0, bottom: window.innerHeight, height: window.innerHeight, clientHeight: window.innerHeight } : container.getBoundingClientRect(); const scrollTop = getScrollPosition(); - const isDown = scrollTop > lastScrollPosition; + //When loading the page always look at the top of the container + const isDown = lastScrollPosition !=0 && scrollTop > lastScrollPosition; lastScrollPosition = scrollTop; let min = Number.MAX_SAFE_INTEGER; let match; @@ -2606,8 +2595,8 @@ window.Radzen = { if (!elm) continue; const rect = elm.getBoundingClientRect(); - // determine threshold based on scroll direction - const threshold = isDown ? containerRect.bottom : containerRect.top; + // determine threshold based on scroll direction with a small offset + const threshold = isDown ? containerRect.bottom -1 : containerRect.top +1; const diff = Math.abs(rect.top - threshold); if (!match && rect.top < threshold) { @@ -2616,7 +2605,7 @@ window.Radzen = { continue; } - if (match && rect.top >= threshold) continue; + if (match && rect.top > threshold) continue; if (diff < min) { match = selectors[i]; @@ -2626,17 +2615,11 @@ window.Radzen = { if (match && match !== currentSelector) { currentSelector = match; - if (!selectedNavigationSelector || (match === selectedNavigationSelector)) { this.navigateTo(currentSelector, false); ref.invokeMethodAsync('ScrollIntoView', currentSelector); - } - } - // clear selected navigation selector after scroll completes - if (selectedNavigationSelector && match === selectedNavigationSelector) { - debounce(() => { selectedNavigationSelector = undefined; }, 100)(); } }; - + document.addEventListener('scroll', element.scrollHandler, true); window.addEventListener('resize', element.scrollHandler, true); }, From 4c2abec9a427f46135afed4c132d6eeb86ddd543 Mon Sep 17 00:00:00 2001 From: Steven Torrelle Date: Mon, 1 Dec 2025 15:20:45 +0100 Subject: [PATCH 3/3] add debounce back into scrollhandler --- Radzen.Blazor/wwwroot/Radzen.Blazor.js | 32 ++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/Radzen.Blazor/wwwroot/Radzen.Blazor.js b/Radzen.Blazor/wwwroot/Radzen.Blazor.js index c763b7f1af3..6b4c100fef7 100644 --- a/Radzen.Blazor/wwwroot/Radzen.Blazor.js +++ b/Radzen.Blazor/wwwroot/Radzen.Blazor.js @@ -21,6 +21,7 @@ var rejectCallbacks = []; var radzenRecognition; window.Radzen = { + selectedNavigationSelector: undefined, isRTL: function (el) { return el && getComputedStyle(el).direction == 'rtl'; }, @@ -2562,8 +2563,9 @@ window.Radzen = { if (scroll) { const target = document.querySelector(selector); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }); + if (target) { + this.selectedNavigationSelector = selector; + target.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }); } } }, @@ -2579,14 +2581,26 @@ window.Radzen = { // store last scroll position on the element so we can determine direction let lastScrollPosition = getScrollPosition(); + let timeoutId = null; + const debounce = (callback, wait) => { + return () => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback(); + }, wait); + }; + } + element.scrollHandler = () => { const containerRect = container && container.tagName === 'HTML' ? { top: 0, bottom: window.innerHeight, height: window.innerHeight, clientHeight: window.innerHeight } : container.getBoundingClientRect(); const scrollTop = getScrollPosition(); - //When loading the page always look at the top of the container - const isDown = lastScrollPosition !=0 && scrollTop > lastScrollPosition; + //When loading the page or when no scrolling has been execute -> always look at the top of the container + const isDown = lastScrollPosition != 0 && scrollTop > lastScrollPosition; + // determine threshold based on scroll direction with a small offset + const threshold = isDown ? containerRect.bottom - 5 : containerRect.top + 5; lastScrollPosition = scrollTop; let min = Number.MAX_SAFE_INTEGER; let match; @@ -2595,8 +2609,6 @@ window.Radzen = { if (!elm) continue; const rect = elm.getBoundingClientRect(); - // determine threshold based on scroll direction with a small offset - const threshold = isDown ? containerRect.bottom -1 : containerRect.top +1; const diff = Math.abs(rect.top - threshold); if (!match && rect.top < threshold) { @@ -2610,13 +2622,19 @@ window.Radzen = { if (diff < min) { match = selectors[i]; min = diff; - } + } } if (match && match !== currentSelector) { currentSelector = match; + if (!this.selectedNavigationSelector || (match === this.selectedNavigationSelector)) { this.navigateTo(currentSelector, false); ref.invokeMethodAsync('ScrollIntoView', currentSelector); + } + } + // clear selected navigation selector after scroll completes + if (this.selectedNavigationSelector && match === this.selectedNavigationSelector) { + debounce(() => { this.selectedNavigationSelector = undefined; }, 100)(); } };