import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useAtomValue } from 'jotai'; import { Box, Icon, IconButton, Icons } from 'folds'; import { mobilePagerCurtainAtom } from '../../state/mobilePagerHeader'; import { Segment } from '../stream-header/Segment'; import * as streamHeaderCss from '../stream-header/StreamHeader.css'; import * as css from './style.css'; type Tab = 'direct' | 'channels' | 'bots'; // Must match the `INLINE_FORM_ID` local constant in // `StreamHeader.tsx`. The shared static header's action icons are // `aria-controls`-linked to the form region that the active pane's // StreamHeader renders inside its curtain — mirroring the original // in-pane buttons' ARIA semantics so assistive tech still announces // the relationship correctly. Keep the two literals in lockstep. const INLINE_FORM_ID = 'stream-header-inline-form'; type MobileTabsPagerHeaderProps = { showBots: boolean; // Active tab name resolved by the parent pager from the URL. We // accept it as a prop rather than re-running `useMatch` here — the // pager already knows the answer and passing it down keeps the // segment highlight in lock-step with the strip's visual position // (one source of truth = `urlActiveIdx`). activeTab: Tab; onSelectDirect: () => void; onSelectChannels: () => void; onSelectBots: () => void; }; // Static shared tabs row painted at the top of MobileTabsPager. Lives // outside the swipe strip so it doesn't translate with the panes — // addresses the user-visible regression where each pane's identical // tabs row sliding underneath felt like "the header is moving" even // though the segments matched at every pixel. // // Reuses `stream-header/StreamHeader.css.ts` classes (`tabsRow`, // `tabsCluster`, `iconsCluster`) so the layout, padding, and segment // styling stay identical to the per-pane tabs row that sits hidden // underneath. The only structural difference is the surrounding // `pagerStaticHeader` wrapper which positions this row absolute at // the top of the pager and reserves the status-bar safe-area inset. // // Segment clicks call the pager's commit callbacks (so the swipe // animation uses the same pendingTargetIdx path as a finger swipe). // Action icons read `mobilePagerCurtainAtom` — the active pane's // StreamHeader writes its curtain controls there, so Plus/Search/X // drive whichever curtain is currently visible. // // ARIA: action icons mirror the original in-pane buttons' // `aria-controls` / `aria-expanded` / `aria-haspopup` relationship to // the form region (`#stream-header-inline-form`). When `iconsDisabled` // (atom not yet populated on initial mount) the buttons report // `aria-disabled` so assistive tech announces the unavailable state // instead of silently failing on activation. export function MobileTabsPagerHeader({ showBots, activeTab, onSelectDirect, onSelectChannels, onSelectBots, }: MobileTabsPagerHeaderProps) { const { t } = useTranslation(); const curtainControls = useAtomValue(mobilePagerCurtainAtom); const isFormActive = curtainControls?.isFormActive ?? false; const openChat = useCallback(() => curtainControls?.openChat(), [curtainControls]); const openSearch = useCallback(() => curtainControls?.openSearch(), [curtainControls]); const closeForm = useCallback(() => curtainControls?.closeForm(), [curtainControls]); const iconsDisabled = curtainControls === null; return (
{showBots && ( )}
{isFormActive ? ( ) : (
)}
); }