diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json index 3c3c59c970..2608266f4c 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/block-party.tasks.json @@ -154,10 +154,10 @@ "id": "jump-to-kickoff", "type": "eventJump", "title": "Fly across the board", - "instruction": "Press F to reveal jump letters, then type the one on Team kickoff.", - "hint": "Press F and letters appear on every block. Type the letter sitting on Team kickoff.", + "instruction": "Press f to reveal jump labels, then type the one on Team kickoff.", + "hint": "Press f and labels appear on every block. Type the lowercase label on Team kickoff.", "keycaps": [ - "F" + "f" ], "targetEventId": "game-kickoff" }, diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json index 87aa2beb62..ed6e935fc0 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/shortcuts.json @@ -1167,7 +1167,7 @@ "eventJump": { "bareLetter": "f", "keycaps": [ - "F" + "f" ] }, "moveEvent": { diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift index e42d524d39..8eb7a4fa0d 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/WeekDayColumnHeaderControl.swift @@ -62,7 +62,7 @@ final class WeekDayColumnHeaderControl: NSView { if showDayJumpPrefixes { let weekday = EffectiveTimeZone.calendar.component(.weekday, from: date) - let prefix = DayJumpPrefixes.prefix(forWeekday: weekday - 1).uppercased() + let prefix = DayJumpPrefixes.prefix(forWeekday: weekday - 1) chipLabel.stringValue = dayJumpUseShiftPrefix ? "⇧\(prefix)" : prefix chipLabel.isHidden = false chipLabel.textColor = palette.text diff --git a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift index dd34702ee0..f432dfe713 100644 --- a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift +++ b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridDayJumpPrefixTests.swift @@ -3,8 +3,8 @@ import CompassKit import XCTest @testable import CompassUI -/// Web `DayLabels`: while Mod is held the weekday name becomes a ⇧SU / ⇧M -/// keycap, and event-jump mode shows the bare letter. +/// Web `DayLabels`: while Mod is held the weekday name becomes a ⇧su / ⇧m +/// keycap, and event-jump mode shows the bare lowercase prefix. @MainActor final class TimeGridDayJumpPrefixTests: XCTestCase { private let dayKeys = ["2026-05-17", "2026-05-18"] @@ -16,14 +16,14 @@ final class TimeGridDayJumpPrefixTests: XCTestCase { func testWeekHeadersShowShiftPrefixedChipsWhileModIsHeld() { let view = render(showDayJumpPrefixes: true) - XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("⇧SU")) - XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-18").contains("⇧M")) + XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("⇧su")) + XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-18").contains("⇧m")) XCTAssertFalse(headerTexts(in: view, dayKey: "2026-05-17").contains("Sun")) } func testEventJumpModeShowsBarePrefixes() { let view = render(showDayJumpPrefixes: true, dayJumpUseShiftPrefix: false) - XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("SU")) + XCTAssertTrue(headerTexts(in: view, dayKey: "2026-05-17").contains("su")) } private func render(showDayJumpPrefixes: Bool, dayJumpUseShiftPrefix: Bool = true) -> TimeGridView { diff --git a/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx b/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx index 828b4a7ed7..de158a55f2 100644 --- a/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx +++ b/apps/calendar-web/src/components/ShortcutShowcase/PracticeCalendar.tsx @@ -99,7 +99,7 @@ const PracticeBlock: FC<{ }`} data-practice-jump-letter={jumpLetter} > - {jumpLetter.toUpperCase()} + {jumpLetter} )} diff --git a/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx b/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx index 2639a7b8cb..ad6eb60fa3 100644 --- a/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx +++ b/apps/calendar-web/src/components/ShortcutShowcase/ShortcutShowcase.test.tsx @@ -195,13 +195,13 @@ describe("ShortcutShowcase", () => { pressKey("Enter"); // Esc-skip to the jump task. With every board task skipped, letters run - // over the three seed events alone, so kickoff's chip is A here. + // over the three seed events alone, so kickoff's chip is a here. for (let i = 0; i < 7; i += 1) pressKey("Escape"); expect(screen.getByText("Fly across the board")).toBeTruthy(); - expect(screen.getByText("A")).toBeTruthy(); + expect(screen.getByText("a")).toBeTruthy(); pressKey("f"); // The chips on the blocks join the one already on the card. - expect(screen.getAllByText("A").length).toBe(2); + expect(screen.getAllByText("a").length).toBe(2); // Skip past the jump and page-jump tasks to reach the palette task: // its Mod+K hint becomes Esc once the palette opens. diff --git a/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts b/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts index 52ba9e2860..036fa5b2c1 100644 --- a/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts +++ b/apps/calendar-web/src/components/ShortcutShowcase/game.state.test.ts @@ -408,7 +408,7 @@ describe("keycap progress", () => { >; expect(task.id).toBe("jump-to-kickoff"); // The full sequence is on the card up front: H, then kickoff's letter. - expect(getDisplayKeycaps(task, state)).toEqual(["F", "S"]); + expect(getDisplayKeycaps(task, state)).toEqual(["f", "s"]); expect(getNextKeycapIndex(task, state)).toBe(0); state = handleGameKey(state, key.jump, T0); diff --git a/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts b/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts index 07c75f0dbc..f5f6db8203 100644 --- a/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts +++ b/apps/calendar-web/src/components/ShortcutShowcase/game.state.ts @@ -536,7 +536,7 @@ export const getDisplayKeycaps = ( ): readonly string[] => { if (task.type === "eventJump") { const letter = getJumpLetters(state.practice)[task.targetEventId]; - return letter ? [...task.keycaps, letter.toUpperCase()] : task.keycaps; + return letter ? [...task.keycaps, letter] : task.keycaps; } if ( (task.type === "palette" && state.simOverlay === "palette") || diff --git a/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts b/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts index 8fdef0ade6..4261483443 100644 --- a/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts +++ b/apps/calendar-web/src/components/ShortcutShowcase/game.tasks.ts @@ -210,8 +210,8 @@ export const RUN_TASKS: readonly GameTask[] = [ type: "eventJump", title: "Fly across the board", instruction: - "Press F to reveal jump letters, then type the one on Team kickoff.", - hint: "Press F and letters appear on every block. Type the letter sitting on Team kickoff.", + "Press f to reveal jump labels, then type the one on Team kickoff.", + hint: "Press f and labels appear on every block. Type the lowercase label on Team kickoff.", keycaps: KEYMAP.eventJump.keycaps, targetEventId: "game-kickoff", }, diff --git a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx index b63f75ba85..20f2fb5597 100644 --- a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx +++ b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.test.tsx @@ -76,7 +76,7 @@ describe("ShiftHintOverlay", () => { render(); - expect(overlayRoot()?.textContent).toBe("M1"); + expect(overlayRoot()?.textContent).toBe("m1"); expect(overlayChip()?.style.top).toBe("224px"); expect(overlayChip()?.style.left).toBe("214px"); }); @@ -108,7 +108,7 @@ describe("ShiftHintOverlay", () => { render(); - expect(overlayChip()?.textContent).toBe("SU1"); + expect(overlayChip()?.textContent).toBe("su1"); expect(overlayChip()?.style.top).toBe("204px"); expect(overlayChip()?.style.left).toBe("206px"); }); diff --git a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx index 913713407b..14c734b59b 100644 --- a/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx +++ b/apps/calendar-web/src/shortcuts/shift-hint/ShiftHintOverlay.tsx @@ -31,8 +31,8 @@ export function ShiftHintOverlay({ hints }: { hints: ActiveShiftHint[] }) { return null; } - const label = hint.hint.toUpperCase(); - // Longer labels (SU1, F10) need more room than the old AA chips. + const label = hint.hint; + // Longer labels (su1, f10) need more room than the old AA chips. const chipWidth = Math.max(22, 10 + label.length * 8); return ( diff --git a/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts b/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts index 96156a61d0..13790bf79a 100644 --- a/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts +++ b/apps/calendar-web/src/shortcuts/shift-hint/assign-shift-hint-keys.ts @@ -1,7 +1,7 @@ /** * Day-prefix event jump labels. * - * SU/M/T/W/R/F/SA + per-day chronological index (W4, SU1, F10), in both week + * su/m/t/w/r/f/sa + per-day chronological index (w4, su1, f10), in both week * and day view. Day view groups on the single date it shows, so its labels are * that weekday's prefix (W1, W2, …). One scheme for both views is what keeps a * bare digit free everywhere for quick-time create (shortcuts/quick-time). diff --git a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx index 87ac942b37..72a1972faa 100644 --- a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx +++ b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.quick-time.test.tsx @@ -433,7 +433,7 @@ describe("typed-time deferred commit", () => { ); }); - it("still toggles jump off with f after a half-typed column time", () => { + it("exits jump with Esc after a half-typed column time", () => { const { createAt, type } = mountColumn(); act(() => { @@ -443,7 +443,7 @@ describe("typed-time deferred commit", () => { }), ); type(["9"]); - dispatch(keydown("f")); + dispatch(keydown("Escape")); }); expect(createAt).not.toHaveBeenCalled(); @@ -519,6 +519,40 @@ describe("typed-time deferred commit", () => { fridayElements.splice(0, fridayElements.length); }); + it("opens the fourth Friday event on bare f4 after flash jump, with no draft", async () => { + const { createAt, openEvent } = mountFriday(); + const fourthId = fridayIds[3]!; + const fourthEl = fridayElements[3]!; + + act(() => { + dispatch(keydown("f")); + }); + expect(useEventJumpStore.getState().isActive).toBe(true); + dispatch(keydown("f")); + dispatch(digit("4")); + + await act( + () => + new Promise((resolve) => { + setTimeout(resolve, DIGIT_AMBIGUOUS_COMMIT_MS * 2 + 50); + }), + ); + + expect(document.activeElement).toBe(fourthEl); + expect(openEvent).not.toHaveBeenCalled(); + expect(createAt).not.toHaveBeenCalled(); + + const enter = new KeyboardEvent("keydown", { + bubbles: true, + cancelable: true, + key: "Enter", + }); + act(() => { + fourthEl.dispatchEvent(enter); + }); + expect(openEvent).toHaveBeenCalledWith(fourthId); + }); + it("opens the fourth Friday event on f then 4 then Enter, with no draft", () => { const { createAt, openEvent, pressFriday } = mountFriday(); const fourthId = fridayIds[3]!; diff --git a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx index a952b05e0f..c0e5fc2172 100644 --- a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx +++ b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.test.tsx @@ -495,7 +495,7 @@ describe("useShiftHoldEventHints", () => { expect(useEventJumpStore.getState()).toMatchObject({ isActive: true, - pointerHintKey: "W2", + pointerHintKey: "w2", pointerHintEventId: EVENT_B, }); expect(focus).toHaveBeenCalledWith( @@ -571,7 +571,7 @@ describe("useShiftHoldEventHints", () => { act(() => { requestPointerEventJump(clicked); }); - expect(useEventJumpStore.getState().pointerHintKey).toBe("W2"); + expect(useEventJumpStore.getState().pointerHintKey).toBe("w2"); focus.mockClear(); act(() => { @@ -683,7 +683,7 @@ describe("useShiftHoldEventHints", () => { act(() => { requestPointerEventJump(EVENT_B); }); - expect(useEventJumpStore.getState().pointerHintKey).toBe("W2"); + expect(useEventJumpStore.getState().pointerHintKey).toBe("w2"); act(() => { rerender({ @@ -694,7 +694,7 @@ describe("useShiftHoldEventHints", () => { }); }); - expect(useEventJumpStore.getState().pointerHintKey).toBe("W3"); + expect(useEventJumpStore.getState().pointerHintKey).toBe("w3"); }); it("publishes every visible column as a day key can select empty ones", () => { @@ -783,20 +783,25 @@ describe("useShiftHoldEventHints", () => { expect(result.current.hints).toEqual([]); }); - it("toggles off with a second f", () => { - const { result } = mountHints(); + it("selects Friday with bare f while jump mode is on", () => { + const friday = WEEK_DAYS[5]!; + const fridayId = EventIdSchema.parse("ffffffffffffffffffffffff"); + const { focus, elements } = mountHints([ + timedFixture(fridayId, friday.hour(10).format()), + ]); act(() => { pressEventJump(); + dispatch("keydown", "f"); }); - expect(useEventJumpStore.getState().isActive).toBe(true); - expect(result.current.hints).toHaveLength(3); - act(() => { - pressEventJump(); - }); - expect(useEventJumpStore.getState().isActive).toBe(false); - expect(result.current.hints).toEqual([]); + expect(useEventJumpStore.getState().isActive).toBe(true); + expect(focus).toHaveBeenCalledWith( + expect.objectContaining({ eventId: fridayId, element: elements[0] }), + ); + expect(useEventJumpStore.getState().activeDayKeys).toEqual([ + friday.format("YYYY-MM-DD"), + ]); }); it("focuses Saturday from idle with Shift+S then a", () => { diff --git a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts index eae17c82f7..7a1e09153f 100644 --- a/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts +++ b/apps/calendar-web/src/shortcuts/shift-hint/useShiftHoldEventHints.ts @@ -174,7 +174,8 @@ const buildDayJumpAssignments = ( * its first one. Shift is what makes the day columns always available: bare * `t`, `p`, and `m` keep their own commands. Once jump mode is on, the labels * are typed bare (`w`, `w1`…) and win over global shortcuts. Esc exits, a - * second `f` toggles off. `s` is only the Sunday/Saturday prefix. `e` is not + * Esc exits jump mode (`f` is Friday's column letter while jump is on). `s` is + * only the Sunday/Saturday prefix. `e` is not * a day prefix: jump yields to an armed (or about-to-arm) edit sequence so * `e` then `t` edits the title instead of selecting Tuesday. `c` is left * unclaimed too, so the create shortcuts can consume the selected column. @@ -604,15 +605,6 @@ export function useShiftHoldEventHints({ return; } - // The flash letter is also Friday's day prefix; bare F toggles off instead - // of jumping to Friday (Shift+F still enters Friday from idle). - if (isBareLetterKey(event, KEYMAP.eventJump.bareLetter)) { - event.preventDefault(); - event.stopPropagation(); - deactivate(); - return; - } - // Empty buffer: digits are times. Once a day letter is typed ("w"), // 1-2 digits still index that day's events. A four-digit HHMM still // creates on the selected column so focusing Tuesday then 1230 lands @@ -749,7 +741,7 @@ export function useShiftHoldEventHints({ eventJumpActions.setActiveDayKeys([assignment.dayKey]); eventJumpActions.setPointerHint({ eventId: assignment.eventId, - key: assignment.hint.toUpperCase(), + key: assignment.hint, }); setHints(toActiveHints(assignments, visibleByIdRef.current)); // Focus now so the advertised Enter path works, including when this @@ -830,7 +822,7 @@ export function useShiftHoldEventHints({ if (!assignment) { eventJumpActions.setPointerHint(null); } else { - const nextKey = assignment.hint.toUpperCase(); + const nextKey = assignment.hint; if (nextKey !== useEventJumpStore.getState().pointerHintKey) { eventJumpActions.setPointerHint({ eventId: assignment.eventId, diff --git a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts index 32fe4bb27b..fe46255143 100644 --- a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts +++ b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.test.ts @@ -26,7 +26,7 @@ describe("getHintPlainText", () => { expect(plainTextById["create-event"]).toBe("C creates an event"); expect(plainTextById["page-jump"]).toBe(`Hold ${mod} to see jump targets`); expect(plainTextById["event-jump"]).toBe( - "F shows event and open-time shortcuts", + "f shows event and open-time shortcuts", ); expect(plainTextById["week-day-focus"]).toBe("Shift+M jumps to Monday"); expect(plainTextById.nudge).toBe("Shift and an arrow moves the event"); @@ -46,10 +46,10 @@ describe("getHintPlainText", () => { "Shift+R jumps to Thursday", ); expect(getHintPlainText(weekDayFocusHint("su"))).toBe( - "Shift+S then U jumps to Sunday", + "Shift+S then u jumps to Sunday", ); expect(getHintPlainText(weekDayFocusHint("sa"))).toBe( - "Shift+S then A jumps to Saturday", + "Shift+S then a jumps to Saturday", ); }); diff --git a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts index 5696b3a251..873d1e8732 100644 --- a/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts +++ b/apps/calendar-web/src/shortcuts/tips/shortcut-tips.data.ts @@ -108,12 +108,7 @@ function weekDayFocusParts(prefix: DayJumpPrefix): readonly ShortcutTipPart[] { const [first, second] = prefix; const shifted = { keys: ["Shift", first.toUpperCase()] } as const; if (!second) return [shifted, ` jumps to ${dayName}`]; - return [ - shifted, - " then ", - { key: second.toUpperCase() }, - ` jumps to ${dayName}`, - ]; + return [shifted, " then ", { key: second }, ` jumps to ${dayName}`]; } /** The weekday tip names whichever column the sidebar picked, so it teaches the diff --git a/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx b/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx index 3188cdc988..65132d4d24 100644 --- a/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx +++ b/apps/calendar-web/src/views/Week/components/Header/DayLabels.test.tsx @@ -80,21 +80,21 @@ describe("DayLabels", () => { expect(screen.getByText("Tue")).toBeTruthy(); expect(screen.getByText("Wed")).toBeTruthy(); expect(screen.getByText("Thu")).toBeTruthy(); - expect(screen.queryByText("M")).toBeNull(); - expect(screen.queryByText("R")).toBeNull(); + expect(screen.queryByText("m")).toBeNull(); + expect(screen.queryByText("r")).toBeNull(); }); it("swaps weekday abbreviations for day-jump prefixes while event jump is on", () => { eventJumpActions.setActive(true); renderLabels(); - expect(screen.getByText("M")).toBeTruthy(); - expect(screen.getByText("T")).toBeTruthy(); - expect(screen.getByText("W")).toBeTruthy(); - expect(screen.getByText("R")).toBeTruthy(); + expect(screen.getByText("m")).toBeTruthy(); + expect(screen.getByText("t")).toBeTruthy(); + expect(screen.getByText("w")).toBeTruthy(); + expect(screen.getByText("r")).toBeTruthy(); expect(screen.queryByText("Mon")).toBeNull(); - expect(screen.getByRole("status").textContent).toContain("Monday M"); - expect(screen.getByRole("status").textContent).toContain("Thursday R"); + expect(screen.getByRole("status").textContent).toContain("Monday m"); + expect(screen.getByRole("status").textContent).toContain("Thursday r"); expect(screen.getByRole("status").textContent).toContain( "Type the day key to focus that column.", ); @@ -105,8 +105,8 @@ describe("DayLabels", () => { renderLabels(); // Jump mode is off here, so the column chip advertises the Shift entry. - expect(screen.getByText("⇧M")).toBeTruthy(); - expect(screen.getByText("⇧R")).toBeTruthy(); + expect(screen.getByText("⇧m")).toBeTruthy(); + expect(screen.getByText("⇧r")).toBeTruthy(); expect(screen.queryByText("Mon")).toBeNull(); expect(screen.getByRole("status").textContent).toContain( "Hold Shift and press the day key to focus that column.", diff --git a/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx b/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx index 49956706dd..23064b2b9e 100644 --- a/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx +++ b/apps/calendar-web/src/views/Week/components/Header/DayLabels.tsx @@ -28,7 +28,7 @@ import { GridTimezoneLabel } from "@web/timezone/GridTimezoneLabel"; const dayJumpPrefix = (day: Dayjs) => { const weekday = day.day() as DayJumpWeekday; - return DAY_JUMP_PREFIX_BY_WEEKDAY[weekday].toUpperCase(); + return DAY_JUMP_PREFIX_BY_WEEKDAY[weekday]; }; interface Props { diff --git a/docs/acceptance/shortcuts.md b/docs/acceptance/shortcuts.md index 84523f5bce..bbaa8793de 100644 --- a/docs/acceptance/shortcuts.md +++ b/docs/acceptance/shortcuts.md @@ -307,23 +307,23 @@ After deleting or moving an event, pressing Cmd+Z (Mac) or Ctrl+Z (Windows/Linux ### UX -Pressing `F` shows event-jump chips. Week view chips use day prefixes (`SU`/`M`/`T`/`W`/`R`/`F`/`SA`) plus a per-day index (`W4`, `SU1`). Day view uses numeric chips (`1`, `2`, …). Pressing a day letter highlights that column and focuses its first event, if it has one; an empty column is still selected, so `C`, `Shift+C`, Shift+Arrow, and typed digits (`1400`) create on it. A following digit focuses that index. From idle, a column is entered with Shift and its day letter (`Shift+W`, `Shift+S` then `U`/`A` for the weekend), which always works: bare `T` stays “go to today”, bare `M` stays “open event menu”, and bare `P` focuses the latest notice. Day view keeps bare digits, since `Shift+1` is `!`. `Esc` exits (a second `F` also toggles off). Holding Mod reveals the same day prefixes on week column headers, shown as `⇧W` while jump mode is off. Bare Shift and Shift+Tab do not show jump chips. +Pressing `f` shows event-jump chips. Week view chips use day prefixes (`su`/`m`/`t`/`w`/`r`/`f`/`sa`) plus a per-day index (`w4`, `su1`). Day view uses numeric chips (`1`, `2`, …). Type the lowercase label on a chip to focus that event (`f4`, `w2`). Uppercase letters work the same. Pressing a day letter highlights that column and focuses its first event, if it has one; an empty column is still selected, so `c`, `Shift+c`, Shift+Arrow, and typed digits (`1400`) create on it. A following digit focuses that index. From idle, a column is entered with Shift and its day letter (`Shift+w`, `Shift+s` then `u`/`a` for the weekend), which always works: bare `t` stays “go to today”, bare `m` stays “open event menu”, and bare `p` focuses the latest notice. Day view keeps bare digits, since `Shift+1` is `!`. `Esc` exits jump mode. Holding Mod reveals the same day prefixes on week column headers, shown as `⇧w` while jump mode is off. Bare Shift and Shift+Tab do not show jump chips. ### Steps 1. Navigate to `/week` with timed events on at least two different days. -2. Press `F` once; chips should appear. -3. Press the day letter on a chip (for example `W` for Wednesday), then optionally a digit (`2`) or use arrow keys. +2. Press `f` once; chips should appear. +3. Press the day letter on a chip (for example `w` for Wednesday), then optionally a digit (`2`) or use arrow keys. 4. Press `Esc` to exit. -5. Without pressing `F`, press `Shift+W` (or `Shift+S` then `A`) and confirm the matching event is focused; a following digit still refines to `W2`. +5. Without pressing `f`, press `Shift+w` (or `Shift+s` then `a`) and confirm the matching event is focused; a following digit still refines to `w2`. 6. Press Shift alone or Shift+Tab and confirm jump mode does not activate. ### Expected Results -- Chips appear on events currently visible in the grid when `F` is pressed and stay until Esc. Scrolled-off events keep their jump keys but hide their chips. -- A day letter highlights that column and focuses the first event; digits refine to `Wn`. On a column with no events the highlight still appears, nothing focuses, and `C` or a typed `HHMM` creates there. -- Shift + the day letter enters a column from idle, including while an event is focused; `F` flashes every chip (a second bare `F` exits; bare `F` is not Friday's column letter while jump is on). Weekend columns use `Shift+S` then `A` / `U`. -- Bare `T` still goes to today while jump is off. With an event focused, bare `M` still opens the event menu. With a visible notice, bare `P` still focuses that notice. +- Chips appear on events currently visible in the grid when `f` is pressed and stay until Esc. Labels are lowercase and match the keys you type. Scrolled-off events keep their jump keys but hide their chips. +- A day letter highlights that column and focuses the first event; digits refine to `wn`. On a column with no events the highlight still appears, nothing focuses, and `c` or a typed `HHMM` creates there. +- Shift + the day letter enters a column from idle, including while an event is focused; `f` flashes every chip. While jump is on, `f` is also Friday's column letter (`f` then `4` focuses `f4`). Weekend columns use `Shift+s` then `a` / `u`. +- Bare `t` still goes to today while jump is off. With an event focused, bare `m` still opens the event menu. With a visible notice, bare `p` still focuses that notice. - Arrow keys keep jump mode on so letter-then-arrows works. - Shift alone / Shift+Tab / Shift+J do not toggle jump mode. - While a modal holds the app lock, or focus is in an editable field, `F` and leaderless jump tokens do not activate jump mode. diff --git a/e2e/timed/shift-hold-event-hints.spec.ts b/e2e/timed/shift-hold-event-hints.spec.ts index 574c35d669..185a3c9354 100644 --- a/e2e/timed/shift-hold-event-hints.spec.ts +++ b/e2e/timed/shift-hold-event-hints.spec.ts @@ -12,16 +12,16 @@ import { type CalendarPage = Parameters[0]; const DAY_PREFIX_KEYS: Record = { - SU: ["s", "u"], - M: ["m"], - T: ["t"], - W: ["w"], - R: ["r"], - F: ["f"], - SA: ["s", "a"], + su: ["s", "u"], + m: ["m"], + t: ["t"], + w: ["w"], + r: ["r"], + f: ["f"], + sa: ["s", "a"], }; -const WEEKDAY_PREFIXES = ["SU", "M", "T", "W", "R", "F", "SA"]; +const WEEKDAY_PREFIXES = ["su", "m", "t", "w", "r", "f", "sa"]; /** * Creates a timed event on today via `c`, then nudges it by quarter-hour @@ -197,6 +197,51 @@ test("Shift+Tab does not toggle event jump keys", async ({ page }) => { await expect(shiftHintOverlay(page)).toHaveCount(0); }); +test("lowercase day prefix plus digit targets an event without a typed-time draft", async ({ + page, +}) => { + await prepareTimedCalendarPage(page); + await blurActive(page); + await page.keyboard.press("Escape"); + await expect(shiftHintOverlay(page)).toHaveCount(0); + + const titles: string[] = []; + for (let index = 0; index < 4; index++) { + const title = createEventTitle(`Ordinal Jump ${index + 1}`); + titles.push(title); + await createTimedEventOffsetBy(page, title, index); + } + + await blurActive(page); + await page.keyboard.press("f"); + + const overlay = shiftHintOverlay(page); + await expect(overlay.locator(":scope > span")).toHaveCount(4); + + const labels = await overlay.evaluate((root) => + [...root.querySelectorAll(":scope > span")].map( + (node) => node.textContent?.trim() ?? "", + ), + ); + expect(labels.every((label) => label === label.toLowerCase())).toBe(true); + + const fourthLabel = labels.find((label) => label.endsWith("4")) ?? labels[3]!; + const fourthTitle = titles[3]!; + const fourthButton = page + .locator("#mainGrid") + .getByRole("button", { name: fourthTitle }); + + for (const char of fourthLabel) { + await page.keyboard.press(char); + } + + await expect + .poll(async () => + fourthButton.evaluate((el) => el === document.activeElement), + ) + .toBe(true); +}); + test("fast Shift+J does not toggle event jump keys", async ({ page }) => { await prepareTimedCalendarPage(page); diff --git a/packages/core/src/shortcuts/keymap.ts b/packages/core/src/shortcuts/keymap.ts index 238d57cdb1..6c4b863a6b 100644 --- a/packages/core/src/shortcuts/keymap.ts +++ b/packages/core/src/shortcuts/keymap.ts @@ -41,7 +41,7 @@ export const KEYMAP = { keycaps: ["ArrowLeft", "ArrowUp", "ArrowDown", "ArrowRight"], }, editTitle: { sequence: { leader: "e", second: "t" }, keycaps: ["E", "T"] }, - eventJump: { bareLetter: "f", keycaps: ["F"] }, + eventJump: { bareLetter: "f", keycaps: ["f"] }, moveEvent: { hotkeys: { up: "Shift+ArrowUp",