From 1969e650ea5e594fe988726b6cc50c8c2f6ee54d Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Mon, 4 Sep 2017 21:14:59 +0600 Subject: [PATCH] feat(tabs): Allow for higher order components (#196) * Allow for higher order components * Added tests with higher order components. * Test if solution works with hoist-non-react-statics --- .eslintrc | 11 ++-- package.json | 4 +- src/components/Tab.js | 2 + src/components/TabList.js | 2 + src/components/TabPanel.js | 2 + src/components/Tabs.js | 2 + src/components/UncontrolledTabs.js | 10 ++-- src/components/__tests__/Tab-test.js | 5 ++ src/components/__tests__/TabList-test.js | 14 +++++ src/components/__tests__/TabPanel-test.js | 5 ++ src/components/__tests__/Tabs-test.js | 14 +++++ .../__tests__/__snapshots__/Tab-test.js.snap | 12 +++++ .../__snapshots__/TabList-test.js.snap | 53 +++++++++++++++++++ .../__snapshots__/TabPanel-test.js.snap | 10 ++++ .../__tests__/__snapshots__/Tabs-test.js.snap | 53 +++++++++++++++++++ .../helpers/higherOrder/TabListWrapper.js | 9 ++++ .../helpers/higherOrder/TabPanelWrapper.js | 9 ++++ .../helpers/higherOrder/TabWrapper.js | 9 ++++ .../__tests__/helpers/higherOrder/index.js | 5 ++ src/helpers/childrenDeepMap.js | 10 ++-- src/helpers/count.js | 7 ++- src/helpers/elementTypes.js | 11 ++++ src/helpers/propTypes.js | 10 ++-- yarn.lock | 4 ++ 24 files changed, 246 insertions(+), 27 deletions(-) create mode 100644 src/components/__tests__/helpers/higherOrder/TabListWrapper.js create mode 100644 src/components/__tests__/helpers/higherOrder/TabPanelWrapper.js create mode 100644 src/components/__tests__/helpers/higherOrder/TabWrapper.js create mode 100644 src/components/__tests__/helpers/higherOrder/index.js create mode 100644 src/helpers/elementTypes.js diff --git a/.eslintrc b/.eslintrc index c606ee459f..3708a52c71 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,9 +17,12 @@ "react/forbid-prop-types": "off", "react/sort-comp": "off", "jsx-a11y/no-static-element-interactions": "off", - "import/no-extraneous-dependencies": ["error", { - "devDependencies": ["**/__tests__/*-test.js"], - "optionalDependencies": false - }] + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": ["**/__tests__/**/*"], + "optionalDependencies": false + } + ] } } diff --git a/package.json b/package.json index 1855a13f3b..91e5660e79 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "eslint-plugin-jsx-a11y": "^5.0.3", "eslint-plugin-prettier": "^2.2.0", "eslint-plugin-react": "^7.0.1", + "hoist-non-react-statics": "^2.3.1", "husky": "^0.14.3", "jest-cli": "^20.0.0", "lint-staged": "^4.0.4", @@ -84,7 +85,8 @@ "jest": { "roots": [ "src" - ] + ], + "testRegex": "/__tests__/.+-test\\.js$" }, "lint-staged": { "src/**/*.js": [ diff --git a/src/components/Tab.js b/src/components/Tab.js index 7d5506bf38..3b2c627a2c 100644 --- a/src/components/Tab.js +++ b/src/components/Tab.js @@ -80,3 +80,5 @@ export default class Tab extends Component { ); } } + +Tab.tabsRole = 'Tab'; diff --git a/src/components/TabList.js b/src/components/TabList.js index 3dfdb4846b..6388e24918 100644 --- a/src/components/TabList.js +++ b/src/components/TabList.js @@ -22,3 +22,5 @@ export default class TabList extends Component { ); } } + +TabList.tabsRole = 'TabList'; diff --git a/src/components/TabPanel.js b/src/components/TabPanel.js index e9d211e8fc..df3b1c4371 100644 --- a/src/components/TabPanel.js +++ b/src/components/TabPanel.js @@ -49,3 +49,5 @@ export default class TabPanel extends Component { ); } } + +TabPanel.tabsRole = 'TabPanel'; diff --git a/src/components/Tabs.js b/src/components/Tabs.js index 31641ee802..8a330eb9ad 100644 --- a/src/components/Tabs.js +++ b/src/components/Tabs.js @@ -106,3 +106,5 @@ For more information about controlled and uncontrolled mode of react-tabs see th return {children}; } } + +Tabs.tabsRole = 'Tabs'; diff --git a/src/components/UncontrolledTabs.js b/src/components/UncontrolledTabs.js index 18cd248626..4b07b7a464 100644 --- a/src/components/UncontrolledTabs.js +++ b/src/components/UncontrolledTabs.js @@ -3,11 +3,9 @@ import React, { cloneElement, Component } from 'react'; import cx from 'classnames'; import uuid from '../helpers/uuid'; import { childrenPropType } from '../helpers/propTypes'; -import Tab from './Tab'; -import TabList from './TabList'; -import TabPanel from './TabPanel'; import { getPanelsCount, getTabsCount } from '../helpers/count'; import { deepMap } from '../helpers/childrenDeepMap'; +import { isTabList, isTabPanel, isTab } from '../helpers/elementTypes'; // Determine if a node from event.target is a Tab element function isTabNode(node) { @@ -145,7 +143,7 @@ export default class UncontrolledTabs extends Component { let result = child; // Clone TabList and Tab components to have refs - if (child.type === TabList) { + if (isTabList(child)) { let listIndex = 0; // Figure out if the current focus in the DOM is set on a Tab @@ -155,7 +153,7 @@ export default class UncontrolledTabs extends Component { if (canUseActiveElement) { wasTabFocused = React.Children .toArray(child.props.children) - .filter(tab => tab.type === Tab) + .filter(isTab) .some((tab, i) => document.activeElement === this.getTab(i)); } @@ -182,7 +180,7 @@ export default class UncontrolledTabs extends Component { return cloneElement(tab, props); }), }); - } else if (child.type === TabPanel) { + } else if (isTabPanel(child)) { const props = { id: this.panelIds[index], tabId: this.tabIds[index], diff --git a/src/components/__tests__/Tab-test.js b/src/components/__tests__/Tab-test.js index 366c52790c..9430a7aff5 100644 --- a/src/components/__tests__/Tab-test.js +++ b/src/components/__tests__/Tab-test.js @@ -2,6 +2,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import Tab from '../Tab'; +import { TabWrapper } from './helpers/higherOrder'; function expectToMatchSnapshot(component) { expect(renderer.create(component).toJSON()).toMatchSnapshot(); @@ -51,4 +52,8 @@ describe('', () => { // eslint-disable-next-line jsx-a11y/aria-role expectToMatchSnapshot(); }); + + it('should allow to be wrapped in higher-order-component', () => { + expectToMatchSnapshot(); + }); }); diff --git a/src/components/__tests__/TabList-test.js b/src/components/__tests__/TabList-test.js index d5d1aa5aac..a612746fa9 100644 --- a/src/components/__tests__/TabList-test.js +++ b/src/components/__tests__/TabList-test.js @@ -5,6 +5,7 @@ import Tab from '../Tab'; import TabList from '../TabList'; import TabPanel from '../TabPanel'; import Tabs from '../Tabs'; +import { TabListWrapper, TabWrapper } from './helpers/higherOrder'; function expectToMatchSnapshot(component) { expect(renderer.create(component).toJSON()).toMatchSnapshot(); @@ -77,4 +78,17 @@ describe('', () => { , ); }); + + it('should allow for higher order components', () => { + expectToMatchSnapshot( + + + Foo + Bar + + Foo + Bar + , + ); + }); }); diff --git a/src/components/__tests__/TabPanel-test.js b/src/components/__tests__/TabPanel-test.js index 55566944f3..485a210842 100644 --- a/src/components/__tests__/TabPanel-test.js +++ b/src/components/__tests__/TabPanel-test.js @@ -2,6 +2,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import TabPanel from '../TabPanel'; +import { TabPanelWrapper } from './helpers/higherOrder'; function expectToMatchSnapshot(component) { expect(renderer.create(component).toJSON()).toMatchSnapshot(); @@ -55,4 +56,8 @@ describe('', () => { // eslint-disable-next-line jsx-a11y/aria-role expectToMatchSnapshot(); }); + + it('should allow for higher-order components', () => { + expectToMatchSnapshot(); + }); }); diff --git a/src/components/__tests__/Tabs-test.js b/src/components/__tests__/Tabs-test.js index 0c3d800467..ab9452beba 100644 --- a/src/components/__tests__/Tabs-test.js +++ b/src/components/__tests__/Tabs-test.js @@ -8,6 +8,7 @@ import TabList from '../TabList'; import TabPanel from '../TabPanel'; import Tabs from '../Tabs'; import { reset as resetIdCounter } from '../../helpers/uuid'; +import { TabListWrapper, TabWrapper, TabPanelWrapper } from './helpers/higherOrder'; function expectToMatchSnapshot(component) { expect(renderer.create(component).toJSON()).toMatchSnapshot(); @@ -483,4 +484,17 @@ describe('', () => { .simulate('click'); assertTabSelected(wrapper, 2); }); + + it('should allow for higher order components', () => { + expectToMatchSnapshot( + + + Foo + Bar + + Foo + Bar + , + ); + }); }); diff --git a/src/components/__tests__/__snapshots__/Tab-test.js.snap b/src/components/__tests__/__snapshots__/Tab-test.js.snap index 553956e0b4..871e1aa3ea 100644 --- a/src/components/__tests__/__snapshots__/Tab-test.js.snap +++ b/src/components/__tests__/__snapshots__/Tab-test.js.snap @@ -12,6 +12,18 @@ exports[` should accept className 1`] = ` /> `; +exports[` should allow to be wrapped in higher-order-component 1`] = ` +
  • should accept className 1`] = ` /> `; +exports[` should allow for higher order components 1`] = ` +
    +
      + + +
    +
    + Foo +
    +
    +
    +`; + exports[` should display the custom classnames for selected and disabled tab 1`] = `
    should accept className 1`] = ` /> `; +exports[` should allow for higher-order components 1`] = ` +
    +`; + exports[` should have sane defaults 1`] = `
    props should honor positive defaultIndex prop 1`] = `
    `; +exports[` should allow for higher order components 1`] = ` +
    +
      + + +
    +
    + Foo +
    +
    +
    +`; + exports[` should not add known props to dom 1`] = `
    ; +} + +export default hoist(TabListWrapper, TabList); diff --git a/src/components/__tests__/helpers/higherOrder/TabPanelWrapper.js b/src/components/__tests__/helpers/higherOrder/TabPanelWrapper.js new file mode 100644 index 0000000000..b110e9c44b --- /dev/null +++ b/src/components/__tests__/helpers/higherOrder/TabPanelWrapper.js @@ -0,0 +1,9 @@ +import React from 'react'; +import hoist from 'hoist-non-react-statics'; +import TabPanel from '../../../../components/TabPanel'; + +function TabPanelWrapper(props) { + return ; +} + +export default hoist(TabPanelWrapper, TabPanel); diff --git a/src/components/__tests__/helpers/higherOrder/TabWrapper.js b/src/components/__tests__/helpers/higherOrder/TabWrapper.js new file mode 100644 index 0000000000..a23d9a2e47 --- /dev/null +++ b/src/components/__tests__/helpers/higherOrder/TabWrapper.js @@ -0,0 +1,9 @@ +import React from 'react'; +import hoist from 'hoist-non-react-statics'; +import Tab from '../../../../components/Tab'; + +function TabWrapper(props) { + return ; +} + +export default hoist(TabWrapper, Tab); diff --git a/src/components/__tests__/helpers/higherOrder/index.js b/src/components/__tests__/helpers/higherOrder/index.js new file mode 100644 index 0000000000..1b183ad447 --- /dev/null +++ b/src/components/__tests__/helpers/higherOrder/index.js @@ -0,0 +1,5 @@ +import TabWrapper from './TabWrapper'; +import TabListWrapper from './TabListWrapper'; +import TabPanelWrapper from './TabPanelWrapper'; + +export { TabWrapper, TabListWrapper, TabPanelWrapper }; diff --git a/src/helpers/childrenDeepMap.js b/src/helpers/childrenDeepMap.js index 287c76881a..2f3d4e1813 100644 --- a/src/helpers/childrenDeepMap.js +++ b/src/helpers/childrenDeepMap.js @@ -1,10 +1,8 @@ import { Children, cloneElement } from 'react'; -import Tab from '../components/Tab'; -import TabList from '../components/TabList'; -import TabPanel from '../components/TabPanel'; +import { isTabPanel, isTab, isTabList } from '../helpers/elementTypes'; function isTabChild(child) { - return child.type === Tab || child.type === TabList || child.type === TabPanel; + return isTab(child) || isTabList(child) || isTabPanel(child); } export function deepMap(children, callback) { @@ -35,10 +33,10 @@ export function deepForEach(children, callback) { // see https://github.com/reactjs/react-tabs/issues/37 if (child === null) return; - if (child.type === Tab || child.type === TabPanel) { + if (isTab(child) || isTabPanel(child)) { callback(child); } else if (child.props && child.props.children && typeof child.props.children === 'object') { - if (child.type === TabList) callback(child); + if (isTabList(child)) callback(child); deepForEach(child.props.children, callback); } }); diff --git a/src/helpers/count.js b/src/helpers/count.js index c386e45113..87721faac4 100644 --- a/src/helpers/count.js +++ b/src/helpers/count.js @@ -1,11 +1,10 @@ import { deepForEach } from '../helpers/childrenDeepMap'; -import Tab from '../components/Tab'; -import TabPanel from '../components/TabPanel'; +import { isTab, isTabPanel } from './elementTypes'; export function getTabsCount(children) { let tabCount = 0; deepForEach(children, child => { - if (child.type === Tab) tabCount++; + if (isTab(child)) tabCount++; }); return tabCount; @@ -14,7 +13,7 @@ export function getTabsCount(children) { export function getPanelsCount(children) { let panelCount = 0; deepForEach(children, child => { - if (child.type === TabPanel) panelCount++; + if (isTabPanel(child)) panelCount++; }); return panelCount; diff --git a/src/helpers/elementTypes.js b/src/helpers/elementTypes.js new file mode 100644 index 0000000000..e75ce5d681 --- /dev/null +++ b/src/helpers/elementTypes.js @@ -0,0 +1,11 @@ +export function isTab(el) { + return el.type.tabsRole === 'Tab'; +} + +export function isTabPanel(el) { + return el.type.tabsRole === 'TabPanel'; +} + +export function isTabList(el) { + return el.type.tabsRole === 'TabList'; +} diff --git a/src/helpers/propTypes.js b/src/helpers/propTypes.js index 78361fb5df..974f13a7df 100644 --- a/src/helpers/propTypes.js +++ b/src/helpers/propTypes.js @@ -1,7 +1,5 @@ import { deepForEach } from '../helpers/childrenDeepMap'; -import Tab from '../components/Tab'; -import TabList from '../components/TabList'; -import TabPanel from '../components/TabPanel'; +import { isTab, isTabList, isTabPanel } from '../helpers/elementTypes'; export function childrenPropType(props, propName, componentName) { let error; @@ -12,7 +10,7 @@ export function childrenPropType(props, propName, componentName) { const children = props[propName]; deepForEach(children, child => { - if (child.type === TabList) { + if (isTabList(child)) { if (child.props && child.props.children && typeof child.props.children === 'object') { deepForEach(child.props.children, listChild => listTabs.push(listChild)); } @@ -24,14 +22,14 @@ export function childrenPropType(props, propName, componentName) { } tabListFound = true; } - if (child.type === Tab) { + if (isTab(child)) { if (!tabListFound || listTabs.indexOf(child) === -1) { error = new Error( "Found a 'Tab' component outside of the 'TabList' component. 'Tab' components have to be inside the 'TabList' component.", ); } tabsCount++; - } else if (child.type === TabPanel) { + } else if (isTabPanel(child)) { panelsCount++; } }); diff --git a/yarn.lock b/yarn.lock index 70048a789c..15d86d1ee5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2634,6 +2634,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoist-non-react-statics@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"