diff --git a/packages/design-system/src/components/Tabs/Primitive/TabAsLink.tsx b/packages/design-system/src/components/Tabs/Primitive/TabAsLink.tsx deleted file mode 100644 index ac7aa85ad6..0000000000 --- a/packages/design-system/src/components/Tabs/Primitive/TabAsLink.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { forwardRef, ReactElement, Ref } from 'react'; -import classnames from 'classnames'; -// eslint-disable-next-line @talend/import-depth -import { IconNameWithSize } from '@talend/icons/dist/typeUtils'; -import { DataAttributes } from '../../../types'; -import { StackHorizontal } from '../../Stack'; -import { SizedIcon } from '../../Icon'; - -import styles from './TabStyles.module.scss'; -import { Tooltip, TooltipChildrenFnProps, TooltipChildrenFnRef } from '../../Tooltip'; -import { TagDefault } from '../../Tag'; -import Linkable, { LinkableType } from '../../Linkable'; -import { mergeRefs } from '../../../mergeRef'; - -type TabChildren = Omit & { - title: string; - icon?: IconNameWithSize<'S'>; - tag?: string | number; - size?: 'M' | 'L'; - isActive?: boolean; -} & ({ tooltip?: string; as?: never } | { tooltip?: never; as?: ReactElement }); - -export type TabAsLinkProps = DataAttributes & TabChildren; - -const TabComponent = forwardRef( - (props: Omit, ref: Ref) => { - const { icon, title, tag, size, isActive, as = 'a', ...rest } = props; - return ( - - - {icon && } - {title} - {tag && {tag}} - - - ); - }, -); -TabComponent.displayName = 'TabComponent'; - -const TabAsLink = forwardRef((props: TabAsLinkProps, ref: Ref) => { - const { tooltip, ...otherProps } = props; - - if (tooltip) { - return ( - - {(triggerProps: TooltipChildrenFnProps, triggerRef: TooltipChildrenFnRef) => ( - - )} - - ); - } - - return ; -}); - -TabAsLink.displayName = 'TabAsLink'; - -export default TabAsLink; diff --git a/packages/design-system/src/components/Tabs/Primitive/TabElement.tsx b/packages/design-system/src/components/Tabs/Primitive/TabElement.tsx deleted file mode 100644 index 90e6cd1629..0000000000 --- a/packages/design-system/src/components/Tabs/Primitive/TabElement.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FC, ReactElement, ReactText } from 'react'; - -import { IconNameWithSize } from '@talend/icons'; - -import { DataAttributes } from '../../../types'; - -export type TabTitleDefault = { - title: ReactText; - - icon?: IconNameWithSize<'S'>; - tag?: string | number; - tooltip?: string; -}; - -export type TabTitleElement = { - title: ReactElement; - - icon?: never; - tag?: never; - tooltip?: never; -}; - -export type TabTitle = TabTitleDefault | TabTitleElement; - -export type TabProps = Omit, 'id' | 'title'> & - DataAttributes & - TabTitle & { id: string }; - -const Tab: FC = () => { - return
I should not be rendered !
; -}; - -Tab.displayName = 'Tab'; - -export default Tab; diff --git a/packages/design-system/src/components/Tabs/Primitive/TabList.tsx b/packages/design-system/src/components/Tabs/Primitive/TabList.tsx deleted file mode 100644 index 72442d6f1a..0000000000 --- a/packages/design-system/src/components/Tabs/Primitive/TabList.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { forwardRef, HTMLAttributes, ReactElement, Ref } from 'react'; -import { DataAttributes } from '../../../types'; -import { StackHorizontal } from '../../Stack'; - -export type TabListPropsTypesWithoutState = DataAttributes & - HTMLAttributes & { children: ReactElement | ReactElement[] }; - -type TabListPropsTypes = TabListPropsTypesWithoutState; - -const TabList = forwardRef((props: TabListPropsTypes, ref: Ref) => { - return ( - - {props.children} - - ); -}); - -TabList.displayName = 'TabList'; - -export default TabList; diff --git a/packages/design-system/src/components/Tabs/Primitive/TabNavigation.tsx b/packages/design-system/src/components/Tabs/Primitive/TabNavigation.tsx deleted file mode 100644 index ce2f71f0e1..0000000000 --- a/packages/design-system/src/components/Tabs/Primitive/TabNavigation.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { forwardRef, isValidElement } from 'react'; -import type { Ref, ReactChild, ReactText, ReactElement } from 'react'; - -import classnames from 'classnames'; - -import { IconNameWithSize } from '@talend/icons'; - -import { mergeRefs } from '../../../mergeRef'; -import { DataAttributes } from '../../../types'; -import { SizedIcon } from '../../Icon'; -import { StackHorizontal } from '../../Stack'; -import { TagDefault } from '../../Tag'; -import { Tooltip, TooltipChildrenFnProps, TooltipChildrenFnRef } from '../../Tooltip'; - -import styles from './TabStyles.module.scss'; - -type TabTitleProps = { - icon?: IconNameWithSize<'S'>; - tag?: string | number; -}; - -// -------------------------------------------------- -// Built-in title (This comes from inside !) -// -------------------------------------------------- - -type BuiltInTitleProps = TabTitleProps & { - children: ReactText; -}; - -const BuiltInTitle = ({ children, icon, tag }: BuiltInTitleProps) => { - return ( - - {icon && } - {children} - {tag && {tag}} - - ); -}; - -// -------------------------------------------------- -// Extraneous title (This comes from outside !) -// -------------------------------------------------- - -type ExtraneousTitleProps = { - children: ReactElement; -}; - -const ExtraneousTitle = ({ children }: ExtraneousTitleProps) => { - return {children}; -}; - -// -------------------------------------------------- -// Tab component (Main component) -// -------------------------------------------------- - -export type TapProps = DataAttributes & - TabTitleProps & { - children: ReactChild; - id: string; - onClickTab?: (tabId: string) => void; - size?: 'M' | 'L'; - tooltip?: string; - }; - -const TabNavigation = forwardRef( - ( - { children, icon, id, onClickTab, size, tag, tooltip, ...dataAttributes }: TapProps, - ref: Ref, - ) => { - const component = ( - triggerProps?: TooltipChildrenFnProps, - triggerRef?: TooltipChildrenFnRef, - ) => { - return ( - - ); - }; - - if (tooltip) { - return {component}; - } - - return component(); - }, -); - -TabNavigation.displayName = 'TabNavigation'; - -export default TabNavigation; diff --git a/packages/design-system/src/components/Tabs/Primitive/TabPanel.tsx b/packages/design-system/src/components/Tabs/Primitive/TabPanel.tsx index ea3ff3a7ee..b9e8ff73d2 100644 --- a/packages/design-system/src/components/Tabs/Primitive/TabPanel.tsx +++ b/packages/design-system/src/components/Tabs/Primitive/TabPanel.tsx @@ -1,18 +1,28 @@ -import { forwardRef, ReactNode, Ref } from 'react'; +import { useContext } from 'react'; +import { TabsInternalContext } from './TabsProvider'; -export type TabPanelProps = { - children: ReactNode; +type TabPanelPropTypes = { id: string; + children: React.ReactNode | React.ReactNode[]; + renderIf?: boolean; }; -const TabPanel = forwardRef(({ children, id }: TabPanelProps, ref: Ref) => { +export function TabPanel({ children, id, renderIf }: TabPanelPropTypes): JSX.Element { + const context = useContext(TabsInternalContext); + const style = { + display: '', + }; + if (id !== context?.value) { + if (renderIf) { + return <>; + } + style.display = 'none'; + } return ( -
+
{children}
); -}); +} TabPanel.displayName = 'TabPanel'; - -export default TabPanel; diff --git a/packages/design-system/src/components/Tabs/Primitive/TabState.ts b/packages/design-system/src/components/Tabs/Primitive/TabState.ts deleted file mode 100644 index 45aa2b1a04..0000000000 --- a/packages/design-system/src/components/Tabs/Primitive/TabState.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useState, useEffect } from 'react'; - -// eslint-disable-next-line @talend/import-depth -import { IconNameWithSize } from '@talend/icons/dist/typeUtils'; -import { randomUUID } from '@talend/utils'; - -import { DataAttributes } from '../../../types'; - -export type TabWithIdPropTypes = DataAttributes & { - id: string; - title: string; - icon?: IconNameWithSize<'S'>; - tag?: string | number; - tooltip?: string; - content: React.ReactNode; - size?: 'M' | 'L'; -}; - -export type TabPropTypes = Omit & { - id?: TabWithIdPropTypes['id']; -}; - -export type TabsPropTypes = { - tabs: TabPropTypes[]; - size?: 'M' | 'L'; - selectedId?: string; -}; - -export type TabState = { - /** - * The current selected tab's `id`. - */ - selectedId?: string; - /** - * Lists all the panels. - */ - tabs: TabWithIdPropTypes[]; -}; - -export type TabActions = { - /** - * Sets `selectedId`. - */ - setSelectedId: (id: string) => void; -}; - -export type TabInitialState = { - selectedId?: string; - tabs?: TabPropTypes[]; -}; - -export type TabStateReturn = TabState & TabActions; - -function getPanels(tabPanels: TabPropTypes[]): TabWithIdPropTypes[] { - // ensure there is an id for each tab - return tabPanels.map(panel => { - const id = panel.id ?? `tab-${randomUUID()}`; - return { ...panel, id }; - }); -} - -export function useTabState(initialState?: TabInitialState): TabStateReturn { - const [panels, setPanels] = useState(getPanels(initialState?.tabs ?? [])); - const [selectedId, setSelectedId] = useState( - initialState?.selectedId ?? undefined, - ); - - useEffect(() => { - // try to detect change in panels - let hasChanged = false; - if (panels.length !== initialState?.tabs?.length) { - hasChanged = true; - } - if (panels.some((panel, index) => panel.title !== initialState?.tabs?.[index].title)) { - hasChanged = true; - } - if (panels.some((panel, index) => panel.content !== initialState?.tabs?.[index].content)) { - hasChanged = true; - } - - if (hasChanged) { - setPanels(getPanels(initialState?.tabs ?? [])); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialState?.tabs]); - - useEffect(() => { - if (selectedId !== initialState?.selectedId && initialState?.selectedId) { - setSelectedId(initialState?.selectedId); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialState?.selectedId]); - - useEffect(() => { - if (selectedId === undefined) { - if (panels.length) { - setSelectedId(panels[0].id); - } - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [panels]); - - return { - selectedId, - setSelectedId, - tabs: panels, - }; -} diff --git a/packages/design-system/src/components/Tabs/Primitive/TabStyles.module.scss b/packages/design-system/src/components/Tabs/Primitive/TabStyles.module.scss index 62478db08e..386c1d69c7 100644 --- a/packages/design-system/src/components/Tabs/Primitive/TabStyles.module.scss +++ b/packages/design-system/src/components/Tabs/Primitive/TabStyles.module.scss @@ -1,5 +1,18 @@ @use '~@talend/design-tokens/lib/tokens'; +.tablist { + list-style-type: none; + margin: 0; + padding: 0; + display: flex; + flex-flow: row; + flex-wrap: nowrap; + align-items: flex-start; + justify-content: flex-start; + row-gap: var(--coral-spacing-m, 1.6rem); + column-gap: var(--coral-spacing-m, 1.6rem); +} + .tab { font: tokens.$coral-heading-s; height: tokens.$coral-sizing-xs; @@ -30,7 +43,6 @@ color: tokens.$coral-color-neutral-icon-weak; } - &::after { content: ''; position: absolute; @@ -73,7 +85,6 @@ } } - &_large { font: tokens.$coral-heading-l; height: tokens.$coral-sizing-s; diff --git a/packages/design-system/src/components/Tabs/Primitive/Tabs.tsx b/packages/design-system/src/components/Tabs/Primitive/Tabs.tsx new file mode 100644 index 0000000000..192b453325 --- /dev/null +++ b/packages/design-system/src/components/Tabs/Primitive/Tabs.tsx @@ -0,0 +1,58 @@ +import { useContext } from 'react'; +import { SizedIcon } from '../../Icon'; +import { TagDefault } from '../../Tag'; +import { StackHorizontal } from '../../Stack'; +import { TabsInternalContext } from './TabsProvider'; +import { Tooltip } from '../../Tooltip'; +import style from './TabStyles.module.scss'; +import { IconNameWithSize } from '@talend/icons'; +import classNames from 'classnames'; + +export type TabsPropTypes = { + children: React.ReactNode[]; +}; + +export function Tabs({ children }: TabsPropTypes) { + return ( +
    + {children} +
