mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
web/elements: add viewport helpers and extend intersection observer (#21508)
web/elements: add viewport helpers and opt-in ancestor-box to intersection observer Some lazy-loaded elements render with `display: contents` so they don't produce a layout box of their own, which makes `IntersectionObserver` report them as never visible. Add `useAncestorBox` to the `intersectionObserver` decorator: when set (or when the element sets `displayBox="contents"`), fall back to the nearest ancestor that actually has a layout box and test that against the viewport. Extract the lookups into a new `elements/utils/viewport.ts` with `findNearestBoxTarget` and `isInViewport` helpers that can be reused outside the decorator.
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
* @file Intersection Observer Decorator for LitElement
|
||||
*/
|
||||
|
||||
import { findNearestBoxTarget, isInViewport } from "#elements/utils/viewport";
|
||||
|
||||
import { LitElement } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
@@ -10,6 +12,17 @@ import { property } from "lit/decorators.js";
|
||||
*/
|
||||
export type IntersectionDecorator = <T extends LitElement>(target: T, propertyKey: keyof T) => void;
|
||||
|
||||
export interface LitElementWithDisplayBox extends LitElement {
|
||||
displayBox?: "contents" | "block";
|
||||
}
|
||||
|
||||
export interface IntersectionObserverDecoratorInit extends IntersectionObserverInit {
|
||||
/**
|
||||
* Whether to ascend the DOM tree to find a parent with a layout box (i.e. non "display: contents") and use that as the target for intersection checking.
|
||||
*/
|
||||
useAncestorBox?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A decorator that applies an IntersectionObserver to the element.
|
||||
* This is useful for lazy-loading elements that are not visible on the screen.
|
||||
@@ -36,21 +49,48 @@ export type IntersectionDecorator = <T extends LitElement>(target: T, propertyKe
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @attr display-box If set to "contents", the element will be considered intersecting.
|
||||
*/
|
||||
export function intersectionObserver(init: IntersectionObserverInit = {}): IntersectionDecorator {
|
||||
return <T extends LitElement, K extends keyof T>(target: T, key: K) => {
|
||||
export function intersectionObserver({
|
||||
useAncestorBox: initialUseAncestorBox = false,
|
||||
...init
|
||||
}: IntersectionObserverDecoratorInit = {}): IntersectionDecorator {
|
||||
return <T extends LitElementWithDisplayBox, K extends keyof T>(target: T, key: K) => {
|
||||
//#region Prepare observer
|
||||
|
||||
let useAncestorBox = initialUseAncestorBox;
|
||||
|
||||
property({ attribute: false, useDefault: false })(target, key);
|
||||
|
||||
const boxTargets = new WeakMap<T, Element>();
|
||||
|
||||
function findAndCacheBoxTarget(instance: T): Element {
|
||||
let boxTarget = boxTargets.get(instance);
|
||||
|
||||
if (!boxTarget) {
|
||||
boxTarget = findNearestBoxTarget(instance);
|
||||
boxTargets.set(instance, boxTarget);
|
||||
}
|
||||
|
||||
return boxTarget;
|
||||
}
|
||||
|
||||
const observerCallback: IntersectionObserverCallback = (entries) => {
|
||||
for (const entry of entries) {
|
||||
const currentTarget = entry.target as T;
|
||||
let intersecting = entry.isIntersecting;
|
||||
|
||||
if (!intersecting && useAncestorBox) {
|
||||
const boxTarget = findAndCacheBoxTarget(currentTarget);
|
||||
intersecting = isInViewport(boxTarget);
|
||||
}
|
||||
|
||||
const cachedIntersecting = currentTarget[key];
|
||||
|
||||
if (cachedIntersecting !== entry.isIntersecting) {
|
||||
if (cachedIntersecting !== intersecting) {
|
||||
Object.assign(currentTarget, {
|
||||
[key]: entry.isIntersecting,
|
||||
[key]: intersecting,
|
||||
});
|
||||
|
||||
currentTarget.requestUpdate(key, cachedIntersecting);
|
||||
@@ -74,6 +114,8 @@ export function intersectionObserver(init: IntersectionObserverInit = {}): Inter
|
||||
target.connectedCallback = function connectedCallbackWrapper(this: T) {
|
||||
connectedCallback?.call(this);
|
||||
|
||||
useAncestorBox = this.displayBox === "contents" || initialUseAncestorBox;
|
||||
|
||||
if (this.hasUpdated) {
|
||||
observer.observe(this);
|
||||
} else {
|
||||
@@ -83,7 +125,9 @@ export function intersectionObserver(init: IntersectionObserverInit = {}): Inter
|
||||
}
|
||||
};
|
||||
|
||||
target.disconnectedCallback = function disconnectedCallbackWrapper(this: LitElement) {
|
||||
target.disconnectedCallback = function disconnectedCallbackWrapper(
|
||||
this: LitElementWithDisplayBox,
|
||||
) {
|
||||
disconnectedCallback?.call(this);
|
||||
|
||||
if (observer) {
|
||||
|
||||
22
web/src/elements/utils/viewport.ts
Normal file
22
web/src/elements/utils/viewport.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export function findNearestBoxTarget(element?: Element | null): Element {
|
||||
if (!element) {
|
||||
return document.documentElement;
|
||||
}
|
||||
|
||||
if (element.getClientRects().length) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return findNearestBoxTarget(element.parentElement);
|
||||
}
|
||||
|
||||
export function isInViewport(element: Element): boolean {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
rect.top < window.innerHeight &&
|
||||
rect.bottom > 0 &&
|
||||
rect.left < window.innerWidth &&
|
||||
rect.right > 0
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user