@@ -119,18 +119,22 @@ export function PublicationList({
119119 const [ propertiesOpen , setPropertiesOpen ] = useState ( false ) ;
120120 const listContainerRef = useRef < HTMLDivElement > ( null ) ;
121121
122- const { pushContext , popContext } = useKeyboardContext ( ) ;
122+ const { setActiveContext } = useKeyboardContext ( ) ;
123123
124- // While any toolbar popup is open, push 'dialog' context so publication-list
125- // shortcuts (j/k/up/down/space/enter) don't fire inside them. Radix handles
126- // arrow key / Escape navigation natively. When all popups close, context pops
127- // back to publication-list automatically.
128- useEffect ( ( ) => {
129- if ( filterOpen || sortDropdownOpen || propertiesOpen ) {
130- pushContext ( 'dialog' ) ;
131- return ( ) => popContext ( ) ;
132- }
133- } , [ filterOpen , sortDropdownOpen , propertiesOpen , pushContext , popContext ] ) ;
124+ const releaseToolbarFocus = useCallback ( ( ) => {
125+ requestAnimationFrame ( ( ) => {
126+ if ( document . activeElement instanceof HTMLElement ) {
127+ document . activeElement . blur ( ) ;
128+ }
129+ listContainerRef . current ?. focus ( ) ;
130+ setActiveContext ( 'publication-list' ) ;
131+ } ) ;
132+ } , [ setActiveContext ] ) ;
133+
134+ const handleToolbarCloseAutoFocus = useCallback ( ( event : Event ) => {
135+ event . preventDefault ( ) ;
136+ releaseToolbarFocus ( ) ;
137+ } , [ releaseToolbarFocus ] ) ;
134138
135139 // Calculate tag usage counts
136140 const tagUsageCounts = useMemo ( ( ) => {
@@ -271,7 +275,8 @@ export function PublicationList({
271275 setFilterOpen ( false ) ;
272276 setSortDropdownOpen ( false ) ;
273277 setPropertiesOpen ( false ) ;
274- } , [ ] ) ;
278+ releaseToolbarFocus ( ) ;
279+ } , [ releaseToolbarFocus ] ) ;
275280
276281 const handleKbExport = useCallback (
277282 ( ids : string [ ] ) => {
@@ -290,6 +295,7 @@ export function PublicationList({
290295 onExport : handleKbExport ,
291296 activateOnMount : true ,
292297 bootstrapOnNav : true ,
298+ appWideShortcuts : true ,
293299 containerRef : listContainerRef as React . RefObject < HTMLElement > ,
294300 resetKey : selectedVault ?. id ?? 'all_papers' ,
295301 } ) ;
@@ -328,7 +334,9 @@ export function PublicationList({
328334 const selectedPublications = publications . filter ( ( p ) => selectedIds . has ( p . id ) ) ;
329335
330336
331- // Meta+K / Ctrl+K → focus search (registered through keyboard system)
337+ // Meta+K / Ctrl+K → focus search (registered through keyboard system).
338+ // Escape always closes the currently active toolbar/search affordance first,
339+ // without disturbing the other shortcuts that now work app-wide.
332340 useHotkeys (
333341 'global' ,
334342 [
@@ -342,19 +350,56 @@ export function PublicationList({
342350 } ,
343351 allowInInput : true ,
344352 } ,
353+ {
354+ combo : 'Escape' ,
355+ description : 'Close active search or popup' ,
356+ handler : ( ) => {
357+ if ( document . activeElement === searchInputRef . current ) {
358+ if ( searchQuery ) setSearchQuery ( '' ) ;
359+ releaseToolbarFocus ( ) ;
360+ return true ;
361+ }
362+
363+ if ( propertiesOpen ) {
364+ setPropertiesOpen ( false ) ;
365+ releaseToolbarFocus ( ) ;
366+ return true ;
367+ }
368+
369+ if ( sortDropdownOpen ) {
370+ setSortDropdownOpen ( false ) ;
371+ releaseToolbarFocus ( ) ;
372+ return true ;
373+ }
374+
375+ if ( filterOpen ) {
376+ setFilterOpen ( false ) ;
377+ releaseToolbarFocus ( ) ;
378+ return true ;
379+ }
380+
381+ return false ;
382+ } ,
383+ allowInInput : true ,
384+ } ,
345385 ] ,
346- [ ] ,
386+ [ filterOpen , propertiesOpen , searchQuery , sortDropdownOpen ] ,
347387 ) ;
348388
349- // publication-list context: p, f, s shortcuts
389+ // Publication page shortcuts are unique app-wide; register them as appWide so
390+ // stale activeContext never forces a click before f/s/p/r work. The keyboard
391+ // provider still blocks them while typing in editable fields and while modal
392+ // contexts are active.
350393 useHotkeys (
351394 kbContext ,
352395 [
353396 {
354397 combo : 'p' ,
355398 description : 'Show properties popup' ,
399+ appWide : true ,
356400 handler : ( e ) => {
357401 e . preventDefault ( ) ;
402+ releaseToolbarFocus ( ) ;
358403 setPropertiesOpen ( ( prev ) => ! prev ) ;
359404 setFilterOpen ( false ) ;
360405 setSortDropdownOpen ( false ) ;
@@ -364,8 +409,10 @@ export function PublicationList({
364409 {
365410 combo : 'f' ,
366411 description : 'Show filter popup' ,
412+ appWide : true ,
367413 handler : ( e ) => {
368414 e . preventDefault ( ) ;
415+ releaseToolbarFocus ( ) ;
369416 setFilterOpen ( ( prev ) => ! prev ) ;
370417 setSortDropdownOpen ( false ) ;
371418 setPropertiesOpen ( false ) ;
@@ -375,8 +422,10 @@ export function PublicationList({
375422 {
376423 combo : 's' ,
377424 description : 'Show sort popup' ,
425+ appWide : true ,
378426 handler : ( e ) => {
379427 e . preventDefault ( ) ;
428+ releaseToolbarFocus ( ) ;
380429 setSortDropdownOpen ( ( prev ) => ! prev ) ;
381430 setFilterOpen ( false ) ;
382431 setPropertiesOpen ( false ) ;
@@ -386,6 +435,7 @@ export function PublicationList({
386435 {
387436 combo : 'r' ,
388437 description : 'Discover related papers' ,
438+ appWide : true ,
389439 handler : ( e ) => {
390440 if ( ! onDiscoverRelated || selectedPublications . length === 0 ) return false ;
391441 e . preventDefault ( ) ;
@@ -394,7 +444,7 @@ export function PublicationList({
394444 } ,
395445 } ,
396446 ] ,
397- [ kbContext , onDiscoverRelated , selectedPublications ] ,
447+ [ kbContext , onDiscoverRelated , releaseToolbarFocus , selectedPublications ] ,
398448 ) ;
399449
400450 return (
@@ -574,6 +624,7 @@ export function PublicationList({
574624 onFiltersChange = { setFilters }
575625 open = { filterOpen }
576626 onOpenChange = { setFilterOpen }
627+ onCloseAutoFocus = { handleToolbarCloseAutoFocus }
577628 />
578629 { persistedFilters . length > 0 && (
579630 < span className = "absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full z-10" > </ span >
@@ -602,7 +653,7 @@ export function PublicationList({
602653 ) }
603654 </ Button >
604655 </ DropdownMenuTrigger >
605- < DropdownMenuContent align = "end" className = "font-mono" >
656+ < DropdownMenuContent align = "end" className = "font-mono" onCloseAutoFocus = { handleToolbarCloseAutoFocus } >
606657 < DropdownMenuItem onClick = { ( ) => { setSortBy ( 'created' ) ; setSortDirection ( 'desc' ) ; } } >
607658 recently_added
608659 </ DropdownMenuItem >
@@ -629,6 +680,7 @@ export function PublicationList({
629680 propertiesHint = { < KbdHint shortcut = "p" size = "xs" className = "hidden md:inline-flex !px-1 !py-0.5 !text-[10px] !leading-none !h-4" /> }
630681 propertiesOpen = { propertiesOpen }
631682 onPropertiesOpenChange = { setPropertiesOpen }
683+ onPropertiesCloseAutoFocus = { handleToolbarCloseAutoFocus }
632684 />
633685
634686 { selectedIds . size > 0 && onDiscoverRelated && (
0 commit comments