Skip to content

Commit

Permalink
Response to feedback from @zburke
Browse files Browse the repository at this point in the history
This commit responds to the first two points made by @zburke in the PR
feedback:

- Change CSS import to use local path
- Split into separate components
  • Loading branch information
Andrew Isherwood committed Feb 4, 2022
1 parent f751e44 commit 5d9a980
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 162 deletions.
62 changes: 62 additions & 0 deletions lib/Tabs/Tab.js
@@ -0,0 +1,62 @@
import React, { useContext, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';

import { TabsContext } from './TabsContext';

import css from './Tabs.css';

const Tab = (props) => {
const {
children,
index
} = props;

const thisTab = useRef(null);

const {
selectedTabIndex,
setSelectedTabIndex
} = useContext(TabsContext);

// Ensure the correct tab has focus
useEffect(() => {
if (selectedTabIndex === index) {
thisTab.current.focus();
}
// Having index as a dep makes no sense, it's never
// going to change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTabIndex]);

const activeStyle = selectedTabIndex === index ? css.primary : css.default;
const finalStyles = [css.tab, activeStyle].join(' ');

return (
// Keyboard based interactivity with the tabs is handled in TabList
// so we don't need a onKey* handler here
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
ref={thisTab}
tabIndex={selectedTabIndex === index ? 0 : -1}
onClick={() => setSelectedTabIndex(index)}
aria-selected={selectedTabIndex === index}
aria-controls={`tabpanel-${index}`}
className={finalStyles}
id={`tab-${index}`}
role="tab"
>
{children}
</li>
);
};

Tab.propTypes = {
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array,
PropTypes.string
]),
index: PropTypes.number
};

export default Tab;
75 changes: 75 additions & 0 deletions lib/Tabs/TabList.js
@@ -0,0 +1,75 @@
import React, { useContext, cloneElement } from 'react';
import PropTypes from 'prop-types';

import { TabsContext } from './TabsContext';

import css from './Tabs.css';

const TabList = (props) => {
const {
ariaLabel,
children
} = props;

const {
selectedTabIndex,
setSelectedTabIndex
} = useContext(TabsContext);

// Add the index to each child, which will allow us to ensure the current
// tab is styled correctly and has focus etc.
const childrenArray = Array.isArray(children) ? children : [children];
const childrenWithIndex = childrenArray.map((child, index) => cloneElement(child, { index, key: index }));

// Handle setting of the next index when navigating
// by keyboard
const calculateNextIndex = (action) => {
if (action === 'increase') {
const maxIndex = children.length - 1;
return selectedTabIndex < maxIndex ?
selectedTabIndex + 1 :
selectedTabIndex;
}
if (action === 'decrease') {
return selectedTabIndex > 0 ?
selectedTabIndex - 1 :
selectedTabIndex;
}
return selectedTabIndex;
};

// Handle the right and left cursor keys for navigating
// via keyboard.
const handleKeyDown = (e) => {
switch (e.keyCode) {
case 39: // Right arrow
setSelectedTabIndex(calculateNextIndex('increase'));
break;
case 37: // Left arrow
setSelectedTabIndex(calculateNextIndex('decrease'));
break;
default:
}
};

return (
<ul
onKeyDown={handleKeyDown}
aria-label={ariaLabel}
className={css.tabList}
role="tablist"
>
{childrenWithIndex}
</ul>
);
};

TabList.propTypes = {
ariaLabel: PropTypes.string.isRequired,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array
])
};

export default TabList;
35 changes: 35 additions & 0 deletions lib/Tabs/TabPanel.js
@@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';

import { TabsContext } from './TabsContext';

import css from './Tabs.css';

const TabPanel = (props) => {
const {
children,
index
} = props;

const { selectedTabIndex } = useContext(TabsContext);

return selectedTabIndex === index && (
<div
tabIndex={selectedTabIndex === index ? 0 : -1}
id={`tabpanel-${index}`}
className={css.tabPanel}
role="tabpanel"
>
{children}
</div>
);
};

TabPanel.propTypes = {
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string
]),
index: PropTypes.number
};
export default TabPanel;
2 changes: 1 addition & 1 deletion lib/Tabs/Tabs.css
@@ -1,4 +1,4 @@
@import '@folio/stripes-components/lib/variables';
@import '../variables.css';

