Public: anyone can read this; the crew can change it.
The confirm page before a destructive action speaks the home page's language #9861
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9860-the-confirm-page-before-a-destructive-ac → main 2 commits fork
3 files
changed +29
−24
docs/design/areas/dialog.md +4
| @@ -8,3 +8,7 @@ limb page's confirm form (`limbPreviewPage()` in `src/web/pages/limb.ts`) both d | ||
| 8 | 8 | - `.dialog-head` — the dialog's first row: its `h2` title (which `aria-labelledby` names) takes the width, anything beside it sits at the end. |
| 9 | 9 | - `.dialog-body` — the dialog's content, padded to the head's inset; add `.stack` when it holds several fields. |
| 10 | 10 | - `.dialog-foot` — the dialog's actions on one row, aligned to the end, the primary or danger button last. |
| 11 | ||
| 12 | The confirm page before a destructive route (`confirmPage()` in `src/web/pages/search.ts`) is not a dialog: it is a page | |
| 13 | heading, the one sentence of what will happen, the typed phrase when needed, and the action beside a way back, with no box | |
| 14 | and no new class. |
src/web/pages/search.ts +17 −24
| @@ -935,30 +935,23 @@ function confirmBody(ctx: Ctx): Response { | ||
| 935 | 935 | |
| 936 | 936 | const body = html`<div class="narrow-wide"> |
| 937 | 937 | ${pageHead(title)} |
| 938 | ${card( | |
| 939 | html`<p>${detail}</p> | |
| 940 | <form method="post" action="${to}" class="mt"> | |
| 941 | ${when(phrase !== "", () => | |
| 942 | formField({ | |
| 943 | label: `Type ${phrase} to continue`, | |
| 944 | name: "confirm", | |
| 945 | required: true, | |
| 946 | autofocus: true, | |
| 947 | mono: true, | |
| 948 | autocomplete: "off", | |
| 949 | }), | |
| 950 | )} | |
| 951 | <div class="form-actions"> | |
| 952 | ${btn(verb, { variant: "danger" })} | |
| 953 | ${btn("Cancel", { href: back, variant: "ghost" })} | |
| 954 | </div> | |
| 955 | </form>`, | |
| 956 | { | |
| 957 | tone: "danger", | |
| 958 | head: html`${icon("alert")} | |
| 959 | <h2>${verb}</h2>`, | |
| 960 | }, | |
| 961 | )} | |
| 938 | <p>${detail}</p> | |
| 939 | <form method="post" action="${to}" class="mt"> | |
| 940 | ${when(phrase !== "", () => | |
| 941 | formField({ | |
| 942 | label: `Type ${phrase} to continue`, | |
| 943 | name: "confirm", | |
| 944 | required: true, | |
| 945 | autofocus: true, | |
| 946 | mono: true, | |
| 947 | autocomplete: "off", | |
| 948 | }), | |
| 949 | )} | |
| 950 | <div class="form-actions"> | |
| 951 | ${btn(verb, { variant: "danger" })} | |
| 952 | ${btn("Cancel", { href: back, variant: "ghost" })} | |
| 953 | </div> | |
| 954 | </form> | |
| 962 | 955 | </div>`; |
| 963 | 956 | return page({ ctx, title: `${verb} · forge`, body }); |
| 964 | 957 | } |
test/confirm-detach.test.ts +8
| @@ -75,3 +75,11 @@ test("the fork's danger zone sends Detach from parent through the confirm page", | ||
| 75 | 75 | expect(body).toContain("There is no undo."); |
| 76 | 76 | expect(body).toContain(`action="${to}"`); |
| 77 | 77 | }); |
| 78 | ||
| 79 | test("the confirm page is a heading, a sentence and a form, not a box (#9860)", async () => { | |
| 80 | const to = `/${fork.ownerName}/${fork.name}/settings/detach`; | |
| 81 | const body = await (await fetch(`${servers.url}/-/confirm?to=${encodeURIComponent(to)}`, { headers: { cookie } })).text(); | |
| 82 | expect(body.match(/<h1[\s>]/g)?.length).toBe(1); | |
| 83 | expect(body).not.toContain('class="card'); | |
| 84 | expect(body).toMatch(new RegExp(`<form method="post" action="${to}"`)); | |
| 85 | }); |