Browse Source

make feed focus, so that keyboard scrolling works

make feed persistent on page reload
imwald
Silberengel 1 month ago
parent
commit
722d596221
  1. 5
      src/PageManager.tsx
  2. 11
      src/components/BottomNavigationBar/WriteButton.tsx
  3. 266
      src/components/KeyboardShortcutsHelp/index.tsx
  4. 1
      src/components/SearchInput/index.tsx
  5. 13
      src/components/Sidebar/KeyboardShortcutsHelpSidebarButton.tsx
  6. 11
      src/components/Sidebar/PostButton.tsx
  7. 2
      src/components/Sidebar/index.tsx
  8. 23
      src/components/Tabs/index.tsx
  9. 24
      src/i18n/locales/de.ts
  10. 24
      src/i18n/locales/en.ts
  11. 24
      src/layouts/PrimaryPageLayout/index.tsx
  12. 29
      src/layouts/SecondaryPageLayout/index.tsx
  13. 22
      src/lib/keyboard-shortcuts.ts
  14. 14
      src/pages/primary/MePage/index.tsx
  15. 2
      src/pages/primary/NoteListPage/index.tsx
  16. 5
      src/services/post-editor.service.ts

5
src/PageManager.tsx

@ -54,6 +54,7 @@ import { useScreenSize } from './providers/ScreenSizeProvider' @@ -54,6 +54,7 @@ import { useScreenSize } from './providers/ScreenSizeProvider'
import { routes } from './routes'
import modalManager from './services/modal-manager.service'
import CreateWalletGuideToast from './components/CreateWalletGuideToast'
import { KeyboardShortcutsHelpProvider } from '@/components/KeyboardShortcutsHelp'
type TPrimaryPageContext = {
@ -1427,6 +1428,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -1427,6 +1428,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
display: secondaryStack.length === 0
}}
>
<KeyboardShortcutsHelpProvider>
<SecondaryPageContext.Provider
value={{
push: pushSecondaryPage,
@ -1534,6 +1536,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -1534,6 +1536,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
</NotificationProvider>
</CurrentRelaysProvider>
</SecondaryPageContext.Provider>
</KeyboardShortcutsHelpProvider>
</PrimaryPageContext.Provider>
)
}
@ -1546,6 +1549,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -1546,6 +1549,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
display: true
}}
>
<KeyboardShortcutsHelpProvider>
<SecondaryPageContext.Provider
value={{
push: pushSecondaryPage,
@ -1672,6 +1676,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -1672,6 +1676,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
</NotificationProvider>
</CurrentRelaysProvider>
</SecondaryPageContext.Provider>
</KeyboardShortcutsHelpProvider>
</PrimaryPageContext.Provider>
)
}

11
src/components/BottomNavigationBar/WriteButton.tsx

