From 36635741138bb5235083724112d653ed8850154a Mon Sep 17 00:00:00 2001
From: Alex <55413326+AlexRubik@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:43:51 -0800
Subject: [PATCH] [PM-31496] Reports back button placement (#18706)
* place back button fixed at bottom right
* fix type errors
* add the new button logic to org reports also
* fix: restore keyboard focus for reports back button in CDK overlay
The CDK Overlay renders outside the cdkTrapFocus boundary, making the
floating "Back to reports" button unreachable via Tab. Add a focus bridge
element that intercepts Tab and programmatically redirects focus to the
overlay button, with a return handler to cycle focus back into the page.
---
.../organization-reporting.module.ts | 9 ++-
.../reporting/reports-home.component.html | 29 ++++++--
.../reporting/reports-home.component.ts | 72 ++++++++++++++++++-
.../reports/reports-layout.component.html | 30 +++++---
.../dirt/reports/reports-layout.component.ts | 71 +++++++++++++++---
.../src/app/dirt/reports/reports.module.ts | 2 +
6 files changed, 187 insertions(+), 26 deletions(-)
diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts
index 46599d7da46..d96e2cbb6c0 100644
--- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts
+++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts
@@ -1,3 +1,4 @@
+import { OverlayModule } from "@angular/cdk/overlay";
import { NgModule } from "@angular/core";
import { ReportsSharedModule } from "../../../dirt/reports";
@@ -8,7 +9,13 @@ import { OrganizationReportingRoutingModule } from "./organization-reporting-rou
import { ReportsHomeComponent } from "./reports-home.component";
@NgModule({
- imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule, HeaderModule],
+ imports: [
+ SharedModule,
+ OverlayModule,
+ ReportsSharedModule,
+ OrganizationReportingRoutingModule,
+ HeaderModule,
+ ],
declarations: [ReportsHomeComponent],
})
export class OrganizationReportingModule {}
diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html
index 59eac5b6300..9a931f66af9 100644
--- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html
+++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html
@@ -8,9 +8,26 @@
-
+@if (!(homepage$ | async)) {
+
+
+}
+
+
+
+
diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts
index 6043bfd3193..503a4f88050 100644
--- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts
+++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts
@@ -1,6 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { Component, OnInit } from "@angular/core";
+import { Overlay, OverlayRef } from "@angular/cdk/overlay";
+import { TemplatePortal } from "@angular/cdk/portal";
+import {
+ AfterViewInit,
+ Component,
+ inject,
+ OnDestroy,
+ OnInit,
+ TemplateRef,
+ viewChild,
+ ViewContainerRef,
+} from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { filter, map, Observable, startWith, concatMap, firstValueFrom } from "rxjs";
@@ -21,16 +33,30 @@ import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/r
templateUrl: "reports-home.component.html",
standalone: false,
})
-export class ReportsHomeComponent implements OnInit {
+export class ReportsHomeComponent implements OnInit, AfterViewInit, OnDestroy {
reports$: Observable;
homepage$: Observable;
+ private readonly backButtonTemplate =
+ viewChild.required>("backButtonTemplate");
+
+ private overlayRef: OverlayRef | null = null;
+ private overlay = inject(Overlay);
+ private viewContainerRef = inject(ViewContainerRef);
+
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private accountService: AccountService,
private router: Router,
- ) {}
+ ) {
+ this.router.events
+ .pipe(
+ takeUntilDestroyed(),
+ filter((event) => event instanceof NavigationEnd),
+ )
+ .subscribe(() => this.updateOverlay());
+ }
async ngOnInit() {
this.homepage$ = this.router.events.pipe(
@@ -51,6 +77,46 @@ export class ReportsHomeComponent implements OnInit {
);
}
+ ngAfterViewInit(): void {
+ this.updateOverlay();
+ }
+
+ ngOnDestroy(): void {
+ this.overlayRef?.dispose();
+ }
+
+ returnFocusToPage(event: Event): void {
+ if ((event as KeyboardEvent).shiftKey) {
+ return; // Allow natural Shift+Tab behavior
+ }
+ event.preventDefault();
+ const firstFocusable = document.querySelector(
+ "[cdktrapfocus] a:not([tabindex='-1'])",
+ ) as HTMLElement;
+ firstFocusable?.focus();
+ }
+
+ focusOverlayButton(event: Event): void {
+ if ((event as KeyboardEvent).shiftKey) {
+ return; // Allow natural Shift+Tab behavior
+ }
+ event.preventDefault();
+ const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement;
+ button?.focus();
+ }
+
+ private updateOverlay(): void {
+ if (this.isReportsHomepageRouteUrl(this.router.url)) {
+ this.overlayRef?.dispose();
+ this.overlayRef = null;
+ } else if (!this.overlayRef) {
+ this.overlayRef = this.overlay.create({
+ positionStrategy: this.overlay.position().global().bottom("20px").right("32px"),
+ });
+ this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef));
+ }
+ }
+
private buildReports(productType: ProductTierType): ReportEntry[] {
const reportRequiresUpgrade =
productType == ProductTierType.Free ? ReportVariant.RequiresUpgrade : ReportVariant.Enabled;
diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.html b/apps/web/src/app/dirt/reports/reports-layout.component.html
index 0cb5d304a34..c290fc88335 100644
--- a/apps/web/src/app/dirt/reports/reports-layout.component.html
+++ b/apps/web/src/app/dirt/reports/reports-layout.component.html
@@ -1,11 +1,25 @@
-
-
+
diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.ts b/apps/web/src/app/dirt/reports/reports-layout.component.ts
index a6d84ccb037..136b70c81e4 100644
--- a/apps/web/src/app/dirt/reports/reports-layout.component.ts
+++ b/apps/web/src/app/dirt/reports/reports-layout.component.ts
@@ -1,4 +1,14 @@
-import { Component } from "@angular/core";
+import { Overlay, OverlayRef } from "@angular/cdk/overlay";
+import { TemplatePortal } from "@angular/cdk/portal";
+import {
+ AfterViewInit,
+ Component,
+ inject,
+ OnDestroy,
+ TemplateRef,
+ viewChild,
+ ViewContainerRef,
+} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import { filter } from "rxjs/operators";
@@ -10,20 +20,65 @@ import { filter } from "rxjs/operators";
templateUrl: "reports-layout.component.html",
standalone: false,
})
-export class ReportsLayoutComponent {
+export class ReportsLayoutComponent implements AfterViewInit, OnDestroy {
homepage = true;
- constructor(router: Router) {
- const reportsHomeRoute = "/reports";
+ private readonly backButtonTemplate =
+ viewChild.required
>("backButtonTemplate");
- this.homepage = router.url === reportsHomeRoute;
- router.events
+ private overlayRef: OverlayRef | null = null;
+ private overlay = inject(Overlay);
+ private viewContainerRef = inject(ViewContainerRef);
+ private router = inject(Router);
+
+ constructor() {
+ this.router.events
.pipe(
takeUntilDestroyed(),
filter((event) => event instanceof NavigationEnd),
)
- .subscribe((event) => {
- this.homepage = (event as NavigationEnd).url == reportsHomeRoute;
+ .subscribe(() => this.updateOverlay());
+ }
+
+ ngAfterViewInit(): void {
+ this.updateOverlay();
+ }
+
+ ngOnDestroy(): void {
+ this.overlayRef?.dispose();
+ }
+
+ returnFocusToPage(event: Event): void {
+ if ((event as KeyboardEvent).shiftKey) {
+ return; // Allow natural Shift+Tab behavior
+ }
+ event.preventDefault();
+ const firstFocusable = document.querySelector(
+ "[cdktrapfocus] a:not([tabindex='-1'])",
+ ) as HTMLElement;
+ firstFocusable?.focus();
+ }
+
+ focusOverlayButton(event: Event): void {
+ if ((event as KeyboardEvent).shiftKey) {
+ return; // Allow natural Shift+Tab behavior
+ }
+ event.preventDefault();
+ const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement;
+ button?.focus();
+ }
+
+ private updateOverlay(): void {
+ if (this.router.url === "/reports") {
+ this.homepage = true;
+ this.overlayRef?.dispose();
+ this.overlayRef = null;
+ } else if (!this.overlayRef) {
+ this.homepage = false;
+ this.overlayRef = this.overlay.create({
+ positionStrategy: this.overlay.position().global().bottom("20px").right("32px"),
});
+ this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef));
+ }
}
}
diff --git a/apps/web/src/app/dirt/reports/reports.module.ts b/apps/web/src/app/dirt/reports/reports.module.ts
index 4fc152917f4..c4bd9fef809 100644
--- a/apps/web/src/app/dirt/reports/reports.module.ts
+++ b/apps/web/src/app/dirt/reports/reports.module.ts
@@ -1,3 +1,4 @@
+import { OverlayModule } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
@@ -29,6 +30,7 @@ import { ReportsSharedModule } from "./shared";
@NgModule({
imports: [
CommonModule,
+ OverlayModule,
SharedModule,
ReportsSharedModule,
ReportsRoutingModule,