+ ); +} +Tabs.displayName = 'Tabs'; + +export type TabPropTypes = { + ['aria-controls']: string; + title: string; + disabled?: boolean; + icon?: IconNameWithSize<'S'>; + tag?: string | number; + tooltip?: string; +}; + +export function Tab(props: TabPropTypes) { + const context = useContext(TabsInternalContext); + const content = ( + + ); + if (props.tooltip) { +
  • + {content} +
  • ; + } + return
  • {content}
  • ; +} +Tab.displayName = 'Tab'; diff --git a/packages/design-system/src/components/Tabs/Primitive/TabsProvider.tsx b/packages/design-system/src/components/Tabs/Primitive/TabsProvider.tsx new file mode 100644 index 0000000000..efba097d25 --- /dev/null +++ b/packages/design-system/src/components/Tabs/Primitive/TabsProvider.tsx @@ -0,0 +1,38 @@ +/* eslint-disable react/no-unused-prop-types */ +import { useControl, UseControlReturns } from '../../../useControl'; +import { StackVertical } from '../../Stack'; +import { createContext } from 'react'; + +export type TabsProviderPropTypes = { + defaultActiveKey?: string; + activeKey?: string; + onSelect?: (event: any, key: string) => void; + size?: string; +}; + +type WithChildren = { + children: React.ReactNode[]; +}; + +export const TabsInternalContext = createContext< + (UseControlReturns & { size?: string }) | null +>(null); + +export function TabsProvider(props: TabsProviderPropTypes & WithChildren) { + const controlled = useControl(props, { + valueKey: 'activeKey', + defaultValueKey: 'defaultActiveKey', + onChangeKey: 'onSelect', + defaultValue: '', + selector: (e: any, id: string) => { + return id; + }, + }); + return ( + + + {props.children} + + + ); +} diff --git a/packages/design-system/src/components/Tabs/index.ts b/packages/design-system/src/components/Tabs/index.ts index bf4ad5cedd..6c1ee59c60 100644 --- a/packages/design-system/src/components/Tabs/index.ts +++ b/packages/design-system/src/components/Tabs/index.ts @@ -1,6 +1,10 @@ -import Tab from './Primitive/TabElement'; -import Tabs from './variants/Tabs'; -import TabsAsLinkList from './variants/TabsAsLinkList'; -import TabsKit from './variants/TabsKit'; +// import Tab from './Primitive/TabElement'; +// import Tabs from './variants/Tabs'; +// import TabsAsLinkList from './variants/TabsAsLinkList'; +// import TabsKit from './variants/TabsKit'; -export { Tab, Tabs, TabsKit, TabsAsLinkList }; +// export { Tab, Tabs, TabsKit, TabsAsLinkList }; + +export * from './Primitive/TabsProvider'; +export * from './Primitive/Tabs'; +export * from './Primitive/TabPanel'; diff --git a/packages/design-system/src/components/Tabs/variants/TabsKit.tsx b/packages/design-system/src/components/Tabs/variants/TabsKit.tsx index 69c4f133f7..b436f00603 100644 --- a/packages/design-system/src/components/Tabs/variants/TabsKit.tsx +++ b/packages/design-system/src/components/Tabs/variants/TabsKit.tsx @@ -3,8 +3,6 @@ import { createContext, forwardRef, ReactNode, Ref, useContext, useMemo } from ' import { IconNameWithSize } from '@talend/icons'; import TabList from '../Primitive/TabList'; -import TabNavigation from '../Primitive/TabNavigation'; -import TabPanel from '../Primitive/TabPanel'; import { TabStateReturn, useTabState } from '../Primitive/TabState'; export type TabsProps = { diff --git a/packages/design-system/src/stories/layout/Modal.mdx b/packages/design-system/src/stories/layout/Modal.mdx index e601765520..25640dc0c0 100644 --- a/packages/design-system/src/stories/layout/Modal.mdx +++ b/packages/design-system/src/stories/layout/Modal.mdx @@ -62,7 +62,7 @@ Transparent layer behind every modal. Clicking it closes the modal by default, b -## -**With disclosure** +## **With disclosure** - diff --git a/packages/design-system/src/stories/navigation/Tabs.mdx b/packages/design-system/src/stories/navigation/Tabs.mdx index 101bc4a8d4..68cc858ce1 100644 --- a/packages/design-system/src/stories/navigation/Tabs.mdx +++ b/packages/design-system/src/stories/navigation/Tabs.mdx @@ -111,62 +111,12 @@ NA ## Usage -### API vs Composition - -Tabs can be used either as a standalone block component, or split into two parts: a list of tab buttons and, somewhere else (nearby) the contents. - -To accommodate these two options, we provide two ways to implement Tabs in your project. - #### API -This is the standard, most straightforward way to use Tabs. - - - - - -It also works for large Tabs if needed: - - - - - -#### Composition - -This is the "I know what I am doing" mode. It lets you build the Tabs through composition, thus letting you split the Tabs list and the Tab panels. - -This is also the implementation that is most likely to break. Use only when necessary! - - - - - -This method supports the same features as the API one: - - - - - ### Default tab mode -You may need to set a default opened tab from the outside. You can: +You may need to set a default opened tab from the outside. You can achive it like this: - -or in composition mode: - - - - - -### Use Tabs as router links - -You may need something that looks like tabs but is actually a navigation pattern. - -For this we have a dedicated component that gives you a list of links and make them look like tabs: - - - - diff --git a/packages/design-system/src/stories/navigation/Tabs.stories.tsx b/packages/design-system/src/stories/navigation/Tabs.stories.tsx index 27405906df..556753cb03 100644 --- a/packages/design-system/src/stories/navigation/Tabs.stories.tsx +++ b/packages/design-system/src/stories/navigation/Tabs.stories.tsx @@ -1,11 +1,13 @@ +import { useState } from 'react'; import { BrowserRouter, Link as RouterLink } from 'react-router-dom'; import { - Divider, TabsAsLinkList, StackHorizontal, StackVertical, Tabs, - TabsKit, + TabsProvider, + Tab, + TabPanel, InlineMessage, } from '../../'; @@ -15,423 +17,96 @@ export const Styles = () => (

    Default

    - , - }, - { - tabTitle: 'Tabs 2', - tabContent: <>, - }, - { - tabTitle: 'Tabs 3', - tabContent: <>, - }, - ]} - /> + + + + + + + Tab content for Home + Tab content for Profile + Tab content for Contact +

    Large

    - , - }, - { - tabTitle: 'Tabs 2', - tabContent: <>, - }, - { - tabTitle: 'Tabs 3', - tabContent: <>, - }, - ]} - /> + + + + + + + Tab content for Home + Tab content for Profile + Tab content for Contact +
    ); export const TabsWithIcon = () => ( - Users tab content, - }, - { - tabTitle: { - icon: 'calendar', - title: 'Calendar', - }, - tabContent:

    Calendar tab content

    , - }, - { - tabTitle: { - icon: 'star', - title: 'Favorite', - }, - tabContent:

    Favorite tab content

    , - }, - ]} - /> + + + + + + + Users tab content + Calendar tab content + Favorite tab content + ); export const TabsWithTag = () => ( - Users tab content, - }, - { - tabTitle: { - title: 'Calendar', - tag: 54, - }, - tabContent:

    Calendar tab content

    , - }, - { - tabTitle: { - title: 'Favorite', - tag: '999+', - tooltip: '1534 Favorite items', - }, - tabContent:

    Favorite tab content

    , - }, - ]} - /> + + + + + + + Users tab content + Calendar tab content + Favorite tab content + ); export const TabsWithLongTitles = () => ( - Users tab content, - }, - { - tabTitle: { - icon: 'information-stroke', - title: 'A much too long title that will trigger the overflow limit', - tag: '999+', - tooltip: - '1239 notifications - A much too long title that will trigger the overflow limit', - }, - tabContent:

    About tab content

    , - }, - ]} - /> -); - -export const TabStandalone = () => ( - -

    Here's some tab content

    - - - ), - }, - { - tabTitle: { - // Advanced object title - icon: 'user', - title: 'Tab 2', - 'data-feature': 'domain.feature.description', - }, - tabContent: ( - <> -

    Different content

    - - - ), - }, - { - tabTitle: { - icon: 'user', - title: 'Tab 3', - tag: '999+', - tooltip: "It's a large number", - }, - tabContent: ( - <> -

    Different content again

    - - - ), - }, - ]} - /> -); - -export const TabStandaloneLarge = () => ( - -

    Here's some tab content

    - - - ), - }, - { - tabTitle: { - icon: 'user', - title: 'Tab 2', - 'data-feature': 'domain.feature.description', - }, - tabContent: ( - <> -

    Different content

    - - - ), - }, - { - tabTitle: { - icon: 'user', - title: 'Tab 3', - tag: '999+', - tooltip: "It's a large number", - }, - tabContent: ( - <> -

    Different content again

    - - - ), - }, - ]} - /> -); - -export const TabsWithComposition = () => ( - - - -

    A header of some sort

    - - - Tab title that hits the size limit should get a tooltip - - - -
    - - - - -

    Here's some tab content

    - -
    - - -

    Different content

    - -
    -
    -
    -); - -export const TabsWithCompositionLarge = () => ( - - - -

    A header of some sort

    - - - Tab title that hits the size limit should get a tooltip - - - -
    - - -

    Here's some tab content

    - -
    - - -

    Different content

    - -
    -
    -
    -); - -export const TabStandaloneControlled = () => ( - -

    Here's some tab content

    - - - ), - }, - { - tabTitle: { - icon: 'user', - title: 'Tab 2', - 'data-feature': 'domain.feature.description', - id: 'tab2', // Set the tab's id for this use, otherwise Reakit will create one randomly - }, - tabContent: ( - <> -

    Different content

    - - - ), - }, - ]} - /> -); - -export const TabsWithCompositionControlled = () => ( - - - -

    A header of some sort

    - - - Tab title that hits the size limit should get a tooltip - - - -
    - - -

    Here's some tab content

    - -
    - - -

    Different content

    - -
    -
    -
    + + + + + + Users tab content + +

    About tab content

    +
    +
    ); -export const TabsAsLinks = () => ( - - - - , // Be careful, polymorphism and tooltips are not compatible. - }, - ]} - /> - -); +export const TabStandaloneControlled = () => { + const [key, setKey] = useState('home'); + return ( + setKey(k)}> + + + + + + Tab content for Home + Tab content for Profile + Tab content for Contact + + ); +}; diff --git a/packages/design-system/src/useControl.ts b/packages/design-system/src/useControl.ts index a60b2d376a..49ea0343fa 100644 --- a/packages/design-system/src/useControl.ts +++ b/packages/design-system/src/useControl.ts @@ -7,8 +7,12 @@ type UseControlOptions = { selector?: (...args: any) => any; defaultValue?: any; }; +export type UseControlReturns = { + value: T | undefined; + onChange: (...args: any) => void; +}; -export function useControl(props: any, opts: UseControlOptions) { +export function useControl(props: any, opts: UseControlOptions): UseControlReturns { const isControlled = props[opts.valueKey] !== undefined && props[opts.onChangeKey] !== undefined; let defaultValue = props[opts.defaultValueKey]; if (defaultValue === undefined) { @@ -20,10 +24,10 @@ export function useControl(props: any, opts: UseControlOptions) { } const [state, setState] = useState(defaultValue); - const onChange = (value: any) => { + const onChange = (value: any, ...args: any) => { let safeValue = value; if (opts.selector) { - safeValue = opts.selector(value); + safeValue = opts.selector(value, ...args); } if (props[opts.onChangeKey]) { props[opts.onChangeKey](value);