diff --git a/packages/web/src/components/OnboardingChecklist/OnboardingChecklist.tsx b/packages/web/src/components/OnboardingChecklist/OnboardingChecklist.tsx index 2689324f0..f013fca33 100644 --- a/packages/web/src/components/OnboardingChecklist/OnboardingChecklist.tsx +++ b/packages/web/src/components/OnboardingChecklist/OnboardingChecklist.tsx @@ -79,7 +79,7 @@ const ChecklistCard: FC = () => {
  • ); diff --git a/packages/web/src/shortcuts/tips/ShortcutTipParts.tsx b/packages/web/src/shortcuts/tips/ShortcutTipParts.tsx new file mode 100644 index 000000000..e7ca634eb --- /dev/null +++ b/packages/web/src/shortcuts/tips/ShortcutTipParts.tsx @@ -0,0 +1,37 @@ +import { type FC } from "react"; +import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys"; +import { + getPartsPlainText, + type ShortcutTipPart, +} from "@web/shortcuts/tips/shortcut-tips.data"; + +const partKeycaps = ( + part: Exclude, +): readonly string[] => ("keys" in part ? part.keys : [part.key]); + +/** + * Prose with inline keycap chips. The visible chips are aria-hidden; the + * reconstituted sentence is the accessible name. + */ +export const ShortcutTipParts: FC<{ + parts: readonly ShortcutTipPart[]; +}> = ({ parts }) => { + const plainText = getPartsPlainText(parts); + + return ( + + {plainText} + + {parts.map((part, i) => + typeof part === "string" ? ( + // biome-ignore lint/suspicious/noArrayIndexKey: parts are a fixed, order-stable literal + {part} + ) : ( + // biome-ignore lint/suspicious/noArrayIndexKey: parts are a fixed, order-stable literal + + ), + )} + + + ); +}; diff --git a/packages/web/src/shortcuts/tips/shortcut-tips.data.test.ts b/packages/web/src/shortcuts/tips/shortcut-tips.data.test.ts index 70f132021..544ef6d06 100644 --- a/packages/web/src/shortcuts/tips/shortcut-tips.data.test.ts +++ b/packages/web/src/shortcuts/tips/shortcut-tips.data.test.ts @@ -1,4 +1,6 @@ +import { expandModInShortcutDisplay } from "@web/shortcuts/shortcut.util"; import { + getPartsPlainText, getShortcutTips, getTipPlainText, } from "@web/shortcuts/tips/shortcut-tips.data"; @@ -23,4 +25,16 @@ describe("getTipPlainText", () => { "Press Tab to move between start and end", ); }); + + it("joins a chord's keys with + and speaks Mod as Cmd or Ctrl", () => { + const mod = expandModInShortcutDisplay("Mod") === "Meta" ? "Cmd" : "Ctrl"; + expect( + getPartsPlainText([ + "Press ", + { keys: ["Mod", "Z"] }, + " then ", + { keys: ["Mod", "Shift", "Z"] }, + ]), + ).toBe(`Press ${mod}+Z then ${mod}+Shift+Z`); + }); }); diff --git a/packages/web/src/shortcuts/tips/shortcut-tips.data.ts b/packages/web/src/shortcuts/tips/shortcut-tips.data.ts index c8c81bbf2..6238c5e57 100644 --- a/packages/web/src/shortcuts/tips/shortcut-tips.data.ts +++ b/packages/web/src/shortcuts/tips/shortcut-tips.data.ts @@ -1,20 +1,39 @@ +import { expandModInShortcutDisplay } from "@web/shortcuts/shortcut.util"; + export type ShortcutTipId = | "edit-sequence" | "nudge" | "target-event" | "edge-cycle"; -export type ShortcutTipPart = string | { key: string }; +export type ShortcutTipPart = + | string + | { key: string } + | { keys: readonly string[] }; export type ShortcutTip = { id: ShortcutTipId; parts: ShortcutTipPart[]; }; +const spokenKey = (token: string): string => { + const expanded = expandModInShortcutDisplay(token); + if (expanded === "Meta") return "Cmd"; + if (expanded === "Control") return "Ctrl"; + return expanded; +}; + +const partPlainText = (part: ShortcutTipPart): string => { + if (typeof part === "string") return part; + const keys = "keys" in part ? part.keys : [part.key]; + return keys.map(spokenKey).join("+"); +}; + +export const getPartsPlainText = (parts: readonly ShortcutTipPart[]): string => + parts.map(partPlainText).join(""); + export const getTipPlainText = (tip: ShortcutTip): string => - tip.parts - .map((part) => (typeof part === "string" ? part : part.key)) - .join(""); + getPartsPlainText(tip.parts); /** Small fixed rotation; content mirrors the shortcut showcase's later lessons. */ export function getShortcutTips(): ShortcutTip[] {