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 { 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);