Skip to content

Commit 1969e65

Browse files
karevndanez
authored andcommitted
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
1 parent 722d52f commit 1969e65

24 files changed

+246
-27
lines changed

.eslintrc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
"react/forbid-prop-types": "off",
1818
"react/sort-comp": "off",
1919
"jsx-a11y/no-static-element-interactions": "off",
20-
"import/no-extraneous-dependencies": ["error", {
21-
"devDependencies": ["**/__tests__/*-test.js"],
22-
"optionalDependencies": false
23-
}]
20+
"import/no-extraneous-dependencies": [
21+
"error",
22+
{
23+
"devDependencies": ["**/__tests__/**/*"],
24+
"optionalDependencies": false
25+
}
26+
]
2427
}
2528
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"eslint-plugin-jsx-a11y": "^5.0.3",
6363
"eslint-plugin-prettier": "^2.2.0",
6464
"eslint-plugin-react": "^7.0.1",
65+
"hoist-non-react-statics": "^2.3.1",
6566
"husky": "^0.14.3",
6667
"jest-cli": "^20.0.0",
6768
"lint-staged": "^4.0.4",
@@ -84,7 +85,8 @@
8485
"jest": {
8586
"roots": [
8687
"src"
87-
]
88+
],
89+
"testRegex": "/__tests__/.+-test\\.js$"
8890
},
8991
"lint-staged": {
9092
"src/**/*.js": [

src/components/Tab.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,5 @@ export default class Tab extends Component {
8080
);
8181
}
8282
}
83+
84+
Tab.tabsRole = 'Tab';

src/components/TabList.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export default class TabList extends Component {
2222
);
2323
}
2424
}
25+
26+
TabList.tabsRole = 'TabList';

src/components/TabPanel.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ export default class TabPanel extends Component {
4949
);
5050
}
5151
}
52+
53+
TabPanel.tabsRole = 'TabPanel';

src/components/Tabs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,5 @@ For more information about controlled and uncontrolled mode of react-tabs see th
106106
return <UncontrolledTabs {...props}>{children}</UncontrolledTabs>;
107107
}
108108
}
109+
110+
Tabs.tabsRole = 'Tabs';

src/components/UncontrolledTabs.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ import React, { cloneElement, Component } from 'react';
33
import cx from 'classnames';
44
import uuid from '../helpers/uuid';
55
import { childrenPropType } from '../helpers/propTypes';
6-
import Tab from './Tab';
7-
import TabList from './TabList';
8-
import TabPanel from './TabPanel';
96
import { getPanelsCount, getTabsCount } from '../helpers/count';
107
import { deepMap } from '../helpers/childrenDeepMap';
8+
import { isTabList, isTabPanel, isTab } from '../helpers/elementTypes';
119

1210
// Determine if a node from event.target is a Tab element
1311
function isTabNode(node) {
@@ -145,7 +143,7 @@ export default class UncontrolledTabs extends Component {
145143
let result = child;
146144

147145
// Clone TabList and Tab components to have refs
148-
if (child.type === TabList) {
146+
if (isTabList(child)) {
149147
let listIndex = 0;
150148

151149
// Figure out if the current focus in the DOM is set on a Tab
@@ -155,7 +153,7 @@ export default class UncontrolledTabs extends Component {
155153
if (canUseActiveElement) {
156154
wasTabFocused = React.Children
157155
.toArray(child.props.children)
158-
.filter(tab => tab.type === Tab)
156+
.filter(isTab)
159157
.some((tab, i) => document.activeElement === this.getTab(i));
160158
}
161159

@@ -182,7 +180,7 @@ export default class UncontrolledTabs extends Component {
182180
return cloneElement(tab, props);
183181
}),
184182
});
185-
} else if (child.type === TabPanel) {
183+
} else if (isTabPanel(child)) {
186184
const props = {
187185
id: this.panelIds[index],
188186
tabId: this.tabIds[index],

src/components/__tests__/Tab-test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import React from 'react';
33
import renderer from 'react-test-renderer';
44
import Tab from '../Tab';
5+
import { TabWrapper } from './helpers/higherOrder';
56

67
function expectToMatchSnapshot(component) {
78
expect(renderer.create(component).toJSON()).toMatchSnapshot();
@@ -51,4 +52,8 @@ describe('<Tab />', () => {
5152
// eslint-disable-next-line jsx-a11y/aria-role
5253
expectToMatchSnapshot(<Tab role="micro-tab" />);
5354
});
55+
56+
it('should allow to be wrapped in higher-order-component', () => {
57+
expectToMatchSnapshot(<TabWrapper />);
58+
});
5459
});

src/components/__tests__/TabList-test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Tab from '../Tab';
55
import TabList from '../TabList';
66
import TabPanel from '../TabPanel';
77
import Tabs from '../Tabs';
8+
import { TabListWrapper, TabWrapper } from './helpers/higherOrder';
89

910
function expectToMatchSnapshot(component) {
1011
expect(renderer.create(component).toJSON()).toMatchSnapshot();
@@ -77,4 +78,17 @@ describe('<TabList />', () => {
7778
</Tabs>,
7879
);
7980
});
81+
82+
it('should allow for higher order components', () => {
83+
expectToMatchSnapshot(
84+
<Tabs>
85+
<TabListWrapper>
86+
<TabWrapper>Foo</TabWrapper>
87+
<TabWrapper>Bar</TabWrapper>
88+
</TabListWrapper>
89+
<TabPanel>Foo</TabPanel>
90+
<TabPanel>Bar</TabPanel>
91+
</Tabs>,
92+
);
93+
});
8094
});