@ -1,13 +1,22 @@ @@ -1,13 +1,22 @@
import PostEditor from '@/components/PostEditor'
import { useNostr } from '@/providers/NostrProvider'
import postEditorService from '@/services/post-editor.service'
import { PencilLine } from 'lucide-react'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import BottomNavigationBarItem from './BottomNavigationBarItem'
export default function WriteButton() {
const { checkLogin } = useNostr()
const [open, setOpen] = useState(false)
useEffect(() => {
const onRequest = () => {
checkLogin(() => setOpen(true))
}
postEditorService.addEventListener('requestOpenNewPost', onRequest)
return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest)
}, [checkLogin])
return (
<>
<BottomNavigationBarItem

266
src/components/KeyboardShortcutsHelp/index.tsx

@ -0,0 +1,266 @@ @@ -0,0 +1,266 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
isRadixDialogOpen,
OPEN_NEW_POST_SHORTCUT_KEY,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
import postEditorService from '@/services/post-editor.service'
import { CircleHelp } from 'lucide-react'
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode
} from 'react'
import { useTranslation } from 'react-i18next'
type KeyboardShortcutsHelpContextValue = {
openHelp: () => void
}
const KeyboardShortcutsHelpContext = createContext<KeyboardShortcutsHelpContextValue | null>(null)
export function useKeyboardShortcutsHelp(): KeyboardShortcutsHelpContextValue {
const ctx = useContext(KeyboardShortcutsHelpContext)
if (!ctx) {
throw new Error('useKeyboardShortcutsHelp must be used within KeyboardShortcutsHelpProvider')
}
return ctx
}
function Kbd({ children }: { children: ReactNode }) {
return (
<kbd className="pointer-events-none inline-flex h-6 min-w-[1.25rem] shrink-0 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[11px] font-medium text-muted-foreground">
{children}
</kbd>
)
}
function KbdRow({ keys, label }: { keys: ReactNode; label: string }) {
return (
<div className="flex flex-col gap-1.5 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
<span className="text-muted-foreground leading-snug">{label}</span>
<div className="flex flex-wrap items-center gap-1 sm:justify-end">{keys}</div>
</div>
)
}
export function KeyboardShortcutsHelpProvider({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false)
const openHelp = useCallback(() => setOpen(true), [])
const { t } = useTranslation()
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (open) return
if (shouldIgnoreKeyboardShortcutEvent(e.target)) return
if (isRadixDialogOpen()) return
const isQuestionMark =
e.key === '?' || (e.shiftKey && e.code === 'Slash' && !e.ctrlKey && !e.metaKey && !e.altKey)
if (isQuestionMark && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault()
setOpen(true)
return
}
if (e.key === 'F1' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault()
setOpen(true)
return
}
if (
e.altKey &&
e.shiftKey &&
e.key.toLowerCase() === OPEN_NEW_POST_SHORTCUT_KEY &&
!e.ctrlKey &&
!e.metaKey
) {
e.preventDefault()
postEditorService.requestOpenNewPost()
}
}
document.addEventListener('keydown', onKeyDown, true)
return () => document.removeEventListener('keydown', onKeyDown, true)
}, [open])
const value = useMemo(() => ({ openHelp }), [openHelp])
return (
<KeyboardShortcutsHelpContext.Provider value={value}>
{children}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg max-h-[min(85vh,32rem)] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('shortcuts.title')}</DialogTitle>
<DialogDescription>{t('shortcuts.intro')}</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-2 text-sm">
<section className="space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t('shortcuts.sectionApp')}
</h3>
<div className="space-y-3">
<KbdRow
label={t('shortcuts.openHelp')}
keys={
<>
<Kbd>?</Kbd>
<span className="text-muted-foreground px-0.5">{t('shortcuts.or')}</span>
<Kbd>F1</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.focusPrimary')}
keys={
<>
<Kbd>Shift</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>Alt</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>F</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.focusSecondary')}
keys={
<>
<Kbd>Shift</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>Alt</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>S</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.newNote')}
keys={
<>
<Kbd>Shift</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>Alt</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>N</Kbd>
</>
}
/>
</div>
</section>
<section className="space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t('shortcuts.sectionSearch')}
</h3>
<div className="space-y-3">
<KbdRow
label={t('shortcuts.searchSuggest')}
keys={
<>
<Kbd></Kbd>
<Kbd></Kbd>
<span className="text-muted-foreground px-1">{t('shortcuts.then')}</span>
<Kbd>Enter</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.searchDismiss')}
keys={<Kbd>Esc</Kbd>}
/>
</div>
</section>
<section className="space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t('shortcuts.sectionStandard')}
</h3>
<div className="space-y-3">
<KbdRow
label={t('shortcuts.tabNavigate')}
keys={
<>
<Kbd>Tab</Kbd>
<span className="text-muted-foreground px-1">{t('shortcuts.or')}</span>
<Kbd>Shift</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd>Tab</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.activate')}
keys={
<>
<Kbd>Enter</Kbd>
<span className="text-muted-foreground px-1">{t('shortcuts.or')}</span>
<Kbd>Space</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.closeOverlays')}
keys={<Kbd>Esc</Kbd>}
/>
<KbdRow
label={t('shortcuts.scrollWhenFocused')}
keys={
<>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd>PgUp</Kbd>
<Kbd>PgDn</Kbd>
<Kbd>Home</Kbd>
<Kbd>End</Kbd>
</>
}
/>
<KbdRow
label={t('shortcuts.browserBack')}
keys={
<>
<Kbd>Alt</Kbd>
<span className="text-muted-foreground">+</span>
<Kbd></Kbd>
</>
}
/>
</div>
</section>
</div>
</DialogContent>
</Dialog>
</KeyboardShortcutsHelpContext.Provider>
)
}
/** Titlebar-sized help control (e.g. home feed, next to profile). */
export function KeyboardShortcutsHelpButton() {
const { openHelp } = useKeyboardShortcutsHelp()
const { t } = useTranslation()
return (
<Button
type="button"
variant="ghost"
size="titlebar-icon"
onClick={() => openHelp()}
title={t('shortcuts.title')}
aria-label={t('shortcuts.title')}
>
<CircleHelp />
</Button>
)
}

1
src/components/SearchInput/index.tsx

@ -22,7 +22,6 @@ const SearchInput = forwardRef<HTMLInputElement, ComponentProps<'input'>>( @@ -22,7 +22,6 @@ const SearchInput = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
return (
<div
tabIndex={0}
className={cn(
'flex h-9 w-full items-center rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors md:text-sm [&:has(:focus-visible)]:ring-ring [&:has(:focus-visible)]:ring-1 [&:has(:focus-visible)]:outline-none',
className

13
src/components/Sidebar/KeyboardShortcutsHelpSidebarButton.tsx

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
import { useKeyboardShortcutsHelp } from '@/components/KeyboardShortcutsHelp'
import { CircleHelp } from 'lucide-react'
import SidebarItem from './SidebarItem'
export default function KeyboardShortcutsHelpSidebarButton() {
const { openHelp } = useKeyboardShortcutsHelp()
return (
<SidebarItem title="shortcuts.title" onClick={openHelp}>
<CircleHelp strokeWidth={2.5} />
</SidebarItem>
)
}

11
src/components/Sidebar/PostButton.tsx

@ -1,13 +1,22 @@ @@ -1,13 +1,22 @@
import PostEditor from '@/components/PostEditor'
import { useNostr } from '@/providers/NostrProvider'
import postEditorService from '@/services/post-editor.service'
import { PencilLine } from 'lucide-react'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import SidebarItem from './SidebarItem'
export default function PostButton() {
const { checkLogin } = useNostr()
const [open, setOpen] = useState(false)
useEffect(() => {
const onRequest = () => {
checkLogin(() => setOpen(true))
}
postEditorService.addEventListener('requestOpenNewPost', onRequest)
return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest)
}, [checkLogin])
return (
<div className="pt-4">
<SidebarItem

2
src/components/Sidebar/index.tsx

@ -2,6 +2,7 @@ import Icon from '@/assets/Icon' @@ -2,6 +2,7 @@ import Icon from '@/assets/Icon'
import Logo from '@/assets/Logo'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import AccountButton from './AccountButton'
import KeyboardShortcutsHelpSidebarButton from './KeyboardShortcutsHelpSidebarButton'
import DiscussionsButton from './DiscussionsButton'
import RelaysButton from './ExploreButton'
import HomeButton from './HomeButton'
@ -44,6 +45,7 @@ export default function PrimaryPageSidebar() { @@ -44,6 +45,7 @@ export default function PrimaryPageSidebar() {
<PostButton />
</div>
<div className="space-y-2">
<KeyboardShortcutsHelpSidebarButton />
<AccountButton />
<PaneModeToggle />
</div>

23
src/components/Tabs/index.tsx

@ -24,7 +24,7 @@ export default function Tabs({ @@ -24,7 +24,7 @@ export default function Tabs({
}) {
const { t } = useTranslation()
const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
const tabRefs = useRef<(HTMLDivElement | null)[]>([])
const tabRefs = useRef<(HTMLButtonElement | null)[]>([])
const containerRef = useRef<HTMLDivElement | null>(null)
const tabsContainerRef = useRef<HTMLDivElement | null>(null)
const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0, top: 0 })
@ -129,13 +129,24 @@ export default function Tabs({ @@ -129,13 +129,24 @@ export default function Tabs({
)}
>
<div className="flex-1 w-0 min-w-0">
<div ref={tabsContainerRef} className="flex relative gap-1 overflow-x-auto scrollbar-hide">
{tabs.map((tab, index) => (
<div
ref={tabsContainerRef}
role="tablist"
className="flex relative gap-1 overflow-x-auto scrollbar-hide"
>
{tabs.map((tab, index) => (
<button
key={tab.value}
ref={(el) => (tabRefs.current[index] = el)}
type="button"
role="tab"
aria-selected={value === tab.value}
ref={(el) => {
tabRefs.current[index] = el
}}
className={cn(
`text-center py-2 px-2 sm:px-4 md:px-6 font-semibold whitespace-nowrap clickable cursor-pointer rounded-lg text-xs sm:text-sm md:text-base shrink-0 flex items-center gap-2 justify-center`,
'text-center py-2 px-2 sm:px-4 md:px-6 font-semibold whitespace-nowrap rounded-lg text-xs sm:text-sm md:text-base shrink-0 flex items-center gap-2 justify-center',
'bg-transparent border-0 shadow-none cursor-pointer transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
value === tab.value ? '' : 'text-muted-foreground'
)}
onClick={() => {
@ -144,7 +155,7 @@ export default function Tabs({ @@ -144,7 +155,7 @@ export default function Tabs({
>
{tab.icon && <span className="shrink-0">{tab.icon}</span>}
{t(tab.label)}
</div>
</button>
))}
<div
className="absolute h-1 bg-primary rounded-full transition-all duration-500"

24
src/i18n/locales/de.ts

@ -596,6 +596,28 @@ export default { @@ -596,6 +596,28 @@ export default {
Connect: 'Verbinden',
'Set up your wallet to send and receive sats!':
'Richte deine Wallet ein, um Sats zu senden und zu empfangen!',
'Set up': 'Einrichten'
'Set up': 'Einrichten',
'shortcuts.title': 'Tastenkürzel',
'shortcuts.intro':
'Kürzel für diese App und übliche Bedienung. Kombinationen: Umschalt+Alt+Taste (unter macOS: Umschalt+Wahltaste); die Reihenfolge der Modifier beim Drücken ist egal.',
'shortcuts.sectionApp': 'Diese App',
'shortcuts.sectionSearch': 'Suchleiste',
'shortcuts.sectionStandard': 'Standard',
'shortcuts.openHelp': 'Diese Hilfe anzeigen',
'shortcuts.or': 'oder',
'shortcuts.then': 'dann',
'shortcuts.focusPrimary':
'Hauptspalte zum Scrollen fokussieren (Desktop; danach Pfeiltasten, Bild auf/ab, Pos1/Ende)',
'shortcuts.focusSecondary':
'Seitenleiste/Panel zum Scrollen fokussieren, wenn geöffnet (Desktop; gleiche Tasten)',
'shortcuts.newNote': 'Neue Notiz / Beitrag (ggf. zuerst anmelden)',
'shortcuts.searchSuggest': 'In den Vorschlägen bewegen',
'shortcuts.searchDismiss': 'Such-Dropdown schließen',
'shortcuts.tabNavigate': 'Fokus zum nächsten oder vorherigen Steuerelement',
'shortcuts.activate': 'Schaltflächen und viele Steuerelemente auslösen',
'shortcuts.closeOverlays': 'Dialoge, Menüs und Such-Dropdown schließen',
'shortcuts.scrollWhenFocused': 'Den fokussierten scrollbaren Bereich scrollen',
'shortcuts.browserBack': 'Zurück im Browser (Verlauf)'
}
}

24
src/i18n/locales/en.ts

@ -677,6 +677,28 @@ export default { @@ -677,6 +677,28 @@ export default {
'Showing {{filtered}} of {{total}} items': 'Showing {{filtered}} of {{total}} items',
'Full': 'Full',
'Compact': 'Compact',
'Expand': 'Expand'
'Expand': 'Expand',
'shortcuts.title': 'Keyboard shortcuts',
'shortcuts.intro':
'Shortcuts for this app and common browsing. Modifier combos are Shift+Alt+key (Option+Shift+key on macOS); either modifier order works when typing.',
'shortcuts.sectionApp': 'This app',
'shortcuts.sectionSearch': 'Search bar',
'shortcuts.sectionStandard': 'Standard',
'shortcuts.openHelp': 'Show this help',
'shortcuts.or': 'or',
'shortcuts.then': 'then',
'shortcuts.focusPrimary':
'Focus main column scroll (desktop; then arrow keys, Page Up/Down, Home/End)',
'shortcuts.focusSecondary':
'Focus side panel scroll when it is open (desktop; same keys to scroll)',
'shortcuts.newNote': 'New note / post (after login if needed)',
'shortcuts.searchSuggest': 'Move through suggestions',
'shortcuts.searchDismiss': 'Close search dropdown',
'shortcuts.tabNavigate': 'Move focus to the next or previous control',
'shortcuts.activate': 'Activate buttons and many controls',
'shortcuts.closeOverlays': 'Close dialogs, menus, and the search dropdown',
'shortcuts.scrollWhenFocused': 'Scroll the focused scrollable area',
'shortcuts.browserBack': 'Browser back (history)'
}
}

24
src/layouts/PrimaryPageLayout/index.tsx

@ -3,6 +3,11 @@ import { Titlebar } from '@/components/Titlebar' @@ -3,6 +3,11 @@ import { Titlebar } from '@/components/Titlebar'
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import {
FOCUS_PRIMARY_SCROLL_SHORTCUT_KEY,
isRadixDialogOpen,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
const PrimaryPageLayout = forwardRef(
@ -69,6 +74,24 @@ const PrimaryPageLayout = forwardRef( @@ -69,6 +74,24 @@ const PrimaryPageLayout = forwardRef(
}
}, [current, isSmallScreen, display])
useEffect(() => {
if (isSmallScreen) return
if (current !== pageName || !display) return
const onKeyDown = (e: KeyboardEvent) => {
if (!e.altKey || !e.shiftKey || e.key.toLowerCase() !== FOCUS_PRIMARY_SCROLL_SHORTCUT_KEY) return
if (e.metaKey || e.ctrlKey) return
if (shouldIgnoreKeyboardShortcutEvent(e.target)) return
if (isRadixDialogOpen()) return
e.preventDefault()
scrollAreaRef.current?.focus({ preventScroll: true })
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [isSmallScreen, current, pageName, display])
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={current === pageName && display}>
@ -101,6 +124,7 @@ const PrimaryPageLayout = forwardRef( @@ -101,6 +124,7 @@ const PrimaryPageLayout = forwardRef(
{subHeader && <div className="shrink-0 bg-background">{subHeader}</div>}
<div
ref={scrollAreaRef}
tabIndex={-1}
className={subHeader ? 'flex-1 min-h-0 overflow-y-auto overflow-x-hidden' : 'absolute top-12 left-0 right-0 bottom-0 overflow-y-auto overflow-x-hidden'}
>
{children}

29
src/layouts/SecondaryPageLayout/index.tsx

@ -1,6 +1,11 @@ @@ -1,6 +1,11 @@
import ScrollToTopButton from '@/components/ScrollToTopButton'
import { Titlebar } from '@/components/Titlebar'
import { Button } from '@/components/ui/button'
import {
FOCUS_SECONDARY_SCROLL_SHORTCUT_KEY,
isRadixDialogOpen,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
import { useSecondaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -57,6 +62,24 @@ const SecondaryPageLayout = forwardRef( @@ -57,6 +62,24 @@ const SecondaryPageLayout = forwardRef(
}
}, [])
useEffect(() => {
if (isSmallScreen) return
if (currentIndex !== index) return
const onKeyDown = (e: KeyboardEvent) => {
if (!e.altKey || !e.shiftKey || e.key.toLowerCase() !== FOCUS_SECONDARY_SCROLL_SHORTCUT_KEY) return
if (e.metaKey || e.ctrlKey) return
if (shouldIgnoreKeyboardShortcutEvent(e.target)) return
if (isRadixDialogOpen()) return
e.preventDefault()
scrollAreaRef.current?.focus({ preventScroll: true })
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [isSmallScreen, currentIndex, index])
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={currentIndex === index}>
@ -107,7 +130,11 @@ const SecondaryPageLayout = forwardRef( @@ -107,7 +130,11 @@ const SecondaryPageLayout = forwardRef(
/>
</>
)}
<div className="flex-1" ref={scrollAreaRef}>
<div
ref={scrollAreaRef}
tabIndex={-1}
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden"
>
{children}
<div className="h-4" />
</div>

22
src/lib/keyboard-shortcuts.ts

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
/** Shared guards for app-wide keyboard shortcuts (help, focus panes, etc.). */
export function isRadixDialogOpen(): boolean {
return !!document.querySelector('[data-radix-dialog-content][data-state="open"]')
}
export function shouldIgnoreKeyboardShortcutEvent(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false
const el = target
const tag = el.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
if (el.isContentEditable) return true
if (el.closest('[contenteditable="true"]')) return true
if (el.closest('.ProseMirror')) return true
if (el.getAttribute('role') === 'textbox') return true
return false
}
export const FOCUS_PRIMARY_SCROLL_SHORTCUT_KEY = 'f'
export const FOCUS_SECONDARY_SCROLL_SHORTCUT_KEY = 's'
/** Shift+Alt+N — open new note composer (handled in KeyboardShortcutsHelpProvider). */
export const OPEN_NEW_POST_SHORTCUT_KEY = 'n'

14
src/pages/primary/MePage/index.tsx

@ -21,7 +21,7 @@ import { @@ -21,7 +21,7 @@ import {
UserRound,
Wallet
} from 'lucide-react'
import { forwardRef, HTMLProps, useState } from 'react'
import { forwardRef, HTMLProps, useState, type KeyboardEvent, type MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
const MePage = forwardRef((_, ref) => {
@ -117,6 +117,8 @@ function Item({ @@ -117,6 +117,8 @@ function Item({
children,
className,
hideChevron = false,
onClick,
onKeyDown,
...props
}: HTMLProps<HTMLDivElement> & { hideChevron?: boolean }) {
return (
@ -126,6 +128,16 @@ function Item({ @@ -126,6 +128,16 @@ function Item({
className
)}
{...props}
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
onKeyDown?.(e)
if (!e.defaultPrevented && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault()
onClick?.(e as unknown as MouseEvent<HTMLDivElement>)
}
}}
>
<div className="flex items-center gap-4">{children}</div>
{!hideChevron && <ChevronRight />}

2
src/pages/primary/NoteListPage/index.tsx

@ -21,6 +21,7 @@ import React, { @@ -21,6 +21,7 @@ import React, {
} from 'react'
import { useTranslation } from 'react-i18next'
import FeedButton from './FeedButton'
import { KeyboardShortcutsHelpButton } from '@/components/KeyboardShortcutsHelp'
import ExploreButton from '@/components/Titlebar/ExploreButton'
import AccountButton from '@/components/Titlebar/AccountButton'
import FollowingFeed from './FollowingFeed'
@ -185,6 +186,7 @@ function NoteListPageTitlebar({ @@ -185,6 +186,7 @@ function NoteListPageTitlebar({
<Info />
</Button>
)}
<KeyboardShortcutsHelpButton />
<AccountButton />
</div>
</div>

5
src/services/post-editor.service.ts

@ -17,6 +17,11 @@ class PostEditorService extends EventTarget { @@ -17,6 +17,11 @@ class PostEditorService extends EventTarget {
this.dispatchEvent(new CustomEvent('closeSuggestionPopup'))
}
}
/** Opens the main “new note” composer (same as sidebar / write button). Listeners run login check. */
requestOpenNewPost() {
this.dispatchEvent(new CustomEvent('requestOpenNewPost'))
}
}
const instance = new PostEditorService()

Loading…
Cancel
Save