diff --git a/src/components/os/Desktop.tsx b/src/components/os/Desktop.tsx index 7d14299..2a8f7c0 100644 --- a/src/components/os/Desktop.tsx +++ b/src/components/os/Desktop.tsx @@ -25,7 +25,7 @@ import { FolderDialog } from './FolderDialog'; import { FolderWindow } from './FolderWindow'; import { WindowLayer } from './WindowLayer'; import { useWindowManager } from '@/os/useWindowManager'; -import { desktopApps } from '@/os/registry'; +import { desktopApps, getApp } from '@/os/registry'; import { MENUBAR_HEIGHT } from '@/os/layout'; import { swapDesktopSlots, type DesktopSlot, type GridGeometry } from '@/os/iconLayout'; import { useIconLayout } from '@/os/useIconLayout'; @@ -108,15 +108,21 @@ export function Desktop() { [folders], ); + // Screen-reader announcements name entries the way the user sees them. + const entryLabel = useCallback( + (id: string) => (id.startsWith(FOLDER_ID_PREFIX) ? folderTitle(id.slice(FOLDER_ID_PREFIX.length)) : getApp(id)?.title) ?? id, + [folderTitle], + ); + const moveToFolder = useCallback((appId: string, folderId: string) => { setAppFolder(appId, folderId); - setAnnouncement(`${appId} moved into folder ${folderTitle(folderId) ?? folderId}.`); - }, [setAppFolder, folderTitle]); + setAnnouncement(`${entryLabel(appId)} moved into folder ${folderTitle(folderId) ?? folderId}.`); + }, [setAppFolder, folderTitle, entryLabel]); const removeFromFolder = useCallback((appId: string) => { setAppFolder(appId, null); - setAnnouncement(`${appId} moved out of ${folderTitle(membership[appId]) ?? 'its folder'} to the desktop.`); - }, [setAppFolder, folderTitle, membership]); + setAnnouncement(`${entryLabel(appId)} moved out of ${folderTitle(membership[appId]) ?? 'its folder'} to the desktop.`); + }, [setAppFolder, folderTitle, membership, entryLabel]); const deleteFolder = useCallback((folder: Folder) => { removeFolder(folder.id); @@ -133,7 +139,7 @@ export function Desktop() { const id = createFolder(name); if (folderDialog.appId) { setAppFolder(folderDialog.appId, id); - setAnnouncement(`Folder ${name} created with ${folderDialog.appId} inside.`); + setAnnouncement(`Folder ${name} created with ${getApp(folderDialog.appId)?.title ?? folderDialog.appId} inside.`); } else { setAnnouncement(`Folder ${name} created.`); } @@ -159,8 +165,8 @@ export function Desktop() { const move = useCallback((id: string, target: { col: number; row: number }) => { setDesktop((current) => swapDesktopSlots(current, id, target)); const occupied = slots.find((slot) => slot.col === target.col && slot.row === target.row && slot.id !== id); - setAnnouncement(occupied ? `${id} swapped positions with ${occupied.id}.` : `${id} moved to column ${target.col + 1}, row ${target.row + 1}.`); - }, [setDesktop, slots]); + setAnnouncement(occupied ? `${entryLabel(id)} swapped positions with ${entryLabel(occupied.id)}.` : `${entryLabel(id)} moved to column ${target.col + 1}, row ${target.row + 1}.`); + }, [setDesktop, slots, entryLabel]); const onPointerMove = useCallback((event: PointerEvent) => { const active = pointerStart.current; @@ -221,11 +227,11 @@ export function Desktop() { if (picked === id) { setPicked(null); setPickedLayout(null); - setAnnouncement(`${id} dropped at column ${slot.col + 1}, row ${slot.row + 1}.`); + setAnnouncement(`${entryLabel(id)} dropped at column ${slot.col + 1}, row ${slot.row + 1}.`); } else { setPicked(id); setPickedLayout(slots); - setAnnouncement(`${id} picked up. Use arrow keys to move, F to move into a folder, Enter to drop, Escape to cancel.`); + setAnnouncement(`${entryLabel(id)} picked up. Use arrow keys to move, F to move into a folder, Enter to drop, Escape to cancel.`); } return; } @@ -240,7 +246,7 @@ export function Desktop() { setAnnouncement('No folders yet. Right-click an app and choose “New folder with it” to create one.'); return; } - setAnnouncement(`Move ${id} into which folder? Press 1 to ${Math.min(9, folders.length)}: ${folders.slice(0, 9).map((folder, index) => `${index + 1} for ${folder.name}`).join(', ')}.`); + setAnnouncement(`Move ${entryLabel(id)} into which folder? Press 1 to ${Math.min(9, folders.length)}: ${folders.slice(0, 9).map((folder, index) => `${index + 1} for ${folder.name}`).join(', ')}.`); return; } if (/^[1-9]$/.test(event.key) && !id.startsWith(FOLDER_ID_PREFIX)) { diff --git a/src/components/os/MobileAppShell.tsx b/src/components/os/MobileAppShell.tsx index fd7a32e..0408e50 100644 --- a/src/components/os/MobileAppShell.tsx +++ b/src/components/os/MobileAppShell.tsx @@ -227,14 +227,20 @@ function HomeScreen({ onOpen }: { onOpen: (id: string) => void }) { [folders], ); + // Screen-reader announcements name entries the way the user sees them. + const entryLabel = useCallback( + (id: string) => (id.startsWith(FOLDER_ID_PREFIX) ? folderTitle(id.slice(FOLDER_ID_PREFIX.length)) : getApp(id)?.title) ?? id, + [folderTitle], + ); + const moveToFolder = (appId: string, folderId: string) => { setAppFolder(appId, folderId); - setAnnouncement(`${appId} moved into folder ${folderTitle(folderId) ?? folderId}.`); + setAnnouncement(`${entryLabel(appId)} moved into folder ${folderTitle(folderId) ?? folderId}.`); }; const removeFromFolder = (appId: string) => { setAppFolder(appId, null); - setAnnouncement(`${appId} moved out of ${folderTitle(membership[appId]) ?? 'its folder'} to the home screen.`); + setAnnouncement(`${entryLabel(appId)} moved out of ${folderTitle(membership[appId]) ?? 'its folder'} to the home screen.`); }; const deleteFolder = (folder: Folder) => { @@ -264,8 +270,8 @@ function HomeScreen({ onOpen }: { onOpen: (id: string) => void }) { return next; }); const position = beforeId ? Math.max(1, layout.mobile.indexOf(beforeId) + 1) : layout.mobile.length; - setAnnouncement(`${id} moved to position ${position}.`); - }, [layout.mobile, setAnnouncement, setMobile]); + setAnnouncement(`${entryLabel(id)} moved to position ${position}.`); + }, [layout.mobile, setAnnouncement, setMobile, entryLabel]); useEffect(() => { const onMove = (event: PointerEvent) => { @@ -321,11 +327,11 @@ function HomeScreen({ onOpen }: { onOpen: (id: string) => void }) { if (picked === id) { setPicked(null); setPickedOrder(null); - setAnnouncement(`${id} dropped at position ${index + 1}.`); + setAnnouncement(`${entryLabel(id)} dropped at position ${index + 1}.`); } else { setPicked(id); setPickedOrder(layout.mobile); - setAnnouncement(`${id} picked up. Use arrow keys to reorder, F to move into a folder, Enter to drop, Escape to cancel.`); + setAnnouncement(`${entryLabel(id)} picked up. Use arrow keys to reorder, F to move into a folder, Enter to drop, Escape to cancel.`); } return; } @@ -340,7 +346,7 @@ function HomeScreen({ onOpen }: { onOpen: (id: string) => void }) { setAnnouncement('No folders yet. Create one with the button below the grid.'); return; } - setAnnouncement(`Move ${id} into which folder? Press 1 to ${Math.min(9, folders.length)}: ${folders.slice(0, 9).map((folder, folderIndex) => `${folderIndex + 1} for ${folder.name}`).join(', ')}.`); + setAnnouncement(`Move ${entryLabel(id)} into which folder? Press 1 to ${Math.min(9, folders.length)}: ${folders.slice(0, 9).map((folder, folderIndex) => `${folderIndex + 1} for ${folder.name}`).join(', ')}.`); return; } if (/^[1-9]$/.test(event.key) && !id.startsWith(FOLDER_ID_PREFIX)) { @@ -364,7 +370,7 @@ function HomeScreen({ onOpen }: { onOpen: (id: string) => void }) { next.splice(targetIndex, 0, id); return next; }); - setAnnouncement(`${id} moved to position ${nextIndex + 1}.`); + setAnnouncement(`${entryLabel(id)} moved to position ${nextIndex + 1}.`); }; const appContextMenu = (appId: string, title: string, currentFolderId: string | null) => ( @@ -599,7 +605,7 @@ function HomeScreen({ onOpen }: { onOpen: (id: string) => void }) { const id = createFolder(name); if (folderDialog.appId) { setAppFolder(folderDialog.appId, id); - setAnnouncement(`Folder ${name} created with ${folderDialog.appId} inside.`); + setAnnouncement(`Folder ${name} created with ${getApp(folderDialog.appId)?.title ?? folderDialog.appId} inside.`); } else { setAnnouncement(`Folder ${name} created.`); }