Skip to content

Commit a413181

Browse files
committed
Fix focus and rename active -> selected
1 parent ad1cfa1 commit a413181

File tree

8 files changed

+125
-101
lines changed

8 files changed

+125
-101
lines changed

src/components/Tab.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@ import cx from 'classnames';
55
export default class Tab extends Component {
66

77
static defaultProps = {
8-
activeClassName: 'ReactTabs__Tab--selected',
98
className: 'ReactTabs__Tab',
109
disabledClassName: 'ReactTabs__Tab--disabled',
1110
focus: false,
1211
id: null,
1312
panelId: null,
1413
selected: false,
14+
selectedClassName: 'ReactTabs__Tab--selected',
1515
};
1616

1717
static propTypes = {
18-
activeClassName: PropTypes.string, // private
1918
children: PropTypes.oneOfType([
2019
PropTypes.array,
2120
PropTypes.object,
@@ -28,6 +27,7 @@ export default class Tab extends Component {
2827
id: PropTypes.string, // private
2928
panelId: PropTypes.string, // private
3029
selected: PropTypes.bool, // private
30+
selectedClassName: PropTypes.string, // private
3131
tabRef: PropTypes.func, // private
3232
};
3333

@@ -47,7 +47,6 @@ export default class Tab extends Component {
4747

4848
render() {
4949
const {
50-
activeClassName,
5150
children,
5251
className,
5352
disabled,
@@ -56,6 +55,7 @@ export default class Tab extends Component {
5655
id,
5756
panelId,
5857
selected,
58+
selectedClassName,
5959
tabRef,
6060
...attributes } = this.props;
6161

@@ -65,7 +65,7 @@ export default class Tab extends Component {
6565
className={cx(
6666
className,
6767
{
68-
[activeClassName]: selected,
68+
[selectedClassName]: selected,
6969
[disabledClassName]: disabled,
7070
},
7171
)}

src/components/TabPanel.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import cx from 'classnames';
55
export default class TabPanel extends Component {
66

77
static defaultProps = {
8-
activeClassName: 'ReactTabs__TabPanel--selected',
98
className: 'ReactTabs__TabPanel',
109
forceRender: false,
10+
selectedClassName: 'ReactTabs__TabPanel--selected',
1111
style: {},
1212
};
1313

1414
static propTypes = {
15-
activeClassName: PropTypes.string, // private
15+
selectedClassName: PropTypes.string, // private
1616
children: PropTypes.node,
1717
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
1818
forceRender: PropTypes.bool,
@@ -24,12 +24,12 @@ export default class TabPanel extends Component {
2424

2525
render() {
2626
const {
27-
activeClassName,
2827
children,
2928
className,
3029
forceRender,
3130
id,
3231
selected,
32+
selectedClassName,
3333
style,
3434
tabId,
3535
...attributes } = this.props;
@@ -40,7 +40,7 @@ export default class TabPanel extends Component {
4040
className={cx(
4141
className,
4242
{
43-
[activeClassName]: selected,
43+
[selectedClassName]: selected,
4444
},
4545
)}
4646
role="tabpanel"

src/components/Tabs.js

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export default class Tabs extends Component {
1414
};
1515

1616
static propTypes = {
17-
activeTabClassName: PropTypes.string,
18-
activeTabPanelClassName: PropTypes.string,
1917
children: childrenPropType,
2018
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
2119
defaultFocus: PropTypes.bool,
@@ -24,12 +22,14 @@ export default class Tabs extends Component {
2422
forceRenderTabPanel: PropTypes.bool,
2523
onSelect: onSelectPropType,
2624
selectedIndex: selectedIndexPropType,
25+
selectedTabClassName: PropTypes.string,
26+
selectedTabPanelClassName: PropTypes.string,
2727
};
2828

2929
constructor(props) {
3030
super(props);
3131

32-
this.state = Tabs.copyPropsToState(this.props, {});
32+
this.state = Tabs.copyPropsToState(this.props, {}, this.props.defaultFocus);
3333
}
3434

3535
componentWillReceiveProps(newProps) {
@@ -74,9 +74,9 @@ For more information about controlled and uncontrolled mode of react-tabs see th
7474

7575
// preserve the existing selectedIndex from state.
7676
// If the state has not selectedIndex, default to the defaultIndex or 0
77-
static copyPropsToState(props, state) {
77+
static copyPropsToState(props, state, focus = false) {
7878
const newState = {
79-
focus: state.focus || props.defaultFocus,
79+
focus,
8080
};
8181

8282
if (Tabs.inUncontrolledMode(props)) {
@@ -95,25 +95,6 @@ For more information about controlled and uncontrolled mode of react-tabs see th
9595
}
9696

9797
render() {
98-
// This fixes an issue with focus management.
99-
//
100-
// Ultimately, when focus is true, and an input has focus,
101-
// and any change on that input causes a state change/re-render,
102-
// focus gets sent back to the active tab, and input loses focus.
103-
//
104-
// Since the focus state only needs to be remembered
105-
// for the current render, we can reset it once the
106-
// render has happened.
107-
//
108-
// Don't use setState, because we don't want to re-render.
109-
//
110-
// See https://github.com/reactjs/react-tabs/pull/7
111-
if (this.state.focus) {
112-
setTimeout(() => {
113-
this.state.focus = false;
114-
}, 0);
115-
}
116-
11798
const { children, defaultIndex, defaultFocus, ...props } = this.props;
11899

119100
props.focus = this.state.focus;

src/components/UncontrolledTabs.js

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ export default class UncontrolledTabs extends Component {
2626
};
2727

2828
static propTypes = {
29-
activeTabClassName: PropTypes.string,
30-
activeTabPanelClassName: PropTypes.string,
3129
children: childrenPropType,
32-
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
30+
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
3331
disabledTabClassName: PropTypes.string,
3432
focus: PropTypes.bool,
3533
forceRenderTabPanel: PropTypes.bool,
3634
onSelect: PropTypes.func.isRequired,
3735
selectedIndex: PropTypes.number.isRequired,
36+
selectedTabClassName: PropTypes.string,
37+
selectedTabPanelClassName: PropTypes.string,
3838
};
3939

4040
tabNodes = [];
@@ -106,7 +106,16 @@ export default class UncontrolledTabs extends Component {
106106

107107
getChildren() {
108108
let index = 0;
109-
const children = this.props.children;
109+
const {
110+
children,
111+
disabledTabClassName,
112+
focus,
113+
forceRenderTabPanel,
114+
selectedIndex,
115+
selectedTabClassName,
116+
selectedTabPanelClassName,
117+
} = this.props;
118+
110119
this.tabIds = this.tabIds || [];
111120
this.panelIds = this.panelIds || [];
112121
let diff = this.tabIds.length - this.getTabsCount();
@@ -132,7 +141,13 @@ export default class UncontrolledTabs extends Component {
132141
// Clone TabList and Tab components to have refs
133142
if (child.type === TabList) {
134143
let listIndex = 0;
135-
// TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel`
144+
145+
// Figure out if the current focus in the DOM is set on a Tab
146+
// If it is we should keep the focus on the next selected tab
147+
const wasTabFocused = React.Children.toArray(child.props.children)
148+
.filter(tab => tab.type === Tab)
149+
.some((tab, i) => document.activeElement === this.getTab(i));
150+
136151
result = cloneElement(child, {
137152
children: React.Children.map(child.props.children, (tab) => {
138153
// null happens when conditionally rendering TabPanel/Tab
@@ -146,43 +161,37 @@ export default class UncontrolledTabs extends Component {
146161
if (tab.type !== Tab) return tab;
147162

148163
const key = `tabs-${listIndex}`;
149-
const tabRef = (node) => { this.tabNodes[key] = node; };
150-
const id = this.tabIds[listIndex];
151-
const panelId = this.panelIds[listIndex];
152-
const selected = this.props.selectedIndex === listIndex;
153-
const focus = selected && this.props.focus;
154-
const activeClassName = this.props.activeTabClassName;
155-
const disabledClassName = this.props.disabledTabClassName;
164+
const selected = selectedIndex === listIndex;
165+
166+
const props = {
167+
tabRef: (node) => { this.tabNodes[key] = node; },
168+
id: this.tabIds[listIndex],
169+
panelId: this.panelIds[listIndex],
170+
selected,
171+
focus: selected && (focus || wasTabFocused),
172+
};
173+
174+
if (selectedTabClassName) props.selectedClassName = selectedTabClassName;
175+
if (disabledTabClassName) props.disabledClassName = disabledTabClassName;
156176

157177
listIndex++;
158178

159-
return cloneElement(tab, {
160-
tabRef,
161-
id,
162-
panelId,
163-
selected,
164-
focus,
165-
activeClassName,
166-
disabledClassName,
167-
});
179+
return cloneElement(tab, props);
168180
}),
169181
});
170182
} else if (child.type === TabPanel) {
171-
const id = this.panelIds[index];
172-
const tabId = this.tabIds[index];
173-
const selected = this.props.selectedIndex === index;
174-
const forceRender = this.props.forceRenderTabPanel;
175-
const activeClassName = this.props.activeTabPanelClassName;
183+
const props = {
184+
id: this.panelIds[index],
185+
tabId: this.tabIds[index],
186+
selected: selectedIndex === index,
187+
};
188+
189+
if (forceRenderTabPanel) props.forceRender = forceRenderTabPanel;
190+
if (selectedTabPanelClassName) props.selectedClassName = selectedTabPanelClassName;
176191

177192
index++;
178193

179-
result = cloneElement(child, {
180-
id,
181-
tabId,
182-
selected,
183-
forceRender,
184-
activeClassName,
185-
});
194+
result = cloneElement(child, props);
186195
}
187196

188197
return result;
@@ -254,12 +263,15 @@ export default class UncontrolledTabs extends Component {
254263
render() {
255264
// Delete all known props, so they don't get added to DOM
256265
const {
266+
children,
257267
className,
258-
selectedIndex,
259-
onSelect,
268+
disabledTabClassName,
260269
focus,
261-
children,
262270
forceRenderTabPanel,
271+
onSelect,
272+
selectedIndex,
273+
selectedTabClassName,
274+
selectedTabPanelClassName,
263275
...attributes
264276
} = this.props;
265277

src/components/__tests__/Tab-test.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('<Tab />', () => {
2626
it('should accept className', () => {
2727
const wrapper = shallow(<Tab className="foobar" />);
2828

29-
expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true);
29+
expect(wrapper.hasClass('ReactTabs__Tab')).toBe(false);
3030
expect(wrapper.hasClass('foobar')).toBe(true);
3131
});
3232

@@ -42,6 +42,15 @@ describe('<Tab />', () => {
4242
expect(wrapper.text()).toBe('Hello');
4343
});
4444

45+
it('should support being selected with custom class', () => {
46+
const wrapper = shallow(<Tab selected selectedClassName="cool" />);
47+
48+
expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true);
49+
expect(wrapper.hasClass('ReactTabs__Tab--selected')).toBe(false);
50+
expect(wrapper.hasClass('cool')).toBe(true);
51+
expect(wrapper.prop('aria-selected')).toBe('true');
52+
});
53+
4554
it('should support being disabled', () => {
4655
const wrapper = shallow(<Tab disabled />);
4756

@@ -50,6 +59,15 @@ describe('<Tab />', () => {
5059
expect(wrapper.prop('aria-disabled')).toBe('true');
5160
});
5261

62+
it('should support being disabled with custom class name', () => {
63+
const wrapper = shallow(<Tab disabled disabledClassName="coolDisabled" />);
64+
65+
expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true);
66+
expect(wrapper.hasClass('ReactTabs__Tab--disabled')).toBe(false);
67+
expect(wrapper.hasClass('coolDisabled')).toBe(true);
68+
expect(wrapper.prop('aria-disabled')).toBe('true');
69+
});
70+
5371
it('should pass through custom properties', () => {
5472
const wrapper = shallow(<Tab data-tooltip="Tooltip contents" />);
5573

src/components/__tests__/TabList-test.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('<TabList />', () => {
2828
it('should accept className', () => {
2929
const wrapper = shallow(<TabList className="foobar" />);
3030

31-
expect(wrapper.hasClass('ReactTabs__TabList')).toBe(true);
31+
expect(wrapper.hasClass('ReactTabs__TabList')).toBe(false);
3232
expect(wrapper.hasClass('foobar')).toBe(true);
3333
});
3434

@@ -62,10 +62,10 @@ describe('<TabList />', () => {
6262
expect(hasClassAt(tabsList, 1, 'ReactTabs__Tab--disabled')).toBe(true);
6363
});
6464

65-
it('should display the custom classnames for active and disabled tab', () => {
65+
it('should display the custom classnames for selected and disabled tab specified on tabs', () => {
6666
const wrapper = mount(
67-
<Tabs defaultIndex={0}>
68-
<TabList activeTabClassName="active" disabledTabClassName="disabled">
67+
<Tabs defaultIndex={0} selectedTabClassName="active" disabledTabClassName="disabled">
68+
<TabList>
6969
<Tab>Foo</Tab>
7070
<Tab disabled>Bar</Tab>
7171
</TabList>
@@ -81,4 +81,24 @@ describe('<TabList />', () => {
8181
expect(hasClassAt(tabsList, 0, 'active')).toBe(true);
8282
expect(hasClassAt(tabsList, 1, 'disabled')).toBe(true);
8383
});
84+
85+
it('should display the custom classnames for selected and disabled tab', () => {
86+
const wrapper = mount(
87+
<Tabs defaultIndex={0}>
88+
<TabList>
89+
<Tab selectedClassName="active" disabledClassName="disabled">Foo</Tab>
90+
<Tab disabled selectedClassName="active" disabledClassName="disabled">Bar</Tab>
91+
</TabList>
92+
<TabPanel>Foo</TabPanel>
93+
<TabPanel>Bar</TabPanel>
94+
</Tabs>,
95+
);
96+
97+
const tabsList = wrapper.childAt(0);
98+
expect(hasClassAt(tabsList, 0, 'ReactTabs__Tab--selected')).toBe(false);
99+
expect(hasClassAt(tabsList, 1, 'ReactTabs__Tab--disabled')).toBe(false);
100+
101+
expect(hasClassAt(tabsList, 0, 'active')).toBe(true);
102+
expect(hasClassAt(tabsList, 1, 'disabled')).toBe(true);
103+
});
84104
});

0 commit comments

Comments
 (0)