@onehat/ui
Version:
Base UI for OneHat apps
528 lines (497 loc) • 12.6 kB
JavaScript
import { cloneElement, useState, useEffect, } from 'react';
import {
Box,
HStack,
HStackNative,
Icon,
ScrollView,
VStack,
VStackNative,
} from '@project-components/Gluestack';
import clsx from 'clsx';
import {
HORIZONTAL,
VERTICAL,
} from '../../Constants/Directions.js';
import Tab from './Tab.js';
import TabButton from './TabButton.js';
import Button from '../Buttons/Button.js';
import UiGlobals from '../../UiGlobals.js';
import getComponentFromType from '../../Functions/getComponentFromType.js';
import testProps from '../../Functions/testProps.js';
import withComponent from '../Hoc/withComponent.js';
import IconButton from '../Buttons/IconButton.js';
import Minimize from '../Icons/Minimize.js';
import Maximize from '../Icons/Maximize.js';
import getSaved from '../../Functions/getSaved.js';
import setSaved from '../../Functions/setSaved.js';
import _ from 'lodash';
function TabBar(props) {
const {
tabs = [], // { _icon, title, content, path, items, }
content, // e.g. Expo Router slot
direction = HORIZONTAL,
tabWidth = 150, // used on VERTICAL mode only
tabHeight = 47, // used on HORIZONTAL mode only
additionalButtons,
initialTabIx = 0,
currentTabIx,
disableCollapse = false,
startsCollapsed = true,
canToggleCollapse = true,
tabsAreButtons = true,
onChangeCurrentTab,
onChangeIsCollapsed,
onPressTab,
onTabClose,
self,
...propsToPass
} = props,
styles = UiGlobals.styles,
id = props.id || props.self?.path,
useLocal = _.isNil(currentTabIx),
[isReady, setIsReady] = useState(false),
[currentTabIxLocal, setCurrentTabIxLocal] = useState(initialTabIx),
[isCollapsed, setIsCollapsedRaw] = useState(startsCollapsed),
setIsCollapsed = (isCollapsed) => {
setIsCollapsedRaw(isCollapsed);
if (onChangeIsCollapsed) {
onChangeIsCollapsed(isCollapsed);
}
if (id) {
setSaved(id + '-isCollapsed', isCollapsed);
}
},
getCurrentTab = () => {
if (useLocal) {
return currentTabIxLocal;
}
return currentTabIx;
},
setCurrentTab = (ix) => {
if ((useLocal && ix === currentTabIxLocal) || ix === currentTabIx) {
if (onPressTab) {
onPressTab(ix); // for when an already shown tab is pressed
}
return; // no change
}
if (tabs[currentTabIx]?.content) {
tabs[currentTabIx].content = null; // free up memory by clearing rendered content
}
if (useLocal) {
setCurrentTabIxLocal(ix);
if (id) {
setSaved(id + '-currentTabIx', ix);
}
}
if (onChangeCurrentTab) {
onChangeCurrentTab(ix);
}
},
renderToggleButton = () => {
const {
tabProps: {
className: tabPropsClassName,
...tabPropsToPass
},
textProps: {
className: textPropsClassName,
...textPropsToPass
},
iconProps: {
className: iconPropsClassName,
...iconPropsToPass
},
} = getTabProps();
let tabClassName = tabPropsClassName,
textClassName = textPropsClassName,
iconClassName = iconPropsClassName;
const
icon = Maximize,
_icon = {
...iconPropsToPass,
className: iconClassName,
},
onPress = () => setIsCollapsed(!isCollapsed);
let button;
if (isCollapsed) {
button = <IconButton
{...testProps('toggleBtn')}
key="toggleBtn"
onPress={onPress}
{...tabPropsToPass}
icon={icon}
_icon={_icon}
className={tabClassName}
tooltip={isCollapsed ? 'Expand' : 'Collapse'}
/>;
} else {
tabClassName += clsx(
direction === VERTICAL ? 'w-[200px]' : '',
'pr-0',
'mr-0',
);
_icon.as = Minimize;
button = <Button
{...testProps('toggleBtn')}
key="toggleBtn"
onPress={onPress}
{...tabPropsToPass}
icon={icon}
_icon={_icon}
className={tabClassName}
text="Collapse"
_text={{
className: textClassName,
...textPropsToPass,
}}
action="none"
variant="none"
/>;
}
return button;
},
getTabProps = () => {
const
tabProps = {
className: clsx(
styles.TAB_BG,
isCollapsed ? 'justify-center' : 'justify-start',
),
},
textProps = {
// numberOfLines: 1,
// ellipsizeMode: 'head',
className: clsx(
styles.TAB_FONTSIZE,
styles.TAB_COLOR,
),
},
iconProps = {
// size: 'md',
className: clsx(
styles.TAB_ICON_COLOR,
),
};
switch(direction) {
case VERTICAL:
tabProps.className += clsx(
'rounded-l-lg',
'rounded-r-none',
'w-full',
'ml-2',
'mr-0',
'mb-1',
'px-4',
);
textProps.className += clsx(
'w-full',
'mr-0',
'mb-1',
'py-0',
'pl-3',
'pr-0',
'flex-1',
'text-left',
);
break;
case HORIZONTAL:
tabProps.className += clsx(
'rounded-t',
'rounded-b-none',
'mr-1',
'py-1',
);
textProps.className += clsx(
'px-1',
'py-0',
'mr-1',
);
break;
default:
}
return {
tabProps,
textProps,
iconProps,
};
},
renderTabs = () => {
const {
tabProps: {
className: tabPropsClassName,
...tabPropsToPass
},
textProps: {
className: textPropsClassName,
...textPropsToPass
},
iconProps: {
className: iconPropsClassName,
...iconPropsToPass
},
} = getTabProps(),
tabComponents = [];
_.each(tabs, (tab, ix) => {
if (!tab.icon) {
throw new Error('tab.icon required!');
}
const
isCurrentTab = ix === getCurrentTab(),
useIconTab = (isCollapsed || !tab.title),
tabIcon = tab._icon ? [...tab._icon] : {};
if (tabIcon.as && _.isString(tabIcon.as)) {
const Type = getComponentFromType(tabIcon.as);
if (Type) {
tabIcon.as = Type;
}
}
let tabClassName = tabPropsClassName,
textClassName = textPropsClassName,
iconClassName = iconPropsClassName;
// overrides
if (tab._button?.className) {
tabClassName += ' ' + tab._button.className;
}
if (tab._text?.className) {
textClassName += ' ' + tab._text.className;
}
if (tab._icon?.className) {
iconClassName += ' ' + tab._icon.className;
}
const
_icon = {
...iconPropsToPass,
...tabIcon,
className: iconClassName,
},
onPress = () => setCurrentTab(ix);
const WhichTabType = tabsAreButtons ? TabButton : Tab
tabComponents.push(<WhichTabType
{...testProps(tab.path)}
key={'tab' + ix}
onPress={onPress}
{...tabPropsToPass}
icon={tab.icon}
_icon={_icon}
className={tabClassName}
tooltip={tab.title}
text={tab.title}
_text={{
className: textClassName,
...textPropsToPass,
}}
isDisabled={tab.isDisabled}
isCurrentTab={isCurrentTab}
useIconOnly={useIconTab}
direction={direction}
useCloseBtn={onTabClose && !tab.disableCloseBox}
onClose={() => onTabClose(ix)}
/>);
});
if (additionalButtons) {
_.each(additionalButtons, (additionalButton, ix) => {
if (!additionalButton._icon) {
throw new Error('additionalButton._icon required!');
}
const
useIconTab = (isCollapsed || !additionalButton.text),
additionalButtonIcon = [...additionalButton._icon];
if (additionalButtonIcon.as && _.isString(additionalButtonIcon.as)) {
const Type = getComponentFromType(additionalButtonIcon.as);
if (Type) {
additionalButtonIcon.as = Type;
}
}
let tabClassName = tabPropsClassName,
textClassName = textPropsClassName,
iconClassName = iconPropsClassName;
// overrides
if (additionalButton._button?.className) {
tabClassName += ' ' + additionalButton._button.className;
}
if (additionalButton._text?.className) {
textClassName += ' ' + additionalButton._text.className;
}
if (additionalButton._icon?.className) {
iconClassName += ' ' + additionalButton._icon.className;
}
if (!ix) {
// First button should have a gap before it
switch(direction) {
case VERTICAL:
tabClassName += ' mt-6';
break;
case HORIZONTAL:
tabClassName += ' ml-6';
break;
default:
}
}
const
_icon = {
...iconPropsToPass,
...additionalButton._icon,
className: iconClassName,
},
onPress = additionalButton.onPress;
let button;
if (useIconTab) {
button = <IconButton
{...testProps('additionalBtn' + ix)}
key={'additionalBtn' + ix}
onPress={onPress}
{...tabPropsToPass}
_icon={_icon}
className={tabClassName}
tooltip={additionalButton.text}
/>;
} else {
if (direction === VERTICAL) {
tabClassName += ' w-[200px]';
}
button = <Button
{...testProps('additionalBtn' + ix)}
key={'additionalBtn' + ix}
onPress={onPress}
{...tabPropsToPass}
_icon={_icon}
className={tabClassName}
text={additionalButton.text}
_text={{
className: textClassName,
...textPropsToPass,
}}
action="none"
variant="none"
/>;
}
tabComponents.push(button);
});
}
return tabComponents;
},
renderCurrentTabContent = () => {
if (content) {
return content;
}
const currentTabIx = getCurrentTab();
if (!tabs[currentTabIx]) {
return null;
}
const currentTab = tabs[currentTabIx];
if (!currentTab.content && !currentTab.items) {
return null;
}
if (currentTab.content) {
return currentTab.content;
}
return _.map(currentTab.items, (item, ix) => {
return cloneElement(item, { key: ix });
});
};
useEffect(() => {
// Restore saved settings
(async () => {
if (id) {
let key, val;
key = id + '-isCollapsed';
val = await getSaved(key);
if (!_.isNil(val)) {
setIsCollapsed(val);
}
if (useLocal) {
key = id + '-currentTabIx';
val = await getSaved(key);
if (!_.isNil(val)) {
setCurrentTab(val);
}
}
}
if (!isReady) {
setIsReady(true);
}
})();
}, []);
if (!isReady) {
return null;
}
if (self) {
self.getCurrentTab = getCurrentTab;
self.setCurrentTab = setCurrentTab;
self.setIsCollapsed = setIsCollapsed;
}
const
renderedTabs = renderTabs(),
renderedCurrentTabContent = renderCurrentTabContent(),
renderedToggleButton = !disableCollapse ? renderToggleButton() : null;
let tabBar = null;
if (direction === VERTICAL) {
tabBar = <VStackNative
{...testProps('TabBar')}
{...propsToPass}
className={clsx(
isCollapsed ? 'w-[50px]' : 'w-[' + tabWidth + 'px]',
isCollapsed ? 'pl-1' : 'pl-4',
'items-center',
'justify-start',
'py-2',
'overflow-x-hidden',
'overflow-y-auto',
styles.TAB_BAR_CLASSNAME
)}
>
{renderedTabs}
{canToggleCollapse ?
<VStack className="flex-1 w-full justify-end">
{renderedToggleButton}
</VStack> : null}
</VStackNative>;
if (renderedCurrentTabContent) {
tabBar = <HStackNative {...propsToPass} className="flex-1 w-full">
{tabBar}
<VStack className="items-center justify-start flex-1">
{renderedCurrentTabContent}
</VStack>
</HStackNative>;
}
}
if (direction === HORIZONTAL) {
tabBar = <HStackNative
{...testProps('TabBar')}
className={clsx(
'h-[' + tabHeight + 'px]',
'items-center',
'justify-start',
'overflow-x-auto',
'overflow-y-hidden',
'p-1',
'pb-0',
styles.TAB_BAR_CLASSNAME
)}
>
<ScrollView
horizontal={true}
className={'h-[' + tabHeight + 'px]'}
>
{renderedTabs}
</ScrollView>
{canToggleCollapse ?
<HStack className="flex-1 h-full justify-end">
<HStack className="h-full">
{renderedToggleButton}
</HStack>
</HStack> : null}
</HStackNative>;
if (renderedCurrentTabContent) {
tabBar = <VStackNative {...propsToPass} className="flex-1 w-full">
{tabBar}
<VStack className="items-center justify-start flex-1">
{renderedCurrentTabContent}
</VStack>
</VStackNative>;
}
}
return tabBar;
}
export default withComponent(TabBar);