Skip to content

Commit e5ea2df

Browse files
feat: add visible tooltips with keyboard shortcuts to floating toolbar buttons (#927) (#939)
* feat: add visible tooltips with keyboard shortcuts to floating toolbar buttons (#927) Co-authored-by: Ona <no-reply@ona.com> * chore: re-trigger PR review Co-authored-by: Ona <no-reply@ona.com> --------- Co-authored-by: Ona <no-reply@ona.com>
1 parent 1f2781f commit e5ea2df

5 files changed

Lines changed: 115 additions & 40 deletions

File tree

.agents/quality.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Tracks code quality per domain. Updated by automations as a side effect of featu
111111
| 2026-05-07 | Client-side Supabase cookie null-safety (#933). Added null guard to `createBrowserClient` cookie options in `client.ts`, matching server-side pattern. Added 1 new Vitest file: `supabase/client.test.ts` (7 tests). Test totals: 132 Vitest files (1802 tests), 69 E2E specs (340 tests). |
112112
| 2026-05-07 | Transient fetch retry on search RPC (#937). Extended `retryOnNetworkError` to handle thrown transient errors. Wrapped search route RPC call with retry. Updated 2 files with grown counts: `search/route.test.ts` (11→14), `retry.test.ts` (6→9). Test totals: 132 Vitest files (1808 tests), 69 E2E specs (340 tests). |
113113
| 2026-05-07 | Prefers-reduced-motion accessibility (#940). Added `@media (prefers-reduced-motion: reduce)` rule to `globals.css`. Added 1 new Vitest file: `reduced-motion.test.ts` (6 tests). Test totals: 133 Vitest files (1818 tests), 69 E2E specs (340 tests). |
114+
| 2026-05-07 | Floating toolbar tooltips with keyboard shortcuts (#927). Added Tooltip wrapping to `ToolbarButton` in `floating-toolbar-plugin.tsx` with OS-aware shortcut labels. Updated stories with TooltipProvider decorator and 2 new stories (WindowsShortcuts, MacShortcuts). No new test files. Test totals unchanged: 131 Vitest files (1795 tests), 69 E2E specs (340 tests). |
114115
| 2026-05-08 | Replace direct sonner imports with lazy `@/lib/toast` wrapper (#949). Updated 5 source files and 5 test mocks. Added ESLint `no-restricted-imports` rule for `sonner` toast. Extended `ToastData` type with `action` property. No new tests. Test totals unchanged: 133 Vitest files (1818 tests), 69 E2E specs (340 tests). |
115116
| 2026-05-08 | Automated accessibility audit with axe-core (#956). Added `@axe-core/playwright` dev dependency. New E2E spec `e2e/accessibility.spec.ts` (5 tests) scanning sign-in, workspace home, page editor, workspace settings, and members pages. Fixed `button-name` violation on role select triggers (added `aria-label`). Fixed `aria-input-field-name` violation on editor contenteditable (added `aria-label`). Known pre-existing violations (color-contrast, link-in-text-block) documented and excluded. Test totals: 133 Vitest files (1818 tests), 70 E2E specs (345 tests). |
116117

7.72 KB
Loading
7.72 KB
Loading

src/components/editor/floating-toolbar-plugin.stories.tsx

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,29 @@ import {
99
Link,
1010
} from "lucide-react";
1111
import { type FontFamilyKey, FONT_FAMILIES } from "@/components/editor/font-family";
12+
import {
13+
Tooltip,
14+
TooltipContent,
15+
TooltipProvider,
16+
TooltipTrigger,
17+
} from "@/components/ui/tooltip";
1218

1319
// Static representation of the floating toolbar. The actual plugin requires
1420
// Lexical context and DOM selection — stories render the same visual output
1521
// with controlled state.
1622

23+
function getToolbarTooltips(isMac: boolean) {
24+
const mod = isMac ? "⌘" : "Ctrl+";
25+
return {
26+
bold: `Bold ${mod}B`,
27+
italic: `Italic ${mod}I`,
28+
underline: `Underline ${mod}U`,
29+
strikethrough: "Strikethrough",
30+
code: "Inline code",
31+
link: `Link ${mod}K`,
32+
};
33+
}
34+
1735
function FontFamilyDropdown({ value }: { value: FontFamilyKey }) {
1836
return (
1937
<select
@@ -41,18 +59,25 @@ function ToolbarButton({
4159
children: ReactNode;
4260
}) {
4361
return (
44-
<button
45-
type="button"
46-
className={`flex h-11 w-11 sm:h-7 sm:w-7 items-center justify-center text-sm ${
47-
active
48-
? "bg-overlay-active text-foreground"
49-
: "text-muted-foreground hover:bg-overlay-hover hover:text-foreground"
50-
}`}
51-
aria-label={label}
52-
aria-pressed={active}
53-
>
54-
{children}
55-
</button>
62+
<Tooltip>
63+
<TooltipTrigger
64+
render={
65+
<button
66+
type="button"
67+
className={`flex h-11 w-11 sm:h-7 sm:w-7 items-center justify-center text-sm ${
68+
active
69+
? "bg-overlay-active text-foreground"
70+
: "text-muted-foreground hover:bg-overlay-hover hover:text-foreground"
71+
}`}
72+
aria-label={label}
73+
aria-pressed={active}
74+
/>
75+
}
76+
>
77+
{children}
78+
</TooltipTrigger>
79+
<TooltipContent side="top">{label}</TooltipContent>
80+
</Tooltip>
5681
);
5782
}
5883

@@ -64,6 +89,7 @@ function StaticFloatingToolbar({
6489
isCode = false,
6590
isLink = false,
6691
fontFamily = "monospace" as FontFamilyKey,
92+
isMac = true,
6793
}: {
6894
isBold?: boolean;
6995
isItalic?: boolean;
@@ -72,7 +98,9 @@ function StaticFloatingToolbar({
7298
isCode?: boolean;
7399
isLink?: boolean;
74100
fontFamily?: FontFamilyKey;
101+
isMac?: boolean;
75102
}) {
103+
const tooltips = getToolbarTooltips(isMac);
76104
return (
77105
<div className="mx-auto max-w-md">
78106
<div
@@ -82,22 +110,22 @@ function StaticFloatingToolbar({
82110
>
83111
<FontFamilyDropdown value={fontFamily} />
84112
<div className="mx-0.5 h-4 w-px bg-overlay-border" aria-hidden="true" />
85-
<ToolbarButton active={isBold} label="Bold (⌘+B)">
113+
<ToolbarButton active={isBold} label={tooltips.bold}>
86114
<Bold className="h-4 w-4" />
87115
</ToolbarButton>
88-
<ToolbarButton active={isItalic} label="Italic (⌘+I)">
116+
<ToolbarButton active={isItalic} label={tooltips.italic}>
89117
<Italic className="h-4 w-4" />
90118
</ToolbarButton>
91-
<ToolbarButton active={isUnderline} label="Underline (⌘+U)">
119+
<ToolbarButton active={isUnderline} label={tooltips.underline}>
92120
<Underline className="h-4 w-4" />
93121
</ToolbarButton>
94-
<ToolbarButton active={isStrikethrough} label="Strikethrough">
122+
<ToolbarButton active={isStrikethrough} label={tooltips.strikethrough}>
95123
<Strikethrough className="h-4 w-4" />
96124
</ToolbarButton>
97-
<ToolbarButton active={isCode} label="Inline code">
125+
<ToolbarButton active={isCode} label={tooltips.code}>
98126
<Code className="h-4 w-4" />
99127
</ToolbarButton>
100-
<ToolbarButton active={isLink} label="Link (⌘+K)">
128+
<ToolbarButton active={isLink} label={tooltips.link}>
101129
<Link className="h-4 w-4" />
102130
</ToolbarButton>
103131
</div>
@@ -110,13 +138,20 @@ const meta: Meta = {
110138
parameters: {
111139
layout: "padded",
112140
},
141+
decorators: [
142+
(Story) => (
143+
<TooltipProvider>
144+
<Story />
145+
</TooltipProvider>
146+
),
147+
],
113148
};
114149

115150
export { meta as default };
116151

117152
type Story = StoryObj;
118153

119-
/** Default state — toolbar visible with no active formats. */
154+
/** Default state — toolbar visible with no active formats. Hover buttons to see tooltips with keyboard shortcuts. */
120155
export const Default: Story = {
121156
render: () => <StaticFloatingToolbar />,
122157
};
@@ -176,3 +211,13 @@ export const FontSerif: Story = {
176211
export const FontMonospace: Story = {
177212
render: () => <StaticFloatingToolbar fontFamily="monospace" />,
178213
};
214+
215+
/** Tooltips with Windows/Linux shortcut format (Ctrl+ prefix). */
216+
export const WindowsShortcuts: Story = {
217+
render: () => <StaticFloatingToolbar isMac={false} />,
218+
};
219+
220+
/** Tooltips with macOS shortcut format (⌘ prefix). */
221+
export const MacShortcuts: Story = {
222+
render: () => <StaticFloatingToolbar isMac={true} />,
223+
};

src/components/editor/floating-toolbar-plugin.tsx

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import {
2626
Code,
2727
Link,
2828
} from "lucide-react";
29+
import {
30+
Tooltip,
31+
TooltipContent,
32+
TooltipTrigger,
33+
} from "@/components/ui/tooltip";
2934
import {
3035
type FontFamilyKey,
3136
FONT_FAMILIES,
@@ -37,6 +42,19 @@ interface FloatingToolbarPluginProps {
3742
anchorElem: HTMLElement;
3843
}
3944

45+
/** OS-aware shortcut labels for toolbar buttons. */
46+
function getToolbarTooltips(isMac: boolean) {
47+
const mod = isMac ? "⌘" : "Ctrl+";
48+
return {
49+
bold: `Bold ${mod}B`,
50+
italic: `Italic ${mod}I`,
51+
underline: `Underline ${mod}U`,
52+
strikethrough: "Strikethrough",
53+
code: "Inline code",
54+
link: `Link ${mod}K`,
55+
};
56+
}
57+
4058
function getSelectedNode(selection: ReturnType<typeof $getSelection>) {
4159
if (!$isRangeSelection(selection)) return null;
4260
const anchor = selection.anchor;
@@ -66,6 +84,10 @@ export function FloatingToolbarPlugin({
6684
const [isCode, setIsCode] = useState(false);
6785
const [isLink, setIsLink] = useState(false);
6886
const [fontFamily, setFontFamily] = useState<FontFamilyKey>("monospace");
87+
const [isMac] = useState(
88+
() => typeof navigator !== "undefined" && navigator.platform.toUpperCase().includes("MAC")
89+
);
90+
const tooltips = getToolbarTooltips(isMac);
6991

7092
const updateToolbar = useCallback(() => {
7193
const selection = $getSelection();
@@ -215,47 +237,47 @@ export function FloatingToolbarPlugin({
215237
<ToolbarButton
216238
active={isBold}
217239
onClick={formatBold}
218-
label="Bold (⌘+B)"
240+
label={tooltips.bold}
219241
testId="editor-toolbar-bold"
220242
>
221243
<Bold className="h-4 w-4" />
222244
</ToolbarButton>
223245
<ToolbarButton
224246
active={isItalic}
225247
onClick={formatItalic}
226-
label="Italic (⌘+I)"
248+
label={tooltips.italic}
227249
testId="editor-toolbar-italic"
228250
>
229251
<Italic className="h-4 w-4" />
230252
</ToolbarButton>
231253
<ToolbarButton
232254
active={isUnderline}
233255
onClick={formatUnderline}
234-
label="Underline (⌘+U)"
256+
label={tooltips.underline}
235257
testId="editor-toolbar-underline"
236258
>
237259
<Underline className="h-4 w-4" />
238260
</ToolbarButton>
239261
<ToolbarButton
240262
active={isStrikethrough}
241263
onClick={formatStrikethrough}
242-
label="Strikethrough"
264+
label={tooltips.strikethrough}
243265
testId="editor-toolbar-strikethrough"
244266
>
245267
<Strikethrough className="h-4 w-4" />
246268
</ToolbarButton>
247269
<ToolbarButton
248270
active={isCode}
249271
onClick={formatCode}
250-
label="Inline code"
272+
label={tooltips.code}
251273
testId="editor-toolbar-code"
252274
>
253275
<Code className="h-4 w-4" />
254276
</ToolbarButton>
255277
<ToolbarButton
256278
active={isLink}
257279
onClick={toggleLink}
258-
label="Link (⌘+K)"
280+
label={tooltips.link}
259281
testId="editor-toolbar-link"
260282
>
261283
<Link className="h-4 w-4" />
@@ -304,20 +326,27 @@ function ToolbarButton({
304326
children: ReactNode;
305327
}) {
306328
return (
307-
<button
308-
type="button"
309-
onMouseDown={(e) => e.preventDefault()}
310-
className={`flex h-11 w-11 sm:h-7 sm:w-7 items-center justify-center text-sm ${
311-
active
312-
? "bg-overlay-active text-foreground"
313-
: "text-muted-foreground hover:bg-overlay-hover hover:text-foreground"
314-
}`}
315-
onClick={onClick}
316-
aria-label={label}
317-
aria-pressed={active}
318-
data-testid={testId}
319-
>
320-
{children}
321-
</button>
329+
<Tooltip>
330+
<TooltipTrigger
331+
render={
332+
<button
333+
type="button"
334+
onMouseDown={(e) => e.preventDefault()}
335+
className={`flex h-11 w-11 sm:h-7 sm:w-7 items-center justify-center text-sm ${
336+
active
337+
? "bg-overlay-active text-foreground"
338+
: "text-muted-foreground hover:bg-overlay-hover hover:text-foreground"
339+
}`}
340+
onClick={onClick}
341+
aria-label={label}
342+
aria-pressed={active}
343+
data-testid={testId}
344+
/>
345+
}
346+
>
347+
{children}
348+
</TooltipTrigger>
349+
<TooltipContent side="top">{label}</TooltipContent>
350+
</Tooltip>
322351
);
323352
}

0 commit comments

Comments
 (0)