From 7f5b5a7024a4e146ae2edcf176e9fed55165338d Mon Sep 17 00:00:00 2001 From: Adam Bottega Date: Thu, 26 Sep 2024 09:53:57 +1000 Subject: [PATCH] replace old tabs with new tabs --- lib/components/Tabs/Tab.mdx | 27 -- lib/components/Tabs/Tab.stories.js | 75 ---- .../{TabsV2/TabsV2.mdx => Tabs/Tabs.mdx} | 4 +- .../Tabs.stories.js} | 12 +- lib/components/Tabs/TabsContainer.mdx | 17 - lib/components/Tabs/TabsContainer.stories.js | 42 -- lib/components/Tabs/index.js | 392 ++++++++++++------ lib/components/TabsV2/index.js | 271 ------------ lib/index.js | 3 +- 9 files changed, 268 insertions(+), 575 deletions(-) delete mode 100644 lib/components/Tabs/Tab.mdx delete mode 100644 lib/components/Tabs/Tab.stories.js rename lib/components/{TabsV2/TabsV2.mdx => Tabs/Tabs.mdx} (81%) rename lib/components/{TabsV2/TabsV2.stories.js => Tabs/Tabs.stories.js} (85%) delete mode 100644 lib/components/Tabs/TabsContainer.mdx delete mode 100644 lib/components/Tabs/TabsContainer.stories.js delete mode 100644 lib/components/TabsV2/index.js diff --git a/lib/components/Tabs/Tab.mdx b/lib/components/Tabs/Tab.mdx deleted file mode 100644 index ba0096fb..00000000 --- a/lib/components/Tabs/Tab.mdx +++ /dev/null @@ -1,27 +0,0 @@ -import * as stories from "./Tab.stories"; -import { Meta, Story, Controls, Canvas } from "@storybook/addon-docs"; -import { TabsContainer, Tab } from "."; - - - -[Default tabs](#tabs) | [Properties](#properties) - -# Tabs - -Tabs are intended to provide a way to navigate between distinct sections within the layout without leaving the page or triggering a page refresh. - -## Active tab - - - -## With notifications tab - - - -## With popover tab - - - -## Properties - - diff --git a/lib/components/Tabs/Tab.stories.js b/lib/components/Tabs/Tab.stories.js deleted file mode 100644 index 025fcbc6..00000000 --- a/lib/components/Tabs/Tab.stories.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from "react"; -import Box from "../Box"; -import { TabsContainer, Tab } from "."; -import Popover from "../Popover"; - -export default { - title: "Components/Tabs/Tab", - decorators: [ - (storyFn) => ( - - {storyFn()} - - ) - ], - component: Tab -}; - -export const activeTab = () => ( - - - Details - - - Planning - - -); - -activeTab.story = { - name: "Active Tab" -}; - -export const withNotificationsTab = () => ( - - - Details - - - Planning - - -); - -activeTab.story = { - name: "With Notifications Tab" -}; - -export const withPopoverTab = () => ( - - - - Details - - - - - - Additional information - - - - -); - -withPopoverTab.story = { - name: "With Popover Tab" -}; diff --git a/lib/components/TabsV2/TabsV2.mdx b/lib/components/Tabs/Tabs.mdx similarity index 81% rename from lib/components/TabsV2/TabsV2.mdx rename to lib/components/Tabs/Tabs.mdx index b2edf7ae..2231be46 100644 --- a/lib/components/TabsV2/TabsV2.mdx +++ b/lib/components/Tabs/Tabs.mdx @@ -1,6 +1,6 @@ import { Story, Controls, Meta, Canvas } from "@storybook/addon-docs"; import Tabs from "."; -import * as stories from "./TabsV2.stories"; +import * as stories from "./Tabs.stories"; @@ -12,7 +12,7 @@ Tabs are intended to provide a way to navigate between distinct sections within ## Default Tabs - + ## Properties diff --git a/lib/components/TabsV2/TabsV2.stories.js b/lib/components/Tabs/Tabs.stories.js similarity index 85% rename from lib/components/TabsV2/TabsV2.stories.js rename to lib/components/Tabs/Tabs.stories.js index 19f134d2..654ff83b 100644 --- a/lib/components/TabsV2/TabsV2.stories.js +++ b/lib/components/Tabs/Tabs.stories.js @@ -1,10 +1,10 @@ import React from "react"; -import TabsV2 from "."; +import Tabs from "."; import { BrowserRouter, Route, Switch } from "react-router-dom"; import Box from "../Box"; export default { - title: "Components/TabsV2", + title: "Components/Tabs", decorators: [ (storyFn) => ( @@ -12,7 +12,7 @@ export default { ) ], - component: TabsV2 + component: Tabs }; const tabsList = [ @@ -54,9 +54,9 @@ const tabsList = [ } ]; -export const defaultTabsV2 = () => ( +export const defaultTabs = () => ( - + {tabsList.map((tab) => ( @@ -68,4 +68,4 @@ export const defaultTabsV2 = () => ( ); -defaultTabsV2.storyName = "Default"; +defaultTabs.storyName = "Default"; diff --git a/lib/components/Tabs/TabsContainer.mdx b/lib/components/Tabs/TabsContainer.mdx deleted file mode 100644 index 0aafe8d9..00000000 --- a/lib/components/Tabs/TabsContainer.mdx +++ /dev/null @@ -1,17 +0,0 @@ -import { Meta, Story, Controls, Canvas } from "@storybook/addon-docs"; -import { TabsContainer, Tab } from "."; -import * as stories from "./TabsContainer.stories"; - - - -[Default tabs](#tabs) | [Properties](#properties) - -# Tabs - -Tabs are intended to provide a way to navigate between distinct sections within the layout without leaving the page or triggering a page refresh. - - - -## Properties - - diff --git a/lib/components/Tabs/TabsContainer.stories.js b/lib/components/Tabs/TabsContainer.stories.js deleted file mode 100644 index 51e5a535..00000000 --- a/lib/components/Tabs/TabsContainer.stories.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; -import Box from "../Box"; -import Popover from "../Popover"; -import { TabsContainer, Tab } from "."; - -export default { - title: "Components/Tabs", - decorators: [ - (storyFn) => ( - - {storyFn()} - - ) - ], - component: TabsContainer -}; - -export const defaultTabs = () => ( - - - Details - - - - - Additional information - - - - - Planning - - -); - -defaultTabs.story = { - name: "Default Tabs" -}; diff --git a/lib/components/Tabs/index.js b/lib/components/Tabs/index.js index 126cd928..90b311e0 100644 --- a/lib/components/Tabs/index.js +++ b/lib/components/Tabs/index.js @@ -1,145 +1,271 @@ -import React from "react"; -import PropTypes from "prop-types"; -import styled, { ThemeProvider } from "styled-components"; +import React, { useState, useRef, useEffect, useCallback } from "react"; +import styled, { css } from "styled-components"; +import { NavLink, useLocation } from "react-router-dom"; +import { isEqual } from "lodash"; +import ActionsMenu, { ActionsMenuItem } from "../ActionsMenu"; +import Icon from "../Icon"; +import FlexItem from "../Flex"; import { themeGet } from "@styled-system/theme-get"; -import { css } from "@styled-system/css"; -import { space, color } from "styled-system"; - -export const TabsContainerItem = styled("div")( - css({ - width: "100%", - display: "flex", - alignItems: "center", - flexWrap: "wrap", - bg: "transparent" - }), - space -); - -export const TabItem = styled("div")( - (props) => - css({ - marginRight: 3, - a: { - borderRadius: themeGet("radii.2")(props), - bg: themeGet("colors.greyLighter")(props), - transition: themeGet("transition.transitionDefault")(props), - px: 4, - py: 3, - fontSize: themeGet("fontSizes.1")(props), - fontWeight: themeGet("fontWeights.2")(props), - color: themeGet("colors.greyDarker")(props), - display: "flex", - alignItems: "center", - position: "relative", - whiteSpace: "nowrap", - textDecoration: "none", - textAlign: "center", - textTransform: "uppercase", - cursor: "pointer", - "&:hover": { - bg: themeGet("colors.greyLight")(props), - color: themeGet("colors.greyDarker")(props), - outline: "0" - }, - "&:focus": { - color: themeGet("colors.greyDarker")(props), - outline: "0", - boxShadow: - themeGet("shadows.thinOutline")(props) + - " " + - themeGet("colors.grey")(props) - }, - button: { - marginLeft: 2 - } - }, - "&.active": { - a: { - color: themeGet("colors.primary")(props), - bg: themeGet("colors.white")(props), - "&:hover": { - cursor: "default" - } - } - }, - "&.notification": { - a: { - "&::after": { - position: "absolute", - top: "calc(8px * -1)", - right: "calc(4px * -2)", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "20px", - height: "20px", - borderRadius: "50%", - fontSize: themeGet("fontSizes.0")(props), - fontWeight: themeGet("fontWeights.2")(props), - content: `"${props.notification}"`, - bg: themeGet("colors.danger")(props), - color: themeGet("colors.white")(props), - zIndex: 4 +import PropTypes from "prop-types"; + +const TabsContainer = styled.div` + position: relative; +`; + +const TabWrapper = styled.div` + position: relative; +`; + +const VisibleTabs = styled.div` + flex-shrink: 1; + display: flex; + align-items: center; + overflow: hidden; +`; + +const activeTabStyle = css` + background-color: ${themeGet("colors.white")}; + color: ${themeGet("colors.primary")}; + cursor: default; + &:hover { + background-color: ${themeGet("colors.white")}; + color: ${themeGet("colors.primary")}; + } + &:focus { + color: ${themeGet("colors.primary")}; + } +`; + +const Tab = styled(NavLink)` + width: ${({ fullWidth }) => (fullWidth ? "100%" : "fit-content")}; + display: block; + border-radius: ${themeGet("radii.2")}; + transition: background 200ms ease-in-out, color 200ms ease-in-out; + padding: ${themeGet("space.3")} ${themeGet("space.4")}; + font-size: ${themeGet("fontSizes.1")}; + font-weight: ${themeGet("fontWeights.2")}; + position: relative; + white-space: nowrap; + text-decoration: none; + text-align: center; + text-transform: uppercase; + background-color: ${themeGet("colors.greyLighter")}; + color: ${themeGet("colors.greyDarker")}; + cursor: pointer; + + ${({ tabInShowMore }) => + tabInShowMore + ? css` + position: absolute; + visibility: hidden; + ` + : ""} + + &:hover { + outline: 0; + background-color: ${themeGet("colors.greyLight")}; + color: ${themeGet("colors.greyDarker")}; + } + &:focus { + outline: 0; + color: ${themeGet("colors.greyDarker")}; + box-shadow: inset ${themeGet("shadows.thinOutline")} + ${themeGet("colors.grey")}; + } + + &.active { + ${activeTabStyle} + } +`; + +const ShowMoreButton = styled.button` + appearance: none; + border: none; + font-family: ${themeGet("fonts.main")}; + font-size: ${themeGet("fontSizes.1")}; + font-weight: ${themeGet("fontWeights.2")}; + border-radius: ${themeGet("radii.2")}; + background-color: ${themeGet("colors.greyLighter")}; + transition: ${themeGet("transition.transitionDefault")}; + padding: ${themeGet("space.3")} ${themeGet("space.4")}; + color: ${themeGet("colors.greyDarker")}; + display: ${({ showMoreVisible }) => (showMoreVisible ? "flex" : "none")}; + align-items: center; + &:hover { + background-color: ${themeGet("colors.greyLight")}; + color: ${themeGet("colors.greyDarker")}; + outline: 0; + } + &:focus { + color: ${themeGet("colors.greyDarker")}; + outline: 0; + box-shadow: inset ${themeGet("shadows.thinOutline")} + ${themeGet("colors.grey")}; + } + &.hasActive { + ${activeTabStyle} + } +`; + +const ShowMoreTabs = styled.div` + border-radius: ${themeGet("radii.2")}; + background-color: ${themeGet("colors.white")}; + min-width: 84px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 6px; + box-shadow: inset ${themeGet("shadows.thinOutline")} + ${themeGet("colors.greyLighter")}, + ${themeGet("shadows.boxDefault")}; +`; + +const tabsGap = 6; + +const Tabs = ({ tabsList }) => { + const containerRef = useRef(null); + + const showMoreButtonRef = useRef(); + + const containerVisibleWidth = useRef(0); + + const [showMoreTabs, setShowMoreTabs] = useState([]); + + const calculateVisibility = useCallback( + (actionElements) => { + const showMoreButtonWidth = showMoreButtonRef.current + ? showMoreButtonRef.current.offsetWidth + : 0; + // as we loop through the tabs, we need to calculate the width of the visible tabs. + let calculatedWidth = showMoreTabs.length ? showMoreButtonWidth : 0; + + // variable for the list of hidden tabs which will be put to react state + const newShowMoreTabs = []; + + [...actionElements] + .filter((el) => el.tagName === "A") + .forEach((actionEl, i) => { + // visibleElementsWidth will be increased by + // the corresponding width of the element + gapWidth + calculatedWidth += actionEl.offsetWidth + tabsGap; + + // compare computed widths and push into newShowMoreTabs if current tab width is bigger than container width + if (calculatedWidth >= containerVisibleWidth.current) { + newShowMoreTabs.push(i); } - } + }); + + if (!isEqual(showMoreTabs, newShowMoreTabs)) { + // update React state with the list of hidden tabs + setShowMoreTabs(newShowMoreTabs); } - }), - space, - color -); - -export const Tab = ({ theme, children, active, notification, ...props }) => { - const component = ( - - {React.Children.map(children, (child) => - React.cloneElement(child, { role: "tab", "aria-selected": `${active}` }) - )} - - ); - return theme ? ( - {component} - ) : ( - component + }, + [showMoreTabs] ); -}; -Tab.propTypes = { - /** Specifies whether the tab is the active tab */ - active: PropTypes.bool, - /** Specifies any notifications attached to the Tab */ - notification: PropTypes.string, - /** Specifies the colour theme */ - theme: PropTypes.object, - /** The content of the Tab is passed as a child. */ - children: PropTypes.node -}; + useEffect(() => { + const actionElements = containerRef.current?.children || []; -export const TabsContainer = ({ theme, children, ...props }) => { - const component = ( - - {children} - + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.contentBoxSize) { + const contentBoxSize = entry.contentBoxSize[0]; + + // Math.ceil is necessary to round up and return + // the smallest integer for the size of observed element + containerVisibleWidth.current = Math.ceil(contentBoxSize.inlineSize); + + // invoke the functions which calculates tabs visibility + // and sets data to the list of hidden tabs + calculateVisibility(actionElements); + } + } + }); + + // adding ResizeObserver to the observed container + resizeObserver.observe(containerRef.current); + }, [calculateVisibility]); + + const visibleTabsList = tabsList + .map((tab) => ({ ...tab, isVisible: tab.isVisible ?? true })) + .filter((tab) => tab.isVisible); + const showMoreTabsList = visibleTabsList.filter((tab, i) => + showMoreTabs.includes(i) + ); + const location = useLocation(); + const showMoreItemActive = showMoreTabsList.find((action) => + location.pathname.endsWith(action.path) ); - return theme ? ( - {component} - ) : ( - component + return ( + + + + {visibleTabsList.map((tab, i) => ( + + {tab.label} + + ))} + + + More + + + + + } + closeOnClick + > + + {showMoreTabsList.map((tab) => ( + + {tab.label} + + ))} + + + + + ); }; -TabsContainer.propTypes = { - /** The contents of the TabsContainer are passed as a child. */ - children: PropTypes.node, - /** Specifies the colour theme of the container */ - theme: PropTypes.object +Tabs.propTypes = { + tabsList: PropTypes.arrayOf( + PropTypes.objectOf({ + label: PropTypes.string, + path: PropTypes.string, + isVisible: PropTypes.bool + }) + ) }; + +export default Tabs; diff --git a/lib/components/TabsV2/index.js b/lib/components/TabsV2/index.js deleted file mode 100644 index 3e6f2ef5..00000000 --- a/lib/components/TabsV2/index.js +++ /dev/null @@ -1,271 +0,0 @@ -import React, { useState, useRef, useEffect, useCallback } from "react"; -import styled, { css } from "styled-components"; -import { NavLink, useLocation } from "react-router-dom"; -import { isEqual } from "lodash"; -import ActionsMenu, { ActionsMenuItem } from "../ActionsMenu"; -import Icon from "../Icon"; -import FlexItem from "../Flex"; -import { themeGet } from "@styled-system/theme-get"; -import PropTypes from "prop-types"; - -const TabsContainer = styled.div` - position: relative; -`; - -const TabWrapper = styled.div` - position: relative; -`; - -const Tabs = styled.div` - flex-shrink: 1; - display: flex; - align-items: center; - overflow: hidden; -`; - -const activeTabStyle = css` - background-color: ${themeGet("colors.white")}; - color: ${themeGet("colors.primary")}; - cursor: default; - &:hover { - background-color: ${themeGet("colors.white")}; - color: ${themeGet("colors.primary")}; - } - &:focus { - color: ${themeGet("colors.primary")}; - } -`; - -const Tab = styled(NavLink)` - width: ${({ fullWidth }) => (fullWidth ? "100%" : "fit-content")}; - display: block; - border-radius: ${themeGet("radii.2")}; - transition: background 200ms ease-in-out, color 200ms ease-in-out; - padding: ${themeGet("space.3")} ${themeGet("space.4")}; - font-size: ${themeGet("fontSizes.1")}; - font-weight: ${themeGet("fontWeights.2")}; - position: relative; - white-space: nowrap; - text-decoration: none; - text-align: center; - text-transform: uppercase; - background-color: ${themeGet("colors.greyLighter")}; - color: ${themeGet("colors.greyDarker")}; - cursor: pointer; - - ${({ tabInShowMore }) => - tabInShowMore - ? css` - position: absolute; - visibility: hidden; - ` - : ""} - - &:hover { - outline: 0; - background-color: ${themeGet("colors.greyLight")}; - color: ${themeGet("colors.greyDarker")}; - } - &:focus { - outline: 0; - color: ${themeGet("colors.greyDarker")}; - box-shadow: inset ${themeGet("shadows.thinOutline")} - ${themeGet("colors.grey")}; - } - - &.active { - ${activeTabStyle} - } -`; - -const ShowMoreButton = styled.button` - appearance: none; - border: none; - font-family: ${themeGet("fonts.main")}; - font-size: ${themeGet("fontSizes.1")}; - font-weight: ${themeGet("fontWeights.2")}; - border-radius: ${themeGet("radii.2")}; - background-color: ${themeGet("colors.greyLighter")}; - transition: ${themeGet("transition.transitionDefault")}; - padding: ${themeGet("space.3")} ${themeGet("space.4")}; - color: ${themeGet("colors.greyDarker")}; - display: ${({ showMoreVisible }) => (showMoreVisible ? "flex" : "none")}; - align-items: center; - &:hover { - background-color: ${themeGet("colors.greyLight")}; - color: ${themeGet("colors.greyDarker")}; - outline: 0; - } - &:focus { - color: ${themeGet("colors.greyDarker")}; - outline: 0; - box-shadow: inset ${themeGet("shadows.thinOutline")} - ${themeGet("colors.grey")}; - } - &.hasActive { - ${activeTabStyle} - } -`; - -const ShowMoreTabs = styled.div` - border-radius: ${themeGet("radii.2")}; - background-color: ${themeGet("colors.white")}; - min-width: 84px; - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - padding: 6px; - box-shadow: inset ${themeGet("shadows.thinOutline")} - ${themeGet("colors.greyLighter")}, - ${themeGet("shadows.boxDefault")}; -`; - -const tabsGap = 6; - -const TabsV2 = ({ tabsList }) => { - const containerRef = useRef(null); - - const showMoreButtonRef = useRef(); - - const containerVisibleWidth = useRef(0); - - const [showMoreTabs, setShowMoreTabs] = useState([]); - - const calculateVisibility = useCallback( - (actionElements) => { - const showMoreButtonWidth = showMoreButtonRef.current - ? showMoreButtonRef.current.offsetWidth - : 0; - // as we loop through the tabs, we need to calculate the width of the visible tabs. - let calculatedWidth = showMoreTabs.length ? showMoreButtonWidth : 0; - - // variable for the list of hidden tabs which will be put to react state - const newShowMoreTabs = []; - - [...actionElements] - .filter((el) => el.tagName === "A") - .forEach((actionEl, i) => { - // visibleElementsWidth will be increased by - // the corresponding width of the element + gapWidth - calculatedWidth += actionEl.offsetWidth + tabsGap; - - // compare computed widths and push into newShowMoreTabs if current tab width is bigger than container width - if (calculatedWidth >= containerVisibleWidth.current) { - newShowMoreTabs.push(i); - } - }); - - if (!isEqual(showMoreTabs, newShowMoreTabs)) { - // update React state with the list of hidden tabs - setShowMoreTabs(newShowMoreTabs); - } - }, - [showMoreTabs] - ); - - useEffect(() => { - const actionElements = containerRef.current?.children || []; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - if (entry.contentBoxSize) { - const contentBoxSize = entry.contentBoxSize[0]; - - // Math.ceil is necessary to round up and return - // the smallest integer for the size of observed element - containerVisibleWidth.current = Math.ceil(contentBoxSize.inlineSize); - - // invoke the functions which calculates tabs visibility - // and sets data to the list of hidden tabs - calculateVisibility(actionElements); - } - } - }); - - // adding ResizeObserver to the observed container - resizeObserver.observe(containerRef.current); - }, [calculateVisibility]); - - const visibleTabsList = tabsList - .map((tab) => ({ ...tab, isVisible: tab.isVisible ?? true })) - .filter((tab) => tab.isVisible); - const showMoreTabsList = visibleTabsList.filter((tab, i) => - showMoreTabs.includes(i) - ); - const location = useLocation(); - const showMoreItemActive = showMoreTabsList.find((action) => - location.pathname.endsWith(action.path) - ); - - return ( - - - - {visibleTabsList.map((tab, i) => ( - - {tab.label} - - ))} - - - More - - - - - } - closeOnClick - > - - {showMoreTabsList.map((tab) => ( - - {tab.label} - - ))} - - - - - - ); -}; - -TabsV2.propTypes = { - tabsList: PropTypes.arrayOf( - PropTypes.objectOf({ - label: PropTypes.string, - path: PropTypes.string, - isVisible: PropTypes.bool - }) - ) -}; - -export default TabsV2; diff --git a/lib/index.js b/lib/index.js index 5640aee5..4957a55f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -56,8 +56,7 @@ export { default as Spacer } from "./components/Spacer"; export { default as StatusDot } from "./components/StatusDot"; export { default as StyledLink, styleLink } from "./components/StyledLink"; export { default as Table } from "./components/Table"; -export { Tab, TabsContainer } from "./components/Tabs"; -export { default as TabsV2 } from "./components/TabsV2"; +export { default as Tabs } from "./components/Tabs"; export { default as Tag } from "./components/Tag"; export { default as TextInput } from "./components/TextInput"; export { default as TextArea } from "./components/TextArea";