Accessibility features (#14518)

* Add screen reader aria labels to buttons and menu items

* Fix sub_label score in search detail dialog
This commit is contained in:
Josh Hawkins
2024-10-22 17:07:42 -05:00
committed by GitHub
parent c7d9f83638
commit ad308252a1
61 changed files with 358 additions and 115 deletions

View File

@@ -121,6 +121,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Login"
>
{isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
Login

View File

@@ -46,6 +46,7 @@ export function DownloadVideoButton({
disabled={isDownloading}
className="flex items-center gap-2"
size="sm"
aria-label="Download Video"
>
<a
href={source}

View File

@@ -55,7 +55,12 @@ export default function DebugCameraImage({
searchParams={searchParams}
cameraClasses="relative w-full h-full flex justify-center"
/>
<Button onClick={handleToggleSettings} variant="link" size="sm">
<Button
onClick={handleToggleSettings}
variant="link"
size="sm"
aria-label="Settings"
>
<span className="h-5 w-5">
<LuSettings />
</span>{" "}

View File

@@ -121,6 +121,7 @@ export function AnimatedEventCard({
<Button
className="absolute right-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
size="xs"
aria-label="Mark as Reviewed"
onClick={async () => {
await axios.post(`reviews/viewed`, { ids: [event.id] });
updateEvents();

View File

@@ -113,6 +113,7 @@ export default function ExportCard({
/>
<DialogFooter>
<Button
aria-label="Save Export"
size="sm"
variant="select"
disabled={(editName?.update?.length ?? 0) == 0}
@@ -206,6 +207,7 @@ export default function ExportCard({
{!exportedRecording.in_progress && (
<Button
className="absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
aria-label="Play"
variant="ghost"
onClick={() => {
onSelect(exportedRecording);

View File

@@ -36,6 +36,7 @@ export default function NewReviewData({
: "invisible",
"mx-auto mt-5 bg-gray-400 text-center text-white",
)}
aria-label="View new review items"
onClick={() => {
pullLatestData();
if (contentRef.current) {

View File

@@ -34,6 +34,7 @@ export default function CalendarFilterButton({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="Select a date to filter by"
variant={day == undefined ? "default" : "select"}
size="sm"
>
@@ -57,6 +58,7 @@ export default function CalendarFilterButton({
<DropdownMenuSeparator />
<div className="flex items-center justify-center p-2">
<Button
aria-label="Reset"
onClick={() => {
updateSelectedDay(undefined);
}}
@@ -99,6 +101,7 @@ export function CalendarRangeFilterButton({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="Select a date to filter by"
variant={range == undefined ? "default" : "select"}
size="sm"
>

View File

@@ -141,6 +141,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary text-secondary-foreground focus:bg-secondary focus:text-secondary-foreground"
}
aria-label="All Cameras"
size="xs"
onClick={() => (group ? setGroup("default", true) : null)}
onMouseEnter={() => (isDesktop ? showTooltip("default") : null)}
@@ -165,6 +166,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary text-secondary-foreground"
}
aria-label="Camera Group"
size="xs"
onClick={() => setGroup(name, group != "default")}
onMouseEnter={() => (isDesktop ? showTooltip(name) : null)}
@@ -191,6 +193,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
<Button
className="bg-secondary text-muted-foreground"
aria-label="Add camera group"
size="xs"
onClick={() => setAddGroup(true)}
>
@@ -355,6 +358,7 @@ function NewGroupDialog({
"size-6 rounded-md bg-secondary-foreground p-1 text-background",
isMobile && "text-secondary-foreground",
)}
aria-label="Add camera group"
onClick={() => {
setEditState("add");
}}
@@ -536,10 +540,16 @@ export function CameraGroupRow({
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuItem onClick={onEditGroup}>
<DropdownMenuItem
aria-label="Edit group"
onClick={onEditGroup}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}>
<DropdownMenuItem
aria-label="Delete group"
onClick={() => setDeleteDialogOpen(true)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
@@ -793,13 +803,19 @@ export function CameraGroupEdit({
<Separator className="my-2 flex bg-secondary" />
<div className="flex flex-row gap-2 py-5 md:pb-0">
<Button type="button" className="flex flex-1" onClick={onCancel}>
<Button
type="button"
className="flex flex-1"
aria-label="Cancel"
onClick={onCancel}
>
Cancel
</Button>
<Button
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
type="submit"
>
{isLoading ? (

View File

@@ -55,6 +55,7 @@ export function CamerasFilterButton({
const trigger = (
<Button
className="flex items-center gap-2 capitalize"
aria-label="Cameras Filter"
variant={selectedCameras?.length == undefined ? "default" : "select"}
size="sm"
>
@@ -202,6 +203,7 @@ export function CamerasFilterContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
variant="select"
disabled={currentCameras?.length === 0}
onClick={() => {
@@ -212,6 +214,7 @@ export function CamerasFilterContent({
Apply
</Button>
<Button
aria-label="Reset"
onClick={() => {
setCurrentCameras(undefined);
updateCameraFilter(undefined);

View File

@@ -17,7 +17,11 @@ export function LogLevelFilterButton({
updateLabelFilter,
}: LogLevelFilterButtonProps) {
const trigger = (
<Button size="sm" className="flex items-center gap-2">
<Button
size="sm"
className="flex items-center gap-2"
aria-label="Filter log level"
>
<FaFilter className="text-secondary-foreground" />
<div className="hidden text-primary md:block">Filter</div>
</Button>

View File

@@ -104,6 +104,7 @@ export default function ReviewActionGroup({
{selectedReviews.length == 1 && (
<Button
className="flex items-center gap-2 p-2"
aria-label="Export"
size="sm"
onClick={() => {
onExport(selectedReviews[0]);
@@ -116,6 +117,7 @@ export default function ReviewActionGroup({
)}
<Button
className="flex items-center gap-2 p-2"
aria-label="Mark as reviewed"
size="sm"
onClick={onMarkAsReviewed}
>
@@ -124,6 +126,7 @@ export default function ReviewActionGroup({
</Button>
<Button
className="flex items-center gap-2 p-2"
aria-label="Delete"
size="sm"
onClick={handleDelete}
>

View File

@@ -278,6 +278,7 @@ function ShowReviewFilter({
<Button
className="block duration-0 md:hidden"
aria-label="Show reviewed"
variant={showReviewedSwitch ? "select" : "default"}
size="sm"
onClick={() =>
@@ -338,6 +339,7 @@ function GeneralFilterButton({
selectedLabels?.length || selectedZones?.length ? "select" : "default"
}
className="flex items-center gap-2 capitalize"
aria-label="Filter"
>
<FaFilter
className={`${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
@@ -538,6 +540,7 @@ export function GeneralFilterContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
variant="select"
onClick={() => {
if (selectedLabels != currentLabels) {
@@ -554,6 +557,7 @@ export function GeneralFilterContent({
Apply
</Button>
<Button
aria-label="Reset"
onClick={() => {
setCurrentLabels(undefined);
setCurrentZones?.(undefined);
@@ -601,6 +605,7 @@ function ShowMotionOnlyButton({
<Button
size="sm"
className="duration-0"
aria-label="Show Motion Only"
variant={motionOnlyButton ? "select" : "default"}
onClick={() => setMotionOnlyButton(!motionOnlyButton)}
>

View File

@@ -227,6 +227,7 @@ function GeneralFilterButton({
size="sm"
variant={selectedLabels?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
aria-label="Labels"
>
<MdLabel
className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
@@ -336,6 +337,7 @@ export function GeneralFilterContent({
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
aria-label="Apply"
variant="select"
onClick={() => {
if (selectedLabels != currentLabels) {
@@ -348,6 +350,7 @@ export function GeneralFilterContent({
Apply
</Button>
<Button
aria-label="Reset"
onClick={() => {
setCurrentLabels(undefined);
updateLabelFilter(undefined);

View File

@@ -21,6 +21,7 @@ export function ZoneMaskFilterButton({
size="sm"
variant={selectedZoneMask?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
aria-label="Filter by zone mask"
>
<FaFilter
className={`${selectedZoneMask?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}

View File

@@ -66,7 +66,10 @@ export default function IconPicker({
>
<PopoverTrigger asChild>
{!selectedIcon?.name || !selectedIcon?.Icon ? (
<Button className="mt-2 w-full text-muted-foreground">
<Button
className="mt-2 w-full text-muted-foreground"
aria-label="Select an icon"
>
Select an icon
</Button>
) : (

View File

@@ -59,11 +59,14 @@ export function SaveSearchDialog({
placeholder="Enter a name for your search"
/>
<DialogFooter>
<Button onClick={onClose}>Cancel</Button>
<Button aria-label="Cancel" onClick={onClose}>
Cancel
</Button>
<Button
onClick={handleSave}
variant="select"
className="mb-2 md:mb-0"
aria-label="Save this search"
>
Save
</Button>

View File

@@ -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"
>
<a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" />

View File

@@ -176,6 +176,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label="Log out"
>
<a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" />
@@ -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"
>
<LuActivity className="mr-2 size-4" />
<span>System metrics</span>
@@ -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"
>
<LuList className="mr-2 size-4" />
<span>System logs</span>
@@ -224,6 +227,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer"
: "flex w-full items-center p-2 text-sm"
}
aria-label="Settings"
>
<LuSettings className="mr-2 size-4" />
<span>Settings</span>
@@ -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"
>
<LuPenSquare className="mr-2 size-4" />
<span>Configuration editor</span>
@@ -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"
>
<LuLifeBuoy className="mr-2 size-4" />
<span>Documentation</span>
@@ -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"
>
<LuGithub className="mr-2 size-4" />
<span>GitHub</span>
@@ -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)}
>
<LuRotateCw className="mr-2 size-4" />
@@ -446,7 +458,12 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<p>This page will reload in {countdown} seconds.</p>
</SheetDescription>
</SheetHeader>
<Button size="lg" className="mt-5" onClick={handleForceReload}>
<Button
size="lg"
className="mt-5"
aria-label="Force reload now"
onClick={handleForceReload}
>
Force Reload Now
</Button>
</div>

View File

@@ -86,7 +86,7 @@ export default function SearchResultActions({
const menuItems = (
<>
{searchResult.has_clip && (
<MenuItem>
<MenuItem aria-label="Download video">
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
@@ -98,7 +98,7 @@ export default function SearchResultActions({
</MenuItem>
)}
{searchResult.has_snapshot && (
<MenuItem>
<MenuItem aria-label="Download snapshot">
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
@@ -109,12 +109,18 @@ export default function SearchResultActions({
</a>
</MenuItem>
)}
<MenuItem onClick={showObjectLifecycle}>
<MenuItem
aria-label="Show the object lifecycle"
onClick={showObjectLifecycle}
>
<FaArrowsRotate className="mr-2 size-4" />
<span>View object lifecycle</span>
</MenuItem>
{config?.semantic_search?.enabled && isContextMenu && (
<MenuItem onClick={findSimilar}>
<MenuItem
aria-label="Find similar tracked objects"
onClick={findSimilar}
>
<MdImageSearch className="mr-2 size-4" />
<span>Find similar</span>
</MenuItem>
@@ -124,12 +130,18 @@ export default function SearchResultActions({
searchResult.has_snapshot &&
searchResult.end_time &&
!searchResult.plus_id && (
<MenuItem onClick={() => setShowFrigatePlus(true)}>
<MenuItem
aria-label="Submit to Frigate Plus"
onClick={() => setShowFrigatePlus(true)}
>
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
<span>Submit to Frigate+</span>
</MenuItem>
)}
<MenuItem onClick={() => setDeleteDialogOpen(true)}>
<MenuItem
aria-label="Delete this tracked object"
onClick={() => setDeleteDialogOpen(true)}
>
<LuTrash2 className="mr-2 size-4" />
<span>Delete</span>
</MenuItem>

View File

@@ -154,6 +154,7 @@ export function MobilePageHeader({
>
<Button
className="absolute left-0 rounded-lg"
aria-label="Go back"
size="sm"
onClick={handleClose}
>

View File

@@ -167,7 +167,11 @@ export default function CameraInfoDialog({
</div>
<DialogFooter>
<Button variant="select" onClick={() => onCopyFfprobe()}>
<Button
variant="select"
aria-label="Copy"
onClick={() => onCopyFfprobe()}
>
Copy
</Button>
</DialogFooter>

View File

@@ -98,7 +98,11 @@ export default function CreateUserDialog({
)}
/>
<DialogFooter className="mt-4">
<Button variant="select" disabled={isLoading}>
<Button
variant="select"
aria-label="Create user"
disabled={isLoading}
>
{isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
Create User
</Button>

View File

@@ -27,6 +27,7 @@ export default function DeleteUserDialog({
<DialogFooter>
<Button
className="flex items-center gap-1"
aria-label="Confirm delete"
variant="destructive"
size="sm"
onClick={onDelete}

View File

@@ -142,6 +142,7 @@ export default function ExportDialog({
<Trigger asChild>
<Button
className="flex items-center gap-2"
aria-label="Export"
size="sm"
onClick={() => {
const now = new Date(latestTime * 1000);
@@ -307,6 +308,7 @@ export function ExportContent({
</div>
<Button
className={isDesktop ? "" : "w-full"}
aria-label="Select or export"
variant="select"
size="sm"
onClick={() => {
@@ -420,6 +422,7 @@ function CustomTimeSelector({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label="Start time"
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
@@ -485,6 +488,7 @@ function CustomTimeSelector({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label="End time"
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {

View File

@@ -59,8 +59,17 @@ export default function GPUInfoDialog({
<ActivityIndicator />
)}
<DialogFooter>
<Button onClick={() => setShowGpuInfo(false)}>Close</Button>
<Button variant="select" onClick={() => onCopyInfo()}>
<Button
aria-label="Close GPU info"
onClick={() => setShowGpuInfo(false)}
>
Close
</Button>
<Button
aria-label="Copy GPU info"
variant="select"
onClick={() => onCopyInfo()}
>
Copy
</Button>
</DialogFooter>
@@ -88,8 +97,17 @@ export default function GPUInfoDialog({
<ActivityIndicator />
)}
<DialogFooter>
<Button onClick={() => setShowGpuInfo(false)}>Close</Button>
<Button variant="select" onClick={() => onCopyInfo()}>
<Button
aria-label="Close GPU info"
onClick={() => setShowGpuInfo(false)}
>
Close
</Button>
<Button
aria-label="Copy GPU info"
variant="select"
onClick={() => onCopyInfo()}
>
Copy
</Button>
</DialogFooter>

View File

@@ -23,7 +23,11 @@ export default function MobileCameraDrawer({
return (
<Drawer open={cameraDrawer} onOpenChange={setCameraDrawer}>
<DrawerTrigger asChild>
<Button className="rounded-lg capitalize" size="sm">
<Button
className="rounded-lg capitalize"
aria-label="Cameras"
size="sm"
>
<FaVideo className="text-secondary-foreground" />
</Button>
</DrawerTrigger>

View File

@@ -132,6 +132,7 @@ export default function MobileReviewSettingsDrawer({
{features.includes("export") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label="Export"
onClick={() => {
setDrawerMode("export");
setMode("select");
@@ -144,6 +145,7 @@ export default function MobileReviewSettingsDrawer({
{features.includes("calendar") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label="Calendar"
variant={filter?.after ? "select" : "default"}
onClick={() => setDrawerMode("calendar")}
>
@@ -156,6 +158,7 @@ export default function MobileReviewSettingsDrawer({
{features.includes("filter") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label="Filter"
variant={filter?.labels || filter?.zones ? "select" : "default"}
onClick={() => setDrawerMode("filter")}
>
@@ -226,6 +229,7 @@ export default function MobileReviewSettingsDrawer({
<SelectSeparator />
<div className="flex items-center justify-center p-2">
<Button
aria-label="Reset"
onClick={() => {
onUpdateFilter({
...filter,
@@ -306,6 +310,7 @@ export default function MobileReviewSettingsDrawer({
<DrawerTrigger asChild>
<Button
className="rounded-lg capitalize"
aria-label="Filters"
variant={
filter?.labels || filter?.after || filter?.zones
? "select"

View File

@@ -22,7 +22,11 @@ export default function MobileTimelineDrawer({
return (
<Drawer open={drawer} onOpenChange={setDrawer}>
<DrawerTrigger asChild>
<Button className="rounded-lg capitalize" size="sm">
<Button
className="rounded-lg capitalize"
aria-label="Select timeline or events list"
size="sm"
>
<FaFlag className="text-secondary-foreground" />
</Button>
</DrawerTrigger>

View File

@@ -28,6 +28,7 @@ export default function SaveExportOverlay({
>
<Button
className="flex items-center gap-1 text-primary"
aria-label="Cancel"
size="sm"
onClick={onCancel}
>
@@ -36,6 +37,7 @@ export default function SaveExportOverlay({
</Button>
<Button
className="flex items-center gap-1"
aria-label="Preview export"
size="sm"
onClick={onPreview}
>
@@ -44,6 +46,7 @@ export default function SaveExportOverlay({
</Button>
<Button
className="flex items-center gap-1"
aria-label="Save export"
variant="select"
size="sm"
onClick={onSave}

View File

@@ -36,6 +36,7 @@ export default function SetPasswordDialog({
<DialogFooter>
<Button
className="flex items-center gap-1"
aria-label="Save Password"
variant="select"
size="sm"
onClick={() => {

View File

@@ -207,12 +207,14 @@ export function AnnotationSettingsPane({
<div className="flex flex-row gap-2 pt-5">
<Button
className="flex flex-1"
aria-label="Apply"
onClick={form.handleSubmit(onApply)}
>
Apply
</Button>
<Button
variant="select"
aria-label="Save"
disabled={isLoading}
className="flex flex-1"
type="submit"

View File

@@ -242,6 +242,7 @@ export default function ObjectLifecycle({
<div className={cn("flex items-center gap-2")}>
<Button
className="mb-2 mt-3 flex items-center gap-2.5 rounded-lg md:mt-0"
aria-label="Go back"
size="sm"
onClick={() => setPane("overview")}
>
@@ -346,6 +347,7 @@ export default function ObjectLifecycle({
<Button
variant={showControls ? "select" : "default"}
className="size-7 p-1.5"
aria-label="Adjust annotation settings"
>
<LuSettings
className="size-5"

View File

@@ -153,6 +153,7 @@ export default function ReviewDetailDialog({
<Tooltip>
<TooltipTrigger>
<Button
aria-label="Share this review item"
size="sm"
onClick={() =>
shareOrCopy(`${baseUrl}review?id=${review.id}`)

View File

@@ -296,7 +296,7 @@ function ObjectDetailsTab({
}
if (search.sub_label) {
return Math.round((search.data?.top_score ?? 0) * 100);
return Math.round((search.data?.sub_label_score ?? 0) * 100);
} else {
return undefined;
}
@@ -440,6 +440,7 @@ function ObjectDetailsTab({
/>
{config?.semantic_search.enabled && (
<Button
aria-label="Find similar tracked objects"
onClick={() => {
setSearch(undefined);
@@ -466,6 +467,7 @@ function ObjectDetailsTab({
<div className="flex items-center">
<Button
className="rounded-r-none border-r-0"
aria-label="Regenerate tracked object description"
onClick={() => regenerateDescription("thumbnails")}
>
Regenerate
@@ -473,19 +475,24 @@ function ObjectDetailsTab({
{search.has_snapshot && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="rounded-l-none border-l-0 px-2">
<Button
className="rounded-l-none border-l-0 px-2"
aria-label="Expand regeneration menu"
>
<FaChevronDown className="size-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="cursor-pointer"
aria-label="Regenerate from snapshot"
onClick={() => regenerateDescription("snapshot")}
>
Regenerate from Snapshot
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
aria-label="Regenerate from thumbnails"
onClick={() => regenerateDescription("thumbnails")}
>
Regenerate from Thumbnails
@@ -495,7 +502,11 @@ function ObjectDetailsTab({
)}
</div>
)}
<Button variant="select" onClick={updateDescription}>
<Button
variant="select"
aria-label="Save"
onClick={updateDescription}
>
Save
</Button>
</div>
@@ -601,6 +612,7 @@ function ObjectSnapshotTab({
<>
<Button
className="bg-success"
aria-label="Confirm this label for Frigate Plus"
onClick={() => {
setState("uploading");
onSubmitToPlus(false);
@@ -610,6 +622,7 @@ function ObjectSnapshotTab({
</Button>
<Button
className="text-white"
aria-label="Do not confirm this label for Frigate Plus"
variant="destructive"
onClick={() => {
setState("uploading");

View File

@@ -131,9 +131,14 @@ export function FrigatePlusDialog({
<DialogFooter className="flex flex-row justify-end gap-2">
{state == "reviewing" && (
<>
{dialog && <Button onClick={onClose}>Cancel</Button>}
{dialog && (
<Button aria-label="Cancel" onClick={onClose}>
Cancel
</Button>
)}
<Button
className="bg-success"
aria-label="Confirm this label for Frigate Plus"
onClick={() => {
setState("uploading");
onSubmitToPlus(false);
@@ -143,6 +148,7 @@ export function FrigatePlusDialog({
</Button>
<Button
className="text-white"
aria-label="Do not confirm this label for Frigate Plus"
variant="destructive"
onClick={() => {
setState("uploading");

View File

@@ -76,6 +76,7 @@ export default function SearchFilterDialog({
const trigger = (
<Button
className="flex items-center gap-2"
aria-label="More Filters"
size="sm"
variant={moreFiltersSelected ? "select" : "default"}
>
@@ -141,6 +142,7 @@ export default function SearchFilterDialog({
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
aria-label="Apply"
onClick={() => {
if (currentFilter != filter) {
onUpdateFilter(currentFilter);
@@ -152,6 +154,7 @@ export default function SearchFilterDialog({
Apply
</Button>
<Button
aria-label="Reset filters to default values"
onClick={() => {
setCurrentFilter((prevFilter) => ({
...prevFilter,
@@ -256,6 +259,7 @@ function TimeRangeFilterContent({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"} `}
aria-label="Select Start Time"
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
@@ -293,6 +297,7 @@ function TimeRangeFilterContent({
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
aria-label="Select End Time"
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {

View File

@@ -308,11 +308,16 @@ export default function MotionMaskEditPane({
/>
<div className="flex flex-1 flex-col justify-end">
<div className="flex flex-row gap-2 pt-5">
<Button className="flex flex-1" onClick={onCancel}>
<Button
className="flex flex-1"
aria-label="Cancel"
onClick={onCancel}
>
Cancel
</Button>
<Button
variant="select"
aria-label="Save"
disabled={isLoading}
className="flex flex-1"
type="submit"

View File

@@ -335,13 +335,18 @@ export default function ObjectMaskEditPane({
</div>
<div className="flex flex-1 flex-col justify-end">
<div className="flex flex-row gap-2 pt-5">
<Button className="flex flex-1" onClick={onCancel}>
<Button
className="flex flex-1"
aria-label="Cancel"
onClick={onCancel}
>
Cancel
</Button>
<Button
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
type="submit"
>
{isLoading ? (

View File

@@ -74,6 +74,7 @@ export default function PolygonEditControls({
<Button
variant="default"
className="size-6 rounded-md p-1"
aria-label="Remove last point"
disabled={!polygons[activePolygonIndex].points.length}
onClick={undo}
>
@@ -87,6 +88,7 @@ export default function PolygonEditControls({
<Button
variant="default"
className="size-6 rounded-md p-1"
aria-label="Clear all points"
disabled={!polygons[activePolygonIndex].points.length}
onClick={reset}
>

View File

@@ -276,6 +276,7 @@ export default function PolygonItem({
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
aria-label="Edit"
onClick={() => {
setActivePolygonIndex(index);
setEditPane(polygon.type);
@@ -283,10 +284,14 @@ export default function PolygonItem({
>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopyCoordinates(index)}>
<DropdownMenuItem
aria-label="Copy"
onClick={() => handleCopyCoordinates(index)}
>
Copy
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Delete"
disabled={isLoading}
onClick={() => setDeleteDialogOpen(true)}
>

View File

@@ -44,7 +44,11 @@ export default function SearchSettings({
]);
const trigger = (
<Button className="flex items-center gap-2" size="sm">
<Button
className="flex items-center gap-2"
aria-label="Search Settings"
size="sm"
>
<FaCog className="text-secondary-foreground" />
Settings
</Button>

View File

@@ -466,13 +466,18 @@ export default function ZoneEditPane({
)}
/>
<div className="flex flex-row gap-2 pt-5">
<Button className="flex flex-1" onClick={onCancel}>
<Button
className="flex flex-1"
aria-label="Cancel"
onClick={onCancel}
>
Cancel
</Button>
<Button
variant="select"
disabled={isLoading}
className="flex flex-1"
aria-label="Save"
type="submit"
>
{isLoading ? (

View File

@@ -283,6 +283,7 @@ export function DateRangePicker({
}): JSX.Element => (
<Button
className={cn(isSelected && "pointer-events-none text-primary")}
aria-label={label}
variant="ghost"
onClick={() => {
setPreset(preset);
@@ -417,6 +418,7 @@ export function DateRangePicker({
<div className="mx-auto flex w-64 items-center justify-evenly gap-2 py-2">
<Button
variant="select"
aria-label="Apply"
onClick={() => {
setIsOpen(false);
if (
@@ -436,6 +438,7 @@ export function DateRangePicker({
onReset?.();
}}
variant="ghost"
aria-label="Reset"
>
Reset
</Button>

View File

@@ -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<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
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<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext)
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
throw new Error("useCarousel must be used within a <Carousel />");
}
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<HTMLDivElement>) => {
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 (
<CarouselContext.Provider
@@ -143,16 +143,16 @@ const Carousel = React.forwardRef<
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
@@ -161,20 +161,20 @@ const CarouselContent = React.forwardRef<
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
className,
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
);
});
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
const { orientation } = useCarousel();
return (
<div
@@ -184,19 +184,19 @@ const CarouselItem = React.forwardRef<
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
className,
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
);
});
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
@@ -204,12 +204,13 @@ const CarouselPrevious = React.forwardRef<
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
className,
)}
aria-label="Previous slide"
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
@@ -217,15 +218,15 @@ const CarouselPrevious = React.forwardRef<
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
);
});
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
@@ -237,8 +238,9 @@ const CarouselNext = React.forwardRef<
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
className,
)}
aria-label="Next slide"
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
@@ -246,9 +248,9 @@ const CarouselNext = React.forwardRef<
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
);
});
CarouselNext.displayName = "CarouselNext";
export {
type CarouselApi,
@@ -257,4 +259,4 @@ export {
CarouselItem,
CarouselPrevious,
CarouselNext,
}
};