Files
mtdb_movie/common/resources/client/ui/tabs/tab-panels.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

112 lines
3.0 KiB
TypeScript
Executable File

import React, {
Children,
cloneElement,
ComponentPropsWithoutRef,
isValidElement,
ReactElement,
ReactNode,
useContext,
useRef,
useState,
} from 'react';
import clsx from 'clsx';
import {useLayoutEffect} from '@react-aria/utils';
import {getFocusableTreeWalker} from '@react-aria/focus';
import {TabContext} from './tabs-context';
export interface TabPanelsProps {
children: ReactNode;
className?: string;
}
export function TabPanels({children, className}: TabPanelsProps) {
const {selectedTab, isLazy} = useContext(TabContext);
// filter out falsy values, in case of conditional rendering
const panelArray = Children.toArray(children).filter(p => !!p);
let rendered: ReactNode;
if (isLazy) {
const el = panelArray[selectedTab] as ReactElement;
rendered = isValidElement(el)
? cloneElement<TabPanelProps>(panelArray[selectedTab] as ReactElement, {
index: selectedTab,
})
: null;
} else {
rendered = panelArray.map((panel, index) => {
if (isValidElement<TabPanelsProps>(panel)) {
const isSelected = index === selectedTab;
return cloneElement<TabPanelProps>(panel, {
index,
'aria-hidden': !isSelected,
className: !isSelected
? clsx(panel.props.className, 'hidden')
: panel.props.className,
});
}
return null;
});
}
return <div className={className}>{rendered}</div>;
}
interface TabPanelProps extends ComponentPropsWithoutRef<'div'> {
className?: string;
children: ReactNode;
index?: number;
}
export function TabPanel({
className,
children,
index,
...domProps
}: TabPanelProps) {
const {id} = useContext(TabContext);
const [tabIndex, setTabIndex] = useState<number | undefined>(0);
const ref = useRef<HTMLDivElement>(null);
// The tabpanel should have tabIndex=0 when there are no tabbable elements within it.
// Otherwise, tabbing from the focused tab should go directly to the first tabbable element
// within the tabpanel.
useLayoutEffect(() => {
if (ref?.current) {
const update = () => {
// Detect if there are any tabbable elements and update the tabIndex accordingly.
const walker = getFocusableTreeWalker(ref.current!, {tabbable: true});
setTabIndex(walker.nextNode() ? undefined : 0);
};
update();
// Update when new elements are inserted, or the tabIndex/disabled attribute updates.
const observer = new MutationObserver(update);
observer.observe(ref.current, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['tabIndex', 'disabled'],
});
return () => {
observer.disconnect();
};
}
}, [ref]);
return (
<div
tabIndex={tabIndex}
ref={ref}
id={`${id}-${index}-tabpanel`}
aria-labelledby={`${id}-${index}-tab`}
className={clsx(className, 'focus-visible:outline-primary-light')}
role="tabpanel"
{...domProps}
>
{children}
</div>
);
}