Signal-Desktop/ts/components/fun/base/FunTabs.tsx

108 lines
2.7 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Transition } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { ReactNode } from 'react';
import React, { useCallback, useId } from 'react';
import type { Key } from 'react-aria';
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
import type { FunPickerTabKey } from '../constants';
export type FunTabsProps = Readonly<{
value: FunPickerTabKey;
onChange: (newTab: FunPickerTabKey) => void;
children: ReactNode;
}>;
export function FunTabs(props: FunTabsProps): JSX.Element {
const { onChange } = props;
const handleTabChange = useCallback(
(key: Key) => {
onChange(key as FunPickerTabKey);
},
[onChange]
);
return (
<Tabs
className="FunTabs__Tabs"
selectedKey={props.value}
onSelectionChange={handleTabChange}
>
{props.children}
</Tabs>
);
}
export type FunTabListProps = Readonly<{
children: ReactNode;
}>;
export function FunTabList(props: FunTabListProps): JSX.Element {
return <TabList className="FunTabs__TabList">{props.children}</TabList>;
}
const FunTabTransition: Transition = {
type: 'spring',
stiffness: 422,
damping: 37.3,
mass: 1,
};
export type FunTabProps = Readonly<{
id: FunPickerTabKey;
children: ReactNode;
}>;
export function FunPickerTab(props: FunTabProps): JSX.Element {
return (
<Tab className="FunTabs__Tab" id={props.id}>
{({ isSelected }) => {
return (
<div className="FunTabs__TabButton">
<div className="FunTabs__TabButtonText">{props.children}</div>
{isSelected && (
<motion.div
className="FunTabs__TabButtonIndicator"
layoutId="FunTabs__TabButtonIndicator"
transition={FunTabTransition}
/>
)}
</div>
);
}}
</Tab>
);
}
export type FunTabPanelProps = Readonly<{
id: FunPickerTabKey;
children: ReactNode;
}>;
export function FunTabPanel(props: FunTabPanelProps): JSX.Element | null {
const motionKey = useId();
return (
<TabPanel className="FunTabs__TabPanel" id={props.id} shouldForceMount>
{({ isInert }) => {
return (
<AnimatePresence initial={false}>
{isInert ? null : (
<motion.div
key={motionKey}
className="FunTabs__TabPanelInner"
initial={{ opacity: 0, zIndex: 1 }}
animate={{ opacity: 1, zIndex: 1 }}
exit={{ opacity: 1, zIndex: 0 }}
>
{props.children}
</motion.div>
)}
</AnimatePresence>
);
}}
</TabPanel>
);
}