From e8c7627c8de6f3975d21cb64dc3f685f8ed34602 Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 13 Aug 2026 15:35:20 +0200 Subject: [PATCH] refactor(tab-view): migrate TabView from Flow to TypeScript --- .../tab-view/{Tab.js => Tab.js.flow} | 0 src/components/tab-view/Tab.tsx | 20 + .../tab-view/{TabView.js => TabView.js.flow} | 0 ...TabView.stories.js => TabView.stories.tsx} | 3 +- src/components/tab-view/TabView.tsx | 113 ++++++ ...wPrimitive.js => TabViewPrimitive.js.flow} | 0 src/components/tab-view/TabViewPrimitive.tsx | 366 ++++++++++++++++++ .../{TabView.test.js => TabView.test.tsx} | 38 +- ...tive.test.js => TabViewPrimitive.test.tsx} | 139 ++++--- .../tab-view/{index.js => index.js.flow} | 0 src/components/tab-view/index.ts | 6 + 11 files changed, 600 insertions(+), 85 deletions(-) rename src/components/tab-view/{Tab.js => Tab.js.flow} (100%) create mode 100644 src/components/tab-view/Tab.tsx rename src/components/tab-view/{TabView.js => TabView.js.flow} (100%) rename src/components/tab-view/{TabView.stories.js => TabView.stories.tsx} (99%) create mode 100644 src/components/tab-view/TabView.tsx rename src/components/tab-view/{TabViewPrimitive.js => TabViewPrimitive.js.flow} (100%) create mode 100644 src/components/tab-view/TabViewPrimitive.tsx rename src/components/tab-view/__tests__/{TabView.test.js => TabView.test.tsx} (85%) rename src/components/tab-view/__tests__/{TabViewPrimitive.test.js => TabViewPrimitive.test.tsx} (87%) rename src/components/tab-view/{index.js => index.js.flow} (100%) create mode 100644 src/components/tab-view/index.ts diff --git a/src/components/tab-view/Tab.js b/src/components/tab-view/Tab.js.flow similarity index 100% rename from src/components/tab-view/Tab.js rename to src/components/tab-view/Tab.js.flow diff --git a/src/components/tab-view/Tab.tsx b/src/components/tab-view/Tab.tsx new file mode 100644 index 0000000000..603ca60289 --- /dev/null +++ b/src/components/tab-view/Tab.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +export interface TabProps extends React.ButtonHTMLAttributes { + /** Content displayed in the tab panel */ + children?: React.ReactNode; + /** Custom class name applied to the tab button */ + className?: string; + /** Custom component used to render a linked tab */ + component?: React.ElementType; + /** URL used to render the tab as a link */ + href?: string; + /** Ref property used by a custom link component */ + refProp?: string; + /** Label displayed in the tab */ + title: string; +} + +const Tab: (props: TabProps) => React.ReactElement = () => ; + +export default Tab; diff --git a/src/components/tab-view/TabView.js b/src/components/tab-view/TabView.js.flow similarity index 100% rename from src/components/tab-view/TabView.js rename to src/components/tab-view/TabView.js.flow diff --git a/src/components/tab-view/TabView.stories.js b/src/components/tab-view/TabView.stories.tsx similarity index 99% rename from src/components/tab-view/TabView.stories.js rename to src/components/tab-view/TabView.stories.tsx index 884e62994d..fc30c66843 100644 --- a/src/components/tab-view/TabView.stories.js +++ b/src/components/tab-view/TabView.stories.tsx @@ -1,4 +1,3 @@ -// @flow import * as React from 'react'; import Tab from './Tab'; @@ -22,7 +21,7 @@ export const basic = () => ( export const withCallback = () => { // eslint-disable-next-line no-alert - const cb = selectedIndex => alert(selectedIndex); + const cb = (selectedIndex: number) => alert(selectedIndex); return ( diff --git a/src/components/tab-view/TabView.tsx b/src/components/tab-view/TabView.tsx new file mode 100644 index 0000000000..e32b42ac58 --- /dev/null +++ b/src/components/tab-view/TabView.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import classNames from 'classnames'; + +import TabViewPrimitive, { TAB_KEY, TAB_PANEL_ROLE } from './TabViewPrimitive'; + +export interface TabViewProps { + /** Tabs and their associated panel content */ + children: React.ReactNode; + /** Custom class name applied to the tab view */ + className?: string; + /** Index of the tab selected initially and when the tab view resets */ + defaultSelectedIndex: number; + /** Whether tabs use the scrollable dynamic layout */ + isDynamic?: boolean; + /** Callback invoked when the selected tab changes */ + onTabSelect?: (selectedIndex: number) => void; +} + +interface TabViewState { + focusedIndex: number; + selectedIndex: number; + showOutline: boolean; +} + +class TabView extends React.Component { + static defaultProps = { + defaultSelectedIndex: 0, + isDynamic: false, + }; + + constructor(props: TabViewProps) { + super(props); + + this.state = { + focusedIndex: props.defaultSelectedIndex, + showOutline: false, + selectedIndex: props.defaultSelectedIndex, + }; + } + + componentDidUpdate(prevProps: TabViewProps) { + const { defaultSelectedIndex } = this.props; + if (prevProps.defaultSelectedIndex !== defaultSelectedIndex) { + this.resetActiveTab(); + } + } + + getActiveDocElement = () => document.activeElement; + + resetActiveTab = () => { + this.setState({ + focusedIndex: this.props.defaultSelectedIndex, + selectedIndex: this.props.defaultSelectedIndex, + }); + }; + + resetFocusedTab = () => { + this.setState({ focusedIndex: this.state.selectedIndex }); + }; + + handleOnTabSelect = (selectedIndex: number): void => + this.setState({ selectedIndex }, () => { + const { onTabSelect } = this.props; + + if (onTabSelect) { + onTabSelect(this.state.selectedIndex); + } + }); + + handleOnTabFocus = (index: number) => this.setState({ focusedIndex: index }); + + // By default the outline is set to none when tabpanel is focused. This is so that + // when clicking into it, it doesn't outline it. + // However, for accessibility, when tabbing into and out of the tabpanel, the focus + // is pretty important to show the user what is being focused. By adding this class, + // we can specify an outline for the focus pseudo state. + handleKeyUp = (event: React.KeyboardEvent) => { + const activeElement = this.getActiveDocElement(); + const isTabPanelFocused = activeElement && activeElement.getAttribute('role') === TAB_PANEL_ROLE; + const isTabPanelFocusedWithTabKey = isTabPanelFocused && event.key === TAB_KEY; + + if (isTabPanelFocusedWithTabKey) { + this.setState({ showOutline: true }); + } else if (!isTabPanelFocused && this.state.showOutline) { + this.setState({ showOutline: false }); + } + }; + + render() { + const { children, className, isDynamic } = this.props; + const { focusedIndex, selectedIndex, showOutline } = this.state; + + return ( + + {children} + + ); + } +} + +export default TabView; diff --git a/src/components/tab-view/TabViewPrimitive.js b/src/components/tab-view/TabViewPrimitive.js.flow similarity index 100% rename from src/components/tab-view/TabViewPrimitive.js rename to src/components/tab-view/TabViewPrimitive.js.flow diff --git a/src/components/tab-view/TabViewPrimitive.tsx b/src/components/tab-view/TabViewPrimitive.tsx new file mode 100644 index 0000000000..02d81b3c5f --- /dev/null +++ b/src/components/tab-view/TabViewPrimitive.tsx @@ -0,0 +1,366 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import omit from 'lodash/omit'; +import uniqueId from 'lodash/uniqueId'; + +import IconPageBack from '../../icons/general/IconPageBack'; +import IconPageForward from '../../icons/general/IconPageForward'; +import LinkButton from '../link/LinkButton'; +import { TabProps } from './Tab'; + +import './Tabs.scss'; + +export const TAB_KEY = 'Tab'; +export const TAB_PANEL_ROLE = 'tabpanel'; + +type TabLinkButtonProps = Omit, 'children'> & { + /** Content rendered within the linked tab */ + children: React.ReactNode; + /** ID of the tab panel controlled by the linked tab */ + 'aria-controls': string; + /** Whether the linked tab is currently selected */ + 'aria-selected': boolean; + /** Unique ID of the linked tab */ + id: string; + /** ARIA role identifying the link as a tab */ + role: 'tab'; + /** Keyboard navigation order for the linked tab */ + tabIndex: number; + /** Destination passed to custom link components */ + to: string; +}; + +const TabLinkButton = LinkButton as React.ComponentType; + +export interface TabViewPrimitiveProps { + /** Tabs and their associated panel content */ + children: React.ReactNode; + /** Custom class name applied to the tab view */ + className?: string; + /** Index of the tab that currently has keyboard focus */ + focusedIndex: number; + /** Whether tabs use the scrollable dynamic layout */ + isDynamic?: boolean; + /** Key-up handler for the tab view container */ + onKeyUp?: React.KeyboardEventHandler; + /** Callback invoked when a tab receives keyboard focus */ + onTabFocus: (focusedIndex: number) => void; + /** Callback invoked when a tab is selected */ + onTabSelect?: (selectedIndex: number) => void; + /** Resets the focused and selected tabs */ + resetActiveTab: () => void; + /** Resets the focused tab to the selected tab */ + resetFocusedTab: () => void; + /** Index of the currently selected tab */ + selectedIndex: number; +} + +interface TabViewPrimitiveState { + /** Horizontal offset of the tabs container in pixels */ + tabsContainerOffsetLeft: number; +} + +class TabViewPrimitive extends React.Component { + constructor(props: TabViewPrimitiveProps) { + super(props); + + this.tabviewID = uniqueId('tabview'); + + this.state = { + tabsContainerOffsetLeft: 0, + }; + } + + componentDidMount() { + const { isDynamic, focusedIndex } = this.props; + if (isDynamic) { + // set initial tabsContainerOffsetLeft state after first mounting + this.scrollToTab(focusedIndex); + } + } + + componentDidUpdate(prevProps: TabViewPrimitiveProps) { + const { focusedIndex: prevFocusedIndex, selectedIndex: prevSelectedIndex } = prevProps; + const { focusedIndex, selectedIndex } = this.props; + + if (this.props.isDynamic) { + if (prevFocusedIndex !== focusedIndex) { + this.scrollToTab(focusedIndex); + } + + // update tabsContainerOffsetLeft state when receiving a new prop + if (prevSelectedIndex !== selectedIndex) { + this.scrollToTab(selectedIndex); + } + } + + if (prevFocusedIndex !== focusedIndex) { + // have to focus after render otherwise, the focus will be lost + this.focusOnTabElement(focusedIndex); + } + } + + onClickTab = (tabIndex: number) => { + const { onTabFocus, onTabSelect } = this.props; + if (onTabSelect) { + onTabSelect(tabIndex); + } + onTabFocus(tabIndex); + }; + + getLastElementsAnchorPoint = () => { + if (this.tabsElements.length === 0) { + return 0; + } + + const lastTabElement = this.tabsElements[this.tabsElements.length - 1]; + return lastTabElement.offsetLeft + lastTabElement.offsetWidth; + }; + + getTabsContainerOffsetLeft = () => { + if (!this.tabsContainer) { + return 0; + } + + const { tabsContainerOffsetLeft } = this.state; + let viewportOffset = parseInt(String(tabsContainerOffsetLeft), 10) * -1; + viewportOffset = viewportOffset || 0; + return viewportOffset; + }; + + getTabsContainerWidth = () => (this.tabsContainer ? parseInt(String(this.tabsContainer.offsetWidth), 10) : 0); + + tabviewID: string; + + scrollToTab = (tabIndex: number) => { + if ( + !this.props.isDynamic || + this.tabsContainer === null || + this.tabsElements.length === 0 || + tabIndex < 0 || + tabIndex > this.tabsElements.length - 1 + ) { + return; + } + + const tabElementOfInterest = this.tabsElements[tabIndex]; + const lastElementsAnchorPoint = this.getLastElementsAnchorPoint(); + + // if tabs don't overflow at all, no need to scroll + const tabsContainerWidth = this.getTabsContainerWidth(); + if (lastElementsAnchorPoint <= tabsContainerWidth) { + this.setState({ tabsContainerOffsetLeft: 0 }); + return; + } + + // do not scroll any more if we will go past the rightmost anchor + const newOffset = Math.min(lastElementsAnchorPoint - tabsContainerWidth, tabElementOfInterest.offsetLeft); + + // move the viewport + const newViewportOffset = -1 * newOffset; + this.setState({ tabsContainerOffsetLeft: newViewportOffset }); + }; + + isRightArrowVisible = () => { + if (!this.tabsContainer) { + return false; + } + + const tabsContainerOffsetLeft = this.getTabsContainerOffsetLeft(); + const lastElementsAnchorPoint = this.getLastElementsAnchorPoint(); + const tabsContainerWidth = this.getTabsContainerWidth(); + + return tabsContainerOffsetLeft + tabsContainerWidth < lastElementsAnchorPoint; + }; + + isLeftArrowVisible = () => { + const { focusedIndex, selectedIndex } = this.props; + const tabsContainerOffsetLeft = this.getTabsContainerOffsetLeft(); + + return tabsContainerOffsetLeft !== 0 && (selectedIndex !== 0 || focusedIndex !== 0); + }; + + focusOnTabElement = (focusedIndex: number) => { + if (focusedIndex + 1 > this.tabsElements.length || focusedIndex < 0) { + return; + } + + this.tabsElements[focusedIndex].focus(); + }; + + tabsElements: Array = []; + + tabsContainer: HTMLDivElement | null = null; + + handleKeyDown = (event: React.KeyboardEvent) => { + const { children, focusedIndex, onTabFocus, resetFocusedTab, resetActiveTab } = this.props; + const childrenCount = React.Children.count(children); + + switch (event.key) { + case 'ArrowRight': + onTabFocus(this.calculateNextIndex(focusedIndex, childrenCount)); + event.preventDefault(); + event.stopPropagation(); + break; + + case 'ArrowLeft': + onTabFocus(this.calculatePrevIndex(focusedIndex, childrenCount)); + event.preventDefault(); + event.stopPropagation(); + break; + + case 'Escape': + resetActiveTab(); + break; + + case TAB_KEY: + resetFocusedTab(); + break; + + default: + break; + } + }; + + calculateNextIndex = (currentIndex: number, childrenCount: number) => (currentIndex + 1) % childrenCount; + + calculatePrevIndex = (currentIndex: number, childrenCount: number) => + (currentIndex - 1 + childrenCount) % childrenCount; + + renderTabs() { + const { children, selectedIndex, isDynamic } = this.props; + const { tabsContainerOffsetLeft } = this.state; + + const style = isDynamic ? { left: `${tabsContainerOffsetLeft}px` } : {}; + + return ( +
{ + this.tabsContainer = ref; + }} + style={style} + onKeyDown={!isDynamic ? this.handleKeyDown : undefined} + > + {React.Children.map(children, (child, i) => { + const tab = child as React.ReactElement; + const buttonProps = omit(tab.props, ['className', 'children', 'title']); + + const classes = classNames('btn-plain', 'tab', i === selectedIndex ? 'is-selected' : ''); + + const ariaControls = `${this.tabviewID}-panel-${i + 1}`; + const ariaSelected = i === selectedIndex; + const id = `${this.tabviewID}-tab-${i + 1}`; + const { href, component, refProp } = tab.props; + + const tabIndex = i === selectedIndex ? 0 : -1; + + if (href) { + return ( + { + this.tabsElements[i] = ref; + }} + refProp={refProp} + tabIndex={tabIndex} + to={href} + component={component} + > +
{tab.props.title}
+
+ + ); + } + return ( + + ); + })} +
+ ); + } + + renderDynamicTabs() { + const { onTabFocus, focusedIndex } = this.props; + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ +
{this.renderTabs()}
+ +
+ ); + } + + render() { + const { children, className = '', isDynamic = false, onKeyUp, selectedIndex } = this.props; + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ {!isDynamic ? this.renderTabs() : this.renderDynamicTabs()} +
+ {React.Children.toArray(children).map((child, i) => ( +
+ {(child as React.ReactElement).props.children} +
+ ))} +
+
+ ); + } +} + +export default TabViewPrimitive; diff --git a/src/components/tab-view/__tests__/TabView.test.js b/src/components/tab-view/__tests__/TabView.test.tsx similarity index 85% rename from src/components/tab-view/__tests__/TabView.test.js rename to src/components/tab-view/__tests__/TabView.test.tsx index 53737ea6e2..d1a9f5e6b4 100644 --- a/src/components/tab-view/__tests__/TabView.test.js +++ b/src/components/tab-view/__tests__/TabView.test.tsx @@ -1,10 +1,14 @@ import React, { act } from 'react'; import { shallow, mount } from 'enzyme'; +import type { ReactWrapper } from 'enzyme'; import sinon from 'sinon'; import { Tab, TabView } from '..'; const sandbox = sinon.sandbox.create(); +type TabViewInstance = InstanceType; +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Enzyme tests replace class internals with Sinon mocks +type TabViewWrapper = ReactWrapper; describe('components/tab-view/TabView', () => { afterEach(() => { @@ -19,9 +23,9 @@ describe('components/tab-view/TabView', () => { , ); - expect(component.find('TabViewPrimitive').length).toEqual(1); + expect(component.find('TabViewPrimitive')).toHaveLength(1); expect(component.props().selectedIndex).toEqual(0); - expect(component.props().onTabSelect).toEqual(component.instance().handleOnTabSelect); + expect(component.props().onTabSelect).toEqual((component.instance() as TabViewInstance).handleOnTabSelect); expect(typeof component.props().resetActiveTab).toBe('function'); }); @@ -33,7 +37,7 @@ describe('components/tab-view/TabView', () => { , ); - component.instance().handleOnTabSelect(100); + (component.instance() as TabViewInstance).handleOnTabSelect(100); expect(component.state('selectedIndex')).toEqual(100); }); @@ -49,7 +53,7 @@ describe('components/tab-view/TabView', () => { const selectedTabId = 100; - component.instance().handleOnTabSelect(selectedTabId); + (component.instance() as TabViewInstance).handleOnTabSelect(selectedTabId); expect(cb).toHaveBeenCalledWith(selectedTabId); }); @@ -61,7 +65,7 @@ describe('components/tab-view/TabView', () => { , ); - component.instance().handleOnTabFocus(100); + (component.instance() as TabViewInstance).handleOnTabFocus(100); expect(component.state('focusedIndex')).toEqual(100); }); @@ -74,7 +78,7 @@ describe('components/tab-view/TabView', () => { , ); - expect(component.find('TabViewPrimitive').length).toEqual(1); + expect(component.find('TabViewPrimitive')).toHaveLength(1); expect(component.props().selectedIndex).toEqual(1); expect(component.props().focusedIndex).toEqual(1); }); @@ -90,7 +94,7 @@ describe('components/tab-view/TabView', () => { const selectedIndex = 1; component.setState({ selectedIndex, focusedIndex: 2 }); expect(component.props().selectedIndex).toEqual(selectedIndex); - component.instance().resetFocusedTab(); + (component.instance() as TabViewInstance).resetFocusedTab(); component.update(); expect(component.props().focusedIndex).toEqual(selectedIndex); }); @@ -105,7 +109,7 @@ describe('components/tab-view/TabView', () => { component.setState({ selectedIndex: 1 }); expect(component.props().selectedIndex).toEqual(1); - component.instance().resetActiveTab(); + (component.instance() as TabViewInstance).resetActiveTab(); component.update(); expect(component.props().selectedIndex).toEqual(0); }); @@ -121,22 +125,12 @@ describe('components/tab-view/TabView', () => { , ); - expect( - component - .find('button') - .at(0) - .prop('data-resin-tag'), - ).toEqual('test1'); - expect( - component - .find('button') - .at(1) - .prop('data-resin-tag'), - ).toEqual('test2'); + expect(component.find('button').at(0).prop('data-resin-tag')).toEqual('test1'); + expect(component.find('button').at(1).prop('data-resin-tag')).toEqual('test2'); }); describe('life cycle methods', () => { - let component; + let component: TabViewWrapper; beforeEach(() => { component = mount( @@ -168,7 +162,7 @@ describe('components/tab-view/TabView', () => { }); describe('handleKeyUp', () => { - let component; + let component: TabViewWrapper; beforeEach(() => { component = mount( diff --git a/src/components/tab-view/__tests__/TabViewPrimitive.test.js b/src/components/tab-view/__tests__/TabViewPrimitive.test.tsx similarity index 87% rename from src/components/tab-view/__tests__/TabViewPrimitive.test.js rename to src/components/tab-view/__tests__/TabViewPrimitive.test.tsx index c884860cdb..b5a4a0418f 100644 --- a/src/components/tab-view/__tests__/TabViewPrimitive.test.js +++ b/src/components/tab-view/__tests__/TabViewPrimitive.test.tsx @@ -1,17 +1,32 @@ import React, { act } from 'react'; import { shallow, mount } from 'enzyme'; +import type { ReactWrapper } from 'enzyme'; import sinon from 'sinon'; import { Tab, TabViewPrimitive } from '..'; const sandbox = sinon.sandbox.create(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Enzyme tests replace class internals and DOM refs +type TabViewPrimitiveWrapper = ReactWrapper; + +interface RenderCase { + tabsContainerOffsetLeft: number; + isDynamic: boolean; + style: React.CSSProperties; +} + describe('components/tab-view/TabViewPrimitive', () => { afterEach(() => { sandbox.verifyAndRestore(); }); - const simulateKeyDown = (comp, key, shouldStopEvent = false, isShiftKey = false) => { + const simulateKeyDown = ( + comp: TabViewPrimitiveWrapper, + key: string, + shouldStopEvent = false, + isShiftKey = false, + ) => { // conveniently ensuring that the keydown event is attached to the tablist element, // not the entire container comp.find('[role="tablist"]') @@ -25,15 +40,15 @@ describe('components/tab-view/TabViewPrimitive', () => { }; test('should render tabview ui with tabs', () => { - const onTabFocus = () => {}; - const onTabSelect = () => {}; + const onTabFocus = jest.fn(); + const onTabSelect = jest.fn(); const component = shallow( {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -42,21 +57,25 @@ describe('components/tab-view/TabViewPrimitive', () => { ); expect(component.hasClass('tab-view')).toBe(true); - expect(component.instance().handleKeyDown).toEqual(component.instance().handleKeyDown); - expect(component.find('.tabs').find('button').length).toEqual(2); - expect(component.find({ tabIndex: 0 }).toBeFalsy); + expect(component.find('.tabs').find('button')).toHaveLength(2); + expect( + component + .find('.tabs') + .find('button') + .filterWhere(tab => tab.prop('tabIndex') === 0), + ).toHaveLength(1); }); test('should render tabview ui with link tabs', () => { - const onTabFocus = () => {}; - const onTabSelect = () => {}; + const onTabFocus = jest.fn(); + const onTabSelect = jest.fn(); const component = shallow( {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -64,20 +83,20 @@ describe('components/tab-view/TabViewPrimitive', () => { , ); - expect(component.find('.tabs').find('button').length).toEqual(1); - expect(component.find('.tabs').find('LinkButton').length).toEqual(1); + expect(component.find('.tabs').find('button')).toHaveLength(1); + expect(component.find('.tabs').find('LinkButton')).toHaveLength(1); }); test('should select appropriate tab when passed selectedIndex', () => { - const onTabFocus = () => {}; - const onTabSelect = () => {}; + const onTabFocus = jest.fn(); + const onTabSelect = jest.fn(); const component = shallow( {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={1} > Tab 1 @@ -93,25 +112,21 @@ describe('components/tab-view/TabViewPrimitive', () => { test('should call onTabSelect when tab selected', () => { const onTabFocus = sinon.spy(); const onTabSelect = sinon.spy(); - const event = { type: 'click' }; + const event = { type: 'click' } as const; const component = shallow( {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={1} > Tab 1 Tab 2 , ); - const tabTwoButton = component - .find('.tabs') - .find('button') - .at(1) - .find('button'); + const tabTwoButton = component.find('.tabs').find('button').at(1).find('button'); tabTwoButton.simulate('click', event); expect(onTabSelect.calledWith(1)).toBe(true); expect(onTabFocus.calledWith(1)).toBe(true); @@ -125,8 +140,8 @@ describe('components/tab-view/TabViewPrimitive', () => { focusedIndex={0} onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -145,8 +160,8 @@ describe('components/tab-view/TabViewPrimitive', () => { focusedIndex={0} onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={1} > Tab 1 @@ -165,8 +180,8 @@ describe('components/tab-view/TabViewPrimitive', () => { focusedIndex={1} onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={1} > Tab 1 @@ -185,8 +200,8 @@ describe('components/tab-view/TabViewPrimitive', () => { focusedIndex={0} onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -208,7 +223,7 @@ describe('components/tab-view/TabViewPrimitive', () => { onTabFocus={onTabFocus} onTabSelect={onTabSelect} resetActiveTab={resetActiveTab} - resetFocusedTab={() => {}} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -220,7 +235,7 @@ describe('components/tab-view/TabViewPrimitive', () => { }); describe('render()', () => { - [ + const renderCases: RenderCase[] = [ { tabsContainerOffsetLeft: 1, isDynamic: true, @@ -231,7 +246,9 @@ describe('components/tab-view/TabViewPrimitive', () => { isDynamic: false, style: {}, }, - ].forEach(({ tabsContainerOffsetLeft, isDynamic, style }) => { + ]; + + renderCases.forEach(({ tabsContainerOffsetLeft, isDynamic, style }) => { test('should render tabs with correct style', () => { const component = shallow( { describe('Dynamic Tabs', () => { describe('scrollToTab', () => { - let component; + let component: TabViewPrimitiveWrapper; beforeEach(() => { const onTabFocus = sinon.mock(); const onTabSelect = sinon.mock(); @@ -267,8 +284,8 @@ describe('components/tab-view/TabViewPrimitive', () => { isDynamic onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -287,10 +304,10 @@ describe('components/tab-view/TabViewPrimitive', () => { {}} - onTabSelect={() => {}} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + onTabFocus={jest.fn()} + onTabSelect={jest.fn()} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -357,7 +374,7 @@ describe('components/tab-view/TabViewPrimitive', () => { }); describe('life cycle methods', () => { - let component; + let component: TabViewPrimitiveWrapper; beforeEach(() => { const onTabFocus = sinon.mock(); const onTabSelect = sinon.mock(); @@ -367,8 +384,8 @@ describe('components/tab-view/TabViewPrimitive', () => { isDynamic onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -460,8 +477,8 @@ describe('components/tab-view/TabViewPrimitive', () => { }); describe('arrows', () => { - let component; - let onTabFocus; + let component: TabViewPrimitiveWrapper; + let onTabFocus: sinon.SinonExpectation; beforeEach(() => { onTabFocus = sinon.mock(); @@ -472,8 +489,8 @@ describe('components/tab-view/TabViewPrimitive', () => { isDynamic onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 @@ -500,7 +517,7 @@ describe('components/tab-view/TabViewPrimitive', () => { const lastElementIsInsideOfTabsContainer = { offsetLeft: 0, offsetWidth: 50, - }; + } as const; instance.tabsElements = [lastElementIsInsideOfTabsContainer]; instance.tabsContainer = { @@ -521,7 +538,7 @@ describe('components/tab-view/TabViewPrimitive', () => { const lastElementIsOutsideOfTabsContainer = { offsetLeft: 100, offsetWidth: 100, - }; + } as const; instance.tabsElements = [lastElementIsOutsideOfTabsContainer]; instance.tabsContainer = { @@ -607,9 +624,9 @@ describe('components/tab-view/TabViewPrimitive', () => { {}} - onTabSelect={() => {}} - resetActiveTab={() => {}} + onTabFocus={jest.fn()} + onTabSelect={jest.fn()} + resetActiveTab={jest.fn()} resetFocusedTab={resetFocusedTab} selectedIndex={1} > @@ -624,9 +641,9 @@ describe('components/tab-view/TabViewPrimitive', () => { }); describe('focusOnTabElement', () => { - let onTabFocus; - let onTabSelect; - let component; + let onTabFocus: sinon.SinonExpectation; + let onTabSelect: sinon.SinonExpectation; + let component: TabViewPrimitiveWrapper; beforeEach(() => { onTabFocus = sinon.mock().withArgs(1); @@ -637,8 +654,8 @@ describe('components/tab-view/TabViewPrimitive', () => { isDynamic onTabFocus={onTabFocus} onTabSelect={onTabSelect} - resetActiveTab={() => {}} - resetFocusedTab={() => {}} + resetActiveTab={jest.fn()} + resetFocusedTab={jest.fn()} selectedIndex={0} > Tab 1 diff --git a/src/components/tab-view/index.js b/src/components/tab-view/index.js.flow similarity index 100% rename from src/components/tab-view/index.js rename to src/components/tab-view/index.js.flow diff --git a/src/components/tab-view/index.ts b/src/components/tab-view/index.ts new file mode 100644 index 0000000000..569de4716b --- /dev/null +++ b/src/components/tab-view/index.ts @@ -0,0 +1,6 @@ +export { default as Tab } from './Tab'; +export type { TabProps } from './Tab'; +export { default as TabView } from './TabView'; +export type { TabViewProps } from './TabView'; +export { default as TabViewPrimitive } from './TabViewPrimitive'; +export type { TabViewPrimitiveProps } from './TabViewPrimitive';