From fd5a21158b0b4db738866ae21fb85525d4f7e078 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 6 Apr 2025 09:02:10 +0300 Subject: [PATCH 1/5] feat(core): scroll-spy-controller hashchange --- .../controllers/scroll-spy-controller.ts | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index 24dd1e60cb..7d90a574f1 100644 --- a/core/pfe-core/controllers/scroll-spy-controller.ts +++ b/core/pfe-core/controllers/scroll-spy-controller.ts @@ -1,4 +1,4 @@ -import type { ReactiveController, ReactiveControllerHost } from 'lit'; +import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit'; export interface ScrollSpyControllerOptions extends IntersectionObserverInit { /** @@ -23,6 +23,7 @@ export interface ScrollSpyControllerOptions extends IntersectionObserverInit { * @default el => el.getAttribute('href'); */ getHash?: (el: Element) => string | null; + /** * Optional callback for when an intersection occurs */ @@ -40,9 +41,15 @@ export class ScrollSpyController implements ReactiveController { }); } }, { passive: true }); + addEventListener('hashchange', () => { + this.#instances.forEach(ssc => { + ssc.#activateHash(); + }); + }); } #tagNames: string[]; + #activeAttribute: string; #io?: IntersectionObserver; @@ -57,17 +64,28 @@ export class ScrollSpyController implements ReactiveController { #intersected = false; #root: ScrollSpyControllerOptions['root']; + #rootMargin?: string; + #threshold: number | number[]; - #intersectingElements: Element[] = []; + + #intersectingTargets = new Set(); + + #linkTargetMap = new Map(); #getRootNode: () => Node; + #getHash: (el: Element) => string | null; + #onIntersection?: () => void; get #linkChildren(): Element[] { - return Array.from(this.host.querySelectorAll(this.#tagNames.join(','))) - .filter(this.#getHash); + if (isServer) { + return []; + } else { + return Array.from(this.host.querySelectorAll(this.#tagNames.join(','))) + .filter(this.#getHash); + } } get root(): Element | Document | null | undefined { @@ -132,12 +150,16 @@ export class ScrollSpyController implements ReactiveController { if (rootNode instanceof Document || rootNode instanceof ShadowRoot) { const { rootMargin, threshold, root } = this; this.#io = new IntersectionObserver(r => this.#onIo(r), { root, rootMargin, threshold }); - this.#linkChildren - .map(x => this.#getHash(x)) - .filter((x): x is string => !!x) - .map(x => rootNode.getElementById(x.replace('#', ''))) - .filter((x): x is HTMLElement => !!x) - .forEach(target => this.#io?.observe(target)); + for (const link of this.#linkChildren) { + const id = this.#getHash(link)?.replace('#', ''); + if (id) { + const target = document.getElementById(id); + if (target) { + this.#io?.observe(target); + this.#linkTargetMap.set(link, target); + } + } + } } } @@ -155,6 +177,17 @@ export class ScrollSpyController implements ReactiveController { } } + async #activateHash() { + const links = this.#linkChildren; + const { hash } = location; + if (!hash) { + this.setActive(links.at(0) ?? null); + } else { + await this.#nextIntersection(); + this.setActive(links.find(x => this.#getHash(x) === hash) ?? null); + } + } + async #nextIntersection() { this.#intersected = false; // safeguard the loop @@ -178,13 +211,15 @@ export class ScrollSpyController implements ReactiveController { this.#setActive(last ?? this.#linkChildren.at(0)); } this.#intersected = true; - this.#intersectingElements = - entries - .filter(x => x.isIntersecting) - .map(x => x.target); + this.#intersectingTargets.clear(); + for (const entry of entries) { + if (entry.isIntersecting) { + this.#intersectingTargets.add(entry.target); + } + } if (this.#initializing) { const ints = entries?.filter(x => x.isIntersecting) ?? []; - if (this.#intersectingElements) { + if (this.#intersectingTargets.size > 0) { const [{ target = null } = {}] = ints; const { id } = target ?? {}; if (id) { From 523f948df20385ac540cd3d48cc0e55533e9e5f1 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 6 Apr 2025 09:08:58 +0300 Subject: [PATCH 2/5] style: whitespace --- core/pfe-core/controllers/scroll-spy-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index 7d90a574f1..aeaf6fe568 100644 --- a/core/pfe-core/controllers/scroll-spy-controller.ts +++ b/core/pfe-core/controllers/scroll-spy-controller.ts @@ -18,6 +18,7 @@ export interface ScrollSpyControllerOptions extends IntersectionObserverInit { * @default the host's root node */ rootNode?: Node; + /** * function to call on link children to get their URL hash (i.e. id to scroll to) * @default el => el.getAttribute('href'); From 2f2c63b97e45276ba74ce1d2225a9f8e408d8dcb Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 4 Apr 2025 14:53:04 -0400 Subject: [PATCH 3/5] fix(core): add isServer to scrollspy controller Co-authored-by: Steven Spriggs --- .../controllers/scroll-spy-controller.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index aeaf6fe568..94a2d3b295 100644 --- a/core/pfe-core/controllers/scroll-spy-controller.ts +++ b/core/pfe-core/controllers/scroll-spy-controller.ts @@ -35,18 +35,20 @@ export class ScrollSpyController implements ReactiveController { static #instances = new Set; static { - addEventListener('scroll', () => { - if (Math.round(window.innerHeight + window.scrollY) >= document.body.scrollHeight) { + if (isServer) { + addEventListener('scroll', () => { + if (Math.round(window.innerHeight + window.scrollY) >= document.body.scrollHeight) { + this.#instances.forEach(ssc => { + ssc.#setActive(ssc.#linkChildren.at(-1)); + }); + } + }, { passive: true }); + addEventListener('hashchange', () => { this.#instances.forEach(ssc => { - ssc.#setActive(ssc.#linkChildren.at(-1)); + ssc.#activateHash(); }); - } - }, { passive: true }); - addEventListener('hashchange', () => { - this.#instances.forEach(ssc => { - ssc.#activateHash(); }); - }); + } } #tagNames: string[]; From a31fb6ecc1391853fd9d0698a9b810ecdf79d515 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 6 Apr 2025 15:19:58 +0300 Subject: [PATCH 4/5] fix: oops --- core/pfe-core/controllers/scroll-spy-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index cdedd704d9..5edd597f7a 100644 --- a/core/pfe-core/controllers/scroll-spy-controller.ts +++ b/core/pfe-core/controllers/scroll-spy-controller.ts @@ -35,7 +35,7 @@ export class ScrollSpyController implements ReactiveController { static #instances = new Set; static { - if (isServer) { + if (!isServer) { addEventListener('scroll', () => { if (Math.round(window.innerHeight + window.scrollY) >= document.body.scrollHeight) { this.#instances.forEach(ssc => { From 5d9abfa2b465c58f8b34ddfa3e3a1184bceb290e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benny=20Powers=20-=20=D7=A2=D7=9D=20=D7=99=D7=A9=D7=A8?= =?UTF-8?q?=D7=90=D7=9C=20=D7=97=D7=99!?= Date: Sun, 6 Apr 2025 15:26:25 +0300 Subject: [PATCH 5/5] docs: changeset --- .changeset/six-phones-joke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-phones-joke.md diff --git a/.changeset/six-phones-joke.md b/.changeset/six-phones-joke.md new file mode 100644 index 0000000000..ad5d4aae06 --- /dev/null +++ b/.changeset/six-phones-joke.md @@ -0,0 +1,5 @@ +--- +"@patternfly/pfe-core": patch +--- + +`ScrollSpyController`: respond to hashchange events