UNPKG

@spark-web/tabs

Version:

--- title: Tabs storybookPath: page-layout-tabs--default isExperimentalPackage: true ---

286 lines (268 loc) 8.56 kB
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 };