Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
71 changes: 71 additions & 0 deletions test-issue-2052-touch-target-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 '));
});
Expand Down
36 changes: 36 additions & 0 deletions test-touch-targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ const PAD_SELECTORS = [
'<button class="region-dropdown-trigger" data-pad="live-region">x</button></div></div>'],
];

// #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', '<input type="text" class="ch-modal-input">', 'Generate &amp; Show QR'],
['chPskAddBtn', 'ch-modal-row', '<input type="text" class="ch-modal-input">', 'Add'],
['chHashtagBtn', 'ch-modal-row ch-hashtag-row',
'<span class="ch-hashtag-prefix" aria-hidden="true">#</span><input type="text" class="ch-modal-input">', 'Monitor'],
];

function wrap(wrapper, html) {
return wrapper ? wrapper.replace('{}', html) : html;
}
Expand All @@ -140,6 +150,10 @@ function buildSampleHtml() {
})
.join('\n ');
const pads = PAD_SELECTORS.map(([, html]) => html).join('\n ');
const dialog = '<div class="modal ch-modal" role="document">' + DIALOG_BUTTONS
.map(([id, rowCls, before, label]) => `<section class="ch-modal-section"><div class="${rowCls}">${before}` +
`<button type="button" id="${id}" class="btn-primary">${label}</button></div></section>`)
.join('') + '</div>';

// .sort-help sample mirrors the markup the JS produces (post-fix):
// tabindex="0" so :focus-within can fire on touch tap.
Expand All @@ -153,6 +167,7 @@ function buildSampleHtml() {
${buttons}
${fields}
${pads}
${dialog}
<span class="sort-help" id="sortHelp" tabindex="0" role="button" aria-label="Sort help">ⓘ
<span class="sort-help-tip">Tip body</span>
</span>
Expand Down Expand Up @@ -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);
Expand Down
Loading