Skip to content

Commit 4ddc2b3

Browse files
tyler-daneclaude
andauthored
style: tooltips (#1880)
* style: tooltips * style: standardize color contrast w/ shortcuts * feat: split keys in shortcut combos * fix: sync e2e shortcut hints and guard empty keycaps - e2e/week-view-switch: assert uppercased view shortcut hints (d/w -> D/W, "Dayd"/"Weekw" -> "DayD"/"WeekW") to match the per-key chip rendering. - MenuItem: render the bare button (no empty tooltip surface) when tooltipContent is omitted, honoring the documented "tooltip disabled" contract. - ShortcutKeys: return null for combos with no keys instead of an empty chip row; add focused tests for per-key split, uppercasing, the cmd alias, and the empty-combo guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: move shortcuts up one dir * chore: reorder * refactor: ShortcutKeys accepts an explicit key array Replace the `combo: string` prop (parsed by splitting on "+") with an explicit `keys: string[]`, and push the array to every boundary so the "+" delimiter disappears rather than relocating: - ShortcutKeys: `keys: string[]`; export `toKeyArray` (a lone string is one key, never split) for the wrappers to reuse. - Shortcut data type `k: string` -> `keys: string[]`; update getShortcuts and the inline Shortcut[] builders in WeekView. - Wrapper props (TooltipWrapper.shortcut, MenuItem.tooltipContent, ShortcutTip.shortcut) accept `string | string[]` via toKeyArray; ShortcutTip drops its join("+")/re-split round trip. - Call sites: single-key hints stay strings ("?", "Delete"); multi-key combos become arrays (["Mod","K"], ["Control","Meta","ArrowUp"]). Tests updated for the new prop/data shapes. No behavior change — the rendered keycap chips are identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: ShortcutKeys accepts a key or key array directly Callers no longer need a helper or a single-element array: ShortcutKeys takes `keys: string | string[]` and normalizes internally, so a one-key hint is just `keys="?"` and a combo is `keys={["Mod", "K"]}`. - toKeyArray is now private to ShortcutKeys (un-exported). - TooltipWrapper / MenuItem / ShortcutTip pass their `string | string[]` shortcut straight through — no toKeyArray call at the call site. - SelectView passes `keys={option.key}` instead of `keys={[option.key]}`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: rename tooltipContent to tooltip --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 076579b commit 4ddc2b3

46 files changed

Lines changed: 401 additions & 376 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

e2e/navigation/week-view-switch.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { expect, type Page, test } from "@playwright/test";
33
type ViewName = "day" | "week";
44

55
const shortcutByView = {
6-
day: "d",
7-
week: "w",
6+
day: "D",
7+
week: "W",
88
} as const satisfies Record<ViewName, string>;
99

1010
const collectUnexpectedConsoleErrors = (page: Page) => {
@@ -60,7 +60,7 @@ test.describe("View dropdown", () => {
6060

6161
await expect(
6262
page.getByTestId("view-select-dropdown").getByRole("option"),
63-
).toHaveText(["Dayd", "Weekw"]);
63+
).toHaveText(["DayD", "WeekW"]);
6464
await expect(viewOption(page, "day")).toHaveAttribute(
6565
"aria-selected",
6666
"false",

packages/web/src/common/constants/web.constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export enum ZIndex {
4848
export const Z_INDEX_FLOATING_FORM = ZIndex.MAX + ZIndex.LAYER_1;
4949
export const Z_INDEX_FLOATING_MENU = Z_INDEX_FLOATING_FORM + 1;
5050
export const Z_INDEX_MODAL = Z_INDEX_FLOATING_MENU + ZIndex.LAYER_1;
51+
// Tooltips are transient and anchored to their trigger, so they should float
52+
// above every persistent layer (floating form/menu/modal).
53+
export const Z_INDEX_TOOLTIP = Z_INDEX_MODAL + ZIndex.LAYER_1;
5154

5255
export const ACCEPTED_TIMES = [
5356
"12:00 AM",

packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts renamed to packages/web/src/common/shortcuts/data/shortcuts.data.test.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
import dayjs from "@core/util/date/dayjs";
2-
import { getShortcuts } from "@web/common/utils/shortcut/data/shortcuts.data";
2+
import { getShortcuts } from "@web/common/shortcuts/data/shortcuts.data";
33

44
describe("shortcuts.data", () => {
55
describe("getShortcuts", () => {
66
it("should return default shortcuts when no config provided", () => {
77
const shortcuts = getShortcuts();
88

99
expect(shortcuts.globalShortcuts).toEqual([
10-
{ k: "d", label: "Day" },
11-
{ k: "w", label: "Week" },
12-
{ k: "[", label: "Close sidebar" },
13-
{ k: "?", label: "Show shortcuts" },
14-
{ k: "Mod+k", label: "Command Palette" },
10+
{ keys: ["d"], label: "Day" },
11+
{ keys: ["w"], label: "Week" },
12+
{ keys: ["["], label: "Close sidebar" },
13+
{ keys: ["?"], label: "Show shortcuts" },
14+
{ keys: ["Mod", "k"], label: "Command Palette" },
1515
]);
1616

1717
expect(shortcuts.dayAgendaShortcuts).toHaveLength(2);
1818
expect(shortcuts.dayAgendaShortcuts[0]).toEqual({
19-
k: "i",
19+
keys: ["i"],
2020
label: "Focus on calendar",
2121
});
2222
expect(shortcuts.dayAgendaShortcuts[1]).toEqual({
23-
k: "m",
23+
keys: ["m"],
2424
label: "Edit event",
2525
});
2626

2727
expect(shortcuts.dayShortcuts).toHaveLength(3);
2828
expect(shortcuts.dayShortcuts[2]).toEqual({
29-
k: "t",
29+
keys: ["t"],
3030
label: "Go to today",
3131
});
3232
});
@@ -37,7 +37,7 @@ describe("shortcuts.data", () => {
3737
currentDate: dayjs(),
3838
});
3939

40-
const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t");
40+
const tShortcut = shortcuts.dayShortcuts.find((s) => s.keys[0] === "t");
4141
expect(tShortcut).toBeDefined();
4242
expect(tShortcut?.label).toBe("Scroll to now");
4343
});
@@ -50,7 +50,7 @@ describe("shortcuts.data", () => {
5050
currentDate: yesterday,
5151
});
5252

53-
const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t");
53+
const tShortcut = shortcuts.dayShortcuts.find((s) => s.keys[0] === "t");
5454
expect(tShortcut).toBeDefined();
5555
expect(tShortcut?.label).toBe("Go to today");
5656
});
@@ -63,7 +63,7 @@ describe("shortcuts.data", () => {
6363
currentDate: tomorrow,
6464
});
6565

66-
const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t");
66+
const tShortcut = shortcuts.dayShortcuts.find((s) => s.keys[0] === "t");
6767
expect(tShortcut).toBeDefined();
6868
expect(tShortcut?.label).toBe("Go to today");
6969
});
@@ -74,7 +74,7 @@ describe("shortcuts.data", () => {
7474
currentDate: undefined,
7575
});
7676

77-
const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t");
77+
const tShortcut = shortcuts.dayShortcuts.find((s) => s.keys[0] === "t");
7878
expect(tShortcut).toBeDefined();
7979
expect(tShortcut?.label).toBe("Go to today");
8080
});
@@ -87,12 +87,15 @@ describe("shortcuts.data", () => {
8787

8888
expect(shortcuts.homeShortcuts).toHaveLength(3);
8989
expect(shortcuts.homeShortcuts[0]).toEqual({
90-
k: "j",
90+
keys: ["j"],
9191
label: "Previous day",
9292
});
93-
expect(shortcuts.homeShortcuts[1]).toEqual({ k: "k", label: "Next day" });
93+
expect(shortcuts.homeShortcuts[1]).toEqual({
94+
keys: ["k"],
95+
label: "Next day",
96+
});
9497
expect(shortcuts.homeShortcuts[2]).toEqual({
95-
k: "Enter",
98+
keys: ["Enter"],
9699
label: "Go to Today",
97100
});
98101

packages/web/src/common/utils/shortcut/data/shortcuts.data.ts renamed to packages/web/src/common/shortcuts/data/shortcuts.data.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => {
1212
const { isHome = false, isToday = true, currentDate } = config;
1313

1414
const globalShortcuts: Shortcut[] = [
15-
{ k: VIEW_SHORTCUTS.day.key, label: VIEW_SHORTCUTS.day.label },
16-
{ k: VIEW_SHORTCUTS.week.key, label: VIEW_SHORTCUTS.week.label },
17-
{ k: "[", label: "Close sidebar" },
18-
{ k: "?", label: "Show shortcuts" },
19-
{ k: "Mod+k", label: "Command Palette" },
15+
{ keys: [VIEW_SHORTCUTS.day.key], label: VIEW_SHORTCUTS.day.label },
16+
{ keys: [VIEW_SHORTCUTS.week.key], label: VIEW_SHORTCUTS.week.label },
17+
{ keys: ["["], label: "Close sidebar" },
18+
{ keys: ["?"], label: "Show shortcuts" },
19+
{ keys: ["Mod", "k"], label: "Command Palette" },
2020
];
2121

2222
let homeShortcuts: Shortcut[] = [];
@@ -26,18 +26,18 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => {
2626

2727
if (isHome) {
2828
homeShortcuts = [
29-
{ k: "j", label: "Previous day" },
30-
{ k: "k", label: "Next day" },
31-
{ k: "Enter", label: "Go to Today" },
29+
{ keys: ["j"], label: "Previous day" },
30+
{ keys: ["k"], label: "Next day" },
31+
{ keys: ["Enter"], label: "Go to Today" },
3232
];
3333
}
3434

3535
if (isToday) {
3636
dayShortcuts = [
37-
{ k: "j", label: "Previous day" },
38-
{ k: "k", label: "Next day" },
37+
{ keys: ["j"], label: "Previous day" },
38+
{ keys: ["k"], label: "Next day" },
3939
{
40-
k: "t",
40+
keys: ["t"],
4141
label: (() => {
4242
if (!currentDate) return "Go to today";
4343

@@ -49,14 +49,14 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => {
4949
];
5050

5151
dayTaskShortcuts = [
52-
{ k: "u", label: "Focus on tasks" },
53-
{ k: "c", label: "Create task" },
54-
{ k: "e", label: "Edit task" },
55-
{ k: "Delete", label: "Delete task" },
52+
{ keys: ["u"], label: "Focus on tasks" },
53+
{ keys: ["c"], label: "Create task" },
54+
{ keys: ["e"], label: "Edit task" },
55+
{ keys: ["Delete"], label: "Delete task" },
5656
];
5757
dayAgendaShortcuts = [
58-
{ k: "i", label: "Focus on calendar" },
59-
{ k: "m", label: "Edit event" },
58+
{ keys: ["i"], label: "Focus on calendar" },
59+
{ keys: ["m"], label: "Edit event" },
6060
];
6161
}
6262
return {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
detectPlatform,
3+
formatWithLabels,
4+
resolveModifier,
5+
} from "@tanstack/react-hotkeys";
6+
7+
/** Resolves TanStack `Mod` tokens to `Meta` / `Control` for icons and labels. */
8+
export function expandModInShortcutDisplay(k: string): string {
9+
const resolvedMod = resolveModifier("Mod");
10+
return k
11+
.split("+")
12+
.map((segment) => {
13+
const part = segment.trim();
14+
return part.toLowerCase() === "mod" ? resolvedMod : part;
15+
})
16+
.join("+");
17+
}
18+
19+
/**
20+
* User-facing primary modifier label (Cmd on macOS, Ctrl on Windows/Linux).
21+
* Uses TanStack's labeled formatting for `Mod`.
22+
*/
23+
export const getModifierKeyLabel = (): string => {
24+
const platform = detectPlatform();
25+
return formatWithLabels("Mod+k", platform).split("+")[0] ?? "Ctrl";
26+
};
27+
28+
export const getModifierKeyTestId = () =>
29+
`${resolveModifier("Mod").toLowerCase()}-icon`;

packages/web/src/common/storage/migrations/external/demo-data-seed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { type Event_Core } from "@core/types/event.types";
33
import dayjs from "@core/util/date/dayjs";
44
import { UNAUTHENTICATED_USER } from "@web/common/constants/auth.constants";
55
import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants";
6+
import { getModifierKeyLabel } from "@web/common/shortcuts/shortcut.util";
67
import { type Task } from "@web/common/types/task.types";
78
import { type Schema_GridEvent } from "@web/common/types/web.event.types";
89
import { gridEventDefaultPosition } from "@web/common/utils/event/event.util";
910
import { createObjectIdString } from "@web/common/utils/id/object-id.util";
10-
import { getModifierKeyLabel } from "@web/common/utils/shortcut/shortcut.util";
1111
import { type StorageAdapter } from "../../adapter/storage.adapter";
1212
import { markLocalDemoEvent } from "../../types/local-event.types";
1313
import { type ExternalMigration } from "../migration.types";

packages/web/src/common/types/global.shortcut.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type Shortcut = { k: string; label: string };
1+
export type Shortcut = { keys: string[]; label: string };
22

33
// Global shortcuts available in all views
44
export const GLOBAL_SHORTCUT_KEYS = {

packages/web/src/common/utils/shortcut/shortcut.util.tsx

Lines changed: 0 additions & 77 deletions
This file was deleted.

packages/web/src/components/PlannerSidebar/PlannerSidebarActions/PlannerSidebarActions.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
} from "@phosphor-icons/react";
66
import { useVersionCheck } from "@web/common/hooks/useVersionCheck";
77
import { reloadLocation } from "@web/common/utils/browser/browser-navigation.util";
8-
import { getModifierKeyLabel } from "@web/common/utils/shortcut/shortcut.util";
98
import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper";
109
import { selectIsCmdPaletteOpen } from "@web/ducks/settings/selectors/settings.selectors";
1110
import { settingsSlice } from "@web/ducks/settings/slices/settings.slice";
@@ -65,7 +64,7 @@ export const PlannerSidebarActions = ({
6564
<div className="flex items-center gap-2">
6665
<TooltipWrapper
6766
description="Open command palette"
68-
shortcut={`${getModifierKeyLabel()}+K`}
67+
shortcut={["Mod", "K"]}
6968
onClick={toggleCmdPalette}
7069
>
7170
<button

packages/web/src/components/PlannerSidebar/ShortcutsOverlay/ShortcutsOverlay.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const sections = [
77
{
88
title: "Day",
99
shortcuts: [
10-
{ k: "j", label: "Previous day" },
11-
{ k: "k", label: "Next day" },
10+
{ keys: ["j"], label: "Previous day" },
11+
{ keys: ["k"], label: "Next day" },
1212
],
1313
},
1414
{

0 commit comments

Comments
 (0)