add skip navigation, page title, and global ARIA live region
This commit is contained in:
10
src/App.tsx
10
src/App.tsx
@@ -13,12 +13,15 @@ import { SettingsDialog } from "@/components/settings/SettingsDialog";
|
||||
import { ToastContainer } from "@/components/toast/ToastContainer";
|
||||
import { ShortcutHelpModal } from "@/components/shortcuts/ShortcutHelpModal";
|
||||
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
|
||||
import { useAnnounceStore } from "@/hooks/useAnnounce";
|
||||
|
||||
export default function App() {
|
||||
const initialized = useAppStore((s) => s.initialized);
|
||||
const init = useAppStore((s) => s.init);
|
||||
const view = useAppStore((s) => s.view);
|
||||
const reduceMotion = useAppStore((s) => s.settings.reduceMotion);
|
||||
const boardTitle = useBoardStore((s) => s.board?.title);
|
||||
const announcement = useAnnounceStore((s) => s.message);
|
||||
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
|
||||
@@ -111,6 +114,10 @@ export default function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = boardTitle ? `${boardTitle} - OpenPylon` : "OpenPylon";
|
||||
}, [boardTitle, view]);
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
setSettingsOpen(true);
|
||||
}, []);
|
||||
@@ -129,6 +136,9 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<MotionConfig reducedMotion={reduceMotion ? "always" : "user"}>
|
||||
<div aria-live="assertive" aria-atomic="true" className="sr-only">
|
||||
{announcement}
|
||||
</div>
|
||||
<AppShell>
|
||||
<AnimatePresence mode="wait">
|
||||
{view.type === "board-list" ? (
|
||||
|
||||
@@ -10,8 +10,14 @@ export function AppShell({ children }: AppShellProps) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex h-screen flex-col bg-pylon-bg">
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-md focus:bg-pylon-accent focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white focus:shadow-lg"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<TopBar />
|
||||
<main className="flex-1 overflow-hidden">{children}</main>
|
||||
<main id="main-content" className="flex-1 overflow-hidden">{children}</main>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
19
src/hooks/useAnnounce.ts
Normal file
19
src/hooks/useAnnounce.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface AnnounceState {
|
||||
message: string;
|
||||
announce: (text: string) => void;
|
||||
}
|
||||
|
||||
export const useAnnounceStore = create<AnnounceState>((set) => ({
|
||||
message: "",
|
||||
announce: (text) => {
|
||||
// Clear first to ensure re-announcement of identical messages
|
||||
set({ message: "" });
|
||||
requestAnimationFrame(() => set({ message: text }));
|
||||
},
|
||||
}));
|
||||
|
||||
export function useAnnounce() {
|
||||
return useAnnounceStore((s) => s.announce);
|
||||
}
|
||||
Reference in New Issue
Block a user