refactor(motion): extraer useEnVista, compartido por las ilustraciones
This commit is contained in:
1 parent
fcd58fe00d
commit
1b1b39c67f
4 files changed
+59
-45
No files matched your search
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { usePrefersReducedMotion } from '@/lib/motion';
|
import { useEnVista } from '@/lib/motion';
|
||||||
|
|
||||||
// A literal small illustration of the section's own idea: a business grows,
|
// A literal small illustration of the section's own idea: a business grows,
|
||||||
// hits its inflection point (the dot), and from there either drifts flat
|
// hits its inflection point (the dot), and from there either drifts flat
|
||||||
@@ -10,25 +9,7 @@ import { usePrefersReducedMotion } from '@/lib/motion';
|
|||||||
// (dorado). Draws itself in once scrolled into view; a decorative aside, not
|
// (dorado). Draws itself in once scrolled into view; a decorative aside, not
|
||||||
// data, so it stays purely presentational.
|
// data, so it stays purely presentational.
|
||||||
export function IlustracionInflexion({ className = '' }: { className?: string }) {
|
export function IlustracionInflexion({ className = '' }: { className?: string }) {
|
||||||
const reduced = usePrefersReducedMotion();
|
const { ref, enVista, reduced } = useEnVista();
|
||||||
const ref = useRef<SVGSVGElement>(null);
|
|
||||||
const [inView, setInView] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el || reduced) return;
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setInView(true);
|
|
||||||
observer.unobserve(el);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.4 },
|
|
||||||
);
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [reduced]);
|
|
||||||
|
|
||||||
// `motion.path` applies its own dash-based pathLength scaffolding even with
|
// `motion.path` applies its own dash-based pathLength scaffolding even with
|
||||||
// no initial/animate props, which left the lines invisible for
|
// no initial/animate props, which left the lines invisible for
|
||||||
@@ -48,7 +29,7 @@ export function IlustracionInflexion({ className = '' }: { className?: string })
|
|||||||
|
|
||||||
const draw = (delay: number) => ({
|
const draw = (delay: number) => ({
|
||||||
initial: { pathLength: 0 },
|
initial: { pathLength: 0 },
|
||||||
animate: inView ? { pathLength: 1 } : { pathLength: 0 },
|
animate: enVista ? { pathLength: 1 } : { pathLength: 0 },
|
||||||
transition: { duration: 1.1, delay, ease: [0.22, 1, 0.36, 1] as const },
|
transition: { duration: 1.1, delay, ease: [0.22, 1, 0.36, 1] as const },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState, useEffect } from 'react';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { usePrefersReducedMotion } from '@/lib/motion';
|
import { useEnVista } from '@/lib/motion';
|
||||||
import { BARRAS_SIMBOLO } from '@/lib/simbolo';
|
import { BARRAS_SIMBOLO } from '@/lib/simbolo';
|
||||||
|
|
||||||
// The manual's own description of the ≡ symbol, made literal: "las líneas van
|
// The manual's own description of the ≡ symbol, made literal: "las líneas van
|
||||||
@@ -20,25 +19,7 @@ const BARS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function SimboloTresLineas({ className = '' }: { className?: string }) {
|
export function SimboloTresLineas({ className = '' }: { className?: string }) {
|
||||||
const reduced = usePrefersReducedMotion();
|
const { ref, enVista, reduced } = useEnVista();
|
||||||
const ref = useRef<SVGSVGElement>(null);
|
|
||||||
const [inView, setInView] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el || reduced) return;
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setInView(true);
|
|
||||||
observer.unobserve(el);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.4 },
|
|
||||||
);
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [reduced]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg ref={ref} viewBox="0 0 64 64" className={className} aria-hidden="true" focusable="false">
|
<svg ref={ref} viewBox="0 0 64 64" className={className} aria-hidden="true" focusable="false">
|
||||||
@@ -51,7 +32,7 @@ export function SimboloTresLineas({ className = '' }: { className?: string }) {
|
|||||||
rx={1.5}
|
rx={1.5}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
initial={bar.from}
|
initial={bar.from}
|
||||||
animate={inView ? bar.to : bar.from}
|
animate={enVista ? bar.to : bar.from}
|
||||||
transition={{ duration: 0.7, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.7, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useEnVista } from '@/lib/motion';
|
||||||
|
|
||||||
|
function Sonda() {
|
||||||
|
const { ref, enVista, reduced } = useEnVista();
|
||||||
|
return <svg ref={ref} data-en-vista={String(enVista)} data-reducido={String(reduced)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useEnVista', () => {
|
||||||
|
it('hands back a ref to attach, plus the two flags a drawing needs', () => {
|
||||||
|
const { container } = render(<Sonda />);
|
||||||
|
const svg = container.querySelector('svg');
|
||||||
|
expect(svg).toHaveAttribute('data-en-vista');
|
||||||
|
expect(svg).toHaveAttribute('data-reducido');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts out not-yet-in-view so drawings do not skip their entrance', () => {
|
||||||
|
const { container } = render(<Sonda />);
|
||||||
|
// jsdom's IntersectionObserver stub never fires, so this also pins the
|
||||||
|
// safe default: nothing claims to be visible until the observer says so.
|
||||||
|
expect(container.querySelector('svg')).toHaveAttribute('data-en-vista', 'false');
|
||||||
|
});
|
||||||
|
});
|
||||||
+29
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { Variants } from 'framer-motion';
|
import type { Variants } from 'framer-motion';
|
||||||
|
|
||||||
// EaseOutExpo — smooth editorial deceleration
|
// EaseOutExpo — smooth editorial deceleration
|
||||||
@@ -30,6 +30,34 @@ export function usePrefersReducedMotion() {
|
|||||||
return reduced;
|
return reduced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared "draw once when scrolled into view" wiring for the SVG illustrations.
|
||||||
|
// Returns the ref to attach, whether the element has been seen, and the
|
||||||
|
// reduced-motion flag the caller needs anyway to pick its render branch.
|
||||||
|
// Observes nothing under reduced motion — those branches render statically.
|
||||||
|
export function useEnVista(umbral = 0.4) {
|
||||||
|
const reduced = usePrefersReducedMotion();
|
||||||
|
const ref = useRef<SVGSVGElement>(null);
|
||||||
|
const [enVista, setEnVista] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el || reduced) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setEnVista(true);
|
||||||
|
observer.unobserve(el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: umbral },
|
||||||
|
);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [reduced, umbral]);
|
||||||
|
|
||||||
|
return { ref, enVista, reduced };
|
||||||
|
}
|
||||||
|
|
||||||
// Pass to <motion.*> as motion-reduced guard. When reduced, variants resolve to visible immediately.
|
// Pass to <motion.*> as motion-reduced guard. When reduced, variants resolve to visible immediately.
|
||||||
export const motionProps = (reduced: boolean) =>
|
export const motionProps = (reduced: boolean) =>
|
||||||
reduced
|
reduced
|
||||||
|
|||||||
Reference in new issue
Block a user