-
-
Notifications
You must be signed in to change notification settings - Fork 385
feat(Menu): improve scrollIntoView performance #7423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getTargetElement, getTransitionDelayDurationFromElement } from "../../modules/utility.js" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getTargetElement, getTransitionDelayDurationFromElement } from "../../modules/utility.js" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Data from "../../modules/data.js" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function init(id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -48,7 +48,7 @@ export function init(id) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Data.set(id, menu) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollElementToView(menu.element) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollElementToView(menu) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function update(id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -74,19 +74,24 @@ export function update(id) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollElementToView(menu.element) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollElementToView(menu) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function scrollToView(id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const menu = Data.get(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (menu) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollElementToView(menu.element); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollElementToView(menu); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function dispose(id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const menu = Data.get(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Data.remove(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Data.remove(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (menu.handler) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(menu.handler); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.handler = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.collapses.forEach(el => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const target = getTargetElement(el) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -97,13 +102,23 @@ export function dispose(id) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollElementToView = element => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (complexity): Consider refactoring the scroll retry logic into an element-based helper with its own timer management instead of storing timer state on the menu object. You can keep the new “wait for
One concrete option: // module-scope map to track timers per menu element
const scrollTimers = new Map();
const clearScrollTimer = (element) => {
const handle = scrollTimers.get(element);
if (handle) {
clearTimeout(handle);
scrollTimers.delete(element);
}
};
const scrollElementToView = (element) => {
if (!element.hasAttribute('data-bb-scroll-view')) {
return;
}
clearScrollTimer(element); // avoid multiple concurrent timers
let count = 0;
const tryScroll = () => {
const links = [...element.querySelectorAll('.nav-link.active')];
if (links.length > 0) {
scrollTimers.delete(element);
const link = links[links.length - 1];
link.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' });
return;
}
if (++count < 3) {
const handle = setTimeout(tryScroll, 100);
scrollTimers.set(element, handle);
} else {
scrollTimers.delete(element);
}
};
const handle = setTimeout(tryScroll, 100);
scrollTimers.set(element, handle);
};Call sites stay simple and decoupled from timer state: // init/update/scrollToView
scrollElementToView(menu.element);And export function dispose(id) {
const menu = Data.get(id);
Data.remove(id);
clearScrollTimer(menu.element);
menu.collapses.forEach(el => {
const target = getTargetElement(el);
const collapse = bootstrap.Collapse.getInstance(target);
if (collapse !== null) {
menu.disposeCollapse(collapse, el);
}
});
}This preserves the retry behavior and cleanup while:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollElementToView = menu => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { element } = menu; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scroll = element.hasAttribute('data-bb-scroll-view') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (scroll) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var links = [...element.querySelectorAll('.nav-link.active')] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (links.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var link = links.pop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| link.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let count = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.handler = setInterval(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (count < 3) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var links = [...element.querySelectorAll('.nav-link.active')] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (links.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(menu.handler); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.handler = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var link = links.pop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| link.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+110
to
+119
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Interval is never cleared when no active links are found, causing a runaway timer. Because the interval is only cleared when
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |
| } | |
| } else { | |
| clearInterval(menu.handler); | |
| menu.handler = null; |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If scrollElementToView is called multiple times before the active links are found (e.g., during rapid updates), multiple intervals will be created and stored in menu.handler, but only the last one will be tracked. Previous intervals will become orphaned and continue running indefinitely. Consider clearing any existing handler before creating a new one.
| menu.handler = setInterval(() => { | |
| if (count < 3) { | |
| var links = [...element.querySelectorAll('.nav-link.active')] | |
| if (links.length > 0) { | |
| clearInterval(menu.handler); | |
| menu.handler = null; | |
| var link = links.pop() | |
| link.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); | |
| } | |
| } | |
| count++; | |
| }, 100); | |
| // Clear any existing interval before creating a new one to avoid orphaned handlers | |
| if (menu.handler) { | |
| clearInterval(menu.handler); | |
| menu.handler = null; | |
| } | |
| const handler = setInterval(() => { | |
| if (count < 3) { | |
| var links = [...element.querySelectorAll('.nav-link.active')] | |
| if (links.length > 0) { | |
| clearInterval(handler); | |
| if (menu.handler === handler) { | |
| menu.handler = null; | |
| } | |
| var link = links.pop() | |
| link.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' }); | |
| } | |
| } else { | |
| // Stop retrying after the maximum number of attempts | |
| clearInterval(handler); | |
| if (menu.handler === handler) { | |
| menu.handler = null; | |
| } | |
| } | |
| count++; | |
| }, 100); | |
| menu.handler = handler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The clearTimeout is called before the scrollIntoView operation. If there's an error during scrollIntoView, the timeout would have already been cleared, potentially masking the issue. Consider moving the clearTimeout call after the scrollIntoView operation completes.