feat(ST-1134): LandscapeGuard component (T-319)
Toont rotate-overlay in portrait, niets in landscape. Kinderen blijven altijd in DOM — geen unmount zodat SSE-streams overleven bij rotatie. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bdd8c1e53a
commit
e68552bcfd
2 changed files with 105 additions and 0 deletions
73
__tests__/components/mobile/landscape-guard.test.tsx
Normal file
73
__tests__/components/mobile/landscape-guard.test.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { render, screen, act } from '@testing-library/react'
|
||||
import { LandscapeGuard } from '@/components/mobile/landscape-guard'
|
||||
|
||||
type Listener = (e: MediaQueryListEvent) => void
|
||||
|
||||
function mockMatchMedia(initialPortrait: boolean) {
|
||||
let matches = initialPortrait
|
||||
let listener: Listener | null = null
|
||||
|
||||
const mql = {
|
||||
get matches() { return matches },
|
||||
media: '(orientation: portrait)',
|
||||
onchange: null,
|
||||
addEventListener: (_: string, l: Listener) => { listener = l },
|
||||
removeEventListener: () => { listener = null },
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: () => mql,
|
||||
})
|
||||
|
||||
return {
|
||||
setPortrait(p: boolean) {
|
||||
matches = p
|
||||
if (listener) listener({ matches: p } as MediaQueryListEvent)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('LandscapeGuard', () => {
|
||||
beforeEach(() => {})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders children always', () => {
|
||||
mockMatchMedia(false)
|
||||
render(<LandscapeGuard><div>kids</div></LandscapeGuard>)
|
||||
expect(screen.getByText('kids')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows overlay in portrait', () => {
|
||||
mockMatchMedia(true)
|
||||
render(<LandscapeGuard><div>kids</div></LandscapeGuard>)
|
||||
expect(screen.getByRole('alert').textContent).toContain('Draai je telefoon naar landscape')
|
||||
// children blijven in DOM (geen unmount → SSE-streams blijven leven)
|
||||
expect(screen.getByText('kids')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('hides overlay in landscape', () => {
|
||||
mockMatchMedia(false)
|
||||
render(<LandscapeGuard><div>kids</div></LandscapeGuard>)
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
})
|
||||
|
||||
it('toggles overlay on orientation change', () => {
|
||||
const ctl = mockMatchMedia(false)
|
||||
render(<LandscapeGuard><div>kids</div></LandscapeGuard>)
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
act(() => ctl.setPortrait(true))
|
||||
expect(screen.getByRole('alert')).toBeTruthy()
|
||||
act(() => ctl.setPortrait(false))
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
})
|
||||
})
|
||||
32
components/mobile/landscape-guard.tsx
Normal file
32
components/mobile/landscape-guard.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { RotateCw } from 'lucide-react'
|
||||
|
||||
export function LandscapeGuard({ children }: { children: React.ReactNode }) {
|
||||
const [isPortrait, setIsPortrait] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(orientation: portrait)')
|
||||
const update = () => setIsPortrait(mq.matches)
|
||||
update()
|
||||
mq.addEventListener('change', update)
|
||||
return () => mq.removeEventListener('change', update)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{isPortrait && (
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-background text-foreground p-6"
|
||||
>
|
||||
<RotateCw className="size-12 text-primary" />
|
||||
<p className="text-base font-medium text-center">Draai je telefoon naar landscape</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue