@spark-web/tabs
Version:
--- title: Tabs storybookPath: page-layout-tabs--default isExperimentalPackage: true ---
286 lines (268 loc) • 8.56 kB
JavaScript
import { createContext, useContext, forwardRef, useRef, useEffect, Fragment, Children, isValidElement } from 'react';
import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray';
import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
import { css } from '@emotion/css';
import { Root, List, Trigger, Content as Content$1 } from '@radix-ui/react-tabs';
import { BaseButton, useButtonStyles } from '@spark-web/button';
import { Divider } from '@spark-web/divider';
import { Inline } from '@spark-web/inline';
import { Stack } from '@spark-web/stack';
import { DefaultTextPropsProvider, Text } from '@spark-web/text';
import { useTheme } from '@spark-web/theme';
import { useComposedRefs } from '@spark-web/utils';
import { buildDataAttributes } from '@spark-web/utils/internal';
import useMeasure from 'react-use-measure';
import { jsx, jsxs } from 'react/jsx-runtime';
/** @todo handle fragments */
var IndexContext = /*#__PURE__*/createContext(0);
var IndexProvider = IndexContext.Provider;
function useIndexContext() {
var index = useContext(IndexContext);
// Radix UI requires that the `value` prop for Tab and TabPanel be a string
return String(index);
}
var Tabs = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) {
var activationMode = _ref.activationMode,
data = _ref.data,
children = _ref.children,
defaultIndex = _ref.defaultIndex;
var defaultValue = typeof defaultIndex === 'undefined' ? String(0) : String(defaultIndex);
return /*#__PURE__*/jsx(Root, _objectSpread(_objectSpread({}, data ? buildDataAttributes(data) : undefined), {}, {
activationMode: activationMode,
defaultValue: defaultValue,
ref: forwardedRef,
children: children
}));
});
Tabs.displayName = 'Tabs';
////////////////////////////////////////////////////////////////////////////////
/**
* TabList
*
* The parent component of the tabs.
*/
function TabList(_ref2) {
var children = _ref2.children,
data = _ref2.data;
return /*#__PURE__*/jsx(List, {
asChild: true,
children: /*#__PURE__*/jsx(Inline, {
data: data,
gap: "small",
children: resolveTabListChildren(children)
})
});
}
////////////////////////////////////////////////////////////////////////////////
/**
* Tab
*
* The interactive element that changes the selected panel.
*/
var GAP = 'small'; // Space between the interactive element and the pseudo border
var Tab = /*#__PURE__*/forwardRef(function (_ref3, forwardedRef) {
var children = _ref3.children,
data = _ref3.data,
disabled = _ref3.disabled;
var _useTabStyles = useTabStyles(),
_useTabStyles2 = _slicedToArray(_useTabStyles, 2),
boxProps = _useTabStyles2[0],
tabStyles = _useTabStyles2[1];
/**
* The font-weight of the tab changes when the button is active.
* This causes the button to get slightly wider (which we don't want).
* To avoid this, we measure the initial width of the button (after the
* first paint) and give it fixed width.
* We're using the style prop for this so that Emotion doesn't need to
* generate a completely new hash for each tab (as the width will vary
* for each tab based on the length of the text).
*/
var _useMeasure = useMeasure(),
_useMeasure2 = _slicedToArray(_useMeasure, 2),
internalRef = _useMeasure2[0],
bounds = _useMeasure2[1];
var composedRef = useComposedRefs(internalRef, forwardedRef);
var widthRef = useRef(undefined);
useEffect(function () {
if (bounds.width && !widthRef.current) {
widthRef.current = bounds.width;
}
}, [bounds.width]);
var index = useIndexContext();
return /*#__PURE__*/jsx(Stack, {
position: "relative",
paddingY: GAP,
children: /*#__PURE__*/jsx(Trigger, {
asChild: true,
disabled: disabled,
value: index,
children: /*#__PURE__*/jsx(BaseButton, _objectSpread(_objectSpread({}, boxProps), {}, {
ref: composedRef,
data: data,
className: css(tabStyles),
style: {
width: widthRef.current
},
children: /*#__PURE__*/jsx(DefaultTextPropsProvider, {
size: "small",
tone: "muted",
children: /*#__PURE__*/jsx(Content, {
children: children
})
})
}))
})
});
});
var TAB_NAME = 'Tab';
Tab.displayName = TAB_NAME;
////////////////////////////////////////////////////////////////////////////////
/**
* TabPanels
*
* The parent component of the panels.
*/
function TabPanels(_ref4) {
var children = _ref4.children,
data = _ref4.data;
return /*#__PURE__*/jsxs(Stack, {
data: data,
children: [/*#__PURE__*/jsx(Divider, {
width: "large"
}), resolveTabPanelsChildren(children)]
});
}
////////////////////////////////////////////////////////////////////////////////
/**
* TabPanel
*
* The panel that displays when it's corresponding tab is active.
*/
var TabPanel = /*#__PURE__*/forwardRef(function (_ref5, forwardedRef) {
var children = _ref5.children,
data = _ref5.data;
var index = useIndexContext();
return /*#__PURE__*/jsx(Content$1, _objectSpread(_objectSpread({}, data ? buildDataAttributes(data) : undefined), {}, {
forceMount: true,
ref: forwardedRef,
value: index,
className: css({
'&[data-state=inactive]': {
display: 'none'
}
}),
children: children
}));
});
var TAB_PANEL_NAME = 'TabPanel';
TabPanel.displayName = TAB_PANEL_NAME;
////////////////////////////////////////////////////////////////////////////////
/**
* Helpers
*/
/**
* Custom hook to encapsulate styles for the Tab component
*/
function useTabStyles() {
var theme = useTheme();
var _useButtonStyles = useButtonStyles({
iconOnly: false,
prominence: 'none',
size: 'medium',
tone: 'primary'
}),
_useButtonStyles2 = _slicedToArray(_useButtonStyles, 2),
boxProps = _useButtonStyles2[0],
buttonStyles = _useButtonStyles2[1];
return [_objectSpread(_objectSpread({}, boxProps), {}, {
paddingX: 'xlarge'
}), _objectSpread(_objectSpread({}, buttonStyles), {}, {
'&[data-state=active]': {
'&:not([aria-disabled=true])': {
':hover': {
background: theme.color.background.primaryMuted
},
'*': {
color: theme.color.foreground.primaryActive,
fontWeight: theme.typography.fontWeight.semibold
}
},
':active': {
transform: 'none'
},
// Pseudo border
'::after': {
content: '""',
position: 'absolute',
background: theme.color.foreground.primaryActive,
bottom: -theme.spacing[GAP],
left: 0,
right: 0,
height: theme.border.width.large,
width: '100%',
transform: 'translateY(100%)'
}
}
})];
}
/**
* Provides base typographic styles when the type of children is `string` or
* `number`.
* Otherwise children are wrapped in a `Fragment` in order to provide custom
* styles.
*/
function Content(_ref6) {
var children = _ref6.children;
if (typeof children === 'string' || typeof children === 'number') {
return /*#__PURE__*/jsx(Text, {
as: "span",
baseline: false,
overflowStrategy: "nowrap",
children: children
});
}
return /*#__PURE__*/jsx(Fragment, {
children: children
});
}
/**
* Throws an error if children are not `Tab` components
*/
function resolveTabListChildren(children) {
return Children.map(children, function (child, index) {
if (child.type.displayName !== TAB_NAME) {
throw new Error('All children of `TabList` must be `Tab` components');
}
return childWithIndexProvider({
child: child,
index: index
});
});
}
/**
* Throws an error if children are not `TabPanel` components
*/
function resolveTabPanelsChildren(children) {
return Children.map(children, function (child, index) {
if (child.type.displayName !== TAB_PANEL_NAME) {
throw new Error('All children of `TabPanels` must be `TabPanel` components');
}
return childWithIndexProvider({
child: child,
index: index
});
});
}
/**
* Ensures that the children are valid `ReactElements` before wrapping them with
* the IndexProvider.
*/
function childWithIndexProvider(_ref7) {
var child = _ref7.child,
index = _ref7.index;
return /*#__PURE__*/isValidElement(child) && /*#__PURE__*/jsx(IndexProvider, {
value: index,
children: child
});
}
export { IndexProvider, Tab, TabList, TabPanel, TabPanels, Tabs, useIndexContext };