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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 358 additions and 115 deletions

View File

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

View File

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

View File

@ -55,7 +55,12 @@ export default function DebugCameraImage({
searchParams={searchParams} searchParams={searchParams}
cameraClasses="relative w-full h-full flex justify-center" 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"> <span className="h-5 w-5">
<LuSettings /> <LuSettings />
</span>{" "} </span>{" "}

View File

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

View File

@ -113,6 +113,7 @@ export default function ExportCard({
/> />
<DialogFooter> <DialogFooter>
<Button <Button
aria-label="Save Export"
size="sm" size="sm"
variant="select" variant="select"
disabled={(editName?.update?.length ?? 0) == 0} disabled={(editName?.update?.length ?? 0) == 0}
@ -206,6 +207,7 @@ export default function ExportCard({
{!exportedRecording.in_progress && ( {!exportedRecording.in_progress && (
<Button <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" 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" variant="ghost"
onClick={() => { onClick={() => {
onSelect(exportedRecording); onSelect(exportedRecording);

View File

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

View File

@ -34,6 +34,7 @@ export default function CalendarFilterButton({
const trigger = ( const trigger = (
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"
aria-label="Select a date to filter by"
variant={day == undefined ? "default" : "select"} variant={day == undefined ? "default" : "select"}
size="sm" size="sm"
> >
@ -57,6 +58,7 @@ export default function CalendarFilterButton({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="flex items-center justify-center p-2"> <div className="flex items-center justify-center p-2">
<Button <Button
aria-label="Reset"
onClick={() => { onClick={() => {
updateSelectedDay(undefined); updateSelectedDay(undefined);
}} }}
@ -99,6 +101,7 @@ export function CalendarRangeFilterButton({
const trigger = ( const trigger = (
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"
aria-label="Select a date to filter by"
variant={range == undefined ? "default" : "select"} variant={range == undefined ? "default" : "select"}
size="sm" 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-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" : "bg-secondary text-secondary-foreground focus:bg-secondary focus:text-secondary-foreground"
} }
aria-label="All Cameras"
size="xs" size="xs"
onClick={() => (group ? setGroup("default", true) : null)} onClick={() => (group ? setGroup("default", true) : null)}
onMouseEnter={() => (isDesktop ? showTooltip("default") : 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-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary text-secondary-foreground" : "bg-secondary text-secondary-foreground"
} }
aria-label="Camera Group"
size="xs" size="xs"
onClick={() => setGroup(name, group != "default")} onClick={() => setGroup(name, group != "default")}
onMouseEnter={() => (isDesktop ? showTooltip(name) : null)} onMouseEnter={() => (isDesktop ? showTooltip(name) : null)}
@ -191,6 +193,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
<Button <Button
className="bg-secondary text-muted-foreground" className="bg-secondary text-muted-foreground"
aria-label="Add camera group"
size="xs" size="xs"
onClick={() => setAddGroup(true)} onClick={() => setAddGroup(true)}
> >
@ -355,6 +358,7 @@ function NewGroupDialog({
"size-6 rounded-md bg-secondary-foreground p-1 text-background", "size-6 rounded-md bg-secondary-foreground p-1 text-background",
isMobile && "text-secondary-foreground", isMobile && "text-secondary-foreground",
)} )}
aria-label="Add camera group"
onClick={() => { onClick={() => {
setEditState("add"); setEditState("add");
}} }}
@ -536,10 +540,16 @@ export function CameraGroupRow({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={onEditGroup}> <DropdownMenuItem
aria-label="Edit group"
onClick={onEditGroup}
>
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}> <DropdownMenuItem
aria-label="Delete group"
onClick={() => setDeleteDialogOpen(true)}
>
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -793,13 +803,19 @@ export function CameraGroupEdit({
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />
<div className="flex flex-row gap-2 py-5 md:pb-0"> <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 Cancel
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={isLoading} disabled={isLoading}
className="flex flex-1" className="flex flex-1"
aria-label="Save"
type="submit" type="submit"
> >
{isLoading ? ( {isLoading ? (

View File

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

View File

@ -17,7 +17,11 @@ export function LogLevelFilterButton({
updateLabelFilter, updateLabelFilter,
}: LogLevelFilterButtonProps) { }: LogLevelFilterButtonProps) {
const trigger = ( 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" /> <FaFilter className="text-secondary-foreground" />
<div className="hidden text-primary md:block">Filter</div> <div className="hidden text-primary md:block">Filter</div>
</Button> </Button>

View File

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

View File

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

View File

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

View File

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

View File

@ -66,7 +66,10 @@ export default function IconPicker({
> >
<PopoverTrigger asChild> <PopoverTrigger asChild>
{!selectedIcon?.name || !selectedIcon?.Icon ? ( {!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 Select an icon
</Button> </Button>
) : ( ) : (

View File

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

View File

@ -72,6 +72,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
className={ className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
} }
aria-label="Log out"
> >
<a className="flex" href={logoutUrl}> <a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" /> <LuLogOut className="mr-2 size-4" />

View File

@ -176,6 +176,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex items-center p-2 text-sm" : "flex items-center p-2 text-sm"
} }
aria-label="Log out"
> >
<a className="flex" href={logoutUrl}> <a className="flex" href={logoutUrl}>
<LuLogOut className="mr-2 size-4" /> <LuLogOut className="mr-2 size-4" />
@ -194,6 +195,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex w-full items-center p-2 text-sm" : "flex w-full items-center p-2 text-sm"
} }
aria-label="System metrics"
> >
<LuActivity className="mr-2 size-4" /> <LuActivity className="mr-2 size-4" />
<span>System metrics</span> <span>System metrics</span>
@ -206,6 +208,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex w-full items-center p-2 text-sm" : "flex w-full items-center p-2 text-sm"
} }
aria-label="System logs"
> >
<LuList className="mr-2 size-4" /> <LuList className="mr-2 size-4" />
<span>System logs</span> <span>System logs</span>
@ -224,6 +227,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex w-full items-center p-2 text-sm" : "flex w-full items-center p-2 text-sm"
} }
aria-label="Settings"
> >
<LuSettings className="mr-2 size-4" /> <LuSettings className="mr-2 size-4" />
<span>Settings</span> <span>Settings</span>
@ -236,6 +240,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex w-full items-center p-2 text-sm" : "flex w-full items-center p-2 text-sm"
} }
aria-label="Configuration editor"
> >
<LuPenSquare className="mr-2 size-4" /> <LuPenSquare className="mr-2 size-4" />
<span>Configuration editor</span> <span>Configuration editor</span>
@ -269,6 +274,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex items-center p-2 text-sm" : "flex items-center p-2 text-sm"
} }
aria-label="Light mode"
onClick={() => setTheme("light")} onClick={() => setTheme("light")}
> >
{theme === "light" ? ( {theme === "light" ? (
@ -286,6 +292,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex items-center p-2 text-sm" : "flex items-center p-2 text-sm"
} }
aria-label="Dark mode"
onClick={() => setTheme("dark")} onClick={() => setTheme("dark")}
> >
{theme === "dark" ? ( {theme === "dark" ? (
@ -303,6 +310,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex items-center p-2 text-sm" : "flex items-center p-2 text-sm"
} }
aria-label="Use the system settings for light or dark mode"
onClick={() => setTheme("system")} onClick={() => setTheme("system")}
> >
{theme === "system" ? ( {theme === "system" ? (
@ -343,6 +351,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
? "cursor-pointer" ? "cursor-pointer"
: "flex items-center p-2 text-sm" : "flex items-center p-2 text-sm"
} }
aria-label={`Color scheme - ${scheme}`}
onClick={() => setColorScheme(scheme)} onClick={() => setColorScheme(scheme)}
> >
{scheme === colorScheme ? ( {scheme === colorScheme ? (
@ -370,6 +379,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
className={ className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
} }
aria-label="Frigate documentation"
> >
<LuLifeBuoy className="mr-2 size-4" /> <LuLifeBuoy className="mr-2 size-4" />
<span>Documentation</span> <span>Documentation</span>
@ -383,6 +393,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
className={ className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
} }
aria-label="Frigate Github"
> >
<LuGithub className="mr-2 size-4" /> <LuGithub className="mr-2 size-4" />
<span>GitHub</span> <span>GitHub</span>
@ -393,6 +404,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
className={ className={
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm" isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
} }
aria-label="Restart Frigate"
onClick={() => setRestartDialogOpen(true)} onClick={() => setRestartDialogOpen(true)}
> >
<LuRotateCw className="mr-2 size-4" /> <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> <p>This page will reload in {countdown} seconds.</p>
</SheetDescription> </SheetDescription>
</SheetHeader> </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 Force Reload Now
</Button> </Button>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -98,7 +98,11 @@ export default function CreateUserDialog({
)} )}
/> />
<DialogFooter className="mt-4"> <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" />} {isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
Create User Create User
</Button> </Button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,11 @@ export default function MobileTimelineDrawer({
return ( return (
<Drawer open={drawer} onOpenChange={setDrawer}> <Drawer open={drawer} onOpenChange={setDrawer}>
<DrawerTrigger asChild> <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" /> <FaFlag className="text-secondary-foreground" />
</Button> </Button>
</DrawerTrigger> </DrawerTrigger>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,7 +44,11 @@ export default function SearchSettings({
]); ]);
const trigger = ( 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" /> <FaCog className="text-secondary-foreground" />
Settings Settings
</Button> </Button>

View File

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

View File

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

View File

@ -1,43 +1,43 @@
import * as React from "react" import * as React from "react";
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react" import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1] type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel> type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0] type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1] type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = { type CarouselProps = {
opts?: CarouselOptions opts?: CarouselOptions;
plugins?: CarouselPlugin plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical" orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void setApi?: (api: CarouselApi) => void;
} };
type CarouselContextProps = { type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0] carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1] api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void scrollPrev: () => void;
scrollNext: () => void scrollNext: () => void;
canScrollPrev: boolean canScrollPrev: boolean;
canScrollNext: boolean canScrollNext: boolean;
} & CarouselProps } & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null) const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { function useCarousel() {
const context = React.useContext(CarouselContext) const context = React.useContext(CarouselContext);
if (!context) { 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< const Carousel = React.forwardRef<
@ -54,69 +54,69 @@ const Carousel = React.forwardRef<
children, children,
...props ...props
}, },
ref ref,
) => { ) => {
const [carouselRef, api] = useEmblaCarousel( const [carouselRef, api] = useEmblaCarousel(
{ {
...opts, ...opts,
axis: orientation === "horizontal" ? "x" : "y", axis: orientation === "horizontal" ? "x" : "y",
}, },
plugins plugins,
) );
const [canScrollPrev, setCanScrollPrev] = React.useState(false) const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false) const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) { if (!api) {
return return;
} }
setCanScrollPrev(api.canScrollPrev()) setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext()) setCanScrollNext(api.canScrollNext());
}, []) }, []);
const scrollPrev = React.useCallback(() => { const scrollPrev = React.useCallback(() => {
api?.scrollPrev() api?.scrollPrev();
}, [api]) }, [api]);
const scrollNext = React.useCallback(() => { const scrollNext = React.useCallback(() => {
api?.scrollNext() api?.scrollNext();
}, [api]) }, [api]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
event.preventDefault() event.preventDefault();
scrollPrev() scrollPrev();
} else if (event.key === "ArrowRight") { } else if (event.key === "ArrowRight") {
event.preventDefault() event.preventDefault();
scrollNext() scrollNext();
} }
}, },
[scrollPrev, scrollNext] [scrollPrev, scrollNext],
) );
React.useEffect(() => { React.useEffect(() => {
if (!api || !setApi) { if (!api || !setApi) {
return return;
} }
setApi(api) setApi(api);
}, [api, setApi]) }, [api, setApi]);
React.useEffect(() => { React.useEffect(() => {
if (!api) { if (!api) {
return return;
} }
onSelect(api) onSelect(api);
api.on("reInit", onSelect) api.on("reInit", onSelect);
api.on("select", onSelect) api.on("select", onSelect);
return () => { return () => {
api?.off("select", onSelect) api?.off("select", onSelect);
} };
}, [api, onSelect]) }, [api, onSelect]);
return ( return (
<CarouselContext.Provider <CarouselContext.Provider
@ -143,16 +143,16 @@ const Carousel = React.forwardRef<
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
) );
} },
) );
Carousel.displayName = "Carousel" Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef< const CarouselContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel() const { carouselRef, orientation } = useCarousel();
return ( return (
<div ref={carouselRef} className="overflow-hidden"> <div ref={carouselRef} className="overflow-hidden">
@ -161,20 +161,20 @@ const CarouselContent = React.forwardRef<
className={cn( className={cn(
"flex", "flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
}) });
CarouselContent.displayName = "CarouselContent" CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef< const CarouselItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { orientation } = useCarousel() const { orientation } = useCarousel();
return ( return (
<div <div
@ -184,19 +184,19 @@ const CarouselItem = React.forwardRef<
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", "min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === "horizontal" ? "pl-4" : "pt-4",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
CarouselItem.displayName = "CarouselItem" CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef< const CarouselPrevious = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<typeof Button> React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => { >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel() const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( return (
<Button <Button
@ -204,12 +204,13 @@ const CarouselPrevious = React.forwardRef<
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute h-8 w-8 rounded-full", "absolute h-8 w-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2" ? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
aria-label="Previous slide"
disabled={!canScrollPrev} disabled={!canScrollPrev}
onClick={scrollPrev} onClick={scrollPrev}
{...props} {...props}
@ -217,15 +218,15 @@ const CarouselPrevious = React.forwardRef<
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
) );
}) });
CarouselPrevious.displayName = "CarouselPrevious" CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef< const CarouselNext = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<typeof Button> React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => { >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel() const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( return (
<Button <Button
@ -237,8 +238,9 @@ const CarouselNext = React.forwardRef<
orientation === "horizontal" orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2" ? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
aria-label="Next slide"
disabled={!canScrollNext} disabled={!canScrollNext}
onClick={scrollNext} onClick={scrollNext}
{...props} {...props}
@ -246,9 +248,9 @@ const CarouselNext = React.forwardRef<
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>
) );
}) });
CarouselNext.displayName = "CarouselNext" CarouselNext.displayName = "CarouselNext";
export { export {
type CarouselApi, type CarouselApi,
@ -257,4 +259,4 @@ export {
CarouselItem, CarouselItem,
CarouselPrevious, CarouselPrevious,
CarouselNext, CarouselNext,
} };

View File

@ -192,6 +192,7 @@ function ConfigEditor() {
<Button <Button
size="sm" size="sm"
className="flex items-center gap-2" className="flex items-center gap-2"
aria-label="Copy config"
onClick={() => handleCopyConfig()} onClick={() => handleCopyConfig()}
> >
<LuCopy className="text-secondary-foreground" /> <LuCopy className="text-secondary-foreground" />
@ -200,6 +201,7 @@ function ConfigEditor() {
<Button <Button
size="sm" size="sm"
className="flex items-center gap-2" className="flex items-center gap-2"
aria-label="Save and restart"
onClick={() => onHandleSaveConfig("restart")} onClick={() => onHandleSaveConfig("restart")}
> >
<div className="relative size-5"> <div className="relative size-5">
@ -211,6 +213,7 @@ function ConfigEditor() {
<Button <Button
size="sm" size="sm"
className="flex items-center gap-2" className="flex items-center gap-2"
aria-label="Save only without restarting"
onClick={() => onHandleSaveConfig("saveonly")} onClick={() => onHandleSaveConfig("saveonly")}
> >
<LuSave className="text-secondary-foreground" /> <LuSave className="text-secondary-foreground" />

View File

@ -125,6 +125,7 @@ function Exports() {
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<Button <Button
className="text-white" className="text-white"
aria-label="Delete Export"
variant="destructive" variant="destructive"
onClick={() => onHandleDelete()} onClick={() => onHandleDelete()}
> >

View File

@ -339,6 +339,7 @@ function Logs() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
className="flex items-center justify-between gap-2" className="flex items-center justify-between gap-2"
aria-label="Copy logs to clipboard"
size="sm" size="sm"
onClick={handleCopyLogs} onClick={handleCopyLogs}
> >
@ -349,6 +350,7 @@ function Logs() {
</Button> </Button>
<Button <Button
className="flex items-center justify-between gap-2" className="flex items-center justify-between gap-2"
aria-label="Download logs"
size="sm" size="sm"
onClick={handleDownloadLogs} onClick={handleDownloadLogs}
> >
@ -365,6 +367,7 @@ function Logs() {
{initialScroll && !endVisible && ( {initialScroll && !endVisible && (
<Button <Button
className="absolute bottom-8 left-[50%] z-20 flex -translate-x-[50%] items-center gap-1 rounded-md p-2" className="absolute bottom-8 left-[50%] z-20 flex -translate-x-[50%] items-center gap-1 rounded-md p-2"
aria-label="Jump to bottom of logs"
onClick={() => onClick={() =>
contentRef.current?.scrollTo({ contentRef.current?.scrollTo({
top: contentRef.current?.scrollHeight, top: contentRef.current?.scrollHeight,

View File

@ -252,6 +252,7 @@ function CameraSelectButton({
const trigger = ( const trigger = (
<Button <Button
className="flex items-center gap-2 bg-selected capitalize hover:bg-selected" className="flex items-center gap-2 bg-selected capitalize hover:bg-selected"
aria-label="Select a camera"
size="sm" size="sm"
> >
<FaVideo className="text-background dark:text-primary" /> <FaVideo className="text-background dark:text-primary" />

View File

@ -737,6 +737,7 @@ function DetectionReview({
<div className="col-span-full flex items-center justify-center"> <div className="col-span-full flex items-center justify-center">
<Button <Button
className="text-white" className="text-white"
aria-label="Mark these items as reviewed"
variant="select" variant="select"
onClick={() => { onClick={() => {
setSelectedReviews([]); setSelectedReviews([]);

View File

@ -144,6 +144,7 @@ export default function LiveBirdseyeView({
{!fullscreen ? ( {!fullscreen ? (
<Button <Button
className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`} className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
aria-label="Go Back"
size={isMobile ? "icon" : "sm"} size={isMobile ? "icon" : "sm"}
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >

View File

@ -352,6 +352,7 @@ export default function LiveCameraView({
> >
<Button <Button
className={`flex items-center gap-2.5 rounded-lg`} className={`flex items-center gap-2.5 rounded-lg`}
aria-label="Go back"
size="sm" size="sm"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
@ -360,6 +361,7 @@ export default function LiveCameraView({
</Button> </Button>
<Button <Button
className="flex items-center gap-2.5 rounded-lg" className="flex items-center gap-2.5 rounded-lg"
aria-label="Show historical footage"
size="sm" size="sm"
onClick={() => { onClick={() => {
navigate("review", { navigate("review", {
@ -388,6 +390,7 @@ export default function LiveCameraView({
{fullscreen && ( {fullscreen && (
<Button <Button
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary" className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
aria-label="Go back"
size="sm" size="sm"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
@ -603,6 +606,7 @@ function PtzControlPanel({
{ptz?.features?.includes("pt") && ( {ptz?.features?.includes("pt") && (
<> <>
<Button <Button
aria-label="Move PTZ camera to the left"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_LEFT"); sendPtz("MOVE_LEFT");
@ -617,6 +621,7 @@ function PtzControlPanel({
<FaAngleLeft /> <FaAngleLeft />
</Button> </Button>
<Button <Button
aria-label="Move PTZ camera up"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_UP"); sendPtz("MOVE_UP");
@ -631,6 +636,7 @@ function PtzControlPanel({
<FaAngleUp /> <FaAngleUp />
</Button> </Button>
<Button <Button
aria-label="Move PTZ camera down"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_DOWN"); sendPtz("MOVE_DOWN");
@ -645,6 +651,7 @@ function PtzControlPanel({
<FaAngleDown /> <FaAngleDown />
</Button> </Button>
<Button <Button
aria-label="Move PTZ camera to the right"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("MOVE_RIGHT"); sendPtz("MOVE_RIGHT");
@ -663,6 +670,7 @@ function PtzControlPanel({
{ptz?.features?.includes("zoom") && ( {ptz?.features?.includes("zoom") && (
<> <>
<Button <Button
aria-label="Zoom PTZ camera in"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("ZOOM_IN"); sendPtz("ZOOM_IN");
@ -677,6 +685,7 @@ function PtzControlPanel({
<MdZoomIn /> <MdZoomIn />
</Button> </Button>
<Button <Button
aria-label="Zoom PTZ camera out"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
sendPtz("ZOOM_OUT"); sendPtz("ZOOM_OUT");
@ -696,6 +705,7 @@ function PtzControlPanel({
<> <>
<Button <Button
className={`${clickOverlay ? "text-selected" : "text-primary"}`} className={`${clickOverlay ? "text-selected" : "text-primary"}`}
aria-label="Click in the frame to center the PTZ camera"
onClick={() => setClickOverlay(!clickOverlay)} onClick={() => setClickOverlay(!clickOverlay)}
> >
<TbViewfinder /> <TbViewfinder />
@ -705,7 +715,7 @@ function PtzControlPanel({
{(ptz?.presets?.length ?? 0) > 0 && ( {(ptz?.presets?.length ?? 0) > 0 && (
<DropdownMenu modal={!isDesktop}> <DropdownMenu modal={!isDesktop}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button> <Button aria-label="PTZ camera presets">
<BsThreeDotsVertical /> <BsThreeDotsVertical />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -717,6 +727,7 @@ function PtzControlPanel({
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={preset} key={preset}
aria-label={preset}
className="cursor-pointer" className="cursor-pointer"
onSelect={() => sendPtz(`preset_${preset}`)} onSelect={() => sendPtz(`preset_${preset}`)}
> >

View File

@ -240,6 +240,7 @@ export default function LiveDashboardView({
? "bg-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60" ? "bg-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary" : "bg-secondary"
}`} }`}
aria-label="Use mobile grid layout"
size="xs" size="xs"
onClick={() => setMobileLayout("grid")} 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-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60"
: "bg-secondary" : "bg-secondary"
}`} }`}
aria-label="Use mobile list layout"
size="xs" size="xs"
onClick={() => setMobileLayout("list")} onClick={() => setMobileLayout("list")}
> >
@ -267,6 +269,7 @@ export default function LiveDashboardView({
? "bg-selected text-primary" ? "bg-selected text-primary"
: "bg-secondary text-secondary-foreground", : "bg-secondary text-secondary-foreground",
)} )}
aria-label="Enter layout editing mode"
size="xs" size="xs"
onClick={() => onClick={() =>
setIsEditMode((prevIsEditMode) => !prevIsEditMode) setIsEditMode((prevIsEditMode) => !prevIsEditMode)

View File

@ -380,6 +380,7 @@ export function RecordingView({
<div className={cn("flex items-center gap-2")}> <div className={cn("flex items-center gap-2")}>
<Button <Button
className="flex items-center gap-2.5 rounded-lg" className="flex items-center gap-2.5 rounded-lg"
aria-label="Go back"
size="sm" size="sm"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
@ -388,6 +389,7 @@ export function RecordingView({
</Button> </Button>
<Button <Button
className="flex items-center gap-2.5 rounded-lg" className="flex items-center gap-2.5 rounded-lg"
aria-label="Go to the main camera live view"
size="sm" size="sm"
onClick={() => { onClick={() => {
navigate(`/#${mainCamera}`); navigate(`/#${mainCamera}`);

View File

@ -95,6 +95,7 @@ export default function AuthenticationView() {
</Heading> </Heading>
<Button <Button
className="flex items-center gap-1" className="flex items-center gap-1"
aria-label="Add a new user"
variant="default" variant="default"
onClick={() => { onClick={() => {
setShowCreate(true); setShowCreate(true);
@ -114,6 +115,7 @@ export default function AuthenticationView() {
<div className="flex flex-1 justify-end space-x-2"> <div className="flex flex-1 justify-end space-x-2">
<Button <Button
className="flex items-center gap-1" className="flex items-center gap-1"
aria-label="Update the user's password"
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setShowSetPassword(true); setShowSetPassword(true);
@ -125,6 +127,7 @@ export default function AuthenticationView() {
</Button> </Button>
<Button <Button
className="flex items-center gap-1" className="flex items-center gap-1"
aria-label="Delete the user"
variant="destructive" variant="destructive"
onClick={() => { onClick={() => {
setShowDelete(true); setShowDelete(true);

View File

@ -475,6 +475,7 @@ export default function CameraSettingsView({
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]"> <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button <Button
className="flex flex-1" className="flex flex-1"
aria-label="Cancel"
onClick={onCancel} onClick={onCancel}
type="button" type="button"
> >
@ -484,6 +485,7 @@ export default function CameraSettingsView({
variant="select" variant="select"
disabled={isLoading} disabled={isLoading}
className="flex flex-1" className="flex flex-1"
aria-label="Save"
type="submit" type="submit"
> >
{isLoading ? ( {isLoading ? (

View File

@ -459,6 +459,7 @@ export default function MasksAndZonesView({
<Button <Button
variant="secondary" variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background" className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label="Add a new zone"
onClick={() => { onClick={() => {
setEditPane("zone"); setEditPane("zone");
handleNewPolygon("zone"); handleNewPolygon("zone");
@ -527,6 +528,7 @@ export default function MasksAndZonesView({
<Button <Button
variant="secondary" variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background" className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label="Add a new motion mask"
onClick={() => { onClick={() => {
setEditPane("motion_mask"); setEditPane("motion_mask");
handleNewPolygon("motion_mask"); handleNewPolygon("motion_mask");
@ -596,6 +598,7 @@ export default function MasksAndZonesView({
<Button <Button
variant="secondary" variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background" className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label="Add a new object mask"
onClick={() => { onClick={() => {
setEditPane("object_mask"); setEditPane("object_mask");
handleNewPolygon("object_mask"); handleNewPolygon("object_mask");

View File

@ -284,13 +284,18 @@ export default function MotionTunerView({
</div> </div>
<div className="flex flex-1 flex-col justify-end"> <div className="flex flex-1 flex-col justify-end">
<div className="flex flex-row gap-2 pt-5"> <div className="flex flex-row gap-2 pt-5">
<Button className="flex flex-1" onClick={onCancel}> <Button
className="flex flex-1"
aria-label="Reset"
onClick={onCancel}
>
Reset Reset
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={!changedValue || isLoading} disabled={!changedValue || isLoading}
className="flex flex-1" className="flex flex-1"
aria-label="Save"
onClick={saveToConfig} onClick={saveToConfig}
> >
{isLoading ? ( {isLoading ? (

View File

@ -270,6 +270,7 @@ export default function NotificationView({
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]"> <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button <Button
className="flex flex-1" className="flex flex-1"
aria-label="Cancel"
onClick={onCancel} onClick={onCancel}
type="button" type="button"
> >
@ -279,6 +280,7 @@ export default function NotificationView({
variant="select" variant="select"
disabled={isLoading} disabled={isLoading}
className="flex flex-1" className="flex flex-1"
aria-label="Save"
type="submit" type="submit"
> >
{isLoading ? ( {isLoading ? (
@ -298,6 +300,7 @@ export default function NotificationView({
<div className="space-y-3"> <div className="space-y-3">
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />
<Button <Button
aria-label="Register or unregister notifications for this device"
disabled={ disabled={
!config?.notifications.enabled || publicKey == undefined !config?.notifications.enabled || publicKey == undefined
} }

View File

@ -266,13 +266,14 @@ export default function SearchSettingsView({
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]"> <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button className="flex flex-1" onClick={onCancel}> <Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
Reset Reset
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={!changedValue || isLoading} disabled={!changedValue || isLoading}
className="flex flex-1" className="flex flex-1"
aria-label="Save"
onClick={saveToConfig} onClick={saveToConfig}
> >
{isLoading ? ( {isLoading ? (

View File

@ -125,7 +125,12 @@ export default function UiSettingsView() {
</p> </p>
</div> </div>
</div> </div>
<Button onClick={clearStoredLayouts}>Clear All Layouts</Button> <Button
aria-label="Clear all saved layouts"
onClick={clearStoredLayouts}
>
Clear All Layouts
</Button>
</div> </div>
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />

View File

@ -541,6 +541,7 @@ export default function GeneralMetrics({
{canGetGpuInfo && ( {canGetGpuInfo && (
<Button <Button
className="cursor-pointer" className="cursor-pointer"
aria-label="Hardware information"
size="sm" size="sm"
onClick={() => setShowVainfo(true)} onClick={() => setShowVainfo(true)}
> >