From 56b4c7b9b84668dd113e4790e05034319dcb3941 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 22 Oct 2020 12:51:42 +0200 Subject: [PATCH] Tabs typescript (#715) * Renamed to typescript files * WIP Tabslist * First working component * Added fallback and tests for if missing children --- .../core-react/src/Tabs/{Tab.jsx => Tab.tsx} | 43 +++++------ .../src/Tabs/{TabList.jsx => TabList.tsx} | 73 ++++++++++--------- .../src/Tabs/{TabPanel.jsx => TabPanel.tsx} | 36 ++++----- libraries/core-react/src/Tabs/TabPanels.jsx | 37 ---------- libraries/core-react/src/Tabs/TabPanels.tsx | 26 +++++++ libraries/core-react/src/Tabs/Tabs.context.js | 15 ---- libraries/core-react/src/Tabs/Tabs.context.ts | 23 ++++++ .../src/Tabs/{Tabs.test.jsx => Tabs.test.tsx} | 53 +++++++++++++- .../Tabs/{Tabs.tokens.js => Tabs.tokens.ts} | 1 - .../src/Tabs/{Tabs.jsx => Tabs.tsx} | 43 ++++------- libraries/core-react/src/Tabs/Tabs.types.ts | 1 + libraries/core-react/src/Tabs/index.js | 13 ---- libraries/core-react/src/Tabs/index.ts | 21 ++++++ 13 files changed, 207 insertions(+), 178 deletions(-) rename libraries/core-react/src/Tabs/{Tab.jsx => Tab.tsx} (77%) rename libraries/core-react/src/Tabs/{TabList.jsx => TabList.tsx} (63%) rename libraries/core-react/src/Tabs/{TabPanel.jsx => TabPanel.tsx} (62%) delete mode 100644 libraries/core-react/src/Tabs/TabPanels.jsx create mode 100644 libraries/core-react/src/Tabs/TabPanels.tsx delete mode 100644 libraries/core-react/src/Tabs/Tabs.context.js create mode 100644 libraries/core-react/src/Tabs/Tabs.context.ts rename libraries/core-react/src/Tabs/{Tabs.test.jsx => Tabs.test.tsx} (66%) rename libraries/core-react/src/Tabs/{Tabs.tokens.js => Tabs.tokens.ts} (99%) rename libraries/core-react/src/Tabs/{Tabs.jsx => Tabs.tsx} (58%) create mode 100644 libraries/core-react/src/Tabs/Tabs.types.ts delete mode 100644 libraries/core-react/src/Tabs/index.js create mode 100644 libraries/core-react/src/Tabs/index.ts diff --git a/libraries/core-react/src/Tabs/Tab.jsx b/libraries/core-react/src/Tabs/Tab.tsx similarity index 77% rename from libraries/core-react/src/Tabs/Tab.jsx rename to libraries/core-react/src/Tabs/Tab.tsx index edd0f03b22..3f531463f8 100644 --- a/libraries/core-react/src/Tabs/Tab.jsx +++ b/libraries/core-react/src/Tabs/Tab.tsx @@ -1,6 +1,4 @@ -// @ts-nocheck import React, { forwardRef } from 'react' -import PropTypes from 'prop-types' import styled, { css } from 'styled-components' import { tab as tokens } from './Tabs.tokens' @@ -29,13 +27,15 @@ const focusedStyles = css` outline-offset: ${outlineOffset}; ` -const StyledTab = styled.button.attrs(({ active, disabled }) => ({ - type: 'button', - role: 'tab', - 'aria-selected': active, - 'aria-disabled': disabled, - tabIndex: active ? '0' : '-1', -}))` +const StyledTab = styled.button.attrs( + ({ active = false, disabled = false }) => ({ + type: 'button', + role: 'tab', + 'aria-selected': active, + 'aria-disabled': disabled, + tabIndex: active ? '0' : '-1', + }), +)` appearance: none; box-sizing: border-box; font-family: inherit; @@ -82,23 +82,16 @@ const StyledTab = styled.button.attrs(({ active, disabled }) => ({ } ` -export const Tab = forwardRef(function Tab(props, ref) { - return -}) - -Tab.propTypes = { +export type Props = { /** If `true`, the tab will be active. */ - active: PropTypes.bool, + active?: boolean /** If `true`, the tab will be disabled. */ - disabled: PropTypes.bool, - /** @ignore */ - className: PropTypes.string, - /** @ignore */ - children: PropTypes.node.isRequired, + disabled?: boolean } -Tab.defaultProps = { - active: false, - disabled: false, - className: null, -} +export const Tab = forwardRef< + HTMLButtonElement, + Props & React.HTMLAttributes +>(function Tab(props, ref) { + return +}) diff --git a/libraries/core-react/src/Tabs/TabList.jsx b/libraries/core-react/src/Tabs/TabList.tsx similarity index 63% rename from libraries/core-react/src/Tabs/TabList.jsx rename to libraries/core-react/src/Tabs/TabList.tsx index ee339feccd..b03871006a 100644 --- a/libraries/core-react/src/Tabs/TabList.jsx +++ b/libraries/core-react/src/Tabs/TabList.tsx @@ -1,38 +1,61 @@ -// @ts-nocheck import React, { forwardRef, useContext, useRef, useCallback, useEffect, + ReactElement, } from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import { useCombinedRefs } from '../_common/useCombinedRefs' import { TabsContext } from './Tabs.context' +import { Variants } from './Tabs.types' -const variants = { +type VariantsRecord = { + fullWidth: string + minWidth: string +} + +const variants: VariantsRecord = { fullWidth: 'minmax(1%, 360px)', minWidth: 'max-content', } -const StyledTabList = styled.div.attrs(() => ({ - role: 'tablist', -}))` +type StyledProps = Props + +const StyledTabList = styled.div.attrs( + (): React.HTMLAttributes => ({ + role: 'tablist', + }), +)` display: grid; grid-auto-flow: column; grid-auto-columns: ${({ variant }) => variants[variant]}; ` -const TabList = forwardRef(function TabsList({ children, ...props }, ref) { - const { activeTab, handleChange, tabsId, variant, tabsFocused } = useContext( - TabsContext, - ) +type Props = { + /** Sets the width of the tabs */ + variant?: Variants +} & React.HTMLAttributes + +type TabChild = JSX.IntrinsicElements['button'] & ReactElement + +const TabList = forwardRef(function TabsList( + { children = [], ...props }, + ref, +) { + const { + activeTab, + handleChange, + tabsId, + variant = 'minWidth', + tabsFocused, + } = useContext(TabsContext) const currentTab = useRef(activeTab) const selectedTabRef = useCallback( - (node) => { + (node: HTMLElement) => { if (node !== null && tabsFocused) { node.focus() } @@ -44,7 +67,7 @@ const TabList = forwardRef(function TabsList({ children, ...props }, ref) { currentTab.current = activeTab }, [activeTab]) - const Tabs = React.Children.map(children, (child, index) => { + const Tabs = React.Children.map(children, (child: TabChild, index) => { const tabRef = index === activeTab ? useCombinedRefs(child.ref, selectedTabRef) @@ -60,9 +83,10 @@ const TabList = forwardRef(function TabsList({ children, ...props }, ref) { }) }) - const focusableChildren = Tabs.filter((child) => !child.props.disabled).map( - (child) => child.props.index, - ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const focusableChildren: number[] = Tabs.filter( + (child) => !child.props.disabled, + ).map((child) => child.props.index) const firstFocusableChild = focusableChildren[0] const lastFocusableChild = focusableChildren[focusableChildren.length - 1] @@ -74,7 +98,7 @@ const TabList = forwardRef(function TabsList({ children, ...props }, ref) { handleChange(nextTab === undefined ? fallbackTab : nextTab) } - const handleKeyPress = (event) => { + const handleKeyPress = (event: React.KeyboardEvent) => { const { key } = event if (key === 'ArrowLeft') { handleTabsChange('left', lastFocusableChild) @@ -96,21 +120,4 @@ const TabList = forwardRef(function TabsList({ children, ...props }, ref) { ) }) -TabList.propTypes = { - /** @ignore */ - className: PropTypes.string, - /** Sets the width of the tabs */ - variant: PropTypes.oneOf(['fullWidth', 'minWidth']), - /** @ignore */ - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.element), - PropTypes.element, - ]).isRequired, -} - -TabList.defaultProps = { - className: null, - variant: 'minWidth', -} - export { TabList } diff --git a/libraries/core-react/src/Tabs/TabPanel.jsx b/libraries/core-react/src/Tabs/TabPanel.tsx similarity index 62% rename from libraries/core-react/src/Tabs/TabPanel.jsx rename to libraries/core-react/src/Tabs/TabPanel.tsx index 230c00e00e..9d4f0c5ce3 100644 --- a/libraries/core-react/src/Tabs/TabPanel.jsx +++ b/libraries/core-react/src/Tabs/TabPanel.tsx @@ -1,6 +1,4 @@ -// @ts-nocheck import React, { forwardRef } from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import { tabPanel as tokens } from './Tabs.tokens' @@ -12,10 +10,12 @@ const { }, } = tokens -const StyledTabPanel = styled.div.attrs(() => ({ - tabIndex: 0, - role: 'tabpanel', -}))({ +const StyledTabPanel = styled.div.attrs( + (): React.HTMLAttributes => ({ + tabIndex: 0, + role: 'tabpanel', + }), +)({ paddingTop, paddingBottom, outline: 'none', @@ -25,7 +25,15 @@ const StyledTabPanel = styled.div.attrs(() => ({ }, }) -const TabPanel = forwardRef(function TabPanel({ ...props }, ref) { +type Props = { + /** If `true`, the panel will be hidden. */ + hidden?: boolean +} & React.HTMLAttributes + +const TabPanel = forwardRef(function TabPanel( + { ...props }, + ref, +) { return ( {props.children} @@ -33,18 +41,4 @@ const TabPanel = forwardRef(function TabPanel({ ...props }, ref) { ) }) -TabPanel.propTypes = { - /** @ignore */ - children: PropTypes.node.isRequired, - /** @ignore */ - className: PropTypes.string, - /** If `true`, the panel will be hidden. */ - hidden: PropTypes.bool, -} - -TabPanel.defaultProps = { - className: null, - hidden: null, -} - export { TabPanel } diff --git a/libraries/core-react/src/Tabs/TabPanels.jsx b/libraries/core-react/src/Tabs/TabPanels.jsx deleted file mode 100644 index 423f706a13..0000000000 --- a/libraries/core-react/src/Tabs/TabPanels.jsx +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-nocheck -import React, { forwardRef, useContext } from 'react' -import PropTypes from 'prop-types' -import { TabsContext } from './Tabs.context' - -const TabPanels = forwardRef(function TabPanels({ children, ...props }, ref) { - const { activeTab, tabsId } = useContext(TabsContext) - - const Panels = React.Children.map(children, (child, index) => - React.cloneElement(child, { - id: `${tabsId}-panel-${index + 1}`, - 'aria-labelledby': `${tabsId}-tab-${index + 1}`, - hidden: activeTab !== index, - }), - ) - return ( -
- {Panels} -
- ) -}) - -TabPanels.propTypes = { - /** @ignore */ - className: PropTypes.string, - /** @ignore */ - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.element), - PropTypes.element, - ]).isRequired, -} - -TabPanels.defaultProps = { - className: null, -} - -export { TabPanels } diff --git a/libraries/core-react/src/Tabs/TabPanels.tsx b/libraries/core-react/src/Tabs/TabPanels.tsx new file mode 100644 index 0000000000..020fa30d0b --- /dev/null +++ b/libraries/core-react/src/Tabs/TabPanels.tsx @@ -0,0 +1,26 @@ +import React, { forwardRef, ReactElement, useContext } from 'react' +import { TabsContext } from './Tabs.context' + +type Props = React.HTMLAttributes + +const TabPanels = forwardRef(function TabPanels( + { children, ...props }, + ref, +) { + const { activeTab, tabsId } = useContext(TabsContext) + + const Panels = React.Children.map(children, (child: ReactElement, index) => + React.cloneElement(child, { + id: `${tabsId}-panel-${index + 1}`, + 'aria-labelledby': `${tabsId}-tab-${index + 1}`, + hidden: activeTab !== index, + }), + ) + return ( +
+ {Panels} +
+ ) +}) + +export { TabPanels } diff --git a/libraries/core-react/src/Tabs/Tabs.context.js b/libraries/core-react/src/Tabs/Tabs.context.js deleted file mode 100644 index 2066edafce..0000000000 --- a/libraries/core-react/src/Tabs/Tabs.context.js +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -import React, { createContext } from 'react' - -const TabsContext = createContext({ - variant: '', - handleChange: () => {}, - activeTab: 0, - tabsId: '', - tabsFocused: false, -}) - -const TabsProvider = TabsContext.Provider -const TabsConsumer = TabsContext.Consumer - -export { TabsContext, TabsProvider, TabsConsumer } diff --git a/libraries/core-react/src/Tabs/Tabs.context.ts b/libraries/core-react/src/Tabs/Tabs.context.ts new file mode 100644 index 0000000000..6c5703bc64 --- /dev/null +++ b/libraries/core-react/src/Tabs/Tabs.context.ts @@ -0,0 +1,23 @@ +import { createContext } from 'react' +import { Variants } from './Tabs.types' + +type State = { + variant: Variants + handleChange: (index: number) => void + activeTab: number + tabsId: string + tabsFocused: boolean +} + +const TabsContext = createContext({ + variant: 'minWidth', + handleChange: () => null, + activeTab: 0, + tabsId: '', + tabsFocused: false, +}) + +const TabsProvider = TabsContext.Provider +const TabsConsumer = TabsContext.Consumer + +export { TabsContext, TabsProvider, TabsConsumer } diff --git a/libraries/core-react/src/Tabs/Tabs.test.jsx b/libraries/core-react/src/Tabs/Tabs.test.tsx similarity index 66% rename from libraries/core-react/src/Tabs/Tabs.test.jsx rename to libraries/core-react/src/Tabs/Tabs.test.tsx index 51e09c7b9b..ccb376a6f6 100644 --- a/libraries/core-react/src/Tabs/Tabs.test.jsx +++ b/libraries/core-react/src/Tabs/Tabs.test.tsx @@ -8,13 +8,13 @@ import { Tabs } from '.' const { TabList, Tab, TabPanels, TabPanel } = Tabs -const noop = () => {} +const noop = () => null afterEach(cleanup) const TabsWithRefs = () => { - const activeRef = useRef(null) - const inactiveRef = useRef(null) + const activeRef = useRef(null) + const inactiveRef = useRef(null) useEffect(() => { activeRef.current.textContent = 'Active tab' @@ -32,7 +32,7 @@ const TabsWithRefs = () => { ) } -const TabsWithPanels = ({ selectedTabIndex }) => { +const TabsWithPanels = ({ selectedTabIndex = 0 }) => { const [activeTab, setActiveTab] = useState(selectedTabIndex) const handleChange = (index) => { @@ -109,4 +109,49 @@ describe('Tabs', () => { }) expect(targetTab).toHaveAttribute('aria-selected', 'true') }) + it("Doesn't crash if no children is provided", () => { + const testId = 'tabs' + render() + expect(screen.queryByTestId(testId)).toBeDefined() + }) + it("Doesn't crash if no children is provided to TabPanel", () => { + const testId = 'tabspanel' + render( + + + Tab one + + + + + , + ) + expect(screen.queryByTestId(testId)).toBeDefined() + }) + it("Doesn't crash if no children is provided to Tab", () => { + const testId = 'tab' + render( + + + Tab one + + + Panel one + + , + ) + expect(screen.queryByTestId(testId)).toBeDefined() + }) + it("Doesn't crash if no children is provided to TabList or TabPanels", () => { + const tablist = 'tablist' + const tabpanels = 'tabpanels' + render( + + + + , + ) + expect(screen.queryByTestId(tablist)).toBeDefined() + expect(screen.queryByTestId(tabpanels)).toBeDefined() + }) }) diff --git a/libraries/core-react/src/Tabs/Tabs.tokens.js b/libraries/core-react/src/Tabs/Tabs.tokens.ts similarity index 99% rename from libraries/core-react/src/Tabs/Tabs.tokens.js rename to libraries/core-react/src/Tabs/Tabs.tokens.ts index 51b0918581..a878e02d2b 100644 --- a/libraries/core-react/src/Tabs/Tabs.tokens.js +++ b/libraries/core-react/src/Tabs/Tabs.tokens.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { tokens } from '@equinor/eds-tokens' const { diff --git a/libraries/core-react/src/Tabs/Tabs.jsx b/libraries/core-react/src/Tabs/Tabs.tsx similarity index 58% rename from libraries/core-react/src/Tabs/Tabs.jsx rename to libraries/core-react/src/Tabs/Tabs.tsx index f7b7c50b39..d3d6646734 100644 --- a/libraries/core-react/src/Tabs/Tabs.jsx +++ b/libraries/core-react/src/Tabs/Tabs.tsx @@ -1,11 +1,19 @@ -// @ts-nocheck import React, { forwardRef, useMemo, useState } from 'react' -import PropTypes from 'prop-types' import createId from 'lodash/uniqueId' import { TabsProvider } from './Tabs.context' +import { Variants } from './Tabs.types' -const Tabs = forwardRef(function Tabs( - { activeTab, onChange, onBlur, onFocus, variant, ...props }, +type Props = { + /** The index of the active tab */ + activeTab?: number + /** The callback function for selecting a tab */ + onChange?: (index: number) => void + /** Sets the width of the tabs */ + variant?: Variants +} & React.HTMLAttributes + +const Tabs = forwardRef(function Tabs( + { activeTab, onChange, onBlur, onFocus, variant = 'minWidth', ...props }, ref, ) { const tabsId = useMemo(() => createId('tabs-'), []) @@ -14,7 +22,7 @@ const Tabs = forwardRef(function Tabs( let blurTimer - const handleBlur = (e) => { + const handleBlur = (e: React.FocusEvent) => { blurTimer = setTimeout(() => { if (tabsFocused) { setTabsFocused(false) @@ -23,7 +31,7 @@ const Tabs = forwardRef(function Tabs( onBlur(e) } - const handleFocus = (e) => { + const handleFocus = (e: React.FocusEvent) => { if (e.target.getAttribute('role') !== 'tab') { return } @@ -49,27 +57,4 @@ const Tabs = forwardRef(function Tabs( ) }) -Tabs.propTypes = { - /** The index of the active tab */ - activeTab: PropTypes.number, - /** The callback function for selecting a tab */ - onChange: PropTypes.func, - /** The callback function for removing focus from a tab */ - onBlur: PropTypes.func, - /** The callback function for focusing on a tab */ - onFocus: PropTypes.func, - /** Sets the width of the tabs */ - variant: PropTypes.oneOf(['fullWidth', 'minWidth']), - /** @ignore */ - children: PropTypes.node.isRequired, -} - -Tabs.defaultProps = { - activeTab: 0, - onChange: () => {}, - onBlur: () => {}, - onFocus: () => {}, - variant: 'minWidth', -} - export { Tabs } diff --git a/libraries/core-react/src/Tabs/Tabs.types.ts b/libraries/core-react/src/Tabs/Tabs.types.ts new file mode 100644 index 0000000000..fbfdad7834 --- /dev/null +++ b/libraries/core-react/src/Tabs/Tabs.types.ts @@ -0,0 +1 @@ +export type Variants = 'fullWidth' | 'minWidth' | '' diff --git a/libraries/core-react/src/Tabs/index.js b/libraries/core-react/src/Tabs/index.js deleted file mode 100644 index c3fb4df50e..0000000000 --- a/libraries/core-react/src/Tabs/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { Tabs } from './Tabs' -import { TabList } from './TabList' -import { Tab } from './Tab' -import { TabPanels } from './TabPanels' -import { TabPanel } from './TabPanel' - -Tabs.Tab = Tab -Tabs.TabList = TabList -Tabs.TabPanels = TabPanels -Tabs.TabPanel = TabPanel - -export { Tabs } from './Tabs' diff --git a/libraries/core-react/src/Tabs/index.ts b/libraries/core-react/src/Tabs/index.ts new file mode 100644 index 0000000000..db1dd495b9 --- /dev/null +++ b/libraries/core-react/src/Tabs/index.ts @@ -0,0 +1,21 @@ +import { Tabs as BaseComponent } from './Tabs' +import { TabList } from './TabList' +import { Tab } from './Tab' +import { TabPanels } from './TabPanels' +import { TabPanel } from './TabPanel' + +type TabsType = typeof BaseComponent & { + Tab: typeof Tab + TabList: typeof TabList + TabPanels: typeof TabPanels + TabPanel: typeof TabPanel +} + +const Tabs = BaseComponent as TabsType + +Tabs.Tab = Tab +Tabs.TabList = TabList +Tabs.TabPanels = TabPanels +Tabs.TabPanel = TabPanel + +export { Tabs }