perf(movimento): sem tempo morto, vídeos regravados e 3D sem React Three Fiber
Legenda do mergulho até Projetos cobrir a cena; transição de página sem blur e com fundo escuro; foco no conteúdo ao trocar de página; listas com marcador e dois pesos de título; vídeos da SEV e da SC Plus regravados sem quadros brancos; scrub a 12-15 fps (-30%); vitrine com uma parada de Tab e setas, contador no celular e CSS sob demanda; âncoras num módulo só (fragment-nav); FramePulse com setInterval; CSS morto removido.
This commit is contained in:
1 parent
f8545cd442
commit
6ac87824f3
68 files changed
+5735
-3300
No files matched your search
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { type FocusEvent, type KeyboardEvent, useState } from "react";
|
||||
|
||||
/** Atributo que marca os itens do grupo; o valor é a chave do item. */
|
||||
const ITEM_ATTRIBUTE = "data-roving";
|
||||
|
||||
type Step = "next" | "previous" | "first" | "last";
|
||||
|
||||
const STEPS: Record<string, Step> = {
|
||||
ArrowDown: "next",
|
||||
ArrowRight: "next",
|
||||
ArrowUp: "previous",
|
||||
ArrowLeft: "previous",
|
||||
Home: "first",
|
||||
End: "last",
|
||||
};
|
||||
|
||||
function targetIndex(step: Step, at: number, count: number) {
|
||||
if (step === "first") return 0;
|
||||
if (step === "last") return count - 1;
|
||||
return Math.min(count - 1, Math.max(0, at + (step === "next" ? 1 : -1)));
|
||||
}
|
||||
|
||||
/** O item aparece na tela? (um item dentro de um bloco escondido por CSS não recebe foco). */
|
||||
const isShown = (element: HTMLElement) => element.checkVisibility?.() ?? true;
|
||||
|
||||
/**
|
||||
* Teclado de um grupo de itens com roving tabindex: as setas (e Home/End) movem o foco entre os itens marcados com
|
||||
* `data-roving`, na ordem do documento, sem passar das pontas. Quem não é um desses gestos segue o caminho normal.
|
||||
*/
|
||||
export function moveRovingFocus(event: KeyboardEvent<HTMLElement>) {
|
||||
const step = STEPS[event.key];
|
||||
if (!step || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
||||
return;
|
||||
const current =
|
||||
event.target instanceof Element
|
||||
? event.target.closest<HTMLElement>(`[${ITEM_ATTRIBUTE}]`)
|
||||
: null;
|
||||
if (!current) return;
|
||||
const items = Array.from(
|
||||
event.currentTarget.querySelectorAll<HTMLElement>(`[${ITEM_ATTRIBUTE}]`),
|
||||
).filter(isShown);
|
||||
const at = items.indexOf(current);
|
||||
if (at < 0) return;
|
||||
event.preventDefault();
|
||||
items[targetIndex(step, at, items.length)]?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Um grupo de controles que ocupa uma parada só no Tab (roving tabindex): só o item da vez tem tabindex 0 e os
|
||||
* demais -1; as setas movem o foco dentro do grupo. A parada é o último item que recebeu foco, enquanto ele existir
|
||||
* (`isValid`); senão, `fallback` (o item atual do grupo).
|
||||
*
|
||||
* @example
|
||||
* const { groupProps, itemProps } = useRovingTabindex("p:0", (key) => key.startsWith("p:"));
|
||||
* <nav {...groupProps}><button {...itemProps("p:0")}>…</button></nav>
|
||||
*/
|
||||
export function useRovingTabindex(
|
||||
fallback: string,
|
||||
isValid: (key: string) => boolean,
|
||||
) {
|
||||
const [stop, setStop] = useState<string | null>(null);
|
||||
const tabStop = stop !== null && isValid(stop) ? stop : fallback;
|
||||
return {
|
||||
groupProps: {
|
||||
onKeyDown: moveRovingFocus,
|
||||
onFocus(event: FocusEvent<HTMLElement>) {
|
||||
const key = event.target
|
||||
.closest(`[${ITEM_ATTRIBUTE}]`)
|
||||
?.getAttribute(ITEM_ATTRIBUTE);
|
||||
if (key) setStop(key);
|
||||
},
|
||||
},
|
||||
itemProps: (key: string) => ({
|
||||
[ITEM_ATTRIBUTE]: key,
|
||||
tabIndex: key === tabStop ? 0 : -1,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user