Testando e implementando rolagem suave em links de navegação para a própria página com React
Uma das estratégias mais utilizadas para permitir ao visitante navegar por seções de uma mesma página é a rolagem suave. Se contrapondo à estratégia de simplesmente rolar a página através de uma âncora adicionada na URL, que transporta o visitante imediatamente até a seção desejada, a rolagem suave oferece uma experiência mais fluida, guiando o visitante até o destino final de uma maneira muito mais agradável.
Vamos fazer do teste uma prancheta e começar a projetar nosso componente. No exemplo a seguir usaremos as ferramentas Jest e React Testing Library (RTL). O primeiro passo é dar nome a esse componente e especificar seu principal comportamento:
describe('Scroll Link', () => {
it('should scoll the page till the element with the given id');
});
Chamaremos o componente de <ScrollLink />
e passaremos para ele o id
do elemento para o qual a rolagem da página levará o visitante quando o link for clicado. Vamos agora escrever o teste usando o método AAA (Arrange, Act, Assert). Na etapa Arrange, precisamos importar módulos e utilitários, fazer eventuais mocks, e tudo mais que for necessário para que possamos agir (Act) e, imediatamente depois, assegurar (Assert) que o resultado era o que de fato esperávamos.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ScrollLink from './scroll-link'
describe('Scroll Link', () => {
beforeEach(() => {
window.scroll = jest.fn();
});
function mockSection({ id, topDistance }){
const section = document.createElement('section');
section.setAttribute('id', id);
section.getBoundingClientRect = jest.fn(() => ({ top: topDistance }));
document.body.appendChild(section);
}
it('should scoll the page till the element with the given id', async () => {
const id = 'intelligence';
const label = "Intelligence";
mockSection({ id, topDistance: 400 });
render(<ScrollLink to={id}>{label}</ScrollLink>);
});
});
Teste 100% arranjado! Note que é necessário mocar o retorno da função getBoundingClientRect
, uma vez que o jsdom
, até o momento que escrevo esse post, ainda não implementa qualquer recurso relacionado a estilos. Hora de agir:
it('should scoll the page till the element with the given id', async () => {
const id = 'intelligence';
const label = "Intelligence";
mockSection({ id, topDistance: 400 });
render(<ScrollLink to={id}>{label}</ScrollLink>);
await userEvent.click(screen.getByRole('link', { name: label }));
});
Imediatamente após clicarmos no link, garantimos então que a rolagem foi executada exatamente com os parâmetros esperados:
it('should scoll the page till the element with the given id', async () => {
const id = 'intelligence';
const label = "Intelligence";
mockSection({ id, topDistance: 400 });
render(<ScrollLink to={id}>{label}</ScrollLink>);
await userEvent.click(screen.getByRole('link', { name: label }));
expect(window.scroll).toHaveBeenCalledWith({
top: 400,
left: 0,
behavior: 'smooth'
});
});
Com o teste concluído e quebrando, vamos agora escrever a implementação do ScrollLink que faça o comportamento projetado se tornar realidade:
const ScrollLink = ({ to, children }) => {
const navigate = evt => {
evt.preventDefault();
window.scroll({
top: getElementTopDistance(to) + window.scrollY,
left: 0,
behavior: 'smooth'
});
}
const getElementTopDistance = elementId => {
const el = document.querySelector(`#${elementId}`);
return el.getBoundingClientRect().top;
}
return (
<a href={`#${to}`} onClick={navigate}>{children}</a>
)
}
Note que além de a implementação ser bastante enxuta por não depender de nenhum código terceiro, o link respeita todos os requisitos relacionados a acessibilidade.
Resultado inicial da implementação de navagação com rolagem suave para a própia página
Há somente um pequeno probleminha com a experiência. Ao final da rolagem, o título da seção fica próximo demais do topo da tela. Para eliminar esse desconforto, podemos aprimorar o ScrollLink adicionando a ele uma propriedade opcional que podemos chamar de offset
. Essa propriedade representará a que distância o elemento alvo deverá ficar afastado do topo da tela. Vamos ao projeto do novo caso de uso:
it('should optionally add an offset to the target element', async () => {
const id = 'intelligence';
const label = "Intelligence";
mockSection({ id, topDistance: 400 });
render(<ScrollLink to={id} offset={40}>{label}</ScrollLink>);
await userEvent.click(screen.getByRole('link', { name: label }));
expect(window.scroll).toHaveBeenCalledWith({
top: 360,
left: 0,
behavior: 'smooth'
});
});
Aprimoramos então o ScrollLink a fim de fazer o novo teste passar:
const ScrollLink = ({ to, children, offset = 0 }) => {
const navigate = evt => {
evt.preventDefault();
window.scroll({
top: getElementTopDistance(to) + window.scrollY - offset,
left: 0,
behavior: 'smooth'
});
}
const getElementTopDistance = elementId => {
const el = document.querySelector(`#${elementId}`);
return el.getBoundingClientRect().top;
}
return (
<a href={`#${to}`} onClick={navigate}>{children}</a>
)
}
Resultado final da implementação de navagação com rolagem suave para a própia página
Trabalho concluído! Nossa base de código passou a ter um componente, que pode ser utilizado em qualquer tela do site ou da aplicação, capaz de rolar a página até qualquer elemento que esteja devidamente identificado por um id
. Nesse gist você encontra um index.html contendo todo código necessário para experimentar o ScrollLink localmente, exatamente como demonstrado pelas animações que ilustram esse post.