/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import FlexColumn from './FlexColumn'; import styled from '@emotion/styled'; import Orderable from './Orderable'; import FlexRow from './FlexRow'; import {colors} from './colors'; import Tab, {Props as TabProps} from './Tab'; import {Property} from 'csstype'; import React, {useContext} from 'react'; import {TabsContext} from './TabsContainer'; import {_wrapInteractionHandler} from 'flipper-plugin'; const TabList = styled(FlexRow)({ justifyContent: 'center', alignItems: 'stretch', }); TabList.displayName = 'Tabs:TabList'; const TabListItem = styled.div<{ active?: boolean; width?: Property.Width; container?: boolean; }>((props) => ({ userSelect: 'none', background: props.container ? props.active ? 'linear-gradient(to bottom, #67a6f7 0%, #0072FA 100%)' : `linear-gradient(to bottom, white 0%,${colors.macOSTitleBarButtonBackgroundBlur} 100%)` : props.active ? colors.light15 : colors.light02, borderBottom: props.container ? '1px solid #B8B8B8' : '1px solid #dddfe2', boxShadow: props.active && props.container ? 'inset 0px 0px 3px rgba(0,0,0,0.25)' : 'none', color: props.container && props.active ? colors.white : colors.dark80, flex: props.container ? 'unset' : 1, top: props.container ? -11 : 0, fontWeight: 500, fontSize: 13, lineHeight: props.container ? '22px' : '28px', overflow: 'hidden', padding: '0 10px', position: 'relative', textAlign: 'center', textOverflow: 'ellipsis', whiteSpace: 'nowrap', '&:first-child': { borderTopLeftRadius: props.container ? 3 : 0, borderBottomLeftRadius: props.container ? 3 : 0, }, '&:last-child': { borderTopRightRadius: props.container ? 3 : 0, borderBottomRightRadius: props.container ? 3 : 0, }, '&:hover': { backgroundColor: props.active ? colors.light15 : colors.light05, }, })); TabListItem.displayName = 'Tabs:TabListItem'; const TabListAddItem = styled(TabListItem)({ borderRight: 'none', flex: 0, flexGrow: 0, fontWeight: 'bold', }); TabListAddItem.displayName = 'Tabs:TabListAddItem'; const CloseButton = styled.div({ color: '#000', float: 'right', fontSize: 10, fontWeight: 'bold', textAlign: 'center', marginLeft: 6, marginTop: 6, width: 16, height: 16, lineHeight: '16px', borderRadius: '50%', '&:hover': { backgroundColor: colors.cherry, color: '#fff', }, }); CloseButton.displayName = 'Tabs:CloseButton'; const OrderableContainer = styled.div({ display: 'inline-block', }); OrderableContainer.displayName = 'Tabs:OrderableContainer'; const TabContent = styled.div({ height: '100%', overflow: 'auto', width: '100%', display: 'flex', }); TabContent.displayName = 'Tabs:TabContent'; /** * A Tabs component. */ export default function Tabs(props: { /** * Callback for when the active tab has changed. */ onActive?: (key: string | null | undefined) => void; /** * The key of the default active tab. */ defaultActive?: string; /** * The key of the currently active tab. */ active?: string | null | undefined; /** * Tab elements. */ children?: React.ReactElement[] | React.ReactElement; /** * Whether the tabs can be reordered by the user. */ orderable?: boolean; /** * Callback when the tab order changes. */ onOrder?: (order: Array) => void; /** * Order of tabs. */ order?: Array; /** * Whether to include the contents of every tab in the DOM and just toggle * its visibility. */ persist?: boolean; /** * Whether to include a button to create additional items. */ newable?: boolean; /** * Callback for when the new button is clicked. */ onNew?: () => void; /** * Elements to insert before all tabs in the tab list. */ before?: Array; /** * Elements to insert after all tabs in the tab list. */ after?: Array; /** * By default tabs are rendered in mac-style tabs, with a negative offset. * By setting classic mode the classic style is rendered. */ classic?: boolean; }) { let tabsContainer = useContext(TabsContext); const scope = useContext((global as any).FlipperTrackingScopeContext); if (props.classic === true) { tabsContainer = false; } const {onActive} = props; const active: string | undefined = props.active == null ? props.defaultActive : props.active; // array of other components that aren't tabs const before = props.before || []; const after = props.after || []; // const tabs: { [key: string]: React.ReactNode; } = {}; // a list of keys const keys = props.order ? props.order.slice() : []; const tabContents: React.ReactNode[] = []; const tabSiblings: React.ReactNode[] = []; function add(comps: React.ReactElement | React.ReactElement[]) { const compsArray: React.ReactElement[] = Array.isArray(comps) ? comps : [comps]; for (const comp of compsArray) { if (Array.isArray(comp)) { add(comp); continue; } if (!comp) { continue; } if (comp.type !== Tab) { // if element isn't a tab then just push it into the tab list tabSiblings.push(comp); continue; } const {children, closable, label, onClose, width} = comp.props; const key = comp.key == null ? label : comp.key; if (typeof key !== 'string') { throw new Error('tab needs a string key or a label'); } if (!keys.includes(key)) { keys.push(key); } const isActive: boolean = active === key; if (isActive || props.persist === true || comp.props.persist === true) { tabContents.push( , ); } // this tab has been hidden from the tab bar but can still be selected if its key is active if (comp.props.hidden) { continue; } let closeButton: HTMLDivElement | undefined | null; tabs[key] = ( ) => { if (event.target !== closeButton) { onActive(key); } }, 'Tabs', 'onTabClick', scope as any, 'tab:' + key + ':' + comp.props.label, ) : undefined }> {comp.props.label} {closable && ( (closeButton = ref)} // eslint-disable-next-line react/jsx-no-bind onMouseDown={() => { if (isActive && onActive) { const index = keys.indexOf(key); const newActive = keys[index + 1] || keys[index - 1] || null; onActive(newActive); } if (onClose) { onClose(); } }}> X )} ); } } if (props.children) { add(props.children); } let tabList: React.ReactNode; if (props.orderable === true) { tabList = ( ); } else { tabList = []; for (const key in tabs) { (tabList as Array).push(tabs[key]); } } if (props.newable === true) { after.push( + , ); } return ( {before} {tabList} {after} {tabContents} {tabSiblings} ); }