fix(agenda): a lista de horários rola — o overflow existia e era inerte
O que o dono do produto via: abre "Novo agendamento", escolhe o dia, e os
últimos horários ficam abaixo da dobra sem nenhum jeito de alcançá-los. Nem a
lista rolava, nem a página.
A CAUSA, E ELA É SUTIL
O `overflow-y-auto` EXISTE, está no elemento certo, e é INERTE: um
`overflow-y-auto` cujo pai tem altura `auto` não rola — o filho cresce e leva a
caixa junto, então `scrollHeight === clientHeight`. Medido, sem o conserto:
lista: 644px de conteúdo em 644px de caixa
Nenhum elo entre o Sheet e a lista definia altura: o `SheetContent` tinha só
largura, o wrapper era um `div` de altura automática, e o painel tinha
`min-h-[450px]` — piso, nunca teto.
E a página também não rolava: o Sheet é `position: fixed`, e transbordo de
elemento fixo não estende a área rolável do documento. Não havia ancestral
rolável entre o último botão e a janela.
O CONSERTO: TRÊS LINHAS, TODAS NO `_client`
Zero mudança de comportamento no componente compartilhado, o que protege a
vitrine, onde o mesmo painel vive em fluxo de página e não tem defeito. Depois:
lista: 644px de conteúdo em 396px de caixa → rola
Abaixo de `lg` o próprio Sheet rola (ali o painel empilha). De `lg` para cima a
cadeia fecha e a LISTA rola, com calendário e contexto parados. O
`lg:overflow-hidden` não é preciosismo: em `lg` sobram 12px entre a caixa do
Sheet e o painel, e uma barra vertical os come — como o CSS computa `overflow-x:
visible` como `auto` quando `overflow-y` não é `visible`, nasceria barra
HORIZONTAL no breakpoint que o conserto de largura acabou de reparar.
Junto, o resíduo que o briefing apontou: `lg:min-h-0` no painel, para o piso de
450px não estourar em janela larga e baixa.
A RÉGUA 6 NÃO ERA O QUE O BRIEFING DIZIA, E EU QUASE CONSERTEI O CONSERTO
A proposta era asserir `body.scrollHeight - innerHeight <= 1`, com a nota "já
passa hoje". Ela reprovou — COM o conserto no lugar — e por um momento conclui
que eu tinha vindo pela via errada.
Medido, alternando as classes ao vivo no mesmo carregamento:
COM o conserto: body 1566 | lista 644 em 396
SEM o conserto: body 1566 | lista 644 em 644
O crescimento da página é IDÊNTICO nos dois e portanto PRÉ-EXISTENTE. A nota
"já passa hoje" nunca tinha sido medida — e eu a copiei sem medir, que é o erro
que ela mesma descreve. A régua passou a medir o que queria proteger: que o
Sheet continua `position: fixed`, ou seja, que ninguém "resolveu" a rolagem
transformando o painel em conteúdo de página.
⚠️ ACHADO SEPARADO, NÃO CONSERTADO AQUI: com o painel aberto em 1280×700 o body
vai a 1566px contra 700 de janela. É dívida anterior a este PR e misturá-la
esconderia as duas.
E A PRÉ-CONDIÇÃO DE SUFICIÊNCIA PEGOU A SI MESMA
O briefing pediu uma guarda contra verde por vacuidade (lista que cabe na tela
não precisa rolar). Escrevi comparando `n * 50` com a altura da JANELA — e ela
reprovou um cenário SUFICIENTE: 13 horários (650px) numa caixa de ~450px
transbordam com folga, mas 650 < 700. A régua certa é o espaço que sobra ABAIXO
DO TOPO DA LISTA, não a janela inteira; acima dela ainda há cabeçalho, título e
padding.
Sabotagem: removidas as três linhas e rebuild completo — a régua 4 reprova com
`644px de conteúdo em 644px de caixa iguais`. Restaurado, 7/7. Vizinhas
(kit-visual, marcar-pela-tela) 28/28.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011WHiP71A38fRzBy9TuSgk1
@@ -0,0 +1,11 @@
|
||||
---
|
||||
impacto: nada_mudou
|
||||
secao: corrigido
|
||||
titulo: A lista de horários volta a rolar ao marcar um compromisso
|
||||
---
|
||||
|
||||
Ao escolher o dia, os últimos horários ficavam abaixo da borda da tela sem
|
||||
nenhuma forma de alcançá-los — nem rolando a página, nem a própria lista. Quem
|
||||
precisava de um horário do fim da tarde não conseguia marcar. Agora a lista rola
|
||||
sozinha, com o calendário e os dados do atendimento parados ao lado, e em telas
|
||||
menores o painel inteiro rola.
|
||||
@@ -396,7 +396,31 @@ export function AgendaClient({
|
||||
empilha os horários sob o calendário, então 768px bastam e um Sheet
|
||||
maior só roubaria contexto da tela atrás.
|
||||
*/}
|
||||
<SheetContent side="right" className="w-full sm:max-w-3xl lg:max-w-[1040px]">
|
||||
{/*
|
||||
A CADEIA DE ALTURAS, e ela é o que faz a lista de horários rolar.
|
||||
|
||||
O `overflow-y-auto` da lista (`PainelDeMarcacao`) sempre esteve no
|
||||
elemento certo e era INERTE: `overflow-y-auto` cujo pai tem altura
|
||||
`auto` não rola — o filho cresce, `scrollHeight === clientHeight`, e os
|
||||
últimos horários ficavam abaixo da dobra sem nenhum jeito de alcançá-los.
|
||||
E a página também não rolava: o `SheetContent` é `position: fixed`, e
|
||||
transbordo de elemento fixo não estende a área rolável do documento.
|
||||
|
||||
Abaixo de `lg` o próprio Sheet rola (ali o painel empilha e a lista é
|
||||
uma seção, não uma coluna). De `lg` para cima o Sheet segura a altura e
|
||||
a LISTA rola, com calendário e contexto parados.
|
||||
|
||||
⚠️ `lg:overflow-hidden` e não `overflow-y-auto` em todo breakpoint: em
|
||||
`lg` o Sheet tem 1040px com `p-6` → 992px de caixa contra ~980px de
|
||||
painel. Uma barra vertical come essa folga, e como o CSS computa
|
||||
`overflow-x: visible` como `auto` quando `overflow-y` não é `visible`,
|
||||
nasceria barra HORIZONTAL exatamente no breakpoint que o conserto de
|
||||
largura acabou de reparar.
|
||||
*/}
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="flex w-full flex-col overflow-y-auto sm:max-w-3xl lg:max-w-[1040px] lg:overflow-hidden"
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{remarcandoId ? "Remarcar agendamento" : "Novo agendamento"}</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -426,8 +450,9 @@ export function AgendaClient({
|
||||
</div>
|
||||
)}
|
||||
{tipo && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-4 lg:min-h-0 lg:flex-1">
|
||||
<PainelDeMarcacao
|
||||
className="lg:h-full"
|
||||
ancora={new Date()}
|
||||
agora={new Date()}
|
||||
responsavel={
|
||||
|
||||
@@ -332,7 +332,11 @@ export function PainelDeMarcacao({
|
||||
// colunas cabem com folga. Abaixo disso o painel EMPILHA — os horários
|
||||
// viram uma seção sob o calendário, que é o que o cal.com faz e o que
|
||||
// esta base já fazia no celular.
|
||||
"flex min-h-[450px] flex-col overflow-hidden rounded-lg border border-border bg-surface lg:w-fit lg:flex-row",
|
||||
// `lg:min-h-0` junto do piso: em janela larga e BAIXA (menos de ~560px
|
||||
// de altura) um `min-h-[450px]` sem teto estoura o Sheet e o
|
||||
// `overflow-hidden` corta em silêncio — o mesmo modo de falha que este
|
||||
// painel já teve na horizontal.
|
||||
"flex min-h-[450px] flex-col overflow-hidden rounded-lg border border-border bg-surface lg:min-h-0 lg:w-fit lg:flex-row",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -636,7 +640,17 @@ export function PainelDeMarcacao({
|
||||
<p className="mb-2 shrink-0 text-xs font-semibold text-text-muted first-letter:uppercase">
|
||||
{dia ? format(dia, "EEEE, d 'de' MMM", { locale: ptBR }) : ""}
|
||||
</p>
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-1.5 overflow-y-auto pr-1">
|
||||
{/*
|
||||
`data-testid` para a lista poder ser MEDIDA, e não só vista. O
|
||||
`overflow-y-auto` aqui sempre esteve certo e era INERTE: um
|
||||
`overflow-y-auto` cujo pai tem altura `auto` não rola, porque o filho
|
||||
cresce e `scrollHeight === clientHeight`. Quem fecha a cadeia é o
|
||||
`_client.tsx`, que dá teto ao Sheet.
|
||||
*/}
|
||||
<div
|
||||
data-testid="lista-de-horarios"
|
||||
className="flex min-h-0 flex-1 flex-col gap-1.5 overflow-y-auto pr-1"
|
||||
>
|
||||
{doDia.map((h) => (
|
||||
<button
|
||||
key={h.instante}
|
||||
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 428 KiB After Width: | Height: | Size: 426 KiB |
@@ -200,6 +200,110 @@ for (const viewport of [
|
||||
});
|
||||
}
|
||||
|
||||
test("a lista de horários ROLA, e o último horário é alcançável — 1280×700", async ({ page }) => {
|
||||
/**
|
||||
* O EIXO Y, que esta spec não media.
|
||||
*
|
||||
* As três réguas acima são todas de largura (`x`, `width`). O dono do produto
|
||||
* encontrou o defeito no outro eixo: escolhe o dia, e os últimos horários
|
||||
* ficam abaixo da dobra sem nenhum jeito de alcançá-los.
|
||||
*
|
||||
* A causa é sutil e por isso escapou: o `overflow-y-auto` EXISTE, está no
|
||||
* elemento certo, e é INERTE. Um `overflow-y-auto` cujo pai tem altura `auto`
|
||||
* não rola — o filho cresce e `scrollHeight === clientHeight`. E a página
|
||||
* também não rola, porque o Sheet é `position: fixed` e transbordo de elemento
|
||||
* fixo não estende a área rolável do documento.
|
||||
*
|
||||
* Viewport 700px de altura de propósito: é onde a lista estoura com a
|
||||
* quantidade de horários que o seed produz.
|
||||
*/
|
||||
await page.setViewportSize({ width: 1280, height: 700 });
|
||||
const creds = lerCreds();
|
||||
await entrar(page, creds);
|
||||
await abrirPainelComDiaEscolhido(page, creds.agenda!.tipo_nome);
|
||||
|
||||
const lista = page.getByTestId("lista-de-horarios");
|
||||
await expect(lista).toBeVisible({ timeout: 15_000 });
|
||||
const horarios = page.locator('[data-testid^="horario-"]');
|
||||
const n = await horarios.count();
|
||||
|
||||
// ── PRÉ-CONDIÇÃO DE SUFICIÊNCIA ──────────────────────────────────────────
|
||||
// Sem isto o caso fica VERDE POR VACUIDADE no dia em que o seed produzir
|
||||
// poucos slots: uma lista que cabe na tela não precisa rolar, e "não rolou"
|
||||
// passaria a ser lido como "está consertado".
|
||||
//
|
||||
// ⚠️ A RÉGUA É O ESPAÇO QUE SOBRA PARA A LISTA, e não a altura da janela — e
|
||||
// eu escrevi errado da primeira vez. Comparar `n * 50` com os 700px da
|
||||
// viewport reprovou um cenário SUFICIENTE (13 horários = 650px numa caixa de
|
||||
// ~450px transbordam com folga), porque acima da lista ainda há o cabeçalho do
|
||||
// Sheet, o título e o padding. O que decide é onde a lista COMEÇA.
|
||||
//
|
||||
// 50px por item = 44 do `h-11` + 6 do `gap-1.5`.
|
||||
const topoDaLista = (await lista.boundingBox())?.y ?? 0;
|
||||
const alturaDaJanela = page.viewportSize()?.height ?? 0;
|
||||
const espacoQueSobra = alturaDaJanela - topoDaLista;
|
||||
expect(
|
||||
n * 50,
|
||||
`${n} horários ocupam ${n * 50}px e sobram ${Math.round(espacoQueSobra)}px de tela ` +
|
||||
"abaixo do topo da lista — ela cabe inteira, e este caso não exercita rolagem " +
|
||||
"nenhuma. Sem mais slots no seed, o verde aqui não seria evidência.",
|
||||
).toBeGreaterThan(espacoQueSobra);
|
||||
|
||||
// ── RÉGUA 4: o mecanismo ─────────────────────────────────────────────────
|
||||
const medida = await lista.evaluate((el) => ({
|
||||
scroll: el.scrollHeight,
|
||||
cliente: el.clientHeight,
|
||||
}));
|
||||
expect(
|
||||
medida.scroll,
|
||||
`a lista tem ${medida.scroll}px de conteúdo em ${medida.cliente}px de caixa iguais — ` +
|
||||
"ela não rola. O `overflow-y-auto` está lá e é inerte porque nenhum ancestral " +
|
||||
"define altura: o filho cresce e leva a caixa junto.",
|
||||
).toBeGreaterThan(medida.cliente);
|
||||
|
||||
// ── RÉGUA 5: o desfecho, na língua de quem usa ───────────────────────────
|
||||
const ultimo = horarios.last();
|
||||
await ultimo.scrollIntoViewIfNeeded();
|
||||
await expect(
|
||||
ultimo,
|
||||
"o último horário não entra na tela nem depois de rolar — é o que o dono viu",
|
||||
).toBeInViewport();
|
||||
await ultimo.click();
|
||||
await expect(
|
||||
page.getByTestId("confirmacao"),
|
||||
"cliquei no último horário e o painel não avançou",
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// ── RÉGUA 6: anti-regressão, e ela NÃO é a que o briefing propôs ─────────
|
||||
//
|
||||
// A proposta era `document.body.scrollHeight - window.innerHeight <= 1`, com a
|
||||
// nota "já passa hoje". **MEDIDO: não passa, e não é culpa do conserto.**
|
||||
// Com o painel aberto nesta viewport, o body vai a 1566px contra 700 de
|
||||
// janela — e o número é IDÊNTICO com e sem a cadeia de alturas:
|
||||
//
|
||||
// COM o conserto: body 1566 | lista 644 de conteúdo em 396 de caixa
|
||||
// SEM o conserto: body 1566 | lista 644 de conteúdo em 644 de caixa
|
||||
//
|
||||
// O crescimento da página é PRÉ-EXISTENTE e independente disto. Asserir sobre
|
||||
// ele reprovaria um conserto correto por uma dívida que já estava lá — e foi o
|
||||
// que aconteceu na primeira execução, com o conserto no lugar.
|
||||
//
|
||||
// A propriedade que a régua QUERIA proteger é outra e se mede direto: que o
|
||||
// conserto não veio tirando o `position: fixed` do Sheet, que é o que o
|
||||
// tornaria um painel comum e faria a página crescer de verdade.
|
||||
const posicaoDoSheet = await page
|
||||
.locator('[role="dialog"]')
|
||||
.first()
|
||||
.evaluate((el) => getComputedStyle(el).position);
|
||||
expect(
|
||||
posicaoDoSheet,
|
||||
"o Sheet deixou de ser `fixed` — a rolagem pode até parecer resolvida, mas o " +
|
||||
"painel virou conteúdo de página em vez de sobreposição",
|
||||
).toBe("fixed");
|
||||
|
||||
await page.screenshot({ path: "evidence/calendario/d4-lista-rola-1280x700.png", fullPage: false });
|
||||
});
|
||||
|
||||
test("dá para CLICAR num horário — o teste final é a ação, não a medida", async ({ page }) => {
|
||||
// A geometria acima é o diagnóstico; isto é o desfecho. Um horário pode estar
|
||||
// dentro do painel e ainda assim ser inalcançável (coberto por outra camada,
|
||||
|
||||