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
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function createConversationTray(workspace) {
tabs.scrollLeft = position.left;
}
if (toggle) {
const label = collapsed ? 'Keep conversation list open' : 'Collapse conversation list';
const label = collapsed ? (peek ? 'Keep conversation list open' : 'Open conversation list') : 'Collapse conversation list';
toggle.setAttribute('aria-label', label);
toggle.title = label;
toggle.setAttribute('aria-expanded', String(!collapsed || peek));
Expand Down Expand Up @@ -104,10 +104,16 @@ export function createConversationTray(workspace) {
}
});
listen(document, 'keydown', (event) => {
if (event.key === 'Escape' && peek && !isMenuOpen()) {
if (event.key === 'Escape' && isVertical() && !isMenuOpen()
&& (peek || (!preferences.collapsed && navigation?.contains(event.target)))) {
event.preventDefault();
clearTimers();
peek = false;
render();
if (peek) {
peek = false;
render();
} else {
setCollapsed(true);
}
toggle?.focus({ preventScroll: true });
}
});
Expand Down
13 changes: 3 additions & 10 deletions src/frontend/web/von_interface/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -8695,7 +8695,7 @@ body[data-active-tab="settingsTab"] .tab-content-area {
transition: grid-template-columns 180ms ease;
}

.conversation-workspace[data-effective-tabs-layout="vertical"][data-tray-collapsed="true"] {
.conversation-workspace[data-effective-tabs-layout="vertical"][data-tray-collapsed="true"][data-tray-peek="false"] {
grid-template-columns: 48px minmax(0, 1fr);
}

Expand Down Expand Up @@ -8730,14 +8730,8 @@ body[data-active-tab="settingsTab"] .tab-content-area {
}

.conversation-workspace[data-effective-tabs-layout="vertical"][data-tray-peek="true"] .chat-session-tabs-row {
width: var(--conversation-tray-width, 260px);
box-shadow: 8px 8px 24px rgba(15, 23, 42, 0.16);
animation: conversationTrayPeek 160ms ease-out;
}

@keyframes conversationTrayPeek {
from { clip-path: inset(0 calc(100% - 48px) 0 0); }
to { clip-path: inset(0 0 0 0); }
/* Use the animated grid track so revealing navigation never covers content. */
width: 100%;
}

.conversation-workspace[data-effective-tabs-layout="vertical"] .conversation-tray-header {
Expand Down Expand Up @@ -8883,7 +8877,6 @@ body[data-active-tab="settingsTab"] .tab-content-area {
}
@media (prefers-reduced-motion: reduce) {
.conversation-workspace[data-effective-tabs-layout="vertical"], .conversation-tray-toggle span { transition: none; }
.conversation-workspace[data-tray-peek="true"] .chat-session-tabs-row { animation: none; }
}

.chat-situation-toggle {
Expand Down
44 changes: 43 additions & 1 deletion tests/browser/responsiveShell.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Run: node tests/browser/responsiveShell.cjs [evidence-directory]
// Run: node tests/browser/responsiveShell.cjs [evidence-directory] [--tray-only]
// Production templates, CSS, layout and tray controller with synthetic content.
// This checks rendering and draft retention, not authentication or live sending.
const assert = require('node:assert/strict');
Expand All @@ -7,6 +7,7 @@ const http = require('node:http');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const baseline = process.env.VON_LAYOUT_BASELINE;
const trayOnly = process.argv.includes('--tray-only');
const { chromium, expect } = require('@playwright/test');
const root = path.resolve(__dirname, '../../src/frontend/web/von_interface');
const evidence = process.argv[2];
Expand Down Expand Up @@ -98,6 +99,47 @@ async function noOverflow(page) {
assert(menu.x >= 0 && menu.x + menu.width <= profile.width + 1 && menu.y >= 0, 'organisation menu fits');
await page.locator('.footer-org-menu-trigger').click();
if (evidence) await page.screenshot({ animations: 'disabled', path: path.join(evidence, `${profile.name}.png`), fullPage: false });
if (profile.name === 'desktop') {
const toggle = page.locator('#conversationTrayToggle');
const selected = page.locator('#chatSessionTabs [role="tab"]').first();
await selected.evaluate(el => {
el.setAttribute('aria-selected', 'true');
el.classList.add('has-unread');
el.dataset.unreadCount = '3';
});
const retained = await page.evaluateHandle(() => document.querySelector('#scrollableField').firstChild);
await toggle.focus();
await page.keyboard.press('Enter');
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
await expect(toggle).toHaveAccessibleName('Open conversation list');
await page.keyboard.press('Space');
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
await selected.focus();
await page.keyboard.press('Escape');
await expect(toggle).toBeFocused();
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
// Hover uses the same grid track, never an overlay on the transcript.
await toggle.hover();
await expect(page.locator('#conversationWorkspace')).toHaveAttribute('data-tray-peek', 'true');
await page.waitForTimeout(220);
const trayBox = await page.locator('.chat-session-tabs-row').boundingBox();
const mainBox = await page.locator('.chat-conversation-main').boundingBox();
assert(trayBox.x + trayBox.width <= mainBox.x, 'expanded tray does not cover conversation');
await page.keyboard.press('Escape');
await page.keyboard.press('Enter');
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
await expect(selected).toHaveAttribute('aria-selected', 'true');
await expect(selected).toHaveAttribute('data-unread-count', '3');
assert(await retained.evaluate(el => el === document.querySelector('#scrollableField').firstChild), 'transcript retained');
await expect(input).toHaveValue('Draft survives resizing and folding.');
await noOverflow(page);
if (evidence) await page.screenshot({ path: path.join(evidence, 'desktop-tray-reopened.png') });
}
if (trayOnly) {
console.log(JSON.stringify({ profile: profile.name, passed: true, scope: 'conversation tray', source: 'synthetic production-template fixture' }));
await page.close();
continue;
}
if (profile.name === 'pixel-8') {
await page.setViewportSize({ width: 412, height: 430 });
await input.focus();
Expand Down
22 changes: 22 additions & 0 deletions tests/frontend/conversationTray.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,25 @@ test('keyboard resize works and horizontal fallback does not erase desktop colla
controller.refresh(preferences({ collapsed: true }));
expect(workspace.dataset.trayCollapsed).toBe('true');
});

test('Escape closes a docked tray from navigation and retains selection and unread state', () => {
tabs.firstChild.setAttribute('data-unread-count', '3');
tabs.firstChild.focus();
tabs.firstChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(workspace.dataset.trayCollapsed).toBe('true');
expect(document.activeElement).toBe(toggle);
expect(toggle.getAttribute('aria-label')).toBe('Open conversation list');
expect(toggle.getAttribute('aria-expanded')).toBe('false');
toggle.click();
expect(toggle.getAttribute('aria-expanded')).toBe('true');
expect(tabs.firstChild.getAttribute('aria-selected')).toBe('true');
expect(tabs.firstChild.getAttribute('data-unread-count')).toBe('3');
});

test('Escape in the conversation does not collapse a docked tray', () => {
const draft = document.getElementById('promptInput');
draft.focus();
draft.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(workspace.dataset.trayCollapsed).toBe('false');
expect(document.activeElement).toBe(draft);
});
Loading