From ad308252a12ececa58184bb192173f196cba703c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:07:42 -0500 Subject: [PATCH] Accessibility features (#14518) * Add screen reader aria labels to buttons and menu items * Fix sub_label score in search detail dialog --- web/src/components/auth/AuthForm.tsx | 1 + .../components/button/DownloadVideoButton.tsx | 1 + .../components/camera/DebugCameraImage.tsx | 7 +- web/src/components/card/AnimatedEventCard.tsx | 1 + web/src/components/card/ExportCard.tsx | 2 + web/src/components/dynamic/NewReviewData.tsx | 1 + .../filter/CalendarFilterButton.tsx | 3 + .../components/filter/CameraGroupSelector.tsx | 22 ++- .../components/filter/CamerasFilterButton.tsx | 3 + web/src/components/filter/LogLevelFilter.tsx | 6 +- .../components/filter/ReviewActionGroup.tsx | 3 + .../components/filter/ReviewFilterGroup.tsx | 5 + .../components/filter/SearchFilterGroup.tsx | 3 + web/src/components/filter/ZoneMaskFilter.tsx | 1 + web/src/components/icons/IconPicker.tsx | 5 +- web/src/components/input/SaveSearchDialog.tsx | 5 +- web/src/components/menu/AccountSettings.tsx | 1 + web/src/components/menu/GeneralSettings.tsx | 19 +- .../components/menu/SearchResultActions.tsx | 24 ++- web/src/components/mobile/MobilePage.tsx | 1 + .../components/overlay/CameraInfoDialog.tsx | 6 +- .../components/overlay/CreateUserDialog.tsx | 6 +- .../components/overlay/DeleteUserDialog.tsx | 1 + web/src/components/overlay/ExportDialog.tsx | 4 + web/src/components/overlay/GPUInfoDialog.tsx | 26 ++- .../components/overlay/MobileCameraDrawer.tsx | 6 +- .../overlay/MobileReviewSettingsDrawer.tsx | 5 + .../overlay/MobileTimelineDrawer.tsx | 6 +- .../components/overlay/SaveExportOverlay.tsx | 3 + .../components/overlay/SetPasswordDialog.tsx | 1 + .../overlay/detail/AnnotationSettingsPane.tsx | 2 + .../overlay/detail/ObjectLifecycle.tsx | 2 + .../overlay/detail/ReviewDetailDialog.tsx | 1 + .../overlay/detail/SearchDetailDialog.tsx | 19 +- .../overlay/dialog/FrigatePlusDialog.tsx | 8 +- .../overlay/dialog/SearchFilterDialog.tsx | 5 + .../settings/MotionMaskEditPane.tsx | 7 +- .../settings/ObjectMaskEditPane.tsx | 7 +- .../settings/PolygonEditControls.tsx | 2 + web/src/components/settings/PolygonItem.tsx | 7 +- .../components/settings/SearchSettings.tsx | 6 +- web/src/components/settings/ZoneEditPane.tsx | 7 +- web/src/components/ui/calendar-range.tsx | 3 + web/src/components/ui/carousel.tsx | 162 +++++++++--------- web/src/pages/ConfigEditor.tsx | 3 + web/src/pages/Exports.tsx | 1 + web/src/pages/Logs.tsx | 3 + web/src/pages/Settings.tsx | 1 + web/src/views/events/EventView.tsx | 1 + web/src/views/live/LiveBirdseyeView.tsx | 1 + web/src/views/live/LiveCameraView.tsx | 13 +- web/src/views/live/LiveDashboardView.tsx | 3 + web/src/views/recording/RecordingView.tsx | 2 + web/src/views/settings/AuthenticationView.tsx | 3 + web/src/views/settings/CameraSettingsView.tsx | 2 + web/src/views/settings/MasksAndZonesView.tsx | 3 + web/src/views/settings/MotionTunerView.tsx | 7 +- .../settings/NotificationsSettingsView.tsx | 3 + web/src/views/settings/SearchSettingsView.tsx | 3 +- web/src/views/settings/UiSettingsView.tsx | 7 +- web/src/views/system/GeneralMetrics.tsx | 1 + 61 files changed, 358 insertions(+), 115 deletions(-) diff --git a/web/src/components/auth/AuthForm.tsx b/web/src/components/auth/AuthForm.tsx index f3a435828..9daa92966 100644 --- a/web/src/components/auth/AuthForm.tsx +++ b/web/src/components/auth/AuthForm.tsx @@ -121,6 +121,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { variant="select" disabled={isLoading} className="flex flex-1" + aria-label="Login" > {isLoading && } Login diff --git a/web/src/components/button/DownloadVideoButton.tsx b/web/src/components/button/DownloadVideoButton.tsx index 8a8e541fa..5ea1f8465 100644 --- a/web/src/components/button/DownloadVideoButton.tsx +++ b/web/src/components/button/DownloadVideoButton.tsx @@ -46,6 +46,7 @@ export function DownloadVideoButton({ disabled={isDownloading} className="flex items-center gap-2" size="sm" + aria-label="Download Video" > - diff --git a/web/src/components/filter/ReviewActionGroup.tsx b/web/src/components/filter/ReviewActionGroup.tsx index f2d67efd7..833039272 100644 --- a/web/src/components/filter/ReviewActionGroup.tsx +++ b/web/src/components/filter/ReviewActionGroup.tsx @@ -104,6 +104,7 @@ export default function ReviewActionGroup({ {selectedReviews.length == 1 && ( ) : ( diff --git a/web/src/components/input/SaveSearchDialog.tsx b/web/src/components/input/SaveSearchDialog.tsx index c5bf29001..89e9217d7 100644 --- a/web/src/components/input/SaveSearchDialog.tsx +++ b/web/src/components/input/SaveSearchDialog.tsx @@ -59,11 +59,14 @@ export function SaveSearchDialog({ placeholder="Enter a name for your search" /> - + diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx index 1b7470b9b..0bc968061 100644 --- a/web/src/components/menu/AccountSettings.tsx +++ b/web/src/components/menu/AccountSettings.tsx @@ -72,6 +72,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) { className={ isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Log out" > diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index c1c46d87b..0341f2500 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -176,6 +176,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Log out" > @@ -194,6 +195,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex w-full items-center p-2 text-sm" } + aria-label="System metrics" > System metrics @@ -206,6 +208,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex w-full items-center p-2 text-sm" } + aria-label="System logs" > System logs @@ -224,6 +227,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex w-full items-center p-2 text-sm" } + aria-label="Settings" > Settings @@ -236,6 +240,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex w-full items-center p-2 text-sm" } + aria-label="Configuration editor" > Configuration editor @@ -269,6 +274,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Light mode" onClick={() => setTheme("light")} > {theme === "light" ? ( @@ -286,6 +292,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Dark mode" onClick={() => setTheme("dark")} > {theme === "dark" ? ( @@ -303,6 +310,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Use the system settings for light or dark mode" onClick={() => setTheme("system")} > {theme === "system" ? ( @@ -343,6 +351,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label={`Color scheme - ${scheme}`} onClick={() => setColorScheme(scheme)} > {scheme === colorScheme ? ( @@ -370,6 +379,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { className={ isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Frigate documentation" > Documentation @@ -383,6 +393,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { className={ isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Frigate Github" > GitHub @@ -393,6 +404,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { className={ isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" } + aria-label="Restart Frigate" onClick={() => setRestartDialogOpen(true)} > @@ -446,7 +458,12 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {

This page will reload in {countdown} seconds.

- diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index d95acc5a5..8a9373bcc 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -86,7 +86,7 @@ export default function SearchResultActions({ const menuItems = ( <> {searchResult.has_clip && ( - +
)} {searchResult.has_snapshot && ( - + )} - + View object lifecycle {config?.semantic_search?.enabled && isContextMenu && ( - + Find similar @@ -124,12 +130,18 @@ export default function SearchResultActions({ searchResult.has_snapshot && searchResult.end_time && !searchResult.plus_id && ( - setShowFrigatePlus(true)}> + setShowFrigatePlus(true)} + > Submit to Frigate+ )} - setDeleteDialogOpen(true)}> + setDeleteDialogOpen(true)} + > Delete diff --git a/web/src/components/mobile/MobilePage.tsx b/web/src/components/mobile/MobilePage.tsx index 37e54a49c..52bc4d9fe 100644 --- a/web/src/components/mobile/MobilePage.tsx +++ b/web/src/components/mobile/MobilePage.tsx @@ -154,6 +154,7 @@ export function MobilePageHeader({ > diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx index 65741b65e..7d44159dd 100644 --- a/web/src/components/overlay/CreateUserDialog.tsx +++ b/web/src/components/overlay/CreateUserDialog.tsx @@ -98,7 +98,11 @@ export default function CreateUserDialog({ )} /> - diff --git a/web/src/components/overlay/DeleteUserDialog.tsx b/web/src/components/overlay/DeleteUserDialog.tsx index a1c0b2a32..8638b9145 100644 --- a/web/src/components/overlay/DeleteUserDialog.tsx +++ b/web/src/components/overlay/DeleteUserDialog.tsx @@ -27,6 +27,7 @@ export default function DeleteUserDialog({ - + @@ -88,8 +97,17 @@ export default function GPUInfoDialog({ )} - - + diff --git a/web/src/components/overlay/MobileCameraDrawer.tsx b/web/src/components/overlay/MobileCameraDrawer.tsx index 0b450ff32..c12bc0ab2 100644 --- a/web/src/components/overlay/MobileCameraDrawer.tsx +++ b/web/src/components/overlay/MobileCameraDrawer.tsx @@ -23,7 +23,11 @@ export default function MobileCameraDrawer({ return ( - diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index fe0e13c11..d58d485b9 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -132,6 +132,7 @@ export default function MobileReviewSettingsDrawer({ {features.includes("export") && ( diff --git a/web/src/components/overlay/SaveExportOverlay.tsx b/web/src/components/overlay/SaveExportOverlay.tsx index 1ce552a9a..6bb899ed8 100644 --- a/web/src/components/overlay/SaveExportOverlay.tsx +++ b/web/src/components/overlay/SaveExportOverlay.tsx @@ -28,6 +28,7 @@ export default function SaveExportOverlay({ > regenerateDescription("snapshot")} > Regenerate from Snapshot regenerateDescription("thumbnails")} > Regenerate from Thumbnails @@ -495,7 +502,11 @@ function ObjectDetailsTab({ )} )} - @@ -601,6 +612,7 @@ function ObjectSnapshotTab({ <> } + {dialog && ( + + )} diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 949cfd1ac..54799db72 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -466,13 +466,18 @@ export default function ZoneEditPane({ )} />
- diff --git a/web/src/components/ui/carousel.tsx b/web/src/components/ui/carousel.tsx index 9c2b9bf37..7667f4e83 100644 --- a/web/src/components/ui/carousel.tsx +++ b/web/src/components/ui/carousel.tsx @@ -1,43 +1,43 @@ -import * as React from "react" +import * as React from "react"; import useEmblaCarousel, { type UseEmblaCarouselType, -} from "embla-carousel-react" -import { ArrowLeft, ArrowRight } from "lucide-react" +} from "embla-carousel-react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; -type CarouselApi = UseEmblaCarouselType[1] -type UseCarouselParameters = Parameters -type CarouselOptions = UseCarouselParameters[0] -type CarouselPlugin = UseCarouselParameters[1] +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { - opts?: CarouselOptions - plugins?: CarouselPlugin - orientation?: "horizontal" | "vertical" - setApi?: (api: CarouselApi) => void -} + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; type CarouselContextProps = { - carouselRef: ReturnType[0] - api: ReturnType[1] - scrollPrev: () => void - scrollNext: () => void - canScrollPrev: boolean - canScrollNext: boolean -} & CarouselProps + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; -const CarouselContext = React.createContext(null) +const CarouselContext = React.createContext(null); function useCarousel() { - const context = React.useContext(CarouselContext) + const context = React.useContext(CarouselContext); if (!context) { - throw new Error("useCarousel must be used within a ") + throw new Error("useCarousel must be used within a "); } - return context + return context; } const Carousel = React.forwardRef< @@ -54,69 +54,69 @@ const Carousel = React.forwardRef< children, ...props }, - ref + ref, ) => { const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: orientation === "horizontal" ? "x" : "y", }, - plugins - ) - const [canScrollPrev, setCanScrollPrev] = React.useState(false) - const [canScrollNext, setCanScrollNext] = React.useState(false) + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((api: CarouselApi) => { if (!api) { - return + return; } - setCanScrollPrev(api.canScrollPrev()) - setCanScrollNext(api.canScrollNext()) - }, []) + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); const scrollPrev = React.useCallback(() => { - api?.scrollPrev() - }, [api]) + api?.scrollPrev(); + }, [api]); const scrollNext = React.useCallback(() => { - api?.scrollNext() - }, [api]) + api?.scrollNext(); + }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if (event.key === "ArrowLeft") { - event.preventDefault() - scrollPrev() + event.preventDefault(); + scrollPrev(); } else if (event.key === "ArrowRight") { - event.preventDefault() - scrollNext() + event.preventDefault(); + scrollNext(); } }, - [scrollPrev, scrollNext] - ) + [scrollPrev, scrollNext], + ); React.useEffect(() => { if (!api || !setApi) { - return + return; } - setApi(api) - }, [api, setApi]) + setApi(api); + }, [api, setApi]); React.useEffect(() => { if (!api) { - return + return; } - onSelect(api) - api.on("reInit", onSelect) - api.on("select", onSelect) + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); return () => { - api?.off("select", onSelect) - } - }, [api, onSelect]) + api?.off("select", onSelect); + }; + }, [api, onSelect]); return ( - ) - } -) -Carousel.displayName = "Carousel" + ); + }, +); +Carousel.displayName = "Carousel"; const CarouselContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const { carouselRef, orientation } = useCarousel() + const { carouselRef, orientation } = useCarousel(); return (
@@ -161,20 +161,20 @@ const CarouselContent = React.forwardRef< className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", - className + className, )} {...props} />
- ) -}) -CarouselContent.displayName = "CarouselContent" + ); +}); +CarouselContent.displayName = "CarouselContent"; const CarouselItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const { orientation } = useCarousel() + const { orientation } = useCarousel(); return (
- ) -}) -CarouselItem.displayName = "CarouselItem" + ); +}); +CarouselItem.displayName = "CarouselItem"; const CarouselPrevious = React.forwardRef< HTMLButtonElement, React.ComponentProps >(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollPrev, canScrollPrev } = useCarousel() + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); return ( - ) -}) -CarouselPrevious.displayName = "CarouselPrevious" + ); +}); +CarouselPrevious.displayName = "CarouselPrevious"; const CarouselNext = React.forwardRef< HTMLButtonElement, React.ComponentProps >(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollNext, canScrollNext } = useCarousel() + const { orientation, scrollNext, canScrollNext } = useCarousel(); return ( - ) -}) -CarouselNext.displayName = "CarouselNext" + ); +}); +CarouselNext.displayName = "CarouselNext"; export { type CarouselApi, @@ -257,4 +259,4 @@ export { CarouselItem, CarouselPrevious, CarouselNext, -} +}; diff --git a/web/src/pages/ConfigEditor.tsx b/web/src/pages/ConfigEditor.tsx index 857c94900..52cb05473 100644 --- a/web/src/pages/ConfigEditor.tsx +++ b/web/src/pages/ConfigEditor.tsx @@ -192,6 +192,7 @@ function ConfigEditor() { @@ -717,6 +727,7 @@ function PtzControlPanel({ return ( sendPtz(`preset_${preset}`)} > diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index cac604e26..7642d5a0d 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -240,6 +240,7 @@ export default function LiveDashboardView({ ? "bg-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60" : "bg-secondary" }`} + aria-label="Use mobile grid layout" size="xs" onClick={() => setMobileLayout("grid")} > @@ -251,6 +252,7 @@ export default function LiveDashboardView({ ? "bg-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60" : "bg-secondary" }`} + aria-label="Use mobile list layout" size="xs" onClick={() => setMobileLayout("list")} > @@ -267,6 +269,7 @@ export default function LiveDashboardView({ ? "bg-selected text-primary" : "bg-secondary text-secondary-foreground", )} + aria-label="Enter layout editing mode" size="xs" onClick={() => setIsEditMode((prevIsEditMode) => !prevIsEditMode) diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index 535c412d4..374201f7c 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -380,6 +380,7 @@ export function RecordingView({
-
- +
diff --git a/web/src/views/system/GeneralMetrics.tsx b/web/src/views/system/GeneralMetrics.tsx index e6b04f5b7..6e85710bc 100644 --- a/web/src/views/system/GeneralMetrics.tsx +++ b/web/src/views/system/GeneralMetrics.tsx @@ -541,6 +541,7 @@ export default function GeneralMetrics({ {canGetGpuInfo && (