diff --git a/desktop/app/src/sandy-chrome/DesignComponentDemos.tsx b/desktop/app/src/sandy-chrome/DesignComponentDemos.tsx
index 9612fe198..d092c098d 100644
--- a/desktop/app/src/sandy-chrome/DesignComponentDemos.tsx
+++ b/desktop/app/src/sandy-chrome/DesignComponentDemos.tsx
@@ -8,9 +8,17 @@
*/
import React from 'react';
-import {Typography, Card, Table, Collapse, Button, Tabs} from 'antd';
+import {Typography, Card, Table, Collapse, Button} from 'antd';
import {Layout, Link} from '../ui';
-import {NUX, Panel, theme, Tracked, TrackingScope} from 'flipper-plugin';
+import {
+ NUX,
+ Panel,
+ theme,
+ Tracked,
+ TrackingScope,
+ Tabs,
+ Tab,
+} from 'flipper-plugin';
import reactElementToJSXString from 'react-element-to-jsx-string';
import {CodeOutlined} from '@ant-design/icons';
@@ -341,6 +349,52 @@ const demos: PreviewProps[] = [
),
},
},
+ {
+ title: 'Tabs / Tab',
+ description:
+ "Tabs represents a tab control and all it's children should be Tab components. By default the Tab control uses all available space, but set grow=false to only use the minimally required space",
+ props: [
+ [
+ 'grow (Tabs)',
+ 'boolean (true)',
+ 'If true, the tab control will grow all tabs to the maximum available vertical space. If false, only the minimal required (natural) vertical space will be used',
+ ],
+ [
+ 'pad / gap (Tab)',
+ 'boolean / number (false)',
+ 'See the pad property of Layout.Container, determines whether the pane contents will have some padding and space between the items. By default no padding / gap is applied.',
+ ],
+ [
+ 'other props',
+ '',
+ 'This component wraps Tabs from ant design, see https://ant.design/components/tabs/ for more details',
+ ],
+ ],
+ demos: {
+ 'Two tabs': (
+
+
+ {aDynamicBox}
+
+ {aFixedHeightBox}
+ {aFixedHeightBox}
+
+
+
+ ),
+ 'Two tabs (no grow)': (
+
+
+ {aDynamicBox}
+
+ {aFixedHeightBox}
+ {aFixedHeightBox}
+
+
+
+ ),
+ },
+ },
{
title: 'NUX',
description:
@@ -420,7 +474,7 @@ function ComponentPreview({title, demos, description, props}: PreviewProps) {
{Object.entries(demos).map(([name, children]) => (
-
+
{children}
-
- } key="2">
+
+ } key="2">
{reactElementToJSXString(children)}
-
+
))}
diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx
index cab2fc4bf..7b85ce0fd 100644
--- a/desktop/flipper-plugin/src/__tests__/api.node.tsx
+++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx
@@ -40,6 +40,8 @@ test('Correct top level API exposed', () => {
"MarkerTimeline",
"NUX",
"Panel",
+ "Tab",
+ "Tabs",
"TestUtils",
"Tracked",
"TrackingScope",
diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts
index befb208c7..8aa757720 100644
--- a/desktop/flipper-plugin/src/index.ts
+++ b/desktop/flipper-plugin/src/index.ts
@@ -91,6 +91,7 @@ export {
InteractiveProps as _InteractiveProps,
} from './ui/Interactive';
export {Panel} from './ui/Panel';
+export {Tabs, Tab} from './ui/Tabs';
export {useLocalStorageState} from './utils/useLocalStorageState';
export {HighlightManager} from './ui/Highlight';
diff --git a/desktop/flipper-plugin/src/ui/Tabs.tsx b/desktop/flipper-plugin/src/ui/Tabs.tsx
new file mode 100644
index 000000000..e04ba25c8
--- /dev/null
+++ b/desktop/flipper-plugin/src/ui/Tabs.tsx
@@ -0,0 +1,89 @@
+/**
+ * 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 * as React from 'react';
+import {Children} from 'react';
+import {Tabs as AntdTabs, TabsProps, TabPaneProps} from 'antd';
+import {css, cx} from '@emotion/css';
+import {Layout} from './Layout';
+import {Spacing} from './theme';
+import {useLocalStorageState} from '../utils/useLocalStorageState';
+
+/**
+ * A Tabs component.
+ */
+export function Tabs({
+ grow,
+ children,
+ className,
+ ...baseProps
+}: {grow?: boolean} & TabsProps) {
+ const keys: string[] = [];
+ const keyedChildren = Children.map(children, (child: any, idx) => {
+ if (!child || typeof child !== 'object') {
+ return;
+ }
+ const tabKey =
+ (typeof child.props.tab === 'string' && child.props.tab) || `tab_${idx}`;
+ keys.push(tabKey);
+ return {
+ ...child,
+ props: {
+ ...child.props,
+ key: tabKey,
+ tabKey,
+ },
+ };
+ });
+
+ const [activeTab, setActiveTab] = useLocalStorageState(
+ 'Tabs:' + keys.join(','),
+ undefined,
+ );
+
+ return (
+ {
+ setActiveTab(key);
+ }}
+ {...baseProps}
+ className={cx(className, grow !== false ? growingTabs : undefined)}>
+ {keyedChildren}
+
+ );
+}
+
+export const Tab: React.FC<
+ TabPaneProps & {
+ pad?: Spacing;
+ gap?: Spacing;
+ }
+> = function Tab({pad, gap, children, ...baseProps}) {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const growingTabs = css`
+ flex: 1;
+ & .tabpanel {
+ display: flex;
+ }
+ & .ant-tabs-content {
+ height: 100%;
+ }
+ & .ant-tabs-tabpane {
+ display: flex;
+ }
+`;
diff --git a/desktop/patches/rc-tabs+11.7.2.patch b/desktop/patches/rc-tabs+11.7.2.patch
index af8ab2e35..51942fe6a 100644
--- a/desktop/patches/rc-tabs+11.7.2.patch
+++ b/desktop/patches/rc-tabs+11.7.2.patch
@@ -16,7 +16,7 @@ index ac93b76..edae9be 100644
+ 'Tabs',
+ 'onTabClick',
+ scope,
-+ 'tab:' + key + ':' + tab,
++ 'tab:' + key,
+ onClick,
+ e
+ );
diff --git a/docs/extending/sandy-migration.mdx b/docs/extending/sandy-migration.mdx
index a27a7f043..bf84dbaf3 100644
--- a/docs/extending/sandy-migration.mdx
+++ b/docs/extending/sandy-migration.mdx
@@ -150,7 +150,7 @@ For conversion, the following table maps the old components to the new ones:
| `ManagedDataInspector` / `DataInspector` | `DataInspector` | `flipper-plugin` ||
| `ManagedElementInspector` / `ElementInspector` | `ElementInspector` | `flipper-plugin` ||
| `Panel` | `Panel` | `flipper-plugin` ||
-| `Tabs` / `Tab` | `Tabs` / `Tab` | `flipper-plugin ||
+| `Tabs` / `Tab` | `Tabs` / `Tab` | `flipper-plugin | Note that `Tab`'s `title` property is now called `tab`. |
Most other components, like `select` elements, tabs, date-pickers, etc etc can all be found in the Ant documentaiton.