Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions public/r/data-grid-keyboard-shortcuts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "data-grid-keyboard-shortcuts",
"type": "registry:component",
"title": "Data Grid Keyboard Shortcuts",
"description": "A keyboard shortcuts dialog component for the data grid showing all available shortcuts",
"dependencies": [
"lucide-react"
],
"registryDependencies": [
"button",
"dialog",
"input",
"kbd",
"separator"
],
"files": [
{
"path": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx",
"content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\";\nimport { Separator } from \"@/components/ui/separator\";\n\nconst SHORTCUT_KEY = \"/\";\n\ninterface ShortcutGroup {\n title: string;\n shortcuts: Array<{\n keys: string[];\n description: string;\n }>;\n}\n\ninterface DataGridKeyboardShortcutsProps {\n enableSearch?: boolean;\n}\n\nexport const DataGridKeyboardShortcuts = React.memo(\n DataGridKeyboardShortcutsImpl,\n (prev, next) => {\n return prev.enableSearch === next.enableSearch;\n },\n);\n\nfunction DataGridKeyboardShortcutsImpl({\n enableSearch = false,\n}: DataGridKeyboardShortcutsProps) {\n const [open, setOpen] = React.useState(false);\n const [input, setInput] = React.useState(\"\");\n const inputRef = React.useRef<HTMLInputElement>(null);\n\n const isMac =\n typeof navigator !== \"undefined\"\n ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)\n : false;\n\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n const onOpenChange = React.useCallback((isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen) {\n setInput(\"\");\n }\n }, []);\n\n const onOpenAutoFocus = React.useCallback((event: Event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }, []);\n\n const onInputChange = React.useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n setInput(event.target.value);\n },\n [],\n );\n\n const shortcutGroups: ShortcutGroup[] = React.useMemo(\n () => [\n {\n title: \"Navigation\",\n shortcuts: [\n {\n keys: [\"↑\", \"↓\", \"←\", \"→\"],\n description: \"Navigate between cells\",\n },\n {\n keys: [\"Tab\"],\n description: \"Move to next cell\",\n },\n {\n keys: [\"Shift\", \"Tab\"],\n description: \"Move to previous cell\",\n },\n {\n keys: [\"Home\"],\n description: \"Move to first column\",\n },\n {\n keys: [\"End\"],\n description: \"Move to last column\",\n },\n {\n keys: [modKey, \"Home\"],\n description: \"Move to first cell\",\n },\n {\n keys: [modKey, \"End\"],\n description: \"Move to last cell\",\n },\n {\n keys: [\"PgUp\"],\n description: \"Move up one page\",\n },\n {\n keys: [\"PgDn\"],\n description: \"Move down one page\",\n },\n ],\n },\n {\n title: \"Selection\",\n shortcuts: [\n {\n keys: [\"Shift\", \"↑↓←→\"],\n description: \"Extend selection\",\n },\n {\n keys: [modKey, \"A\"],\n description: \"Select all cells\",\n },\n {\n keys: [modKey, \"Click\"],\n description: \"Toggle cell selection\",\n },\n {\n keys: [\"Shift\", \"Click\"],\n description: \"Select range\",\n },\n {\n keys: [\"Esc\"],\n description: \"Clear selection\",\n },\n ],\n },\n {\n title: \"Editing\",\n shortcuts: [\n {\n keys: [\"Enter\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Double Click\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Delete\"],\n description: \"Clear selected cells\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Clear selected cells\",\n },\n ],\n },\n ...(enableSearch\n ? [\n {\n title: \"Search\",\n shortcuts: [\n {\n keys: [modKey, \"F\"],\n description: \"Open search\",\n },\n {\n keys: [\"Enter\"],\n description: \"Next match\",\n },\n {\n keys: [\"Shift\", \"Enter\"],\n description: \"Previous match\",\n },\n {\n keys: [\"Esc\"],\n description: \"Close search\",\n },\n ],\n },\n ]\n : []),\n {\n title: \"Sorting\",\n shortcuts: [\n {\n keys: [modKey, \"Shift\", \"S\"],\n description: \"Toggle the sort menu\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Remove sort (when focused)\",\n },\n {\n keys: [\"Delete\"],\n description: \"Remove sort (when focused)\",\n },\n ],\n },\n {\n title: \"General\",\n shortcuts: [\n {\n keys: [modKey, \"/\"],\n description: \"Show keyboard shortcuts\",\n },\n ],\n },\n ],\n [modKey, enableSearch],\n );\n\n const filteredGroups = React.useMemo(() => {\n if (!input.trim()) return shortcutGroups;\n\n const query = input.toLowerCase();\n return shortcutGroups\n .map((group) => ({\n ...group,\n shortcuts: group.shortcuts.filter(\n (shortcut) =>\n shortcut.description.toLowerCase().includes(query) ||\n shortcut.keys.some((key) => key.toLowerCase().includes(query)),\n ),\n }))\n .filter((group) => group.shortcuts.length > 0);\n }, [shortcutGroups, input]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if ((event.ctrlKey || event.metaKey) && event.key === SHORTCUT_KEY) {\n event.preventDefault();\n setOpen(true);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", onKeyDown);\n };\n }, []);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className=\"max-w-2xl px-0\"\n onOpenAutoFocus={onOpenAutoFocus}\n showCloseButton={false}\n >\n <DialogClose className=\"absolute top-6 right-6\" asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"size-6\">\n <XIcon />\n </Button>\n </DialogClose>\n <DialogHeader className=\"px-6\">\n <DialogTitle>Keyboard shortcuts</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Use these keyboard shortcuts to navigate and interact with the data\n grid more efficiently.\n </DialogDescription>\n </DialogHeader>\n <div className=\"px-6\">\n <div className=\"relative\">\n <SearchIcon className=\"-translate-y-1/2 absolute top-1/2 left-3 size-3.5 text-muted-foreground\" />\n <Input\n ref={inputRef}\n placeholder=\"Search shortcuts...\"\n className=\"h-8 pl-8\"\n value={input}\n onChange={onInputChange}\n />\n </div>\n </div>\n <Separator className=\"mx-auto data-[orientation=horizontal]:w-[calc(100%-(--spacing(12)))]\" />\n <div className=\"h-[40vh] overflow-y-auto px-6\">\n {filteredGroups.length === 0 ? (\n <div className=\"flex h-full flex-col items-center justify-center gap-3 text-center\">\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground\">\n <SearchIcon className=\"pointer-events-none size-6\" />\n </div>\n <div className=\"flex flex-col gap-1\">\n <div className=\"font-medium text-lg tracking-tight\">\n No shortcuts found\n </div>\n <p className=\"text-muted-foreground text-sm\">\n Try searching for a different term.\n </p>\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-6\">\n {filteredGroups.map((shortcutGroup) => (\n <div key={shortcutGroup.title} className=\"flex flex-col gap-2\">\n <h3 className=\"font-semibold text-foreground text-sm\">\n {shortcutGroup.title}\n </h3>\n <div className=\"divide-y divide-border rounded-md border\">\n {shortcutGroup.shortcuts.map((shortcut, index) => (\n <ShortcutCard\n key={index}\n keys={shortcut.keys}\n description={shortcut.description}\n />\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction ShortcutCard({\n keys,\n description,\n}: ShortcutGroup[\"shortcuts\"][number]) {\n return (\n <div className=\"flex items-center gap-4 px-3 py-2\">\n <span className=\"flex-1 text-sm\">{description}</span>\n <KbdGroup className=\"shrink-0\">\n {keys.map((key, index) => (\n <React.Fragment key={key}>\n {index > 0 && (\n <span className=\"text-muted-foreground text-xs\">+</span>\n )}\n <Kbd>{key}</Kbd>\n </React.Fragment>\n ))}\n </KbdGroup>\n </div>\n );\n}\n",
"type": "registry:component",
"target": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx"
}
]
}
Loading