Skip to content

Commit 682b449

Browse files
Add Quick Actions toolbar with keyboard shortcuts to Monitor page (#269)
* Initial plan * Add QuickActions component and integrate with Monitor page Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Add comprehensive tests for QuickActions component Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Add E2E tests for QuickActions component (manual verification pending) Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Address PR review feedback: fix grid notification, extract bookmark storage, improve shortcuts API, update icon imports, increase button size Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Address second round of PR feedback: implement keyboard shortcuts, improve UX, consolidate bookmark storage Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Fix keyboard shortcut conflict: remove 'R' for recording, use documented Ctrl/Cmd+S Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Fix E2E test selectors to match updated aria-labels without (R) shortcut Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Add clarifying comments for duplicate checking and recording state placeholder Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Optimize performance: use refs to avoid event listener churn, fix E2E test anti-patterns Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * Further optimize: use ref pattern for grid state to eliminate final event listener churn Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com>
1 parent 9117cc6 commit 682b449

File tree

12 files changed

+961
-14
lines changed

12 files changed

+961
-14
lines changed

e2e/quickactions.spec.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* E2E test for QuickActions component on Monitor page
3+
*/
4+
5+
import { test, expect } from "@playwright/test";
6+
7+
test.describe("QuickActions on Monitor Page", () => {
8+
test("should display quick actions toolbar", async ({ page }) => {
9+
await page.goto("/monitor");
10+
11+
// Wait for the page to load
12+
await page.waitForSelector('[aria-label="Quick actions toolbar"]');
13+
14+
// Take screenshot of the Monitor page with Quick Actions
15+
await page.screenshot({
16+
path: "test-results/quick-actions-monitor-page.png",
17+
fullPage: true,
18+
});
19+
20+
// Check that Quick Actions toolbar is visible
21+
const quickActions = page.locator('[aria-label="Quick actions toolbar"]');
22+
await expect(quickActions).toBeVisible();
23+
24+
// Check that all 4 buttons are present
25+
const bookmarkButton = page.locator(
26+
'button[aria-label="Bookmark current frequency (B)"]',
27+
);
28+
const recordButton = page.locator('button[aria-label*="recording"]');
29+
const gridButton = page.locator(
30+
'button[aria-label*="grid"][aria-label*="(G)"]',
31+
);
32+
const helpButton = page.locator(
33+
'button[aria-label="Show keyboard shortcuts (?)"]',
34+
);
35+
36+
await expect(bookmarkButton).toBeVisible();
37+
await expect(recordButton).toBeVisible();
38+
await expect(gridButton).toBeVisible();
39+
await expect(helpButton).toBeVisible();
40+
});
41+
42+
test("should show tooltip on hover", async ({ page }) => {
43+
await page.goto("/monitor");
44+
45+
// Wait for Quick Actions to be visible
46+
await page.waitForSelector('[aria-label="Quick actions toolbar"]');
47+
48+
// Hover over bookmark button
49+
const bookmarkButton = page.locator(
50+
'button[aria-label="Bookmark current frequency (B)"]',
51+
);
52+
await bookmarkButton.hover();
53+
54+
// Check for tooltip
55+
const tooltip = page.locator('[role="tooltip"]');
56+
await expect(tooltip).toBeVisible();
57+
await expect(tooltip).toHaveText("Bookmark (B)");
58+
59+
// Take screenshot with tooltip
60+
await page.screenshot({
61+
path: "test-results/quick-actions-tooltip.png",
62+
});
63+
});
64+
65+
test("should toggle recording state", async ({ page }) => {
66+
await page.goto("/monitor");
67+
68+
// Wait for Quick Actions to be visible
69+
await page.waitForSelector('[aria-label="Quick actions toolbar"]');
70+
71+
const recordButton = page.locator('button[aria-label*="recording"]');
72+
73+
// Check initial state
74+
await expect(recordButton).toHaveAttribute("aria-pressed", "false");
75+
76+
// Click to start recording
77+
await recordButton.click();
78+
79+
// Wait for aria-pressed to become true
80+
await expect(recordButton).toHaveAttribute("aria-pressed", "true", {
81+
timeout: 1000,
82+
});
83+
84+
// Check that the button has the recording class
85+
await expect(recordButton).toHaveClass(/recording/);
86+
87+
// Take screenshot of recording state
88+
await page.screenshot({
89+
path: "test-results/quick-actions-recording-active.png",
90+
});
91+
});
92+
93+
test("should toggle grid visibility", async ({ page }) => {
94+
await page.goto("/monitor");
95+
96+
// Wait for Quick Actions to be visible
97+
await page.waitForSelector('[aria-label="Quick actions toolbar"]');
98+
99+
const gridButton = page.locator(
100+
'button[aria-label*="grid"][aria-label*="(G)"]',
101+
);
102+
103+
// Get initial aria-pressed value
104+
const initialPressed = await gridButton.getAttribute("aria-pressed");
105+
106+
// Click to toggle grid
107+
await gridButton.click();
108+
109+
// Wait for aria-pressed to toggle
110+
const toggledPressed = initialPressed === "true" ? "false" : "true";
111+
await expect(gridButton).toHaveAttribute("aria-pressed", toggledPressed, {
112+
timeout: 1000,
113+
});
114+
115+
// Take screenshot
116+
await page.screenshot({
117+
path: "test-results/quick-actions-grid-toggled.png",
118+
});
119+
});
120+
});

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"webpack-dev-server": "^5.2.1"
101101
},
102102
"dependencies": {
103+
"@phosphor-icons/react": "^2.1.10",
103104
"react": "^19.1.0",
104105
"react-dom": "^19.1.0",
105106
"react-router-dom": "^7.9.4",
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Quick Actions Bar Component
3+
*
4+
* Provides quick access to common Monitor page actions via icon buttons.
5+
* Implements UI-DESIGN-SPEC.md Section 4 requirements.
6+
*
7+
* Features:
8+
* - Bookmark current frequency
9+
* - Start/stop recording
10+
* - Toggle grid overlay
11+
* - Show keyboard shortcuts help
12+
*
13+
* Accessibility:
14+
* - ARIA labels for all buttons
15+
* - Tooltips showing keyboard shortcuts
16+
* - Keyboard navigation support
17+
* - Visual state indicators (e.g., recording active)
18+
*/
19+
20+
import {
21+
BookmarkSimpleIcon,
22+
GridFourIcon,
23+
QuestionIcon,
24+
RecordIcon,
25+
} from "@phosphor-icons/react";
26+
import { useState } from "react";
27+
28+
export interface QuickActionsProps {
29+
/** Current frequency in Hz */
30+
currentFrequencyHz: number;
31+
/** Whether recording is currently active */
32+
isRecording: boolean;
33+
/** Whether grid overlay is currently visible */
34+
showGrid: boolean;
35+
/** Callback to add current frequency to bookmarks */
36+
onBookmark: (frequencyHz: number) => void;
37+
/** Callback to toggle recording */
38+
onToggleRecording: () => void;
39+
/** Callback to toggle grid overlay */
40+
onToggleGrid: () => void;
41+
/** Callback to show help/shortcuts overlay */
42+
onShowHelp: () => void;
43+
}
44+
45+
/**
46+
* Quick actions toolbar for Monitor page
47+
*/
48+
function QuickActions({
49+
currentFrequencyHz,
50+
isRecording,
51+
showGrid,
52+
onBookmark,
53+
onToggleRecording,
54+
onToggleGrid,
55+
onShowHelp,
56+
}: QuickActionsProps): React.JSX.Element {
57+
const [tooltipVisible, setTooltipVisible] = useState<string | null>(null);
58+
59+
return (
60+
<div
61+
className="quick-actions"
62+
role="toolbar"
63+
aria-label="Quick actions toolbar"
64+
>
65+
{/* Bookmark Button */}
66+
<button
67+
className="quick-action-btn"
68+
onClick={() => onBookmark(currentFrequencyHz)}
69+
onMouseEnter={() => setTooltipVisible("bookmark")}
70+
onMouseLeave={() => setTooltipVisible(null)}
71+
onFocus={() => setTooltipVisible("bookmark")}
72+
onBlur={() => setTooltipVisible(null)}
73+
aria-label="Bookmark current frequency (B)"
74+
title="Bookmark (B)"
75+
>
76+
<BookmarkSimpleIcon size={20} weight="regular" aria-hidden="true" />
77+
{tooltipVisible === "bookmark" && (
78+
<span className="quick-action-tooltip" role="tooltip">
79+
Bookmark (B)
80+
</span>
81+
)}
82+
</button>
83+
84+
{/* Recording Button */}
85+
<button
86+
className={`quick-action-btn ${isRecording ? "active recording" : ""}`}
87+
onClick={onToggleRecording}
88+
onMouseEnter={() => setTooltipVisible("record")}
89+
onMouseLeave={() => setTooltipVisible(null)}
90+
onFocus={() => setTooltipVisible("record")}
91+
onBlur={() => setTooltipVisible(null)}
92+
aria-label={isRecording ? "Stop recording" : "Start recording"}
93+
aria-pressed={isRecording}
94+
title="Record (Ctrl/Cmd+S)"
95+
>
96+
<RecordIcon
97+
size={20}
98+
weight={isRecording ? "fill" : "regular"}
99+
aria-hidden="true"
100+
/>
101+
{tooltipVisible === "record" && (
102+
<span className="quick-action-tooltip" role="tooltip">
103+
Record (Ctrl/Cmd+S)
104+
</span>
105+
)}
106+
</button>
107+
108+
{/* Grid Toggle Button */}
109+
<button
110+
className={`quick-action-btn ${showGrid ? "active" : ""}`}
111+
onClick={onToggleGrid}
112+
onMouseEnter={() => setTooltipVisible("grid")}
113+
onMouseLeave={() => setTooltipVisible(null)}
114+
onFocus={() => setTooltipVisible("grid")}
115+
onBlur={() => setTooltipVisible(null)}
116+
aria-label={showGrid ? "Hide grid (G)" : "Show grid (G)"}
117+
aria-pressed={showGrid}
118+
title="Grid (G)"
119+
>
120+
<GridFourIcon size={20} weight="regular" aria-hidden="true" />
121+
{tooltipVisible === "grid" && (
122+
<span className="quick-action-tooltip" role="tooltip">
123+
Grid (G)
124+
</span>
125+
)}
126+
</button>
127+
128+
{/* Help Button */}
129+
<button
130+
className="quick-action-btn"
131+
onClick={onShowHelp}
132+
onMouseEnter={() => setTooltipVisible("help")}
133+
onMouseLeave={() => setTooltipVisible(null)}
134+
onFocus={() => setTooltipVisible("help")}
135+
onBlur={() => setTooltipVisible(null)}
136+
aria-label="Show keyboard shortcuts (?)"
137+
title="Help (?)"
138+
>
139+
<QuestionIcon size={20} weight="regular" aria-hidden="true" />
140+
{tooltipVisible === "help" && (
141+
<span className="quick-action-tooltip" role="tooltip">
142+
Help (?)
143+
</span>
144+
)}
145+
</button>
146+
</div>
147+
);
148+
}
149+
150+
export default QuickActions;

0 commit comments

Comments
 (0)