/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * @backward-compat { version 153 } * The entire logo-variation feature can be removed after Firefox 153 hits * Release, when the 2026 World Cup is over. Delete this file, the * `logo-variation-small`/`spin-ball-small` SCSS blocks plus their * `@keyframes`, the `logo.variation` pref entry in * `ActivityStream.sys.mjs`, and the logo-variation selection logic in * `Logo.jsx` (Logo reverts to its original default-only rendering). */ import React, { useEffect, useRef, useState } from "react"; /** * The "spin ball, small" logo variation. Renders the supplied animated * Firefox SVG (inline JSX) into the newtab logo slot. The SVG is purely * decorative — it's `aria-hidden`, has no interactive ARIA role, and is not * keyboard-focusable. Mouse users discover the click affordance via * `cursor: pointer` (defined in `_Logo.scss`). * * All animations declared on the SVG's children load `paused` (per the * `animation-play-state: paused` rule in `_Logo.scss`). They begin running * on the first click and re-run on each subsequent click (see the click * handler below). * * @returns {React.ReactElement} The animated SVG element. */ function SpinBallSmall() { const svgRef = useRef(null); const [isAnimating, setIsAnimating] = useState(false); // Track whether any of the SVG's CSS animations are in flight. The SVG // contains four parallel animations (spin, blur, classic-fade, nova-fade); // count starts and ends so we only clear `isAnimating` once they're all // done. CSS `animationstart`/`animationend` events bubble from the // animated children up to the SVG ref. useEffect(() => { const svg = svgRef.current; if (!svg) { return undefined; } let inflight = 0; const onStart = () => { inflight += 1; setIsAnimating(true); }; const onEnd = () => { inflight = Math.max(0, inflight - 1); if (inflight === 0) { setIsAnimating(false); } }; svg.addEventListener("animationstart", onStart); svg.addEventListener("animationend", onEnd); return () => { svg.removeEventListener("animationstart", onStart); svg.removeEventListener("animationend", onEnd); }; }, []); /** * Plays every CSS animation declared on the SVG (and its descendants), * resetting them to t=0 first so the cross-fade between the classic and * "nova" Firefox icons stays synchronised across replays. * * Two guards: * - `prefers-reduced-motion: reduce` short-circuits without invoking * `play()`. The SVG remains visible at its frame-0 keyframe state * (effectively the static Firefox logo), preserving the click * affordance for users who have reduced motion enabled while * honouring their preference. * - `playState !== "running"` makes the variation one-shot per click. * Clicking again while the animation is in flight does nothing; * clicking after it finishes restarts cleanly thanks to the * explicit `currentTime = 0` reset. */ const handleClick = () => { const svg = svgRef.current; if (!svg) { return; } if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { return; } const animations = svg.getAnimations({ subtree: true }); if (animations.length && animations[0].playState !== "running") { animations.forEach(a => { a.currentTime = 0; a.play(); }); } }; return ( ); } export { SpinBallSmall };