ul {
margin: 0;
Expand Down
162 changes: 3 additions & 159 deletions lib/Tabs/Tabs.js
@@ -1,9 +1,7 @@
import React, { useContext, cloneElement, useEffect, useRef } from 'react';
import React, { cloneElement } from 'react';
import PropTypes from 'prop-types';

import { TabsContext, TabsContextProvider } from './TabsContext';

import css from './Tabs.css';
import { TabsContextProvider } from './TabsContext';

const Tabs = (props) => {
const { children } = props;
Expand Down Expand Up @@ -38,158 +36,4 @@ Tabs.propTypes = {
])
};

const TabList = (props) => {
const {
ariaLabel,
children
} = props;

const {
selectedTabIndex,
setSelectedTabIndex
} = useContext(TabsContext);

// Add the index to each child, which will allow us to ensure the current
// tab is styled correctly and has focus etc.
const childrenArray = Array.isArray(children) ? children : [children];
const childrenWithIndex = childrenArray.map((child, index) => cloneElement(child, { index, key: index }));

// Handle setting of the next index when navigating
// by keyboard
const calculateNextIndex = (action) => {
if (action === 'increase') {
const maxIndex = children.length - 1;
return selectedTabIndex < maxIndex ?
selectedTabIndex + 1 :
selectedTabIndex;
}
if (action === 'decrease') {
return selectedTabIndex > 0 ?
selectedTabIndex - 1 :
selectedTabIndex;
}
return selectedTabIndex;
};

// Handle the right and left cursor keys for navigating
// via keyboard.
const handleKeyDown = (e) => {
switch (e.keyCode) {
case 39: // Right arrow
setSelectedTabIndex(calculateNextIndex('increase'));
break;
case 37: // Left arrow
setSelectedTabIndex(calculateNextIndex('decrease'));
break;
default:
}
};

return (
<ul
onKeyDown={handleKeyDown}
aria-label={ariaLabel}
className={css.tabList}
role="tablist"
>
{childrenWithIndex}
</ul>
);
};

TabList.propTypes = {
ariaLabel: PropTypes.string.isRequired,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array
])
};

const Tab = (props) => {
const {
children,
index
} = props;

const thisTab = useRef(null);

const {
selectedTabIndex,
setSelectedTabIndex
} = useContext(TabsContext);

// Ensure the correct tab has focus
useEffect(() => {
if (selectedTabIndex === index) {
thisTab.current.focus();
}
// Having index as a dep makes no sense, it's never
// going to change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTabIndex]);

const activeStyle = selectedTabIndex === index ? css.primary : css.default;
const finalStyles = [css.tab, activeStyle].join(' ');

return (
// Keyboard based interactivity with the tabs is handled in TabList
// so we don't need a onKey* handler here
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
ref={thisTab}
tabIndex={selectedTabIndex === index ? 0 : -1}
onClick={() => setSelectedTabIndex(index)}
aria-selected={selectedTabIndex === index}
aria-controls={`tabpanel-${index}`}
className={finalStyles}
id={`tab-${index}`}
role="tab"
>
{children}
</li>
);
};

Tab.propTypes = {
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array,
PropTypes.string
]),
index: PropTypes.number
};

const TabPanel = (props) => {
const {
children,
index
} = props;

const { selectedTabIndex } = useContext(TabsContext);

return selectedTabIndex === index && (
<div
tabIndex={selectedTabIndex === index ? 0 : -1}
id={`tabpanel-${index}`}
className={css.tabPanel}
role="tabpanel"
>
{children}
</div>
);
};

TabPanel.propTypes = {
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string
]),
index: PropTypes.number
};

export {
TabList,
Tabs,
Tab,
TabPanel
};
export default Tabs;
5 changes: 4 additions & 1 deletion lib/Tabs/index.js
@@ -1 +1,4 @@
export { Tabs, TabList, Tab, TabPanel } from './Tabs';
export { default as Tabs } from './Tabs';
export { default as TabList } from './TabList';
export { default as Tab } from './Tab';
export { default as TabPanel } from './TabPanel';
6 changes: 5 additions & 1 deletion lib/Tabs/tests/Tabs-test.js
Expand Up @@ -14,7 +14,11 @@ import {
} from '@folio/stripes-testing';

import { mount } from '../../../tests/helpers';
import { Tabs, Tab, TabList, TabPanel } from '../Tabs';

import Tabs from '../Tabs';
import TabList from '../TabList';
import Tab from '../Tab';
import TabPanel from '../TabPanel';

const doMount = () => {
return mount(
Expand Down

0 comments on commit 5d9a980

Please sign in to comment.