fix(react): ensure tabs are resilient to optional tabs. (#17862)

This commit is contained in:
Josh Thomas
2019-03-22 14:45:40 -05:00
committed by GitHub
parent 424879df8e
commit c29f5a65b4
8 changed files with 117 additions and 9 deletions

View File

@ -0,0 +1,102 @@
import React, { ReactElement } from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history'
import { IonTabs, IonTab, IonTabBar, IonTabButton, IonLabel, IonIcon} from '../index';
import { render, cleanup } from 'react-testing-library';
afterEach(cleanup)
function renderWithRouter(
ui: ReactElement<any>,
{
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
} = {}
) {
return {
...render(<Router history={history}>{ui}</Router>),
history
}
}
describe('IonTabs', () => {
test('should render happy path', () => {
const { container } = renderWithRouter(
<IonTabs>
<IonTab tab="schedule">Schedule Content</IonTab>
<IonTab tab="speakers">Speakers Content</IonTab>
<IonTab tab="map">Map Content</IonTab>
<IonTab tab="about">About Content</IonTab>
<IonTabBar slot="bottom">
<IonTabButton tab="schedule">
<IonLabel>Schedule</IonLabel>
<IonIcon name="schedule"></IonIcon>
</IonTabButton>
<IonTabButton tab="speakers">
<IonLabel>Speakers</IonLabel>
<IonIcon name="speakers"></IonIcon>
</IonTabButton>
<IonTabButton tab="map">
<IonLabel>Map</IonLabel>
<IonIcon name="map"></IonIcon>
</IonTabButton>
<IonTabButton tab="about">
<IonLabel>About</IonLabel>
<IonIcon name="about"></IonIcon>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
expect(container.children[0].children.length).toEqual(2);
expect(container.children[0].children[0].tagName).toEqual('DIV');
expect(container.children[0].children[0].className).toEqual('tabs-inner');
expect(container.children[0].children[1].tagName).toEqual('ION-TAB-BAR');
expect(container.children[0].children[1].children.length).toEqual(4);
expect(Array.from(container.children[0].children[1].children).map(c => c.tagName)).toEqual(['ION-TAB-BUTTON', 'ION-TAB-BUTTON', 'ION-TAB-BUTTON', 'ION-TAB-BUTTON']);
});
test('should allow for conditional children', () => {
const { container } = renderWithRouter(
<IonTabs>
{false &&
<IonTab tab="schedule">Schedule Content</IonTab>
}
<IonTab tab="speakers">Speakers Content</IonTab>
<IonTab tab="map">Map Content</IonTab>
<IonTab tab="about">About Content</IonTab>
<IonTabBar slot="bottom">
{false &&
<IonTabButton tab="schedule">
<IonLabel>Schedule</IonLabel>
<IonIcon name="schedule"></IonIcon>
</IonTabButton>
}
<IonTabButton tab="speakers">
<IonLabel>Speakers</IonLabel>
<IonIcon name="speakers"></IonIcon>
</IonTabButton>
<IonTabButton tab="map">
<IonLabel>Map</IonLabel>
<IonIcon name="map"></IonIcon>
</IonTabButton>
<IonTabButton tab="about">
<IonLabel>About</IonLabel>
<IonIcon name="about"></IonIcon>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
expect(container.children[0].children.length).toEqual(2);
expect(container.children[0].children[0].tagName).toEqual('DIV');
expect(container.children[0].children[0].className).toEqual('tabs-inner');
expect(container.children[0].children[1].tagName).toEqual('ION-TAB-BAR');
expect(container.children[0].children[1].children.length).toEqual(3);
expect(Array.from(container.children[0].children[1].children).map(c => c.tagName)).toEqual(['ION-TAB-BUTTON', 'ION-TAB-BUTTON', 'ION-TAB-BUTTON']);
});
});

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Components } from '@ionic/core' import { Components } from '@ionic/core';
import { createReactComponent } from '../createComponent'; import { createReactComponent } from '../createComponent';
import { render, fireEvent, cleanup } from 'react-testing-library'; import { render, fireEvent, cleanup } from 'react-testing-library';

View File

@ -25,7 +25,7 @@ class IonTabBar extends Component<Props, State> {
const tabActiveUrls: { [key: string]: Tab } = {}; const tabActiveUrls: { [key: string]: Tab } = {};
React.Children.forEach(this.props.children, (child) => { React.Children.forEach(this.props.children, (child) => {
if (typeof child === 'object' && child.type === IonTabButton) { if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
tabActiveUrls[child.props.tab] = { tabActiveUrls[child.props.tab] = {
originalHref: child.props.href, originalHref: child.props.href,
currentHref: child.props.href currentHref: child.props.href
@ -72,12 +72,15 @@ class IonTabBar extends Component<Props, State> {
} }
renderChild = (activeTab: string) => (child: React.ReactElement<Components.IonTabButtonAttributes & { onIonTabButtonClick: (e: CustomEvent) => void }>) => { renderChild = (activeTab: string) => (child: React.ReactElement<Components.IonTabButtonAttributes & { onIonTabButtonClick: (e: CustomEvent) => void }>) => {
const href = (child.props.tab === activeTab) ? this.props.location.pathname : (this.state.tabs[child.props.tab].currentHref); if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
const href = (child.props.tab === activeTab) ? this.props.location.pathname : (this.state.tabs[child.props.tab].currentHref);
return React.cloneElement(child, { return React.cloneElement(child, {
href, href,
onIonTabButtonClick: this.onTabButtonClick onIonTabButtonClick: this.onTabButtonClick
}) });
}
return null;
} }
render() { render() {

View File

@ -32,10 +32,13 @@ export default class IonTabs extends Component<Props> {
let tabBar: React.ReactElement<{ slot: 'bottom' | 'top' }>; let tabBar: React.ReactElement<{ slot: 'bottom' | 'top' }>;
React.Children.forEach(this.props.children, child => { React.Children.forEach(this.props.children, child => {
if (typeof child === 'object' && child.type === IonRouterOutlet) { if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
return;
}
if (child.type === IonRouterOutlet) {
outlet = child; outlet = child;
} }
if (typeof child === 'object' && child.type === IonTabBar) { if (child.type === IonTabBar) {
tabBar = child; tabBar = child;
} }
}); });