Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,80 @@ class SimpleTabs extends React.Component {
}
```

## Simple tabs with children that mount on tab click
```js
import React from 'react';
import { Tabs, Tab, TabsVariant, TabContent } from '@patternfly/react-core';

class MountingSimpleTabs extends React.Component {
constructor(props) {
super(props);
this.state = {
activeTabKey: 0
};
// Toggle currently active tab
this.handleTabClick = (event, tabIndex) => {
this.setState({
activeTabKey: tabIndex
});
};
}

render() {
return (
<Tabs mountOnEnter activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}>
<Tab eventKey={0} title="Tab item 1">
Tab 1 section
</Tab>
<Tab eventKey={1} title="Tab item 2">
Tab 2 section
</Tab>
<Tab eventKey={2} title="Tab item 3">
Tab 3 section
</Tab>
</Tabs>
);
}
}
```

## Simple tabs with children that unmount when they're no longer visible
```js
import React from 'react';
import { Tabs, Tab, TabsVariant, TabContent } from '@patternfly/react-core';

class UnmountingSimpleTabs extends React.Component {
constructor(props) {
super(props);
this.state = {
activeTabKey: 0
};
// Toggle currently active tab
this.handleTabClick = (event, tabIndex) => {
this.setState({
activeTabKey: tabIndex
});
};
}

render() {
return (
<Tabs unmountOnExit activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}>
<Tab eventKey={0} title="Tab item 1">
Tab 1 section
</Tab>
<Tab eventKey={1} title="Tab item 2">
Tab 2 section
</Tab>
<Tab eventKey={2} title="Tab item 3">
Tab 3 section
</Tab>
</Tabs>
);
}
}
```

## Scroll buttons primary tabs

```js
Expand Down
42 changes: 32 additions & 10 deletions packages/patternfly-4/react-core/src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { Omit } from '../../helpers/typeUtils';
import { AngleLeftIcon, AngleRightIcon } from '@patternfly/react-icons';
import { getUniqueId, isElementInView, sideElementIsOutOfView } from '../../helpers/util';
import { SIDE } from '../../helpers/constants';
import { TabContent } from './TabContent';
import { Tab } from './Tab';
import { TabContent } from './TabContent';
import { InjectedOuiaProps, withOuiaContext } from '../withOuia';

export enum TabsVariant {
Expand Down Expand Up @@ -38,13 +38,18 @@ export interface TabsProps extends Omit<React.HTMLProps<HTMLElement | HTMLDivEle
variant?: 'div' | 'nav';
/** provides an accessible label for the Tabs. Labels should be unique for each set of Tabs that are present on a page. When variant is set to nav, this prop should be defined to differentiate the Tabs from other navigation regions on the page. */
'aria-label'?: string;
/** waits until the first "enter" transition to mount tab children (add them to the DOM) */
mountOnEnter?: boolean;
/** unmounts tab children (removes them from the DOM) when they are no longer visible */
unmountOnExit?: boolean;
}

export interface TabsState {
showLeftScrollButton: boolean;
showRightScrollButton: boolean;
highlightLeftScrollButton: boolean;
highlightRightScrollButton: boolean;
shownKeys: (string|number)[];
}

class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
Expand All @@ -55,7 +60,8 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
showLeftScrollButton: false,
showRightScrollButton: false,
highlightLeftScrollButton: false,
highlightRightScrollButton: false
highlightRightScrollButton: false,
shownKeys: [this.props.activeKey] // only for mountOnEnter case
};
}

Expand All @@ -67,14 +73,18 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
isSecondary: false,
leftScrollAriaLabel: 'Scroll left',
rightScrollAriaLabel: 'Scroll right',
variant: TabsVariant.div
variant: TabsVariant.div,
mountOnEnter: false,
unmountOnExit: false,
};

