Skip to content

Commit

Permalink
Issue #23 Bug Fix (#25)
Browse files Browse the repository at this point in the history
Fixing of bug found when adding tabs dynamically the user is unable to focus the first tab. During fix, also found bug when the focused tab is changed via state without clicking/focusing a tab, the previously focused tab cannot be clicked. All fixes revolved around needing to set the active tab via the manager when these changes happen.
  • Loading branch information
driponfleek authored and davidtheclark committed May 29, 2018
1 parent fc7ccad commit 21012b2
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,8 @@
## HEAD

- Add `role` prop to `TabList`, `Tab`, and `TabPanel`.
- Added support for controlling tab state externally.
- Added support for dynamically adding and removing tabs.

## 4.3.0
- Use `prop-types` and `create-react-class` packages for compatibility with newer versions of React.
Expand Down
11 changes: 11 additions & 0 deletions demo/index.html
Expand Up @@ -56,6 +56,17 @@ <h2>
<div id="stateless-demo"></div>
</div>

<h2>
Dynamic Tabs demo
</h2>
<p>
The following is an example of adding/removing tabs dynamically and setting the active tab programmatically via a wrapper component's state. Similar to the Stateless Demo, it does not allow letter-key navigation.
</p>

<div style="margin-bottom: 2em;">
<div id="dynamic-tabs-demo"></div>
</div>

<h2>
Fancier Demo
</h2>
Expand Down
1 change: 1 addition & 0 deletions demo/js/demo.js
@@ -1,3 +1,4 @@
require('./statefulDemo');
require('./statelessDemo');
require('./dynamicTabsDemo');
require('./fancyDemo');
213 changes: 213 additions & 0 deletions demo/js/dynamicTabsDemo.jsx
@@ -0,0 +1,213 @@
import React from 'react';
import ReactDOM from 'react-dom';
import AriaTabPanel from '../..';

const uniqueId = function() {
return 'id-' + Math.random().toString(36).substr(2, 16);
};

const tabTitles = [
'I am a tab',
'Tabs are Cool!',
'New Tab Here',
'Just Another Tab',
'Doing Tab Things',
];

const tabsData = [
{
title: 'one',
id: uniqueId(),
content: (
<div>
Lorem <a href='#'>ipsum</a> dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</div>
),
},
{
title: 'two',
id: uniqueId(),
content: (
<div>
Ut <a href='#'>enim</a> ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</div>
),
},
{
title: 'three',
id: uniqueId(),
content: (
<div>
Duis <a href='#'>aute</a> irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
),
},
];

class DynamicTabsDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
activeTab: tabsData[1].id,
tabDescriptions: tabsData,
};
}

setTab(newActiveTabId) {
this.setState({ activeTab: newActiveTabId });
}

render() {
const { activeTab, tabDescriptions } = this.state;
const tabs = tabDescriptions.map((tabDescription) => this.renderTab(tabDescription));
const panels = tabDescriptions.map((tabDescription) => this.renderTabContent(tabDescription));
const hasMultipleTabs = tabDescriptions.length > 1;

return (
<div>
<AriaTabPanel.Wrapper
onChange={this.setTab.bind(this)}
activeTabId={this.state.activeTab}
>
<AriaTabPanel.TabList>
<ul className='Tabs-tablist Tabs-dynamic-tablist'>
{tabs}
</ul>
</AriaTabPanel.TabList>
<div className='Tabs-panel'>
{panels}
</div>
</AriaTabPanel.Wrapper>
<div style={ { marginTop: '15px' } }>
<button
className="dynamic-tabs__add-tab-btn"
onClick={ this.handleAddNewTabClick.bind(this) }
>
Add a New Tab
</button>
{' '}
<button
className="dynamic-tabs__remove-tab-btn"
disabled={ !hasMultipleTabs }
onClick={ this.handleRemoveRandomTabClick.bind(this) }
>
Remove a Random Tab
</button>
{' '}
<button
className="dynamic-tabs__change-active-tab-btn"
disabled={ !hasMultipleTabs }
onClick={ this.handleChangeActiveTabClick.bind(this) }
>
Change Active Tab
</button>
</div>
</div>
);
}

renderTab(tabDescription) {
const { activeTab } = this.state;
let innerCl = 'Tabs-tabInner';

if (tabDescription.id === activeTab) innerCl += ' is-active';

return (
<li className='Tabs-tablistItem' key={ tabDescription.id }>
<AriaTabPanel.Tab
id={tabDescription.id}
className='Tabs-tab'
active={tabDescription.id === activeTab}
>
<div className={innerCl}>
<span className="Tabs-tabInner-text">
{tabDescription.title}
</span>
</div>
</AriaTabPanel.Tab>
</li>
);
}

renderTabContent(tabDescription) {
const { activeTab } = this.state;

return (
<AriaTabPanel.TabPanel
key={ tabDescription.id }
tabId={tabDescription.id}
active={tabDescription.id === activeTab}
>
{tabDescription.content}
</AriaTabPanel.TabPanel>
);
}

handleAddNewTabClick() {
const { tabDescriptions } = this.state;
const tabsData = tabDescriptions.slice();
const tabId = uniqueId();
const newTab = {
title: this.generateTabTitle(),
id: tabId,
content: this.generateTabContent(),
};

tabsData.push(newTab);

this.setState({
activeTab: tabId,
tabDescriptions: tabsData,
});
}

handleRemoveRandomTabClick() {
const { tabDescriptions, activeTab } = this.state;
const newState = {};
const tabsData = tabDescriptions.slice();
const tabToRemoveIdx = Math.floor(Math.random() * tabDescriptions.length);
const tabToRemove = tabDescriptions[tabToRemoveIdx];
const isActiveTab = tabToRemove.id === activeTab;
const nextActiveTabId = tabDescriptions[(tabToRemoveIdx === 0) ? 1 : (tabToRemoveIdx - 1)].id;

tabsData.splice(tabToRemoveIdx, 1);
newState.tabDescriptions = tabsData;

if (isActiveTab) {
newState.activeTab = nextActiveTabId;
}

this.setState(newState);
}

