Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PF4: feat(Tabs) Add Tabs for PF4 #1144

Merged
merged 10 commits into from
Jan 29, 2019
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
10 changes: 10 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tab.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SFC, HTMLProps } from 'react';

export interface TabProps extends HTMLProps<HTMLDivElement> {
title: string;
eventKey: number;
}

declare const Tab: SFC<TabProps>;

export default Tab;
25 changes: 25 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
/** content rendered inside the Tab content area. */
children: PropTypes.node,
/** additional classes added to the Modal */
className: PropTypes.string,
/** Tab title */
title: PropTypes.string.isRequired,
/** uniquely identifies the tab */
eventKey: PropTypes.number.isRequired
};

const defaultProps = {
children: null,
className: ''
};

const Tab = ({ className, children, title, eventKey, ...props }) => <React.Fragment>{children}</React.Fragment>;

Tab.propTypes = propTypes;
Tab.defaultProps = defaultProps;

export default Tab;
21 changes: 21 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tab.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import Tab from './Tab';

test('should render tab', () => {
const view = shallow(
<Tab id="tab1" eventKey={0} title="Tab item 1">
Tab 1 section
</Tab>
);
expect(view).toMatchSnapshot();
});

test('should render active tab', () => {
const view = shallow(
<Tab id="tab1" eventKey={0} title="Tab item 1" isActive>
Tab 1 section
</Tab>
);
expect(view).toMatchSnapshot();
});
16 changes: 16 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tabs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SFC, HTMLProps, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface TabsProps extends Omit<HTMLProps<HTMLDivElement>, 'onSelect'> {
children: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

children included in html props

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't I need to override . it here since it is required?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should import this and add this to the typescript integration app since it's more complicated component.

activeKey?: number;
onSelect?(event: FormEvent<HTMLInputElement>, eventKey: number): void;
isFilled?: boolean;
isSecondary?: boolean;
leftScrollAriaLabel?: string;
rightScrollAriaLabel?: string;
}

declare const Tabs: SFC<TabsProps>;

export default Tabs;
20 changes: 20 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tabs.docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Tabs, Tab } from '@patternfly/react-core';
import SimpleTabs from './examples/SimpleTabs';
import ScrollButtonsTabs from './examples/ScrollButtonsTabs';
import FilledTabs from './examples/FilledTabs';
import SecondaryTabs from './examples/SecondaryTabs';

export default {
title: 'Tabs',
live: false,
components: {
Tabs,
Tab
},
examples: [
{ component: SimpleTabs, title: 'Primary tabs with sections' },
{ component: ScrollButtonsTabs, title: 'Scroll buttons' },
{ component: SecondaryTabs, title: 'Secondary buttons' },
{ component: FilledTabs, title: 'Filled buttons' }
]
};
224 changes: 224 additions & 0 deletions packages/patternfly-4/react-core/src/components/Tabs/Tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import React from 'react';
import styles from '@patternfly/patternfly-next/components/Tabs/tabs.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';
import { AngleLeftIcon, AngleRightIcon } from '@patternfly/react-icons';
import { getUniqueId, isElementInView, sideElementIsOutOfView } from '../../internal/util';
import { SIDE } from '../../internal/constants';

const propTypes = {
/** content rendered inside the Tabs Component. */
children: PropTypes.node.isRequired,
/** additional classes added to the Tabs */
className: PropTypes.string,
/** the index of the active tab */
activeKey: PropTypes.number,
/** handel tab selection */
onSelect: PropTypes.func,
/** Enables the filled tab list layout */
isFilled: PropTypes.bool,
/** Enables Secondary Tab styling */
isSecondary: PropTypes.bool,
/** Aria Label for the left Scroll Button */
leftScrollAriaLabel: PropTypes.string,
/** Aria Label for the right Scroll Button */
rightScrollAriaLabel: PropTypes.string
};

const defaultProps = {
className: '',
activeKey: 0,
onSelect: () => undefined,
isFilled: false,
isSecondary: false,
leftScrollAriaLabel: 'Scroll left',
rightScrollAriaLabel: 'Scroll Right'
};

