mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Make keyboard shortcuts searchable (#2160)
* feat(app): make keyboard shortcuts searchable * test(app): cover shortcut search empty state * fix(app): index shortcut modifier aliases
This commit is contained in:
@@ -81,6 +81,32 @@ test("opens support and release destinations", async ({ page }) => {
|
||||
await expectExternalPage(page, "sidebar-help-changelog", CHANGELOG_DESTINATION);
|
||||
});
|
||||
|
||||
test("searches keyboard shortcuts from the sidebar help menu", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "platform", { get: () => "MacIntel" });
|
||||
});
|
||||
await gotoAppShell(page);
|
||||
await openHelpMenu(page);
|
||||
await page.getByTestId("sidebar-help-shortcuts").click();
|
||||
|
||||
const dialog = page.getByTestId("keyboard-shortcuts-dialog");
|
||||
const search = page.getByPlaceholder("Search shortcuts");
|
||||
|
||||
await search.fill("command+n");
|
||||
await expect(dialog.getByText("New workspace", { exact: true })).toBeVisible();
|
||||
|
||||
await search.fill("interrupt");
|
||||
|
||||
await expect(dialog.getByText("Interrupt agent", { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText("New workspace", { exact: true })).toHaveCount(0);
|
||||
|
||||
await search.fill("no matching shortcut");
|
||||
await expect(dialog.getByText("No results found", { exact: true })).toBeVisible();
|
||||
|
||||
await search.fill("");
|
||||
await expect(dialog.getByText("New workspace", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps diagnostics available from Settings after globalizing the sheet", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
@@ -6,25 +6,91 @@ import { getIsElectronRuntime } from "@/constants/layout";
|
||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import { formatShortcut } from "@/utils/format-shortcut";
|
||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||
import { buildKeyboardShortcutHelpSections } from "@/keyboard/keyboard-shortcuts";
|
||||
|
||||
const SNAP_POINTS: string[] = ["70%", "92%"];
|
||||
|
||||
function shortcutSearchAliases(keys: string[], shortcutOs: "mac" | "non-mac"): string {
|
||||
const aliases = keys.map((key) => {
|
||||
if (shortcutOs === "mac") {
|
||||
if (key === "mod" || key === "meta") return ["cmd", "command"];
|
||||
if (key === "alt") return ["alt", "option"];
|
||||
} else {
|
||||
if (key === "mod" || key === "ctrl") return ["ctrl", "control"];
|
||||
if (key === "meta") return ["win", "windows"];
|
||||
}
|
||||
return [key];
|
||||
});
|
||||
const combinations = aliases.reduce<string[][]>(
|
||||
(prefixes, choices) =>
|
||||
prefixes.flatMap((prefix) => choices.map((choice) => [...prefix, choice])),
|
||||
[[]],
|
||||
);
|
||||
return combinations
|
||||
.flatMap((combination) => [combination.join(" "), combination.join("+")])
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function KeyboardShortcutsDialog() {
|
||||
const { t } = useTranslation();
|
||||
const open = useKeyboardShortcutsStore((s) => s.shortcutsDialogOpen);
|
||||
const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const isMac = getShortcutOs() === "mac";
|
||||
const shortcutOs = getShortcutOs();
|
||||
const isMac = shortcutOs === "mac";
|
||||
const isDesktopApp = getIsElectronRuntime();
|
||||
const sections = useMemo(
|
||||
() => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }),
|
||||
[isDesktopApp, isMac],
|
||||
);
|
||||
const visibleSections = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase();
|
||||
if (!normalizedQuery) return sections;
|
||||
|
||||
return sections.flatMap((section) => {
|
||||
const sectionTitle = t(section.titleKey);
|
||||
if (sectionTitle.toLocaleLowerCase().includes(normalizedQuery)) {
|
||||
return [section];
|
||||
}
|
||||
|
||||
const rows = section.rows.filter((row) => {
|
||||
const searchText = [
|
||||
t(row.labelKey),
|
||||
row.noteKey ? t(row.noteKey) : row.note,
|
||||
row.keys.join(" "),
|
||||
formatShortcut(row.keys, shortcutOs),
|
||||
shortcutSearchAliases(row.keys, shortcutOs),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLocaleLowerCase();
|
||||
return searchText.includes(normalizedQuery);
|
||||
});
|
||||
|
||||
return rows.length > 0 ? [{ ...section, rows }] : [];
|
||||
});
|
||||
}, [query, sections, shortcutOs, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setQuery("");
|
||||
}, [open]);
|
||||
|
||||
const handleClose = useCallback(() => setOpen(false), [setOpen]);
|
||||
const header = useMemo<SheetHeader>(() => ({ title: t("settings.shortcuts.dialogTitle") }), [t]);
|
||||
const header = useMemo<SheetHeader>(
|
||||
() => ({
|
||||
title: t("settings.shortcuts.dialogTitle"),
|
||||
search: {
|
||||
onChange: setQuery,
|
||||
resetKey: Number(open),
|
||||
placeholder: t("settings.shortcuts.searchPlaceholder"),
|
||||
autoFocus: true,
|
||||
},
|
||||
}),
|
||||
[open, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
@@ -35,7 +101,7 @@ export function KeyboardShortcutsDialog() {
|
||||
snapPoints={SNAP_POINTS}
|
||||
>
|
||||
<View testID="keyboard-shortcuts-dialog-content" style={styles.content}>
|
||||
{sections.map((section) => (
|
||||
{visibleSections.map((section) => (
|
||||
<View key={section.title} style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t(section.titleKey)}</Text>
|
||||
<View style={styles.rows}>
|
||||
@@ -53,6 +119,9 @@ export function KeyboardShortcutsDialog() {
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{visibleSections.length === 0 ? (
|
||||
<Text style={styles.empty}>{t("common.empty.noResults")}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
);
|
||||
@@ -102,4 +171,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
rowShortcut: {
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
empty: {
|
||||
paddingVertical: theme.spacing[6],
|
||||
textAlign: "center",
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1713,6 +1713,7 @@ export const ar: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "الاختصارات",
|
||||
searchPlaceholder: "البحث في الاختصارات",
|
||||
unavailableOnMobile: "اختصارات لوحة المفاتيح متاحة فقط على سطح المكتب",
|
||||
capturePrompt: "اضغط على الاختصار...",
|
||||
actions: {
|
||||
|
||||
@@ -1724,6 +1724,7 @@ export const en = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "Shortcuts",
|
||||
searchPlaceholder: "Search shortcuts",
|
||||
unavailableOnMobile: "Keyboard shortcuts are only available on desktop",
|
||||
capturePrompt: "Press shortcut...",
|
||||
actions: {
|
||||
|
||||
@@ -1761,6 +1761,7 @@ export const es: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "Atajos",
|
||||
searchPlaceholder: "Buscar atajos",
|
||||
unavailableOnMobile: "Los atajos de teclado solo están disponibles en el escritorio",
|
||||
capturePrompt: "Presione el acceso directo...",
|
||||
actions: {
|
||||
|
||||
@@ -1762,6 +1762,7 @@ export const fr: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "Raccourcis",
|
||||
searchPlaceholder: "Rechercher des raccourcis",
|
||||
unavailableOnMobile: "Les raccourcis clavier ne sont disponibles que sur le bureau",
|
||||
capturePrompt: "Appuyez sur le raccourci...",
|
||||
actions: {
|
||||
|
||||
@@ -1731,6 +1731,7 @@ export const ja: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "ショートカット",
|
||||
searchPlaceholder: "ショートカットを検索",
|
||||
unavailableOnMobile: "キーボードショートカットはデスクトップでのみ利用できます",
|
||||
capturePrompt: "ショートカットを押してください...",
|
||||
actions: {
|
||||
|
||||
@@ -1745,6 +1745,7 @@ export const ptBR: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "Atalhos",
|
||||
searchPlaceholder: "Pesquisar atalhos",
|
||||
unavailableOnMobile: "Atalhos de teclado estão disponíveis apenas no desktop",
|
||||
capturePrompt: "Pressione o atalho...",
|
||||
actions: {
|
||||
|
||||
@@ -1752,6 +1752,7 @@ export const ru: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "Ярлыки",
|
||||
searchPlaceholder: "Поиск сочетаний клавиш",
|
||||
unavailableOnMobile: "Сочетания клавиш доступны только на рабочем столе.",
|
||||
capturePrompt: "Нажмите ярлык...",
|
||||
actions: {
|
||||
|
||||
@@ -1694,6 +1694,7 @@ export const zhCN: TranslationResources = {
|
||||
},
|
||||
shortcuts: {
|
||||
dialogTitle: "快捷键",
|
||||
searchPlaceholder: "搜索快捷键",
|
||||
unavailableOnMobile: "键盘快捷键仅在桌面端可用",
|
||||
capturePrompt: "按下快捷键...",
|
||||
actions: {
|
||||
|
||||
Reference in New Issue
Block a user