feat: add flyout menu to sidebar session items
- Add data-session-key attribute to buildSidebarHTML article element so
openFlyoutMenu() can read session info via closest('[data-session-key]')
- Add tile-options-btn button (with aria-label and aria-haspopup) inside
sidebar-item-header — reuses the same class as tile version so the
existing delegated document click handler picks it up automatically
- Guard renderSidebar click handler against .tile-options-btn clicks so
clicking ⋮ doesn't also trigger openSession()
- Add .sidebar-item-header .tile-options-btn CSS: compact 20×20px, subtle
opacity-based visibility to fit the narrower sidebar layout
- Tests: buildSidebarHTML data-session-key, tile-options-btn presence/aria,
renderSidebar click guard, CSS sidebar btn styling
This commit is contained in:
@@ -596,10 +596,11 @@ function buildSidebarHTML(session, currentSession) {
|
||||
// which is not a registered remote_instance, so routing through federation would 404.
|
||||
var _sidebarEffRemoteId = session.remoteId != null ? String(session.remoteId) : '';
|
||||
return (
|
||||
`<article class="${classes}" data-session="${escapedName}" data-remote-id="${escapeHtml(_sidebarEffRemoteId)}" tabindex="0" role="listitem">` +
|
||||
`<article class="${classes}" data-session="${escapedName}" data-session-key="${escapeHtml(session.sessionKey || name)}" data-remote-id="${escapeHtml(_sidebarEffRemoteId)}" tabindex="0" role="listitem">` +
|
||||
`<div class="sidebar-item-header">` +
|
||||
`<span class="sidebar-item-name">${escapedName}</span>` +
|
||||
badgeHtml +
|
||||
`<button class="tile-options-btn" data-session="${escapedName}" aria-label="Session options" aria-haspopup="true">⋮</button>` +
|
||||
`</div>` +
|
||||
`<div class="sidebar-item-body"><pre>${ansiToHtml(lastLines)}</pre></div>` +
|
||||
`</article>`
|
||||
@@ -738,6 +739,7 @@ function renderSidebar(sessions, currentSession) {
|
||||
const name = item.dataset.session;
|
||||
const remoteId = item.dataset.remoteId || '';
|
||||
on(item, 'click', (e) => {
|
||||
if (e.target.closest && e.target.closest('.tile-options-btn')) return;
|
||||
if (name !== currentSession) openSession(name, { remoteId });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -658,6 +658,24 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-item-header .tile-options-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sidebar-item:hover .sidebar-item-header .tile-options-btn,
|
||||
.sidebar-item-header .tile-options-btn:hover,
|
||||
.sidebar-item-header .tile-options-btn:focus-visible {
|
||||
opacity: 1;
|
||||
background: var(--bg-surface);
|
||||
border-color: var(--border-subtle);
|
||||
}
|
||||
|
||||
.sidebar-empty {
|
||||
padding: 16px 12px;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -1717,6 +1717,39 @@ test('buildSidebarHTML includes snapshot preview in a pre element', () => {
|
||||
assert.ok(/<pre>[\s\S]*line1[\s\S]*<\/pre>/.test(html), 'snapshot content should be inside a <pre> element');
|
||||
});
|
||||
|
||||
// --- buildSidebarHTML flyout menu button ---
|
||||
|
||||
test('buildSidebarHTML article element has data-session-key attribute', () => {
|
||||
const session = { name: 'my-session', sessionKey: 'key::my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||
const html = app.buildSidebarHTML(session, '');
|
||||
assert.ok(html.includes('data-session-key='), 'article must have data-session-key attribute');
|
||||
assert.ok(html.includes('data-session-key="key::my-session"'), 'data-session-key must use sessionKey value');
|
||||
});
|
||||
|
||||
test('buildSidebarHTML article element uses session name as data-session-key when sessionKey is absent', () => {
|
||||
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||
const html = app.buildSidebarHTML(session, '');
|
||||
assert.ok(html.includes('data-session-key="my-session"'), 'data-session-key must fall back to session.name when sessionKey absent');
|
||||
});
|
||||
|
||||
test('buildSidebarHTML header contains a tile-options-btn button', () => {
|
||||
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||
const html = app.buildSidebarHTML(session, '');
|
||||
assert.ok(html.includes('tile-options-btn'), 'sidebar item header must include tile-options-btn button');
|
||||
});
|
||||
|
||||
test('buildSidebarHTML tile-options-btn has aria-label="Session options"', () => {
|
||||
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||
const html = app.buildSidebarHTML(session, '');
|
||||
assert.ok(html.includes('aria-label="Session options"'), 'tile-options-btn must have aria-label="Session options"');
|
||||
});
|
||||
|
||||
test('buildSidebarHTML tile-options-btn has aria-haspopup="true"', () => {
|
||||
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||
const html = app.buildSidebarHTML(session, '');
|
||||
assert.ok(html.includes('aria-haspopup="true"'), 'tile-options-btn must have aria-haspopup="true"');
|
||||
});
|
||||
|
||||
// --- renderSidebar ---
|
||||
|
||||
test('renderSidebar populates sidebar-list innerHTML when viewMode is fullscreen', () => {
|
||||
|
||||
@@ -2362,3 +2362,22 @@ def test_device_badge_height_matches_flyout_button() -> None:
|
||||
".device-badge must have increased height to match the ~24px ⋮ button — "
|
||||
"add min-height or increase padding/line-height so the badge is ~22-24px tall"
|
||||
)
|
||||
|
||||
|
||||
# ── Sidebar flyout menu (feat: add flyout menu to sidebar session items) ──────
|
||||
|
||||
|
||||
def test_css_sidebar_options_btn_styled() -> None:
|
||||
"""CSS must contain a rule targeting .tile-options-btn inside .sidebar-item-header or .sidebar-item."""
|
||||
css = read_css()
|
||||
# Look for a CSS rule that targets the options btn specifically in the sidebar context
|
||||
has_sidebar_btn_style = (
|
||||
".sidebar-item-header .tile-options-btn" in css
|
||||
or ".sidebar-item .tile-options-btn" in css
|
||||
or ".sidebar-item-header > .tile-options-btn" in css
|
||||
)
|
||||
assert has_sidebar_btn_style, (
|
||||
"CSS must include a rule targeting .tile-options-btn inside .sidebar-item-header "
|
||||
"or .sidebar-item to ensure the button fits the sidebar layout "
|
||||
"(smaller and compact for the narrower sidebar width)"
|
||||
)
|
||||
|
||||
@@ -4632,3 +4632,82 @@ def test_domcontentloaded_calls_render_view_dropdown_after_restore() -> None:
|
||||
"DOMContentLoaded must call renderViewDropdown() after restoreState() so the "
|
||||
"dropdown label correctly reflects _activeView on page reload"
|
||||
)
|
||||
|
||||
|
||||
# ── Sidebar flyout menu (feat: add flyout menu to sidebar session items) ──────
|
||||
|
||||
|
||||
def test_build_sidebar_html_article_has_data_session_key() -> None:
|
||||
"""buildSidebarHTML must include data-session-key on the article element."""
|
||||
match = re.search(
|
||||
r"function buildSidebarHTML\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildSidebarHTML function not found"
|
||||
body = match.group(1)
|
||||
assert "data-session-key" in body, (
|
||||
"buildSidebarHTML must include data-session-key attribute on the article element "
|
||||
"so openFlyoutMenu can look up session data via closest('[data-session-key]')"
|
||||
)
|
||||
|
||||
|
||||
def test_build_sidebar_html_has_tile_options_btn() -> None:
|
||||
"""buildSidebarHTML must include a .tile-options-btn button in the header."""
|
||||
match = re.search(
|
||||
r"function buildSidebarHTML\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildSidebarHTML function not found"
|
||||
body = match.group(1)
|
||||
assert "tile-options-btn" in body, (
|
||||
"buildSidebarHTML must include a button with class 'tile-options-btn' "
|
||||
"in the sidebar item header so users can access the flyout menu"
|
||||
)
|
||||
|
||||
|
||||
def test_build_sidebar_html_options_btn_has_aria_label() -> None:
|
||||
"""buildSidebarHTML tile-options-btn must have aria-label='Session options'."""
|
||||
match = re.search(
|
||||
r"function buildSidebarHTML\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildSidebarHTML function not found"
|
||||
body = match.group(1)
|
||||
assert 'aria-label="Session options"' in body or "aria-label='Session options'" in body, (
|
||||
"buildSidebarHTML tile-options-btn must have aria-label='Session options' "
|
||||
"for accessibility (same as tile version)"
|
||||
)
|
||||
|
||||
|
||||
def test_build_sidebar_html_options_btn_has_aria_haspopup() -> None:
|
||||
"""buildSidebarHTML tile-options-btn must have aria-haspopup='true'."""
|
||||
match = re.search(
|
||||
r"function buildSidebarHTML\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildSidebarHTML function not found"
|
||||
body = match.group(1)
|
||||
assert 'aria-haspopup="true"' in body or "aria-haspopup='true'" in body, (
|
||||
"buildSidebarHTML tile-options-btn must have aria-haspopup='true' "
|
||||
"for accessibility (same as tile version)"
|
||||
)
|
||||
|
||||
|
||||
def test_render_sidebar_click_handler_guards_tile_options_btn() -> None:
|
||||
"""renderSidebar click handler must guard against tile-options-btn clicks."""
|
||||
match = re.search(
|
||||
r"function renderSidebar\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "renderSidebar function not found"
|
||||
body = match.group(1)
|
||||
assert "tile-options-btn" in body, (
|
||||
"renderSidebar click handler must guard against .tile-options-btn clicks "
|
||||
"so clicking ⋮ doesn't also trigger openSession() — "
|
||||
"use: if (e.target.closest('.tile-options-btn')) return;"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user