From 84eb71383c652f99b2fc63b037a96daa1e97aa88 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 11 May 2026 11:03:11 +0200 Subject: [PATCH] feat(portal-shell): thin full-width footer with copyright + a11y links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-add a 40px (h-10) footer pinned to the bottom of the shell, spanning the full viewport width below header + sidebar + main. It hosts the "© APF France handicap" copyright on the left and the FR + EN accessibility statement links on the right. The accessibility links move out of the sidebar bottom — putting legal / compliance links in the footer matches universal web convention and keeps the sidebar focused on product navigation. Both languages stay visible until the language selector lands; at that point we drop the one matching the current locale. The footer is reserved for cross-cutting page furniture and stays the canonical home for upcoming additions: a language toggle (FR / EN) and a dev-only version badge. Layout: `:host` is a flex column of header (shrink-0) + shell-body (flex-1) + footer (shrink-0). Sidebar height tracks shell-body so the toggle button still sits flush above the footer at all viewport sizes. --- apps/portal-shell/src/app/app.html | 1 + apps/portal-shell/src/app/app.spec.ts | 3 +- apps/portal-shell/src/app/app.ts | 3 +- .../src/app/components/footer/footer.html | 26 ++++++++++++++ .../src/app/components/footer/footer.spec.ts | 35 +++++++++++++++++++ .../src/app/components/footer/footer.ts | 25 +++++++++++++ .../src/app/components/header/header.html | 2 +- .../src/app/components/sidebar/sidebar.html | 35 ------------------- .../app/components/sidebar/sidebar.spec.ts | 6 ++-- 9 files changed, 95 insertions(+), 41 deletions(-) create mode 100644 apps/portal-shell/src/app/components/footer/footer.html create mode 100644 apps/portal-shell/src/app/components/footer/footer.spec.ts create mode 100644 apps/portal-shell/src/app/components/footer/footer.ts diff --git a/apps/portal-shell/src/app/app.html b/apps/portal-shell/src/app/app.html index bd16644..0f124a2 100644 --- a/apps/portal-shell/src/app/app.html +++ b/apps/portal-shell/src/app/app.html @@ -6,3 +6,4 @@ + diff --git a/apps/portal-shell/src/app/app.spec.ts b/apps/portal-shell/src/app/app.spec.ts index 1e3cd49..b3ee85f 100644 --- a/apps/portal-shell/src/app/app.spec.ts +++ b/apps/portal-shell/src/app/app.spec.ts @@ -10,7 +10,7 @@ describe('App', () => { }).compileComponents(); }); - it('renders the layout shell — skip link, header, sidebar, main', async () => { + it('renders the layout shell — skip link, header, sidebar, main, footer', async () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); const root = fixture.nativeElement as HTMLElement; @@ -18,5 +18,6 @@ describe('App', () => { expect(root.querySelector('app-header')).not.toBeNull(); expect(root.querySelector('app-sidebar')).not.toBeNull(); expect(root.querySelector('main#main-content')).not.toBeNull(); + expect(root.querySelector('app-footer')).not.toBeNull(); }); }); diff --git a/apps/portal-shell/src/app/app.ts b/apps/portal-shell/src/app/app.ts index 5f82fdd..e6d1be8 100644 --- a/apps/portal-shell/src/app/app.ts +++ b/apps/portal-shell/src/app/app.ts @@ -2,10 +2,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { Header } from './components/header/header'; import { Sidebar } from './components/sidebar/sidebar'; +import { Footer } from './components/footer/footer'; @Component({ selector: 'app-root', - imports: [RouterOutlet, Header, Sidebar], + imports: [RouterOutlet, Header, Sidebar, Footer], templateUrl: './app.html', styleUrl: './app.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/apps/portal-shell/src/app/components/footer/footer.html b/apps/portal-shell/src/app/components/footer/footer.html new file mode 100644 index 0000000..e845a8d --- /dev/null +++ b/apps/portal-shell/src/app/components/footer/footer.html @@ -0,0 +1,26 @@ + diff --git a/apps/portal-shell/src/app/components/footer/footer.spec.ts b/apps/portal-shell/src/app/components/footer/footer.spec.ts new file mode 100644 index 0000000..1080fe5 --- /dev/null +++ b/apps/portal-shell/src/app/components/footer/footer.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { Footer } from './footer'; + +describe('Footer', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Footer], + providers: [provideRouter([])], + }).compileComponents(); + }); + + it('exposes a footer landmark', async () => { + const fixture = TestBed.createComponent(Footer); + await fixture.whenStable(); + const root = fixture.nativeElement as HTMLElement; + expect(root.querySelector('footer[aria-label="Page footer"]')).not.toBeNull(); + }); + + it('shows the APF copyright with the current year', async () => { + const fixture = TestBed.createComponent(Footer); + await fixture.whenStable(); + const text = (fixture.nativeElement as HTMLElement).textContent ?? ''; + expect(text).toContain('APF France handicap'); + expect(text).toContain(String(new Date().getFullYear())); + }); + + it('renders both accessibility statement links (EN + FR)', async () => { + const fixture = TestBed.createComponent(Footer); + await fixture.whenStable(); + const root = fixture.nativeElement as HTMLElement; + expect(root.querySelector('a[href="/accessibility"]')).not.toBeNull(); + expect(root.querySelector('a[href="/accessibilite"]')).not.toBeNull(); + }); +}); diff --git a/apps/portal-shell/src/app/components/footer/footer.ts b/apps/portal-shell/src/app/components/footer/footer.ts new file mode 100644 index 0000000..5c06715 --- /dev/null +++ b/apps/portal-shell/src/app/components/footer/footer.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +/** + * Thin full-width footer pinned to the bottom of the shell. + * + * v1 ships: + * - APF France handicap copyright with the current year. + * - Accessibility statement links in both languages (RGAA-mandated; + * ADR-0016). Both stay visible until the language selector lands, + * after which we drop the one matching the current locale. + * + * The footer is reserved for cross-cutting page furniture — once we + * have them, the language toggle and a dev-only version badge land + * here. Real product navigation stays in the sidebar. + */ +@Component({ + selector: 'app-footer', + imports: [RouterLink], + templateUrl: './footer.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Footer { + protected readonly year = new Date().getFullYear(); +} diff --git a/apps/portal-shell/src/app/components/header/header.html b/apps/portal-shell/src/app/components/header/header.html index aaba46d..9ef243d 100644 --- a/apps/portal-shell/src/app/components/header/header.html +++ b/apps/portal-shell/src/app/components/header/header.html @@ -1,5 +1,5 @@
- - @if (!collapsed()) {

{ expect(text).toContain('Communication'); }); - it('renders the FR + EN accessibility links', async () => { + it('does not render the accessibility links (now lives in the footer)', async () => { const fixture = TestBed.createComponent(Sidebar); await fixture.whenStable(); const root = fixture.nativeElement as HTMLElement; - expect(root.querySelector('a[href="/accessibility"]')).not.toBeNull(); - expect(root.querySelector('a[href="/accessibilite"]')).not.toBeNull(); + expect(root.querySelector('a[href="/accessibility"]')).toBeNull(); + expect(root.querySelector('a[href="/accessibilite"]')).toBeNull(); }); it('starts expanded and collapses on toggle, persisting to localStorage', async () => { -- 2.30.2