class Tabs extends React.Component {
static propTypes = propTypes;
static defaultProps = defaultProps;

state = {
showLeftScrollButton: false,
showRightScrollButton: false,
highlightLeftScrollButton: false,
highlightRightScrollButton: false
};

id = getUniqueId();
tabList = React.createRef();

handleTabClick(event, eventKey) {
this.props.onSelect(event, eventKey);
}

handleScrollButtons = () => {
if (this.tabList.current) {
const container = this.tabList.current;
// get first element and check if it is in view
const showLeftScrollButton = !isElementInView(container, container.firstChild, false);

// get lase element and check if it is in view
const showRightScrollButton = !isElementInView(container, container.lastChild, false);

// determine if selected tab is out of view and apply styles
let selectedTab;
const childrenArr = Array.from(container.children);
childrenArr.forEach(child => {
const { className } = child;
if (className.search('pf-m-current') > 0) {
selectedTab = child;
}
});

const sideOutOfView = sideElementIsOutOfView(container, selectedTab);

this.setState({
showLeftScrollButton,
showRightScrollButton,
highlightLeftScrollButton: (sideOutOfView === SIDE.LEFT || sideOutOfView === SIDE.BOTH) && showLeftScrollButton,
highlightRightScrollButton:
(sideOutOfView === SIDE.RIGHT || sideOutOfView === SIDE.BOTH) && showRightScrollButton
});
}
};

scrollLeft = () => {
// find first Element that is fully in view on the left, then scroll to the element before it
if (this.tabList.current) {
const container = this.tabList.current;
const childrenArr = Array.from(container.children);
let firstElementInView;
let lastElementOutOfView;
let i;
for (i = 0; i < childrenArr.length && !firstElementInView; i++) {
if (isElementInView(container, childrenArr[i], false)) {
firstElementInView = childrenArr[i];
lastElementOutOfView = childrenArr[i - 1];
}
}
lastElementOutOfView && (container.scrollLeft -= lastElementOutOfView.scrollWidth);
}
};

scrollRight = () => {
// find last Element that is fully in view on the right, then scroll to the element after it
if (this.tabList.current) {
const container = this.tabList.current;
const childrenArr = Array.from(container.children);
let lastElementInView;
let firstElementOutOfView;
let i;
for (i = childrenArr.length - 1; i >= 0 && !lastElementInView; i--) {
if (isElementInView(container, childrenArr[i], false)) {
lastElementInView = childrenArr[i];
firstElementOutOfView = childrenArr[i + 1];
}
}
firstElementOutOfView && (container.scrollLeft += firstElementOutOfView.scrollWidth);
}
};

componentDidMount() {
window.addEventListener('resize', this.handleScrollButtons, false);

// call the handle resize function to check if scroll buttons should be shown
this.handleScrollButtons();
}

componentWillUnmount() {
document.removeEventListener('resize', this.handleScrollButtons, false);
}

render() {
const {
className,
children,
activeKey,
isFilled,
isSecondary,
leftScrollAriaLabel,
rightScrollAriaLabel,
...props
} = this.props;
const {
showLeftScrollButton,
showRightScrollButton,
highlightLeftScrollButton,
highlightRightScrollButton
} = this.state;
return (
<React.Fragment>
<div
{...props}
className={css(
styles.tabs,
isFilled && styles.modifiers.fill,
isSecondary && styles.modifiers.tabsSecondary,
showLeftScrollButton && styles.modifiers.start,
showRightScrollButton && styles.modifiers.end,
highlightLeftScrollButton && styles.modifiers.startCurrent,
highlightRightScrollButton && styles.modifiers.endCurrent,
className
)}
>
{!isSecondary && (
<button
className={css(styles.tabsScrollButton)}
variant="plain"
aria-label={leftScrollAriaLabel}
onClick={this.scrollLeft}
>
<AngleLeftIcon />
</button>
)}
<ul className={css(styles.tabsList)} ref={this.tabList} onScroll={this.handleScrollButtons}>
{children.map((child, index) => (
<li
key={index}
className={css(
styles.tabsItem,
child.props.eventKey === activeKey && styles.modifiers.current,
className
)}
>
<button
className={css(styles.tabsButton)}
onClick={event => this.handleTabClick(event, child.props.eventKey)}
id={`pf-tab-${child.props.eventKey}-${child.props.id || this.id}`}
aria-controls={`pf-tab-section-${child.props.eventKey}-${child.props.id || this.id}`}
>
{child.props.title}
</button>
</li>
))}
</ul>
{!isSecondary && (
<button
className={css(styles.tabsScrollButton)}
variant="plain"
aria-label={rightScrollAriaLabel}
onClick={this.scrollRight}
>
<AngleRightIcon />
</button>
)}
</div>
{children.map((child, index) => (
<section
key={index}
hidden={child.props.eventKey !== activeKey}
className={css(child.props.className)}
id={`pf-tab-section-${child.props.eventKey}-${child.props.id || this.id}`}
aria-labelledby={`pf-tab-${child.props.eventKey}-${child.props.id || this.id}`}
>
{child.props.children}
</section>
))}
</React.Fragment>
);
}
}

export default Tabs;
Loading