src/components/__tests__/TabPanel-test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import React from 'react';
33
import renderer from 'react-test-renderer';
44
import TabPanel from '../TabPanel';
5+
import { TabPanelWrapper } from './helpers/higherOrder';
56

67
function expectToMatchSnapshot(component) {
78
expect(renderer.create(component).toJSON()).toMatchSnapshot();
@@ -55,4 +56,8 @@ describe('<TabPanel />', () => {
5556
// eslint-disable-next-line jsx-a11y/aria-role
5657
expectToMatchSnapshot(<TabPanel role="micro-tab" />);
5758
});
59+
60+
it('should allow for higher-order components', () => {
61+
expectToMatchSnapshot(<TabPanelWrapper />);
62+
});
5863
});

src/components/__tests__/Tabs-test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TabList from '../TabList';
88
import TabPanel from '../TabPanel';
99
import Tabs from '../Tabs';
1010
import { reset as resetIdCounter } from '../../helpers/uuid';
11+
import { TabListWrapper, TabWrapper, TabPanelWrapper } from './helpers/higherOrder';
1112

1213
function expectToMatchSnapshot(component) {
1314
expect(renderer.create(component).toJSON()).toMatchSnapshot();
@@ -483,4 +484,17 @@ describe('<Tabs />', () => {
483484
.simulate('click');
484485
assertTabSelected(wrapper, 2);
485486
});
487+
488+
it('should allow for higher order components', () => {
489+
expectToMatchSnapshot(
490+
<Tabs>
491+
<TabListWrapper>
492+
<TabWrapper>Foo</TabWrapper>
493+
<TabWrapper>Bar</TabWrapper>
494+
</TabListWrapper>
495+
<TabPanelWrapper>Foo</TabPanelWrapper>
496+
<TabPanelWrapper>Bar</TabPanelWrapper>
497+
</Tabs>,
498+
);
499+
});
486500
});

src/components/__tests__/__snapshots__/Tab-test.js.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ exports[`<Tab /> should accept className 1`] = `
1212
/>
1313
`;
1414

15+
exports[`<Tab /> should allow to be wrapped in higher-order-component 1`] = `
16+
<li
17+
aria-controls={null}
18+
aria-disabled="false"
19+
aria-selected="false"
20+
className="react-tabs__tab"
21+
id={null}
22+
role="tab"
23+
tabIndex={null}
24+
/>
25+
`;
26+
1527
exports[`<Tab /> should have sane defaults 1`] = `
1628
<li
1729
aria-controls={null}

src/components/__tests__/__snapshots__/TabList-test.js.snap

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,59 @@ exports[`<TabList /> should accept className 1`] = `
77
/>
88
`;
99

10+
exports[`<TabList /> should allow for higher order components 1`] = `
11+
<div
12+
className="react-tabs"
13+
data-tabs={true}
14+
onClick={[Function]}
15+
onKeyDown={[Function]}
16+
>
17+
<ul
18+
className="react-tabs__tab-list"
19+
role="tablist"
20+
>
21+
<li
22+
aria-controls="react-tabs-13"
23+
aria-disabled="false"
24+
aria-selected="true"
25+
className="react-tabs__tab react-tabs__tab--selected"
26+
id="react-tabs-12"
27+
role="tab"
28+
tabIndex="0"
29+
>
30+
Foo
31+
</li>
32+
<li
33+
aria-controls="react-tabs-15"
34+
aria-disabled="false"
35+
aria-selected="false"
36+
className="react-tabs__tab"
37+
id="react-tabs-14"
38+
role="tab"
39+
tabIndex={null}
40+
>
41+
Bar
42+
</li>
43+
</ul>
44+
<div
45+
aria-labelledby="react-tabs-12"
46+
className="react-tabs__tab-panel react-tabs__tab-panel--selected"
47+
id="react-tabs-13"
48+
role="tabpanel"
49+
style={Object {}}
50+
>
51+
Foo
52+
</div>
53+
<div
54+
aria-labelledby="react-tabs-14"
55+
className="react-tabs__tab-panel"
56+
id="react-tabs-15"
57+
role="tabpanel"
58+
style={Object {}}
59+
/>
60+
</div>
61+
`;
62+
1063
exports[`<TabList /> should display the custom classnames for selected and disabled tab 1`] = `
1164
<div
1265
className="react-tabs"

