33 * License: MS-RSL – see LICENSE.md for details
44 */
55
6+ import type { InputRef } from "antd" ;
67import { Col , Grid , Row , Space } from "antd" ;
78import { Map , Set } from "immutable" ;
8- import { useLayoutEffect , useRef } from "react" ;
9+ import { useCallback , useEffect , useLayoutEffect , useRef } from "react" ;
910import { useIntl } from "react-intl" ;
1011
1112// ensure redux stuff (actions and store) are initialized:
@@ -30,6 +31,7 @@ import { LoadAllProjects } from "./projects-load-all";
3031import { ProjectsOperations } from "./projects-operations" ;
3132import { StarredProjectsBar } from "./projects-starred" ;
3233import { ProjectsTable } from "./projects-table" ;
34+ import type { ProjectsTableHandle } from "./projects-table" ;
3335import { ProjectsTableControls } from "./projects-table-controls" ;
3436import ProjectsPageTour from "./tour" ;
3537import { useBookmarkedProjects } from "./use-bookmarked-projects" ;
@@ -63,6 +65,8 @@ export const ProjectsPage: React.FC = () => {
6365 const createNewRef = useRef < any > ( null ) ;
6466 const projectListRef = useRef < any > ( null ) ;
6567 const filenameSearchRef = useRef < any > ( null ) ;
68+ const searchInputRef = useRef < InputRef > ( null ) ;
69+ const projectsTableRef = useRef < ProjectsTableHandle > ( null ) ;
6670
6771 // Calculating table height
6872 const containerRef = useRef < HTMLDivElement > ( null ) ;
@@ -91,6 +95,49 @@ export const ProjectsPage: React.FC = () => {
9195 string [ ] | null
9296 > ( null ) ;
9397
98+ const focusSearchInput = useCallback ( ( ) => {
99+ const input = searchInputRef . current ;
100+ if ( ! input ) return false ;
101+ input . focus ( { cursor : "end" } ) ;
102+ return true ;
103+ } , [ ] ) ;
104+
105+ const handleSearchNavigate = useCallback ( ( direction : "down" | "up" ) => {
106+ if ( direction === "down" ) {
107+ projectsTableRef . current ?. focusFirstRow ( ) ;
108+ } else {
109+ projectsTableRef . current ?. focusLastRow ( ) ;
110+ }
111+ } , [ ] ) ;
112+
113+ useEffect ( ( ) => {
114+ const handleGlobalShortcut = ( event : KeyboardEvent ) => {
115+ if ( event . defaultPrevented ) return ;
116+ const target = event . target as HTMLElement | null ;
117+ const isEditable =
118+ ! ! target &&
119+ ( target . tagName === "INPUT" ||
120+ target . tagName === "TEXTAREA" ||
121+ target . isContentEditable ) ;
122+ const hasModifier = event . metaKey || event . ctrlKey || event . altKey ;
123+
124+ if ( event . key === "/" && ! hasModifier && ! isEditable ) {
125+ if ( focusSearchInput ( ) ) {
126+ event . preventDefault ( ) ;
127+ }
128+ return ;
129+ }
130+
131+ if ( event . key === "ArrowDown" && ! hasModifier && ! isEditable ) {
132+ projectsTableRef . current ?. focusFirstRow ( ) ;
133+ event . preventDefault ( ) ;
134+ }
135+ } ;
136+
137+ window . addEventListener ( "keydown" , handleGlobalShortcut ) ;
138+ return ( ) => window . removeEventListener ( "keydown" , handleGlobalShortcut ) ;
139+ } , [ focusSearchInput ] ) ;
140+
94141 // if not shown, trigger a re-calculation
95142 const allLoaded = ! ! useTypedRedux (
96143 "projects" ,
@@ -313,6 +360,8 @@ export const ProjectsPage: React.FC = () => {
313360 createNewRef = { createNewRef }
314361 searchRef = { searchRef }
315362 filtersRef = { filtersRef }
363+ searchInputRef = { searchInputRef }
364+ onSearchNavigate = { handleSearchNavigate }
316365 tour = {
317366 < ProjectsPageTour
318367 searchRef = { searchRef }
@@ -341,11 +390,13 @@ export const ProjectsPage: React.FC = () => {
341390 aria-label = { `Projects list (${ visible_projects . length } total)` }
342391 >
343392 < ProjectsTable
393+ ref = { projectsTableRef }
344394 visible_projects = { visible_projects }
345395 height = { tableHeight }
346396 narrow = { narrow }
347397 filteredCollaborators = { filteredCollaborators }
348398 onFilteredCollaboratorsChange = { setFilteredCollaborators }
399+ onRequestSearchFocus = { focusSearchInput }
349400 />
350401 </ div >
351402
0 commit comments