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
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ jobs:
node test-issue-1518-home-url.js
node test-channel-decrypt-insecure-context.js
node test-live-region-filter.js
node test-issue-2052-touch-target-css.js
node test-issue-1136-observer-iata-map.js
node test-channel-qr.js
node test-channel-qr-wiring.js
Expand Down Expand Up @@ -478,6 +479,7 @@ jobs:
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1657-analytics-channels-group-sprites-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-2052-touch-target-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1329-map-controls-accordion-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1273-qr-overlay-height-e2e.js 2>&1 | tee -a e2e-output.txt
Expand Down
28 changes: 11 additions & 17 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,10 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
}

/* === Touch Targets === */
/* WCAG 2.5.5 / Apple HIG / Material: 48x48 CSS px minimum touch target for
all interactive controls. Targets are achieved with min-height/min-width
/* WCAG 2.5.5 and Apple HIG use a 44x44 CSS px target; Material uses 48x48.
Most controls in this section use the 48x48 house preference. Compact
.nav-btn and .ch-icon-btn controls retain 44x44 in their component rules.
Targets are achieved with min-height/min-width
plus inline-flex centering so existing visual styling (font-size, padding,
icon size) is preserved on desktop while the *hit area* grows for touch.
Issue #1060.
Expand Down Expand Up @@ -558,13 +560,6 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
touch-action: manipulation;
}

.nav-btn {
min-height: 48px;
min-width: 48px;
touch-action: manipulation;
}

.ch-icon-btn,
.ch-remove-btn,
.ch-share-btn {
min-height: 48px;
Expand Down Expand Up @@ -891,6 +886,7 @@ img.brand-logo {
background: none; border: 1px solid var(--border); color: var(--nav-text-muted); padding: 6px 12px;
border-radius: 6px; cursor: pointer; font-size: 14px; transition: all .15s;
min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
touch-action: manipulation;
}
.nav-btn:hover { background: var(--nav-bg2); color: var(--nav-text); }

Expand Down Expand Up @@ -1870,6 +1866,7 @@ button.ch-item.selected { background: var(--selected-bg); }
background: none; border: none; color: var(--text-muted);
cursor: pointer; padding: 4px; margin-left: 2px;
min-width: 44px; min-height: 44px; box-sizing: border-box;
touch-action: manipulation;
opacity: 0.55; transition: opacity 0.15s, color 0.15s;
line-height: 1; user-select: none;
}
Expand Down Expand Up @@ -2464,19 +2461,16 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
.ch-header-region .region-pill { padding: 2px 6px; font-size: 11px; }
.ch-header-region .region-dropdown-trigger { padding: 3px 6px; font-size: 11px; }

/* Channel list row: name must dominate. The inline share/remove icons
were eating >88px because of their 44x44 touch target — push them
onto a per-row reveal pattern: show a single ⋮ trigger that toggles
the action set. Until then, shrink them to icon-only and let the
name grow via flex:1. */
/* Channel list row: name must dominate. Below 768px channels.js renders
the flat .ch-row list (#1367), which has no inline share/remove
actions, so there is no size override for .ch-icon-btn here: wherever
those actions render they keep the 44x44 target from the base rule. */
#chList .ch-item { padding: 10px 12px; gap: 10px; }
#chList .ch-item-name { flex: 1 1 auto; min-width: 0; }
#chList .ch-item-top { gap: 6px; }
#chList .ch-icon-btn { min-width: 32px; min-height: 32px; padding: 2px 4px; }
/* #1648 M5: share-btn now renders an inline <svg.ph-icon> from channels.js,
* so the legacy `font-size: 0` + `::before { content: '<emoji>' }` shim is
* obsolete. The icon-button class above already sets the touch target;
* the SVG inherits color via currentColor. */
* obsolete. The SVG inherits color via currentColor. */
#chList .ch-share-btn .ph-icon { font-size: 13px; }
#chList .ch-share-btn { font-size: 0; }
#chList .ch-share-btn .ph-icon, #chList .ch-share-btn svg { font-size: 13px; }
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ node test-issue-1883-redirect-history.js
node test-issue-1890-og-url.js
node test-traces.js
node test-live-multibyte-filter.js
node test-issue-2052-touch-target-css.js