src/components/__tests__/__snapshots__/TabPanel-test.js.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ exports[`<TabPanel /> should accept className 1`] = `
1010
/>
1111
`;
1212

13+
exports[`<TabPanel /> should allow for higher-order components 1`] = `
14+
<div
15+
aria-labelledby={undefined}
16+
className="react-tabs__tab-panel"
17+
id={undefined}
18+
role="tabpanel"
19+
style={Object {}}
20+
/>
21+
`;
22+
1323
exports[`<TabPanel /> should have sane defaults 1`] = `
1424
<div
1525
aria-labelledby={undefined}

src/components/__tests__/__snapshots__/Tabs-test.js.snap

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,59 @@ exports[`<Tabs /> props should honor positive defaultIndex prop 1`] = `
641641
</div>
642642
`;
643643

644+
exports[`<Tabs /> should allow for higher order components 1`] = `
645+
<div
646+
className="react-tabs"
647+
data-tabs={true}
648+
onClick={[Function]}
649+
onKeyDown={[Function]}
650+
>
651+
<ul
652+
className="react-tabs__tab-list"
653+
role="tablist"
654+
>
655+
<li
656+
aria-controls="react-tabs-1"
657+
aria-disabled="false"
658+
aria-selected="true"
659+
className="react-tabs__tab react-tabs__tab--selected"
660+
id="react-tabs-0"
661+
role="tab"
662+
tabIndex="0"
663+
>
664+
Foo
665+
</li>
666+
<li
667+
aria-controls="react-tabs-3"
668+
aria-disabled="false"
669+
aria-selected="false"
670+
className="react-tabs__tab"
671+
id="react-tabs-2"
672+
role="tab"
673+
tabIndex={null}
674+
>
675+
Bar
676+
</li>
677+
</ul>
678+
<div
679+
aria-labelledby="react-tabs-0"
680+
className="react-tabs__tab-panel react-tabs__tab-panel--selected"
681+
id="react-tabs-1"
682+
role="tabpanel"
683+
style={Object {}}
684+
>
685+
Foo
686+
</div>
687+
<div
688+
aria-labelledby="react-tabs-2"
689+
className="react-tabs__tab-panel"
690+
id="react-tabs-3"
691+
role="tabpanel"
692+
style={Object {}}
693+
/>
694+
</div>
695+
`;
696+
644697
exports[`<Tabs /> should not add known props to dom 1`] = `
645698
<div
646699
className="react-tabs"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import hoist from 'hoist-non-react-statics';
3+
import TabList from '../../../../components/TabList';
4+
5+
function TabListWrapper(props) {
6+
return <TabList {...props} />;
7+
}
8+
9+
export default hoist(TabListWrapper, TabList);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import hoist from 'hoist-non-react-statics';
3+
import TabPanel from '../../../../components/TabPanel';
4+
5+
function TabPanelWrapper(props) {
6+
return <TabPanel {...props} />;
7+
}
8+
9+
export default hoist(TabPanelWrapper, TabPanel);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import hoist from 'hoist-non-react-statics';
3+
import Tab from '../../../../components/Tab';
4+
5+
function TabWrapper(props) {
6+
return <Tab {...props} />;
7+
}
8+
9+
export default hoist(TabWrapper, Tab);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import TabWrapper from './TabWrapper';
2+
import TabListWrapper from './TabListWrapper';
3+
import TabPanelWrapper from './TabPanelWrapper';
4+
5+
export { TabWrapper, TabListWrapper, TabPanelWrapper };

0 commit comments

Comments
 (0)