Glow-Überschriften: Fix A aus der Hover-Analyse
Der Glow der Überschriften (Leistungen, Ablauf) wacht jetzt etwa 180 px vor der SVG-Box auf (Zeiger-Tracking am window, nur feine Zeiger), bildet den Mauszeiger über getScreenCTM korrekt auf die viewBox ab, blendet weich per Opacity ein und aus und klebt auf Touch-Geräten nicht mehr. Screenreader hören das Wort einmal (role=img mit aria-label, Textknoten aria-hidden). Der Diff aus /root/reports/webklar-relaunch-2026-10-01/ hover/fix/text-hover-effect.fixA.diff ließ sich unverändert anwenden. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d12bbc06a
commit
194301c6af
2 files changed
+179
-25
No files matched your search
@@ -486,3 +486,117 @@ describe("A3 Navigation: Theme-Schalter, Kontakt und Logo bekommen echte Mauskli
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("A4 Glow-Überschrift (Fix A)", () => {
|
||||
const BOX = { left: 100, top: 400, right: 1300, bottom: 624, width: 1200, height: 224, x: 100, y: 400 };
|
||||
|
||||
/** Browser-Umgebung für die Überschrift: Maus-Gerät, feste Box, manuell abgespielte Animation-Frames. */
|
||||
const umgebung = (feinerZeiger = true) => {
|
||||
const frames: FrameRequestCallback[] = [];
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => frames.push(cb));
|
||||
vi.spyOn(window, "matchMedia").mockImplementation(
|
||||
(query) =>
|
||||
({
|
||||
matches: feinerZeiger,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}) as MediaQueryList,
|
||||
);
|
||||
vi.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue({ ...BOX, toJSON: () => ({}) });
|
||||
// jsdom hat weder getScreenCTM noch DOMPoint: Abbildung Bildschirm -> viewBox (300 x 60) mit Faktor 4
|
||||
Object.defineProperty(SVGSVGElement.prototype, "getScreenCTM", {
|
||||
configurable: true,
|
||||
value: () => ({ inverse: () => ({}) }),
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"DOMPoint",
|
||||
class {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
) {}
|
||||
matrixTransform() {
|
||||
return { x: (this.x - BOX.left) / 4, y: (this.y - BOX.top) / 4 };
|
||||
}
|
||||
},
|
||||
);
|
||||
return () => act(() => frames.splice(0).forEach((cb) => cb(0)));
|
||||
};
|
||||
afterEach(() => {
|
||||
delete (SVGSVGElement.prototype as { getScreenCTM?: unknown }).getScreenCTM;
|
||||
});
|
||||
|
||||
/** jsdom kennt keine PointerEvents: MouseEvent mit pointerType reicht dem Fix. */
|
||||
const zeiger = (x: number, y: number, pointerType = "mouse") => {
|
||||
const e = new MouseEvent("pointermove", { clientX: x, clientY: y, bubbles: true });
|
||||
Object.defineProperty(e, "pointerType", { value: pointerType });
|
||||
window.dispatchEvent(e);
|
||||
};
|
||||
const glow = () => document.querySelector("svg text[mask]") as SVGTextElement;
|
||||
const mitte = () => document.querySelector("radialGradient") as SVGRadialGradientElement;
|
||||
|
||||
it("wird als ein Bild vorgelesen, die Textknoten sind für Screenreader versteckt", () => {
|
||||
umgebung();
|
||||
render(<TextHoverEffect text="Leistungen" />);
|
||||
expect(screen.getByRole("img", { name: "Leistungen" })).toBeInTheDocument();
|
||||
const texte = [...document.querySelectorAll("svg text")];
|
||||
expect(texte).toHaveLength(3);
|
||||
for (const t of texte) expect(t).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
|
||||
it("weckt den Glow ab etwa 180 px vor der Box und bildet den Zeiger korrekt auf die viewBox ab", () => {
|
||||
const abspielen = umgebung();
|
||||
render(<TextHoverEffect text="Leistungen" />);
|
||||
expect(glow().style.opacity).toBe("0");
|
||||
expect(glow().style.transition).toContain("opacity");
|
||||
|
||||
act(() => zeiger(700, BOX.top - 180));
|
||||
abspielen();
|
||||
expect(glow().style.opacity).toBe("1");
|
||||
expect(mitte().getAttribute("cx")).toBe("150");
|
||||
expect(mitte().getAttribute("cy")).toBe("-45");
|
||||
|
||||
act(() => zeiger(700, BOX.top - 400));
|
||||
abspielen();
|
||||
expect(glow().style.opacity).toBe("0");
|
||||
});
|
||||
|
||||
it("Touch lässt den Glow nicht kleben, und Verlassen der Seite schaltet ihn aus", () => {
|
||||
const abspielen = umgebung();
|
||||
render(<TextHoverEffect text="Leistungen" />);
|
||||
act(() => zeiger(700, BOX.top + 100, "touch"));
|
||||
abspielen();
|
||||
expect(glow().style.opacity).toBe("0");
|
||||
|
||||
act(() => zeiger(700, BOX.top + 100));
|
||||
abspielen();
|
||||
expect(glow().style.opacity).toBe("1");
|
||||
act(() => {
|
||||
document.documentElement.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
});
|
||||
expect(glow().style.opacity).toBe("0");
|
||||
});
|
||||
|
||||
it("auf Geräten ohne feinen Zeiger (Touch) werden gar keine Zeiger-Listener angehängt", () => {
|
||||
const abspielen = umgebung(false);
|
||||
const hinzu = vi.spyOn(window, "addEventListener");
|
||||
render(<TextHoverEffect text="Leistungen" />);
|
||||
expect(hinzu.mock.calls.filter(([typ]) => typ === "pointermove")).toHaveLength(0);
|
||||
act(() => zeiger(700, BOX.top + 100));
|
||||
abspielen();
|
||||
expect(glow().style.opacity).toBe("0");
|
||||
});
|
||||
|
||||
it("räumt seine Listener beim Entfernen wieder auf", () => {
|
||||
umgebung();
|
||||
const weg = vi.spyOn(window, "removeEventListener");
|
||||
const { unmount } = render(<TextHoverEffect text="Leistungen" />);
|
||||
unmount();
|
||||
expect(weg.mock.calls.map(([typ]) => typ)).toEqual(expect.arrayContaining(["pointermove", "scroll"]));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user