From 4df9ecc9804a2c9925c547d1964791363f72c9c1 Mon Sep 17 00:00:00 2001 From: dborup Date: Mon, 5 Oct 2026 14:24:59 +0000 Subject: [PATCH 1/2] test(touch): reproduce the 32 px add-channel dialog buttons (#249) test-touch-targets.js renders #chGenerateBtn, #chPskAddBtn and #chHashtagBtn in their real dialog rows and requires 48x48, a centred label and no overflow out of the dialog. The #2052 E2E checks the same in the real DOM at 320, 390, 640 and 768 px with touch, and pins the desktop height at 32 px, since the fix is touch-only. Red on master: all three buttons render 32 px tall. Co-Authored-By: Claude Opus 5.5 --- test-issue-2052-touch-target-e2e.js | 71 +++++++++++++++++++++++++++++ test-touch-targets.js | 36 +++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/test-issue-2052-touch-target-e2e.js b/test-issue-2052-touch-target-e2e.js index face2554c..211a477ef 100644 --- a/test-issue-2052-touch-target-e2e.js +++ b/test-issue-2052-touch-target-e2e.js @@ -21,6 +21,11 @@ * remove are at least 48x48, visible, unclipped, hit at their centre and * do not overlap. Before upstream PR 2078 the remove action was clipped * here; user-added rows now let their controls wrap. + * - The add-channel dialog's primary buttons (#249): at 320, 390, 640 and + * 768 px with touch each is at least 48x48, unclipped, hit at its centre + * with a centred label, and inside a dialog that does not scroll + * sideways. At 1440 px without touch they keep their 32 px height: the + * change is touch-only, like the #630 coarse-pointer block. * * 48 is the house preference for these controls (upstream PR 2078 settled * #2052 on 48; WCAG 2.5.5 asks for 44). @@ -142,6 +147,34 @@ async function assertActionTargets(page) { assert(!overlaps(share, remove), 'share and remove overlap'); } +// #249: the add-channel dialog's primary buttons. +const DIALOG_BUTTONS = ['#chGenerateBtn', '#chPskAddBtn', '#chHashtagBtn']; + +async function openAddChannelDialog(page) { + await page.click('#chAddChannelBtn'); + await page.waitForSelector('#chModalClose', { state: 'visible' }); +} + +// Where a button sits in the dialog, with the button scrolled into view: its +// label's offset from the centre, and whether it is inside the dialog. +function dialogPlacement(page, selector) { + return page.evaluate((sel) => { + const el = document.querySelector(sel); + el.scrollIntoView({ block: 'center' }); + const b = el.getBoundingClientRect(); + const m = el.closest('.ch-modal').getBoundingClientRect(); + const range = document.createRange(); + range.selectNodeContents(el); + const t = range.getBoundingClientRect(); + return { + h: b.height, + dx: (t.left + t.width / 2) - (b.left + b.width / 2), + dy: (t.top + t.height / 2) - (b.top + b.height / 2), + inside: b.left >= m.left && b.right <= m.right, + }; + }, selector); +} + function horizontalOverflow(page) { return page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); } @@ -316,6 +349,44 @@ async function main() { } } + // ── Add-channel dialog buttons (#249) ──────────────────────────────────── + for (const [width, height] of [[320, 640], [390, 844], [640, 900], [768, 1024]]) { + const dlg = await openChannels(browser, errors, { viewport: { width, height }, hasTouch: true, isMobile: true }); + try { + await step(`${width}x${height} touch: the add-channel dialog's buttons are at least ${MIN}x${MIN}, unclipped, hit at their centre with a centred label, and the dialog does not scroll sideways (#249)`, async () => { + await openAddChannelDialog(dlg.page); + for (const sel of DIALOG_BUTTONS) { + const p = await dialogPlacement(dlg.page, sel); + await assertTarget(dlg.page, sel, sel); + assert(Math.abs(p.dx) <= 1 && Math.abs(p.dy) <= 1, `${sel} label is off centre by ${p.dx.toFixed(1)}/${p.dy.toFixed(1)}px`); + assert(p.inside, sel + ' sticks out of the dialog'); + } + const r = await dlg.page.evaluate(() => { + const m = document.querySelector('#chAddChannelModal .ch-modal'); + return { dialog: m.scrollWidth - m.clientWidth, page: document.documentElement.scrollWidth - document.documentElement.clientWidth }; + }); + assert(r.dialog <= 0, 'the dialog scrolls horizontally by ' + r.dialog + 'px'); + assert(r.page <= 0, 'the page overflows horizontally by ' + r.page + 'px'); + }); + } finally { + await dlg.ctx.close(); + } + } + + const deskDlg = await openChannels(browser, errors, { viewport: { width: 1440, height: 900 } }); + try { + await step('desktop 1440x900: the add-channel dialog\'s buttons keep their 32 px height (#249 is touch-only)', async () => { + await openAddChannelDialog(deskDlg.page); + for (const sel of DIALOG_BUTTONS) { + const p = await dialogPlacement(deskDlg.page, sel); + assert(Math.abs(p.h - 32) < 0.5, `${sel} is ${p.h.toFixed(1)}px tall on desktop, expected 32`); + assert(p.inside, sel + ' sticks out of the dialog'); + } + }); + } finally { + await deskDlg.ctx.close(); + } + await step('no page errors, console errors or unhandled rejections', async () => { assert(errors.length === 0, errors.length + ' error(s):\n ' + errors.join('\n ')); }); diff --git a/test-touch-targets.js b/test-touch-targets.js index b1631ea8e..d33f4320e 100644 --- a/test-touch-targets.js +++ b/test-touch-targets.js @@ -121,6 +121,16 @@ const PAD_SELECTORS = [ ''], ]; +// #249: the add-channel dialog's primary buttons, in their real dialog rows +// (from channels.js) next to their 48 px inputs, with their real labels. +// Entry: [id, row classes, row markup before the button, label]. +const DIALOG_BUTTONS = [ + ['chGenerateBtn', 'ch-modal-row', '', 'Generate & Show QR'], + ['chPskAddBtn', 'ch-modal-row', '', 'Add'], + ['chHashtagBtn', 'ch-modal-row ch-hashtag-row', + '', 'Monitor'], +]; + function wrap(wrapper, html) { return wrapper ? wrapper.replace('{}', html) : html; } @@ -140,6 +150,10 @@ function buildSampleHtml() { }) .join('\n '); const pads = PAD_SELECTORS.map(([, html]) => html).join('\n '); + const dialog = ''; // .sort-help sample mirrors the markup the JS produces (post-fix): // tabindex="0" so :focus-within can fire on touch tap. @@ -153,6 +167,7 @@ function buildSampleHtml() { ${buttons} ${fields} ${pads} + ${dialog} ⓘ Tip body @@ -241,6 +256,27 @@ async function run() { `expected >=${DEFAULT_MIN}x${DEFAULT_MIN}, got ${dim.w}x${dim.h}`); } + // #249: each dialog button is at least 48 px tall, centres its label and + // stays inside the dialog. + for (const [id] of DIALOG_BUTTONS) { + const d = await page.$eval('#' + id, (el) => { + const b = el.getBoundingClientRect(); + const m = el.closest('.ch-modal').getBoundingClientRect(); + const range = document.createRange(); + range.selectNodeContents(el); + const t = range.getBoundingClientRect(); + return { + w: b.width, h: b.height, + dx: (t.left + t.width / 2) - (b.left + b.width / 2), + dy: (t.top + t.height / 2) - (b.top + b.height / 2), + inside: b.left >= m.left && b.right <= m.right, + }; + }); + record(`#${id} (add-channel dialog): rendered ${d.w.toFixed(1)}x${d.h.toFixed(1)}, label offset ${d.dx.toFixed(1)}/${d.dy.toFixed(1)}`, + d.w >= DEFAULT_MIN && d.h >= DEFAULT_MIN && Math.abs(d.dx) <= 1 && Math.abs(d.dy) <= 1 && d.inside, + `expected >=${DEFAULT_MIN}x${DEFAULT_MIN}, label centred within 1px and inside the dialog, got ${JSON.stringify(d)}`); + } + // #239 F2: the hit area keeps the text cursor it had as an inline style // before #235; .live-toggles label (cursor: pointer) must not win over it. const hitCursor = await page.$eval('.live-toggles .live-node-filter-hitarea', (el) => getComputedStyle(el).cursor); From e2ac1b5bbc5e3ed8890ce45c31973e9e2119d54b Mon Sep 17 00:00:00 2001 From: dborup Date: Mon, 5 Oct 2026 14:25:54 +0000 Subject: [PATCH 2/2] fix(css): 48 px touch targets for the add-channel dialog buttons (#249) #chGenerateBtn, #chPskAddBtn and #chHashtagBtn were 32 px tall next to their 48 px inputs. Under (pointer: coarse) they now get min-height: 48px, like the #630 coarse-pointer block. The labels stay centred, the rows stay 48 px tall, and desktop is unchanged. Co-Authored-By: Claude Opus 5.5 --- public/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/style.css b/public/style.css index ff1243935..683f47f4d 100644 --- a/public/style.css +++ b/public/style.css @@ -1737,6 +1737,13 @@ body.scroll-locked { overflow: hidden; } } } +/* #249: the add-channel dialog's primary buttons were 32 px tall next to + their 48 px inputs. Touch only, like the #630 coarse-pointer block; + desktop keeps them at 32 px. */ +@media (pointer: coarse) { + #chGenerateBtn, #chPskAddBtn, #chHashtagBtn { min-height: 48px; } +} + /* === Map Controls Panel === */ .map-controls {