handleChangeActiveTabClick() {
const { tabDescriptions, activeTab } = this.state;
const activeTabIdx = tabDescriptions.findIndex((tabDescription) => tabDescription.id === activeTab);
let newActiveTabIdx = Math.floor(Math.random() * tabDescriptions.length);

do {
if (newActiveTabIdx === activeTabIdx) {
newActiveTabIdx = Math.floor(Math.random() * tabDescriptions.length);
}
} while (newActiveTabIdx === activeTabIdx);

const newActiveTabId = tabDescriptions[newActiveTabIdx].id;

this.setState({
activeTab: newActiveTabId,
});
}

generateTabContent() {
return tabsData[Math.floor(Math.random() * tabsData.length)].content;
}

generateTabTitle() {
return tabTitles[Math.floor(Math.random() * tabTitles.length)];
}
}

ReactDOM.render(
<DynamicTabsDemo />,
document.getElementById('dynamic-tabs-demo')
);
50 changes: 50 additions & 0 deletions demo/tabStyle.css
Expand Up @@ -49,6 +49,56 @@
right: 0;
}

/* Dynamic */
.Tabs-tablist.Tabs-dynamic-tablist {
width: 100%;
display: flex;
}

.Tabs-tablist.Tabs-dynamic-tablist
.Tabs-tabInner-text {
white-space: nowrap;
text-overflow: ellipsis;
display: block;
overflow: hidden;
}

.Tabs-tablist.Tabs-dynamic-tablist
.Tabs-tablistItem {
min-width: 10px;
flex: 0 1 auto;
}

.dynamic-tabs__add-tab-btn,
.dynamic-tabs__remove-tab-btn,
.dynamic-tabs__change-active-tab-btn {
font-size: 13px;
font-weight: bold;
border-radius: 3px;
padding: 10px;
}

.dynamic-tabs__add-tab-btn:disabled,
.dynamic-tabs__remove-tab-btn:disabled,
.dynamic-tabs__change-active-tab-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.dynamic-tabs__add-tab-btn {
color: #fff;
background-color: #1976d2;
}

.dynamic-tabs__remove-tab-btn {
color: #fff;
background-color: #d32f2f;
}

.dynamic-tabs__change-active-tab-btn {
color: #263238;
}

/* Fancy */

.FancyTabs-tablist {
Expand Down
19 changes: 18 additions & 1 deletion lib/Tab.js
Expand Up @@ -39,6 +39,13 @@ module.exports = createReactClass({
this.context.atpManager.handleTabFocus(this.props.id);
},

handleRef: function(el) {
if (el) {
this.elRef = el;
this.registerWithManager(this.elRef);
}
},

updateActiveState: function(nextActiveState) {
this.setState({ isActive: nextActiveState });
},
Expand All @@ -52,9 +59,15 @@ module.exports = createReactClass({
update: this.updateActiveState,
index: this.props.index,
letterNavigationText: this.props.letterNavigationText,
active: (this.props.active === undefined) ? this.state.isActive : this.props.active,
});
},

unregisterWithManager: function() {
var props = this.props;
this.context.atpManager.unregisterTab(props.id);
},

render: function() {
var props = this.props;
var isActive = (props.active === undefined) ? this.state.isActive : props.active;
Expand All @@ -74,10 +87,14 @@ module.exports = createReactClass({
role: props.role,
'aria-selected': isActive,
'aria-controls': this.context.atpManager.getTabPanelId(props.id),
ref: this.registerWithManager,
ref: this.handleRef,
};
specialAssign(elProps, props, checkedProps);

return React.createElement(props.tag, elProps, kids);
},

componentWillUnmount: function() {
this.unregisterWithManager();
},
});
8 changes: 8 additions & 0 deletions lib/Wrapper.js
Expand Up @@ -45,6 +45,14 @@ module.exports = createReactClass({
this.manager.activate();
},

componentDidUpdate: function(prevProps) {
var updateActiveTab = (prevProps.activeTabId === this.manager.activeTabId) && (prevProps.activeTabId !== this.props.activeTabId);

if (updateActiveTab) {
this.manager.activateTab(this.props.activeTabId);
}
},

render: function() {
var props = this.props;
var elProps = {};
Expand Down
26 changes: 25 additions & 1 deletion lib/createManager.js
Expand Up @@ -49,10 +49,34 @@ Manager.prototype.registerTab = function(tabMember) {
} : tabMember.node;

this.focusGroup.addMember(focusGroupMember, tabMember.index);
var activeTabId = this.activeTabId;

this.activateTab(this.activeTabId || tabMember.id);
if (!this.activeTabId || (tabMember.active && (tabMember.id !== this.activeTabId))) {
activeTabId = tabMember.id;
}

this.activateTab(activeTabId);
};

Manager.prototype.unregisterTab = function(tabId) {
var tabIdx;
var tab;

if (this.tabs && this.tabs.length > 0) {
this.tabs.forEach(function(tabMember, idx) {
if (tabMember.id === tabId) {
tabIdx = idx;
tab = tabMember;
}
});

if (tab && tab.node) {
this.tabs.splice(tabIdx, 1);
this.focusGroup.removeMember(tab.node)
}
}
}

Manager.prototype.registerTabPanel = function(tabPanelMember) {
this.tabPanels.push(tabPanelMember);
this.activateTab(this.activeTabId);
Expand Down

0 comments on commit 21012b2

Please sign in to comment.