import { useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { SUPPORTED_LANGUAGES } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import { useAnchoredPosition } from '../../hooks/useAnchoredPosition' /** Wide enough for the longest label ("Português (Brasil)") at the default text size. */ const MENU_MIN_WIDTH = 190 const MENU_MAX_HEIGHT = 320 interface PublicLanguagePickerProps { /** Current UI locale, e.g. `de` or `pt-BR`. */ locale?: string | null open: boolean onOpenChange: React.Dispatch> } /** * The language pill both anonymous share pages carry in their top-right corner (#2248). * * Each page used to inline the same absolutely-positioned menu. On the journey * page that menu sits inside a hero with `overflow: hidden` — the clip that keeps * the hero's decorative circles from widening the document — so the list of 23 * languages was amputated at the hero's bottom edge and most of it could never be * picked. The menu is portalled to `document.body` and positioned against the * trigger now, which no ancestor can clip, and it caps its own height at the room * the viewport actually has: the treatment `CustomSelect` already gets. */ export default function PublicLanguagePicker({ locale, open, onOpenChange }: PublicLanguagePickerProps) { const wrapRef = useRef(null) const menuRef = useRef(null) // matchWidth stays on: the anchored box's `width` is the trigger's own width, // which is what the right-edge alignment below needs. const anchored = useAnchoredPosition(wrapRef, open, { estimatedHeight: MENU_MAX_HEIGHT, offset: 6 }) useEffect(() => { if (!open) return const onPointerDown = (e: MouseEvent) => { if (wrapRef.current?.contains(e.target as Node)) return if (menuRef.current?.contains(e.target as Node)) return onOpenChange(false) } const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onOpenChange(false) } document.addEventListener('mousedown', onPointerDown) document.addEventListener('keydown', onKeyDown) return () => { document.removeEventListener('mousedown', onPointerDown) document.removeEventListener('keydown', onKeyDown) } }, [open, onOpenChange]) // The two pages hand in different shapes: the journey page passes the stored // setting (`br`, `zh-TW`), the trip page the resolved locale (`de-DE`). Try the // whole string before its prefix, or `zh-TW` would label itself 简体中文. const raw = locale || 'en' const current = SUPPORTED_LANGUAGES.some(l => l.value === raw) ? raw : raw.split('-')[0] // Pinned by its RIGHT edge, the way `right: 0` did inside the trigger's box. That // also lets the panel size to its content: a reader on the largest text setting // gets a wider menu instead of "Português (Brasil)" wrapping to two lines. const right = anchored ? Math.max(8, (typeof window === 'undefined' ? 0 : window.innerWidth) - anchored.left - anchored.width) : 8 return (
{open && createPortal(
{SUPPORTED_LANGUAGES.map(lang => ( ))}
, document.body, )}
) }