PromucFlow_constructor/app/client/src/components/ads/Tabs.tsx
2022-06-16 11:48:44 +05:30

431 lines
11 KiB
TypeScript

import React, { RefObject, useCallback, useState } from "react";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import "react-tabs/style/react-tabs.css";
import styled from "styled-components";
import Icon, { IconName, IconSize } from "./Icon";
import { Classes, CommonComponentProps } from "./common";
import { useEffect } from "react";
import { Indices } from "constants/Layers";
import { theme } from "constants/DefaultTheme";
import useResizeObserver from "utils/hooks/useResizeObserver";
export const TAB_MIN_HEIGHT = `36px`;
export type TabProp = {
key: string;
title: string;
count?: number;
panelComponent?: JSX.Element;
icon?: IconName;
iconSize?: IconSize;
};
const TabsWrapper = styled.div<{
shouldOverflow?: boolean;
vertical?: boolean;
responseViewer?: boolean;
}>`
border-radius: 0px;
height: 100%;
overflow: hidden;
.react-tabs {
height: 100%;
}
.react-tabs__tab-panel {
height: ${() => `calc(100% - ${TAB_MIN_HEIGHT})`};
overflow: auto;
}
.react-tabs__tab-list {
margin: 0px;
display: flex;
flex-direction: ${(props) => (!!props.vertical ? "column" : "row")};
align-items: ${(props) => (!!props.vertical ? "stretch" : "center")};
border-bottom: none;
gap: ${(props) =>
!props.vertical ? `${props.theme.spaces[12] + 2}px` : 0};
color: ${(props) => props.theme.colors.tabs.normal};
path {
fill: ${(props) => props.theme.colors.tabs.icon};
}
${(props) =>
props.shouldOverflow &&
`
overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
`}
${(props) =>
props.responseViewer &&
`
margin-left: 30px;
display: flex;
align-items: center;
height: 24px;
background-color: ${props.theme.colors.multiSwitch.bg} !important;
width: fit-content;
padding-left: 1px;
margin-top: 10px !important;
margin-bottom: 10px !important;
`}
}
.react-tabs__tab {
align-items: center;
text-align: center;
display: inline-flex;
justify-content: center;
border-color: transparent;
position: relative;
padding: 0;
${(props) =>
props.responseViewer &&
`
`}
}
.react-tabs__tab,
.react-tabs__tab:focus {
box-shadow: none;
border: none;
&:after {
content: none;
}
${(props) =>
props.responseViewer &&
`
display: flex;
align-items: center;
cursor: pointer;
height: 22px;
padding: 0 12px;
border: 1px solid ${props.theme.colors.multiSwitch.border};
margin-right: -1px;
margin-left: -1px;
margin-top: -2px;
height: 100%;
`}
}
.react-tabs__tab--selected {
background-color: transparent;
path {
fill: ${(props) => props.theme.colors.tabs.hover};
}
${(props) =>
props.responseViewer &&
`
background-color: ${props.theme.colors.multiSwitch.selectedBg};
border: 1px solid ${props.theme.colors.multiSwitch.border};
border-radius: 0px;
font-weight: normal;
`}
}
${(props) =>
props.responseViewer &&
`
padding: 0px;
margin-top: 10px;
`}
`;
export const TabTitle = styled.span<{ responseViewer?: boolean }>`
font-size: ${(props) => props.theme.typography.h4.fontSize}px;
font-weight: ${(props) => props.theme.fontWeights[1]};
line-height: ${(props) => props.theme.spaces[11]}px;
letter-spacing: ${(props) => props.theme.typography.h4.letterSpacing}px;
margin: 0;
display: flex;
align-items: center;
${(props) =>
props.responseViewer &&
`
font-size: 12px;
font-weight: normal;
line-height: 16px;
letter-spacing: normal;
text-transform: uppercase;
color: ${props.theme.colors.text.normal};
`}
`;
export const TabCount = styled.div`
background-color: ${(props) => props.theme.colors.tabs.countBg};
border-radius: 8px;
min-width: 17px;
height: 17px;
font-size: 9px;
margin-left: 4px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 2px;
`;
const TabTitleWrapper = styled.div<{
selected: boolean;
vertical: boolean;
responseViewer?: boolean;
}>`
display: flex;
width: 100%;
padding: ${(props) => props.theme.spaces[3] - 1}px
${(props) => (props.vertical ? `${props.theme.spaces[4] - 1}px` : 0)}
${(props) => props.theme.spaces[4] - 1}px
${(props) => (props.vertical ? `${props.theme.spaces[4] - 1}px` : 0)};
color: ${(props) => props.theme.colors.tabs.normal};
&:hover {
color: ${(props) => props.theme.colors.tabs.hover};
.${Classes.ICON} {
svg {
fill: ${(props) => props.theme.colors.tabs.hover};
path {
fill: ${(props) => props.theme.colors.tabs.hover};
}
}
}
}
${(props) =>
props.responseViewer &&
`
padding: 0px;
`}
.${Classes.ICON} {
margin-right: ${(props) => props.theme.spaces[1]}px;
border-radius: 50%;
svg {
width: 16px;
height: 16px;
margin: auto;
fill: ${(props) => props.theme.colors.tabs.normal};
path {
fill: ${(props) => props.theme.colors.tabs.normal};
}
}
}
${(props) =>
props.selected
? `
background-color: transparent;
color: var(--appsmith-color-black-900);
.${Classes.ICON} {
svg {
path {
fill: var(--appsmith-color-black-900)
}
}
}
.tab-title {
${props.responseViewer &&
`
font-weight: normal;
`}
}
&::after {
content: "";
position: absolute;
width: ${props.vertical ? `${props.theme.spaces[1] - 2}px` : "100%"};
bottom: ${props.vertical ? "0%" : `${props.theme.spaces[0] - 1}px`};
top: ${
props.vertical ? `${props.theme.spaces[0] - 1}px` : "calc(100% - 2px)"
};
left: ${props.theme.spaces[0]}px;
height: ${props.vertical ? "100%" : `${props.theme.spaces[1] - 2}px`};
background-color: ${props.theme.colors.info.main};
z-index: ${Indices.Layer3};
${props.responseViewer &&
`
display: none;
`}
}
`
: ""}
`;
const CollapseIconWrapper = styled.div`
position: absolute;
right: 14px;
top: ${() => theme.spaces[3] - 1}px;
cursor: pointer;
`;
export type TabItemProps = {
tab: TabProp;
selected: boolean;
vertical: boolean;
responseViewer?: boolean;
};
function DefaultTabItem(props: TabItemProps) {
const { responseViewer, selected, tab, vertical } = props;
return (
<TabTitleWrapper
responseViewer={responseViewer}
selected={selected}
vertical={vertical}
>
{tab.icon ? (
<Icon
name={tab.icon}
size={tab.iconSize ? tab.iconSize : IconSize.XXXL}
/>
) : null}
<TabTitle className="tab-title" responseViewer={responseViewer}>
{tab.title}
</TabTitle>
{tab.count && tab.count > 0 ? <TabCount>{tab.count}</TabCount> : null}
</TabTitleWrapper>
);
}
export type TabbedViewComponentType = CommonComponentProps & {
tabs: Array<TabProp>;
selectedIndex?: number;
onSelect?: (tabIndex: number) => void;
overflow?: boolean;
vertical?: boolean;
tabItemComponent?: (props: TabItemProps) => JSX.Element;
responseViewer?: boolean;
canCollapse?: boolean;
// Reference to container for collapsing or expanding content
containerRef?: RefObject<HTMLElement>;
// height of container when expanded
expandedHeight?: string;
};
// Props required to support a collapsible (foldable) tab component
export type CollapsibleTabProps = {
// Reference to container for collapsing or expanding content
containerRef: RefObject<HTMLDivElement>;
// height of container when expanded( usually the default height of the tab component)
expandedHeight: string;
};
export type CollapsibleTabbedViewComponentType = TabbedViewComponentType &
CollapsibleTabProps;
export const collapsibleTabRequiredPropKeys: Array<keyof CollapsibleTabProps> = [
"containerRef",
"expandedHeight",
];
// Tab is considered collapsible only when all required collapsible props are present
export const isCollapsibleTabComponent = (
props: TabbedViewComponentType | CollapsibleTabbedViewComponentType,
): props is CollapsibleTabbedViewComponentType =>
collapsibleTabRequiredPropKeys.every((key) => key in props);
export function TabComponent(
props: TabbedViewComponentType | CollapsibleTabbedViewComponentType,
) {
const TabItem = props.tabItemComponent || DefaultTabItem;
// for setting selected state of an uncontrolled component
const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex || 0);
const [isExpanded, setIsExpanded] = useState(true);
useEffect(() => {
if (typeof props.selectedIndex === "number")
setSelectedIndex(props.selectedIndex);
}, [props.selectedIndex]);
const handleContainerResize = () => {
if (!isCollapsibleTabComponent(props)) return;
const { containerRef, expandedHeight } = props;
if (containerRef?.current && expandedHeight) {
containerRef.current.style.height = isExpanded
? TAB_MIN_HEIGHT
: expandedHeight;
}
setIsExpanded((prev) => !prev);
};
const resizeCallback = useCallback(
(entries: ResizeObserverEntry[]) => {
if (entries && entries.length) {
const {
contentRect: { height },
} = entries[0];
if (height > Number(TAB_MIN_HEIGHT.replace("px", "")) + 6) {
!isExpanded && setIsExpanded(true);
} else {
isExpanded && setIsExpanded(false);
}
}
},
[isExpanded],
);
useResizeObserver(
isCollapsibleTabComponent(props) ? props.containerRef?.current : null,
resizeCallback,
);
useEffect(() => {
if (!isCollapsibleTabComponent(props)) return;
const { containerRef } = props;
if (!isExpanded && containerRef.current) {
containerRef.current.style.height = TAB_MIN_HEIGHT;
}
}, [isExpanded]);
return (
<TabsWrapper
data-cy={props.cypressSelector}
responseViewer={props.responseViewer}
shouldOverflow={props.overflow}
vertical={props.vertical}
>
{isCollapsibleTabComponent(props) && (
<CollapseIconWrapper>
<Icon
name={isExpanded ? "expand-more" : "expand-less"}
onClick={handleContainerResize}
size={IconSize.XXXXL}
/>
</CollapseIconWrapper>
)}
<Tabs
onSelect={(index: number) => {
props.onSelect && props.onSelect(index);
setSelectedIndex(index);
}}
selectedIndex={props.selectedIndex}
>
<TabList>
{props.tabs.map((tab, index) => (
<Tab
data-cy={`t--tab-${tab.key}`}
data-replay-id={tab.key}
key={tab.key}
>
<TabItem
responseViewer={props.responseViewer}
selected={
index === props.selectedIndex || index === selectedIndex
}
tab={tab}
vertical={!!props.vertical}
/>
</Tab>
))}
</TabList>
{props.tabs.map((tab) => (
<TabPanel key={tab.key}>{tab.panelComponent}</TabPanel>
))}
</Tabs>
</TabsWrapper>
);
}