From 642b37ebecb8fe198a3ab228fcb231d9e6220a46 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 8 Sep 2026 01:18:17 +0200 Subject: [PATCH] fix(planning): affiche les recettes du picker sur mobile (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/web/cypress/e2e/planning-page.cy.ts | 73 +++++++++++++++++++ .../planning/recipe-picker-dialog.scss | 44 +++++++++-- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/apps/web/cypress/e2e/planning-page.cy.ts b/apps/web/cypress/e2e/planning-page.cy.ts index b7e6922..b3c23df 100644 --- a/apps/web/cypress/e2e/planning-page.cy.ts +++ b/apps/web/cypress/e2e/planning-page.cy.ts @@ -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[]> = { + favoris: [summary(1, "Ratatouille")], + perso: [summary(2, "Omelette")], + foyer: [summary(3, "Gratin dauphinois")], + publique: [summary(4, "Soupe de potiron")], + }; + const tabLabel: Record = { + 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", + ); + } + }); +}); diff --git a/apps/web/src/features/planning/recipe-picker-dialog.scss b/apps/web/src/features/planning/recipe-picker-dialog.scss index edddd46..c18ec36 100644 --- a/apps/web/src/features/planning/recipe-picker-dialog.scss +++ b/apps/web/src/features/planning/recipe-picker-dialog.scss @@ -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; } } }