handleTabClick(
event: React.MouseEvent<HTMLElement, MouseEvent>,
eventKey: number,
tabContentRef: React.RefObject<any>
tabContentRef: React.RefObject<any>,
mountOnEnter: boolean
) {
const { shownKeys } = this.state;
this.props.onSelect(event, eventKey);
// process any tab content sections outside of the component
if (tabContentRef) {
Expand All @@ -88,6 +98,11 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
setTimeout(() => {
this.handleScrollButtons();
}, 1);
if (mountOnEnter) {
this.setState({
shownKeys: shownKeys.concat(eventKey)
});
}
}

handleScrollButtons = () => {
Expand Down Expand Up @@ -184,13 +199,16 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
variant,
ouiaContext,
ouiaId,
mountOnEnter,
unmountOnExit,
...props
} = this.props;
const {
showLeftScrollButton,
showRightScrollButton,
highlightLeftScrollButton,
highlightRightScrollButton
highlightRightScrollButton,
shownKeys
} = this.state;

const uniqueId = id || getUniqueId();
Expand All @@ -214,6 +232,7 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
'data-ouia-component-type': 'Tabs',
'data-ouia-component-id': ouiaId || ouiaContext.ouiaId
}}
id={id && id}
{...props}
>
<button
Expand All @@ -233,7 +252,7 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
>
<Tab
className={css(styles.tabsButton)}
onClick={(event: any) => this.handleTabClick(event, eventKey, tabContentRef)}
onClick={(event: any) => this.handleTabClick(event, eventKey, tabContentRef, mountOnEnter)}
id={`pf-tab-${eventKey}-${childId || uniqueId}`}
aria-controls={
tabContentId ? `${tabContentId}` : `pf-tab-section-${eventKey}-${childId || uniqueId}`
Expand All @@ -257,10 +276,13 @@ class Tabs extends React.Component<TabsProps & InjectedOuiaProps, TabsState> {
<AngleRightIcon />
</button>
</Component>
{React.Children.map(children, (child: any, index) =>
!child.props.children ? null : (
<TabContent key={index} activeKey={activeKey} child={child} id={child.props.id || uniqueId} />
)
{React.Children.map(children, (child: any, index) => {
if (!child.props.children || (unmountOnExit && child.props.eventKey !== activeKey) || (mountOnEnter && shownKeys.indexOf(child.props.eventKey) === -1)) {
return null;
Comment thread
rebeccaalpert marked this conversation as resolved.
} else {
return <TabContent key={index} activeKey={activeKey} child={child} id={child.props.id || uniqueId} />;
}
}
)}
</React.Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ exports[`should call handleScrollButtons tabs with scrolls 1`] = `
isFilled={true}
isSecondary={false}
leftScrollAriaLabel="Scroll left"
mountOnEnter={false}
onSelect={[Function]}
ouiaContext={
Object {
Expand All @@ -53,10 +54,12 @@ exports[`should call handleScrollButtons tabs with scrolls 1`] = `
}
}
rightScrollAriaLabel="Scroll right"
unmountOnExit={false}
variant="div"
>
<div
className="pf-c-tabs pf-m-fill"
id="handleScrollButtons"
onSelect={[Function]}
>
<button
Expand Down Expand Up @@ -413,6 +416,7 @@ exports[`should call scrollLeft tabs with scrolls 1`] = `
isFilled={true}
isSecondary={false}
leftScrollAriaLabel="Scroll left"
mountOnEnter={false}
onSelect={[Function]}
ouiaContext={
Object {
Expand All @@ -421,10 +425,12 @@ exports[`should call scrollLeft tabs with scrolls 1`] = `
}
}
rightScrollAriaLabel="Scroll right"
unmountOnExit={false}
variant="div"
>
<div
className="pf-c-tabs pf-m-fill"
id="scrollLeft"
onSelect={[Function]}
>
<button
Expand Down Expand Up @@ -781,6 +787,7 @@ exports[`should call scrollRight tabs with scrolls 1`] = `
isFilled={true}
isSecondary={false}
leftScrollAriaLabel="Scroll left"
mountOnEnter={false}
onSelect={[Function]}
ouiaContext={
Object {
Expand All @@ -789,10 +796,12 @@ exports[`should call scrollRight tabs with scrolls 1`] = `
}
}
rightScrollAriaLabel="Scroll right"
unmountOnExit={false}
variant="div"
>
<div
className="pf-c-tabs pf-m-fill"
id="scrollRight"
onSelect={[Function]}
>
<button
Expand Down Expand Up @@ -1109,6 +1118,7 @@ Array [
<nav
aria-label="accessible Tabs example"
class="pf-c-tabs"
id="accessibleTabs"
>
<button
aria-label="Scroll left"
Expand Down Expand Up @@ -1225,6 +1235,7 @@ exports[`should render filled tabs 1`] = `
Array [
<div
class="pf-c-tabs pf-m-fill"
id="filledTabs"
>
<button
aria-label="Scroll left"
Expand Down Expand Up @@ -1338,6 +1349,7 @@ exports[`should render secondary tabs 1`] = `
Array [
<div
class="pf-c-tabs"
id="primarieTabs"
>
<button
aria-label="Scroll left"
Expand Down Expand Up @@ -1424,6 +1436,7 @@ Array [
>
<div
class="pf-c-tabs"
id="secondaryTabs"
>
<button
aria-label="Scroll left"
Expand Down Expand Up @@ -1559,6 +1572,7 @@ exports[`should render simple tabs 1`] = `
Array [
<div
class="pf-c-tabs"
id="simpleTabs"
>
<button
aria-label="Scroll left"
Expand Down Expand Up @@ -1698,6 +1712,7 @@ exports[`should render tabs with eventKey Strings 1`] = `
Array [
<div
class="pf-c-tabs"
id="eventKeyTabs"
>
<button
aria-label="Scroll left"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ describe('Tab Demo Test', () => {
it('Navigate to demo section', () => {
cy.visit('http://localhost:3000/');
cy.get('#tab-demo-nav-item-link').click();
cy.url().should('eq', 'http://localhost:3000/tab-demo-nav-link')
cy.url().should('eq', 'http://localhost:3000/tab-demo-nav-link')
});

it('Verify tabs, tab sections, and tab navigation', () => {
cy.get('.pf-c-tabs__button').each((demoButton: JQuery<HTMLButtonElement>, index: number) => {
cy.get('div#unconnectedChildren').find('.pf-c-tabs__button').each((demoButton: JQuery<HTMLButtonElement>, index: number) => {
const currentItem: number = index + 1;
expect(demoButton.text()).to.equal(`Tab item ${currentItem}`);
cy.wrap(demoButton).click();
Expand All @@ -16,4 +16,20 @@ describe('Tab Demo Test', () => {
});
});
});

it('Verify tabs mount on enter when specified', () => {
cy.get('#pf-tab-section-0-mountOnEnter').should('exist');
cy.get('#pf-tab-section-1-mountOnEnter').should('not.exist');
cy.get('#pf-tab-1-mountOnEnter').click();
cy.get('#pf-tab-section-0-mountOnEnter').should('exist');
cy.get('#pf-tab-section-1-mountOnEnter').should('exist');
});

it('Verify tabs unmount on exit when specified', () => {
cy.get('#pf-tab-section-0-unmountOnExit').should('exist');
cy.get('#pf-tab-section-1-unmountOnExit').should('not.exist');
cy.get('#pf-tab-1-unmountOnExit').click();
cy.get('#pf-tab-section-0-unmountOnExit').should('not.exist');
cy.get('#pf-tab-section-1-unmountOnExit').should('exist');
});
});
Loading