# #1418 — route-view v2 (Tufte) coverage
node test-issue-1418-raw-hex-extraction.js
Expand Down
86 changes: 86 additions & 0 deletions test-issue-2052-touch-target-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/* Issue #2052 — static touch-target policy for the compact controls.
*
* Supplement only: test-issue-2052-touch-target-e2e.js measures the rendered
* boxes in a browser and is the real guarantee.
*
* This test reads the shipped stylesheet and, for every rule anywhere in it
* (including inside @media blocks), looks at each selector whose last compound
* targets .nav-btn, .ch-icon-btn, .ch-share-btn or .ch-remove-btn. More
* specific selectors such as `#chList .ch-icon-btn` count too; an earlier
* version matched only the exact selector text and missed a 32px override.
* Every min-width/min-height declared for these controls must be at least
* 44px. Pseudo-class rules (:hover, :active, :focus, ...) are skipped because
* they do not set target dimensions. The base .nav-btn and .ch-icon-btn rules
* must also keep touch-action: manipulation.
*/
'use strict';

const assert = require('assert');
const fs = require('fs');
const path = require('path');

const css = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '');

const CONTROLS = ['.nav-btn', '.ch-icon-btn', '.ch-share-btn', '.ch-remove-btn'];

// The innermost { } blocks are the style rules, also inside @media.
function rules() {
const out = [];
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
let match;
while ((match = rulePattern.exec(css)) !== null) {
out.push({ selectors: match[1].split(',').map((s) => s.trim()), body: match[2] });
}
return out;
}

// The compact control a selector targets, or null. Only the last compound
// counts (`.ch-icon-btn .ph-icon` targets the icon, not the button).
function targetedControl(selector) {
const last = selector.split(/[\s>+~]+/).filter(Boolean).pop() || '';
if (/:/.test(last)) return null;
return CONTROLS.find((c) => new RegExp('\\' + c + '(?![\\w-])').test(last)) || null;
}

function declared(body, property) {
const values = [];
const pattern = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, 'g');
let m;
while ((m = pattern.exec(body)) !== null) values.push(m[1].trim());
return values;
}

const violations = [];
const seen = new Set();
let checked = 0;
for (const rule of rules()) {
for (const selector of rule.selectors) {
const control = targetedControl(selector);
if (!control) continue;
seen.add(control);
for (const property of ['min-width', 'min-height']) {
for (const value of declared(rule.body, property)) {
checked++;
const px = /^(\d+(?:\.\d+)?)px$/.exec(value);
if (!px || Number(px[1]) < 44) violations.push(`${selector} { ${property}: ${value} }`);
}
}
}
}

assert.deepStrictEqual(CONTROLS.filter((c) => !seen.has(c)), [], 'every compact control must have at least one rule');
assert(checked > 0, 'no min-width/min-height declarations found for the compact controls');
assert.deepStrictEqual(violations, [], 'compact controls must not declare a touch target below 44px:\n ' + violations.join('\n '));
console.log(`PASS ${checked} min-width/min-height declarations for ${CONTROLS.join(', ')} are all >= 44px`);

for (const base of ['.nav-btn', '.ch-icon-btn']) {
const values = rules()
.filter((r) => r.selectors.includes(base))
.flatMap((r) => declared(r.body, 'touch-action'));
assert.deepStrictEqual([...new Set(values)], ['manipulation'], `${base} must keep touch-action: manipulation; got ${JSON.stringify(values)}`);
console.log(`PASS ${base}: touch-action manipulation`);
}

console.log('test-issue-2052-touch-target-css.js: OK');
Loading
Loading