fix(planning): affiche les recettes du picker sur mobile (#111)
All checks were successful
CI / lint (push) Successful in 1m39s
CI / build (push) Successful in 1m43s
CI / intent-service-test (push) Successful in 3m56s
CI / e2e (push) Successful in 8m37s
CI / test (push) Successful in 16m12s
CI / build-android (push) Has been skipped

Sur téléphone, le dialogue d'ajout d'une recette au planning devenait
plein écran mais `.recipe-picker-dialog` gardait un `height: 80vh` figé.
Dans cette colonne flex de hauteur bornée, le bloc `.recipe-picker__filters`
(rendu pour les 4 onglets non-source) consommait toute la place et
`.recipes-page__catalog` (`flex: 1`) s'effondrait à ~0 : la table des
recettes était dans le DOM mais dans un scrollport de hauteur nulle, donc
les onglets Favoris / Perso / Foyer / Publique paraissaient vides.

Sous le breakpoint phone : le dialogue passe vraiment plein écran (comme
tout autre `Dialog`), `.recipe-picker__filters` ne se laisse plus
compresser, et le catalogue prend sa hauteur de contenu — c'est
`.dialog-panel__body` qui scrolle.

Closes #111

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-08 01:18:17 +02:00
parent a87e3da025
commit 642b37ebec
2 changed files with 112 additions and 5 deletions

View file

@ -300,3 +300,76 @@ describe("Planning day view (phone)", () => {
cy.contains(".recipe-picker-dialog", "Choisir une recette").should("be.visible");
});
});
// Issue #111 — on a phone the recipe picker is a full-height flex column;
// a regression in the mobile full-screen-dialog CSS let its tall filter
// block (search + ingredient/diet pickers, rendered only for the four
// non-source tabs) consume the column and collapse the recipe table below
// it into a zero-height scrollport. The table was in the DOM the whole
// time, so Favoris / Perso / Foyer / Publique all just looked empty.
describe("Recipe picker dialog (phone)", () => {
// Full `RecipeSummaryView` shape — `RecipeTable` reads `allergens`/`diets`
// (`.length`/`.map`) on every row, so `{ id, name }` alone crashes it.
function summary(id: number, name: string) {
return {
id,
name,
description: null,
picture: null,
portions: 4,
authorId: 1,
visibility: "PERSONAL" as const,
allergens: [],
diets: [],
isFavorite: false,
};
}
const recipesByTab: Record<string, ReturnType<typeof summary>[]> = {
favoris: [summary(1, "Ratatouille")],
perso: [summary(2, "Omelette")],
foyer: [summary(3, "Gratin dauphinois")],
publique: [summary(4, "Soupe de potiron")],
};
const tabLabel: Record<string, string> = {
favoris: "Favoris",
perso: "Perso",
foyer: "Foyer",
publique: "Publique",
};
beforeEach(() => {
cy.setViewport("phone");
freezeToday();
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
// The dialog's own reference-data / source fetches — all empty, so the
// filter block is only as tall as its always-present controls, which is
// already enough to trigger the collapse this covers.
cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [] });
cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] });
cy.intercept("GET", "**/reference/sources", { statusCode: 200, body: [] });
cy.intercept("GET", "**/house/current", { statusCode: 200, body: null });
cy.intercept("GET", "**/house/current/sources", { statusCode: 200, body: [] });
cy.intercept("GET", /\/recipes\?/, (req) => {
const tab = new URL(req.url).searchParams.get("tab") ?? "favoris";
req.reply({ statusCode: 200, body: recipesByTab[tab] ?? [] });
});
});
it("shows the recipe table on every non-source tab, not a collapsed empty area", () => {
cy.visit("/");
cy.get(".add-recipe-btn").first().click();
cy.get(".recipe-picker-dialog").should("be.visible");
for (const tab of Object.keys(recipesByTab)) {
cy.contains(".recipe-tabs__tab", tabLabel[tab]).click();
// `be.visible` fails on a zero-height / clipped element — the exact
// shape of the regression.
cy.get(".recipe-picker-dialog .recipe-table").should("be.visible");
cy.get(".recipe-picker-dialog .recipe-table tbody tr").should("have.length.greaterThan", 0);
cy.contains(".recipe-picker-dialog .recipe-table__name", recipesByTab[tab][0].name).should(
"be.visible",
);
}
});
});

View file

@ -58,14 +58,48 @@
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
}
// recipes.scss already collapses `&__catalog` to a single column under
// 1024px, but the rule just above (2 classes) outweighs that page-scoped
// `@media` block and would keep two ~90px columns side by side on a
// phone. Re-collapse it here for the dialog's own narrow case — the list
// then stacks above the detail pane, same as the full `/recettes` page.
// On a phone this dialog goes truly full screen — same as every other
// `Dialog` (see `dialog.scss`'s own `bp.phone` block) — rather than
// keeping the 80vh pin above. That pin sizes a roomy desktop browsing
// area, but on a ~375px-wide screen it left far too little height once
// the tall `.recipe-picker__filters` block (search + ingredient chips +
// `IngredientPicker` + `DietTagSelect` + household toggle, rendered only
// for the four non-source tabs) was laid out first — the catalog below
// it collapsed to ~0 and the recipe table, though in the DOM, was
// rendered in a zero-height scrollport, so every non-source tab looked
// empty (issue #111).
@include bp.phone {
height: 100vh;
max-height: 100vh;
// recipes.scss already collapses `&__catalog` to a single column under
// 1024px, but the rule further up (2 classes) outweighs that
// page-scoped `@media` block and would keep two ~90px columns side by
// side on a phone. Re-collapse it here — the list stacks above the
// detail pane, same as the full `/recettes` page.
.recipes-page__catalog {
grid-template-columns: 1fr;
// ...and stop it competing for leftover flex space it won't get. As a
// `flex: 1` (⇒ `flex-basis: 0`) child of the flex-column
// `.dialog-panel__body`, with the filter block above consuming the
// column, it otherwise shrinks to ~0. On a phone the whole body
// scrolls instead (`.dialog-panel__body`'s own `overflow-y: auto`),
// so the catalog just takes its content height.
flex: none;
}
// Same reasoning: size the table wrapper to its content and let it
// scroll with the body, instead of clipping inside a collapsed catalog.
.recipe-table-wrap {
flex: none;
height: auto;
}
// The filter block must stay fully reachable — it's the one part a
// person needs to actually narrow the list down — so never let the
// flex column squeeze it.
.recipe-picker__filters {
flex-shrink: 0;
}
}
}