22
33import Link from 'next/link' ;
44import { usePathname , useRouter } from 'next/navigation' ;
5+ import { useEffect , useRef } from 'react' ;
56import { form } from 'shared/form' ;
67import { useLinkProps } from 'state-in-url/useLinkProps' ;
78
@@ -22,10 +23,34 @@ export function LanguagePicker({ current, label }: { current: Locale; label: str
2223 const path = stripLocale ( usePathname ( ) ) . replace ( / \/ $ / , '' ) ;
2324 // `form` is the demo's own object — `useSharedState` keys on its identity.
2425 const linkProps = useLinkProps ( form , useRouter ( ) . push ) ;
26+ const ref = useRef < HTMLDetailsElement > ( null ) ;
27+
28+ // `<details>` has no light dismiss of its own — left alone, the panel stays
29+ // open until the summary is clicked a second time. Read through the ref on
30+ // every event rather than closing over it: this component renders null on
31+ // the fixture routes, so the element does not exist when the effect runs.
32+ useEffect ( ( ) => {
33+ const close = ( ) => {
34+ if ( ref . current ?. open ) ref . current . open = false ;
35+ } ;
36+ const onPointerDown = ( e : PointerEvent ) => {
37+ if ( ! ref . current ?. contains ( e . target as Node ) ) close ( ) ;
38+ } ;
39+ const onKeyDown = ( e : KeyboardEvent ) => {
40+ if ( e . key === 'Escape' ) close ( ) ;
41+ } ;
42+ document . addEventListener ( 'pointerdown' , onPointerDown ) ;
43+ document . addEventListener ( 'keydown' , onKeyDown ) ;
44+ return ( ) => {
45+ document . removeEventListener ( 'pointerdown' , onPointerDown ) ;
46+ document . removeEventListener ( 'keydown' , onKeyDown ) ;
47+ } ;
48+ } , [ ] ) ;
49+
2550 if ( ! ( PAGES as readonly string [ ] ) . includes ( path ) ) return null ;
2651
2752 return (
28- < details className = "lang-picker" >
53+ < details className = "lang-picker" ref = { ref } >
2954 < summary >
3055 < svg
3156 viewBox = "0 0 24 24"
0 commit comments