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 @@ -469,6 +469,8 @@ jobs:
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
# Standalone harness (no server): loads public/style.css into Chromium.
CHROMIUM_REQUIRE=1 node test-touch-targets.js 2>&1 | tee -a e2e-output.txt
# Starts its own ingestor + server on a temp copy of the fixture.
CHROMIUM_REQUIRE=1 CORESCOPE_SERVER_BIN=./corescope-server CORESCOPE_INGESTOR_BIN=./corescope-ingestor FIXTURE_DB=test-fixtures/e2e-fixture.db node test-channel-proposals-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
Expand Down
29 changes: 16 additions & 13 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,8 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
}

/* === Touch Targets === */
/* 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
/* House preference: 48x48 CSS px minimum for the controls below (#2052),
above WCAG 2.5.5's 44x44 minimum. Targets use 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 @@ -560,6 +558,13 @@ 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 @@ -603,8 +608,7 @@ button.ch-item {
}

/* Additional button-like controls discovered during PR #1067 review (Issue
#1060 follow-up). Same 48x48 minimums + touch-action so all interactive
surfaces meet WCAG 2.5.5. */
#1060 follow-up). Same 48x48 house minimums + touch-action. */
.btn-link,
.col-toggle-btn,
.filter-toggle-btn,
Expand Down Expand Up @@ -885,8 +889,7 @@ img.brand-logo {
.nav-btn {
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;
display: inline-flex; align-items: center; justify-content: center;
}
.nav-btn:hover { background: var(--nav-bg2); color: var(--nav-text); }

Expand Down Expand Up @@ -1844,6 +1847,8 @@ button.ch-item.selected { background: var(--selected-bg); }
}
.ch-item-body { flex: 1; min-width: 0; }
.ch-item-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; }
/* Keep both 48px actions inside narrow user-channel rows (#2052). */
.ch-item.ch-user-added .ch-item-top { flex-wrap: wrap; }
.ch-item-name { font-weight: 600; font-size: 14px; }
.ch-item-time { font-size: 12px; font-weight: var(--fw-medium); color: var(--text-muted); white-space: nowrap; } /* #1668 M3 — was 11px/400 */
.ch-unread-badge {
Expand All @@ -1860,13 +1865,12 @@ button.ch-item.selected { background: var(--selected-bg); }
line-height: 1.4;
}
/* Shared icon button base for sidebar row controls (remove ✕, share ⤴). EMOJI-OK-COMMENT
WCAG 2.5.5 / Apple HIG: 44x44 CSS px minimum touch target. */
Inherits the shared 48x48 house minimum above. */
.ch-icon-btn {
display: inline-flex; align-items: center; justify-content: center;
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;
box-sizing: border-box;
opacity: 0.55; transition: opacity 0.15s, color 0.15s;
line-height: 1; user-select: none;
}
Expand Down Expand Up @@ -2476,7 +2480,7 @@ button.ch-item:hover .ch-icon-btn { opacity: 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. */
those actions render they keep the shared 48x48 house minimum. */
#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; }
Expand Down Expand Up @@ -4457,7 +4461,6 @@ button.region-pill-active:hover { opacity: 0.85; color: var(--text-on-accent); }
.tab-btn,
.filter-bar input,
.filter-bar select,
.nav-btn,
.region-pill,
.region-dropdown-trigger,
.multi-select-trigger,
Expand Down
58 changes: 58 additions & 0 deletions test-channel-fluid-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,64 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
}
});

// #2052: exercise the actual controls in both pointer modes, not just
// standalone CSS samples. Mobile channels use rows without inline icons;
// the packets navbar supplies the visible .nav-btn mirrors on phones.
for (const width of [375, 390, 768, 1280]) {
await step(`viewport ${width}: 48px controls fit and respond to clicks`, async () => {
const mobile = width < 768;
const touchContext = await browser.newContext({
viewport: { width, height: 900 }, hasTouch: mobile, isMobile: mobile,
});
const target = await touchContext.newPage();
try {
await target.addInitScript(() => localStorage.setItem('corescope_channel_keys',
JSON.stringify({ Touch2052: '00112233445566778899aabbccddeeff' })));
await target.goto(BASE + (mobile ? '/#/packets' : '/#/channels'),
{ waitUntil: 'domcontentloaded' });
const action = mobile ? '.filter-toggle-btn-mirror' : '[data-share-channel]';
await target.waitForSelector(action);
await target.evaluate(() => document.fonts.ready);
// Mobile page-actions rebuilds the mirror while packets initializes.
// Query and measure in one browser turn so a detached selector snapshot
// cannot look like missing controls. Bad dimensions still fail below.
const controlsHandle = await target.waitForFunction(({ mobile, action }) => {
if (mobile && !document.querySelector('#pktLeft[data-loaded="true"] #pktPauseBtn')) return false;
const els = Array.from(document.querySelectorAll('.top-nav .nav-btn, #chList .ch-icon-btn'))
.filter(el => el.getClientRects().length && getComputedStyle(el).visibility !== 'hidden');
if (!els.length || !els.some(el => el.matches(action))) return false;
return els.map(el => {
const r = el.getBoundingClientRect();
const container = el.closest('.top-nav, .ch-item').getBoundingClientRect();
return { name: el.id || el.getAttribute('aria-label'), w: r.width, h: r.height,
fits: r.left >= container.left - 1 && r.right <= container.right + 1
&& r.top >= container.top - 1 && r.bottom <= container.bottom + 1 };
});
}, { mobile, action }, { timeout: 8000 });
const controls = await controlsHandle.jsonValue();
await controlsHandle.dispose();
assert(controls.length > 0, 'expected visible navbar or channel controls');
for (const control of controls) {
assert(control.w >= 48 && control.h >= 48,
`${control.name}: expected >=48x48, got ${control.w}x${control.h}`);
assert(control.fits, `${control.name}: clipped outside its navbar/channel row`);
}
assert(await target.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1),
'controls caused horizontal overflow');
await target.locator(action).click();
if (mobile) {
await target.waitForSelector('.filter-bar.filters-expanded');
} else {
await target.waitForSelector('#chShareModal:not(.hidden)');
assert((await target.inputValue('#chShareKey')) === '00112233445566778899aabbccddeeff',
'Share must open the selected channel key');
}
} finally {
await touchContext.close();
}
});
}

console.log(`\n${passed} passed, ${failed} failed`);
await browser.close();
process.exit(failed ? 1 : 0);
Expand Down
15 changes: 8 additions & 7 deletions test-channel-ux-followup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Follow-up UX fixes to #1037 channel modal/sidebar redesign:
*
* 1. ✕ remove button must hit a 44×44px touch target (WCAG 2.5.5).
* 1. ✕ remove button must hit a 48×48px house touch target (#2052).
* 2. Channel rows must NOT display "0 messages" — when no messages
* have been decrypted yet, omit the count entirely.
* 3. Modal footer wording: keys removed via ✕ button, not by
Expand All @@ -25,12 +25,13 @@ function assert(cond, msg) {
else { failed++; console.error(' ✗ ' + msg); }
}

console.log('\n=== Fix 1: ✕ touch target ≥ 44×44px (on shared .ch-icon-btn base) ===');
const iconBtnRule = (cssSrc.match(/\.ch-icon-btn\s*\{[^}]*\}/) || [''])[0];
assert(/min-width:\s*44px/.test(iconBtnRule),
'.ch-icon-btn declares min-width: 44px');
assert(/min-height:\s*44px/.test(iconBtnRule),
'.ch-icon-btn declares min-height: 44px');
console.log('\n=== Fix 1: shared touch-target sizing owns channel icon minimums ===');
// Computed 48x48 hit areas are asserted by test-touch-targets.js. Keep the
// component rules from silently overriding that shared policy again (#2052).
const iconBtnRules = cssSrc.match(/(?:^|\n)[^{}]*\.ch-icon-btn\s*\{[^}]*\}/g) || [];
assert(iconBtnRules.length > 0, '.ch-icon-btn component rules exist');
assert(iconBtnRules.every(rule => !/min-(?:width|height)\s*:/.test(rule)),
'.ch-icon-btn component rules inherit minimum sizes from the shared touch-target group');

console.log('\n=== Fix 2: no "0 messages" in default row ===');
// renderChannelRow must not emit a literal "0 messages" preview when
Expand Down
20 changes: 14 additions & 6 deletions test-issue-2052-touch-target-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
* 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.
* MIN (48px, the house preference; upstream PR 2078 settled #2052 on 48), and
* each control must declare both somewhere, so dropping the 48px group rule
* cannot pass silently. Pseudo-class rules (:hover, :active, :focus, ...) are
* skipped because they do not set target dimensions. Every rule naming
* .nav-btn or .ch-icon-btn that sets touch-action must use manipulation, and
* at least one must set it.
*/
'use strict';

Expand All @@ -24,6 +27,7 @@ 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'];
const MIN = 48;

// The innermost { } blocks are the style rules, also inside @media.
function rules() {
Expand Down Expand Up @@ -54,6 +58,7 @@ function declared(body, property) {

const violations = [];
const seen = new Set();
const sized = new Set();
let checked = 0;
for (const rule of rules()) {
for (const selector of rule.selectors) {
Expand All @@ -63,17 +68,20 @@ for (const rule of rules()) {
for (const property of ['min-width', 'min-height']) {
for (const value of declared(rule.body, property)) {
checked++;
sized.add(control + ' ' + property);
const px = /^(\d+(?:\.\d+)?)px$/.exec(value);
if (!px || Number(px[1]) < 44) violations.push(`${selector} { ${property}: ${value} }`);
if (!px || Number(px[1]) < MIN) 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`);
const unsized = CONTROLS.flatMap((c) => ['min-width', 'min-height'].map((p) => c + ' ' + p)).filter((k) => !sized.has(k));
assert.deepStrictEqual(unsized, [], 'every compact control must declare min-width and min-height');
assert.deepStrictEqual(violations, [], `compact controls must not declare a touch target below ${MIN}px:\n ` + violations.join('\n '));
console.log(`PASS ${checked} min-width/min-height declarations for ${CONTROLS.join(', ')} are all >= ${MIN}px`);

for (const base of ['.nav-btn', '.ch-icon-btn']) {
const values = rules()
Expand Down
Loading
Loading