braid-design-system
Version: 
Themeable design system for the SEEK Group
195 lines (194 loc) • 7.07 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import assert from "assert";
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { useContext, useRef, useState, useEffect, useCallback } from "react";
import { negativeMargin } from "../../css/negativeMargin/negativeMargin.mjs";
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.mjs";
import { flattenChildren } from "../../utils/flattenChildren.mjs";
import { Box } from "../Box/Box.mjs";
import { useBraidTheme } from "../BraidProvider/BraidThemeContext.mjs";
import { Divider } from "../Divider/Divider.mjs";
import { buildDataAttributes } from "../private/buildDataAttributes.mjs";
import { dividerSpacingForSize } from "./Tab.mjs";
import { TabListContext } from "./TabListContext.mjs";
import { TAB_LIST_UPDATED } from "./Tabs.actions.mjs";
import { TabsContext } from "./TabsProvider.mjs";
import { scroll, nowrap, mask, marginAuto, divider, tabUnderline, tabUnderlineActiveDarkMode, underlineWidth, underlineLeft } from "./Tabs.css.mjs";
const tabLinePositionDefault = { left: 0, width: 0 };
const getActiveTabLinePosition = (button) => {
  if (!button) {
    return tabLinePositionDefault;
  }
  const computedStyle = getComputedStyle(button);
  const elWidth = button.getBoundingClientRect().width;
  const paddingLeft = parseFloat(computedStyle.paddingLeft);
  const paddingRight = parseFloat(computedStyle.paddingRight);
  const width = elWidth - paddingLeft - paddingRight;
  return { left: button.offsetLeft + paddingLeft, width };
};
const Tabs = (props) => {
  const tabsContext = useContext(TabsContext);
  const tabsRef = useRef(null);
  const [activeTabPosition, setActiveTabPosition] = useState(
    tabLinePositionDefault
  );
  const {
    children,
    label,
    data,
    align = "left",
    size = "standard",
    gutter,
    reserveHitArea = false,
    divider: divider$1 = "minimal",
    ...restProps
  } = props;
  assert(
    tabsContext !== null,
    "Tabs must be rendered as a child of TabsProvider. See the documentation for correct usage: https://seek-oss.github.io/braid-design-system/components/Tabs"
  );
  if (!tabsContext) {
    throw new Error("Tabs rendered outside TabsProvider");
  }
  const { dispatch, a11y, selectedIndex, selectedItem } = tabsContext;
  const tabItems = [];
  const childTabs = flattenChildren(children);
  const tabs = childTabs.map((tab, index) => {
    assert(
      // @ts-expect-error
      typeof tab === "object" && tab.type.__isTab__,
      "Only Tab elements can be direct children of a Tabs"
    );
    tabItems.push(tab.props.item ?? index);
    return /* @__PURE__ */ jsx(
      TabListContext.Provider,
      {
        value: {
          tabListItemIndex: index,
          scrollContainer: tabsRef.current,
          isLast: childTabs.length === index + 1,
          size
        },
        children: tab
      },
      index
    );
  });
  useEffect(() => {
    dispatch({ type: TAB_LIST_UPDATED, tabItems });
  }, [tabItems.join(), dispatch]);
  const {
    space: { grid, space }
  } = useBraidTheme();
  const [showMask, setShowMask] = useState(true);
  const updateMask = useCallback(() => {
    if (!tabsRef.current) {
      return;
    }
    setShowMask(
      tabsRef.current.scrollWidth - tabsRef.current.offsetWidth - tabsRef.current.scrollLeft > grid * space.small
    );
  }, [tabsRef, setShowMask, grid, space]);
  useIsomorphicLayoutEffect(() => {
    updateMask();
    window.addEventListener("resize", updateMask);
    return () => window.removeEventListener("resize", updateMask);
  }, [updateMask]);
  const selectedTabIndex = typeof selectedItem !== "undefined" ? tabItems.indexOf(selectedItem) : selectedIndex;
  const selectedTabButtonEl = tabsContext.tabButtonElements[selectedTabIndex.toString()];
  useIsomorphicLayoutEffect(() => {
    setActiveTabPosition(getActiveTabLinePosition(selectedTabButtonEl));
  }, [selectedTabButtonEl, size]);
  return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
    Box,
    {
      className: reserveHitArea ? void 0 : negativeMargin("top", dividerSpacingForSize[size]),
      children: /* @__PURE__ */ jsx(Box, { position: "relative", children: /* @__PURE__ */ jsxs(
        Box,
        {
          ref: tabsRef,
          className: [
            scroll,
            nowrap,
            showMask ? mask : null
          ],
          display: "flex",
          onScroll: updateMask,
          flexWrap: "nowrap",
          children: [
            /* @__PURE__ */ jsx(
              Box,
              {
                display: "flex",
                className: align === "center" ? marginAuto : void 0,
                paddingX: gutter,
                flexWrap: "nowrap",
                position: "relative",
                zIndex: 1,
                children: /* @__PURE__ */ jsxs(
                  Box,
                  {
                    ...a11y.tabListProps({ label }),
                    display: "flex",
                    ...buildDataAttributes({ data, validateRestProps: restProps }),
                    flexWrap: "nowrap",
                    position: "relative",
                    children: [
                      tabs,
                      divider$1 === "minimal" ? /* @__PURE__ */ jsx(
                        Box,
                        {
                          position: "absolute",
                          bottom: 0,
                          left: 0,
                          right: 0,
                          className: divider,
                          children: /* @__PURE__ */ jsx(Divider, {})
                        }
                      ) : null,
                      selectedTabButtonEl ? /* @__PURE__ */ jsx(
                        Box,
                        {
                          component: "span",
                          position: "absolute",
                          display: "block",
                          left: 0,
                          right: 0,
                          bottom: 0,
                          background: "formAccent",
                          pointerEvents: "none",
                          className: [
                            tabUnderline,
                            tabUnderlineActiveDarkMode
                          ],
                          style: assignInlineVars({
                            [underlineLeft]: activeTabPosition.left.toString(),
                            [underlineWidth]: activeTabPosition.width.toString()
                          })
                        }
                      ) : null
                    ]
                  }
                )
              }
            ),
            divider$1 === "full" ? /* @__PURE__ */ jsx(
              Box,
              {
                position: "absolute",
                bottom: 0,
                left: 0,
                right: 0,
                className: divider,
                children: /* @__PURE__ */ jsx(Divider, {})
              }
            ) : null
          ]
        }
      ) })
    }
  ) });
};
export {
  Tabs
};