From 42ce61e49c69e4e4e6147ce9affc3625748f5d2e Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Sun, 29 Apr 2018 17:30:45 -0700 Subject: [PATCH] Add EuiTabbedContent. --- CHANGELOG.md | 2 +- src-docs/src/views/tabs/controlled.js | 97 +++++++++ src-docs/src/views/tabs/tabbed_content.js | 68 ++++++ src-docs/src/views/tabs/tabs_example.js | 49 +++++ src/components/index.js | 33 +-- src/components/tabs/index.js | 1 + .../__snapshots__/tabbed_content.test.js.snap | 195 ++++++++++++++++++ src/components/tabs/tabbed_content/index.js | 3 + .../tabs/tabbed_content/tabbed_content.js | 74 +++++++ .../tabbed_content/tabbed_content.test.js | 66 ++++++ 10 files changed, 571 insertions(+), 17 deletions(-) create mode 100644 src-docs/src/views/tabs/controlled.js create mode 100644 src-docs/src/views/tabs/tabbed_content.js create mode 100644 src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap create mode 100644 src/components/tabs/tabbed_content/index.js create mode 100644 src/components/tabs/tabbed_content/tabbed_content.js create mode 100644 src/components/tabs/tabbed_content/tabbed_content.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index df6f733cfc94..e61615747fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Added `EuiBetaBadge` for non-GA labelling including options to add it to `EuiCard` and `EuiKeyPadMenuItem` ([#705](https://github.com/elastic/eui/pull/705)) - +- Added `EuiTabbedContent` ([#737](https://github.com/elastic/eui/pull/737)) ## [`0.0.44`](https://github.com/elastic/eui/tree/v0.0.44) diff --git a/src-docs/src/views/tabs/controlled.js b/src-docs/src/views/tabs/controlled.js new file mode 100644 index 000000000000..ad9ee1d2bf1a --- /dev/null +++ b/src-docs/src/views/tabs/controlled.js @@ -0,0 +1,97 @@ +import React, { Component, Fragment } from 'react'; + +import { + EuiButton, + EuiTabbedContent, + EuiTitle, + EuiText, + EuiSpacer, +} from '../../../../src/components'; + +class EuiTabsExample extends Component { + constructor(props) { + super(props); + + this.tabs = [{ + id: 'cobalt', + name: 'Cobalt', + content: ( + + +

Cobalt

+ Cobalt is a chemical element with symbol Co and atomic number 27. Like nickel, cobalt is found in the Earth’s crust only in chemically combined form, save for small deposits found in alloys of natural meteoric iron. The free element, produced by reductive smelting, is a hard, lustrous, silver-gray metal. +
+ ), + }, { + id: 'dextrose', + name: 'Dextrose', + content: ( + + +

Dextrose

+ Intravenous sugar solution, also known as dextrose solution, is a mixture of dextrose (glucose) and water. It is used to treat low blood sugar or water loss without electrolyte loss. +
+ ), + }, { + id: 'hydrogen', + name: 'Hydrogen', + content: ( + + +

Hydrogen

+ Hydrogen is a chemical element with symbol H and atomic number 1. With a standard atomic weight of 1.008, hydrogen is the lightest element on the periodic table +
+ ), + }, { + id: 'monosodium_glutammate', + name: 'Monosodium Glutamate', + content: ( + + +

Monosodium Glutamate

+ Monosodium glutamate (MSG, also known as sodium glutamate) is the sodium salt of glutamic acid, one of the most abundant naturally occurring non-essential amino acids. Monosodium glutamate is found naturally in tomatoes, cheese and other foods. +
+ ), + }]; + + this.state = { + selectedTab: this.tabs[1], + }; + } + + onTabClick = (selectedTab) => { + this.setState({ selectedTab }); + }; + + cycleTab = () => { + const selectedTabIndex = this.tabs.indexOf(this.state.selectedTab) + const nextTabIndex = selectedTabIndex < this.tabs.length - 1 ? selectedTabIndex + 1 : 0; + this.setState({ + selectedTab: this.tabs[nextTabIndex], + }); + }; + + render() { + return ( + + + Cycle through the tabs + + + + + + + ); + } +} + +export default EuiTabsExample; diff --git a/src-docs/src/views/tabs/tabbed_content.js b/src-docs/src/views/tabs/tabbed_content.js new file mode 100644 index 000000000000..258153769d37 --- /dev/null +++ b/src-docs/src/views/tabs/tabbed_content.js @@ -0,0 +1,68 @@ +import React, { Component, Fragment } from 'react'; + +import { + EuiTabbedContent, + EuiTitle, + EuiText, + EuiSpacer, +} from '../../../../src/components'; + +class EuiTabsExample extends Component { + constructor(props) { + super(props); + + this.tabs = [{ + id: 'cobalt', + name: 'Cobalt', + content: ( + + +

Cobalt

+ Cobalt is a chemical element with symbol Co and atomic number 27. Like nickel, cobalt is found in the Earth’s crust only in chemically combined form, save for small deposits found in alloys of natural meteoric iron. The free element, produced by reductive smelting, is a hard, lustrous, silver-gray metal. +
+ ), + }, { + id: 'dextrose', + name: 'Dextrose', + content: ( + + +

Dextrose

+ Intravenous sugar solution, also known as dextrose solution, is a mixture of dextrose (glucose) and water. It is used to treat low blood sugar or water loss without electrolyte loss. +
+ ), + }, { + id: 'hydrogen', + name: 'Hydrogen', + content: ( + + +

Hydrogen

+ Hydrogen is a chemical element with symbol H and atomic number 1. With a standard atomic weight of 1.008, hydrogen is the lightest element on the periodic table +
+ ), + }, { + id: 'monosodium_glutammate', + name: 'Monosodium Glutamate', + content: ( + + +

Monosodium Glutamate

+ Monosodium glutamate (MSG, also known as sodium glutamate) is the sodium salt of glutamic acid, one of the most abundant naturally occurring non-essential amino acids. Monosodium glutamate is found naturally in tomatoes, cheese and other foods. +
+ ), + }]; + } + + render() { + return ( + { console.log('clicked tab', tab); }} + /> + ); + } +} + +export default EuiTabsExample; diff --git a/src-docs/src/views/tabs/tabs_example.js b/src-docs/src/views/tabs/tabs_example.js index 7edaa184c445..c982f3e3913e 100644 --- a/src-docs/src/views/tabs/tabs_example.js +++ b/src-docs/src/views/tabs/tabs_example.js @@ -8,12 +8,21 @@ import { import { EuiCode, EuiTabs, + EuiTabbedContent, } from '../../../../src/components'; import Tabs from './tabs'; const tabsSource = require('!!raw-loader!./tabs'); const tabsHtml = renderToHtml(Tabs); +import TabbedContent from './tabbed_content'; +const tabbedContentSource = require('!!raw-loader!./tabbed_content'); +const tabbedContentHtml = renderToHtml(TabbedContent); + +import Controlled from './controlled'; +const controlledSource = require('!!raw-loader!./controlled'); +const controlledHtml = renderToHtml(Controlled); + export const TabsExample = { title: 'Tabs', sections: [{ @@ -35,5 +44,45 @@ export const TabsExample = { EuiTabs, }, demo: , + }, { + title: 'Tabbed content', + source: [{ + type: GuideSectionTypes.JS, + code: tabbedContentSource, + }, { + type: GuideSectionTypes.HTML, + code: tabbedContentHtml, + }], + text: ( +

+ EuiTabbedContent makes it easier to associate tabs with content based + on the selected tab. Use the initialSelectedTab prop to specify which + tab to initially select. +

+ ), + props: { + EuiTabbedContent, + }, + demo: , + }, { + title: 'Controlled tabbed content', + source: [{ + type: GuideSectionTypes.JS, + code: controlledSource, + }, { + type: GuideSectionTypes.HTML, + code: controlledHtml, + }], + text: ( +

+ You can also use the selectedTab and onTabClick props + to take complete control over tab selection. This can be useful if you want to change tabs + based on user interaction with another part of the UI. +

+ ), + props: { + EuiTabbedContent, + }, + demo: , }], }; diff --git a/src/components/index.js b/src/components/index.js index d5d34becdff2..2036fa153b24 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -184,14 +184,14 @@ export { EuiModalHeaderTitle, } from './modal'; -export { - EuiOverlayMask, -} from './overlay_mask'; - export { EuiOutsideClickDetector, } from './outside_click_detector'; +export { + EuiOverlayMask, +} from './overlay_mask'; + export { EuiPage, EuiPageBody, @@ -230,6 +230,10 @@ export { EuiSearchBar } from './search_bar'; +export { + EuiSideNav, +} from './side_nav'; + export { EuiSpacer, } from './spacer'; @@ -265,11 +269,18 @@ export { export { EuiTab, EuiTabs, + EuiTabbedContent, } from './tabs'; export { - EuiSideNav, -} from './side_nav'; + EuiText, + EuiTextColor, + EuiTextAlign, +} from './text'; + +export { + EuiTitle, +} from './title'; export { EuiGlobalToastList, @@ -281,13 +292,3 @@ export { EuiIconTip, EuiToolTip, } from './tool_tip'; - -export { - EuiTitle, -} from './title'; - -export { - EuiText, - EuiTextColor, - EuiTextAlign, -} from './text'; diff --git a/src/components/tabs/index.js b/src/components/tabs/index.js index ef515e86ac59..bab8934d915f 100644 --- a/src/components/tabs/index.js +++ b/src/components/tabs/index.js @@ -1,2 +1,3 @@ export { EuiTab } from './tab'; export { EuiTabs } from './tabs'; +export { EuiTabbedContent } from './tabbed_content'; diff --git a/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap b/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap new file mode 100644 index 000000000000..5d50ec52a819 --- /dev/null +++ b/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap @@ -0,0 +1,195 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiTabbedContent is rendered 1`] = ` +
+
+ + +
+

+ Elasticsearch content +

+
+`; + +exports[`EuiTabbedContent props initialSelectedTab is rendered 1`] = ` +
+
+ + +
+

+ Kibana content +

+
+`; + +exports[`EuiTabbedContent props selectedTab is rendered 1`] = ` +
+
+ + +
+

+ Kibana content +

+
+`; + +exports[`EuiTabbedContent props selectedTab when not set, defaults to the first tab 1`] = ` +
+
+ + +
+

+ Elasticsearch content +

+
+`; + +exports[`EuiTabbedContent props size is rendered 1`] = ` +
+
+ + +
+

+ Elasticsearch content +

+
+`; diff --git a/src/components/tabs/tabbed_content/index.js b/src/components/tabs/tabbed_content/index.js new file mode 100644 index 000000000000..9aa96864c193 --- /dev/null +++ b/src/components/tabs/tabbed_content/index.js @@ -0,0 +1,3 @@ +export { + EuiTabbedContent, +} from './tabbed_content'; diff --git a/src/components/tabs/tabbed_content/tabbed_content.js b/src/components/tabs/tabbed_content/tabbed_content.js new file mode 100644 index 000000000000..71677dd82a83 --- /dev/null +++ b/src/components/tabs/tabbed_content/tabbed_content.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types'; + +import { EuiTabs, SIZES } from '../tabs'; +import { EuiTab } from '../tab'; + +export class EuiTabbedContent extends Component { + static propTypes = { + className: PropTypes.string, + tabs: PropTypes.array.isRequired, + onTabClick: PropTypes.func, + selectedTab: PropTypes.object, + initialSelectedTab: PropTypes.object, + size: PropTypes.oneOf(SIZES), + }; + + constructor(props) { + super(props); + + const { initialSelectedTab, selectedTab, tabs } = props; + + this.state = { + selectedTab: selectedTab || initialSelectedTab || tabs[0], + }; + } + + onTabClick = (selectedTab) => { + const { onTabClick } = this.props; + + if (onTabClick) { + onTabClick(selectedTab) + } + + this.setState({ selectedTab }) + }; + + render() { + const { + className, + tabs, + onTabClick, // eslint-disable-line no-unused-vars + initialSelectedTab, // eslint-disable-line no-unused-vars + selectedTab: externalSelectedTab, + size, + ...rest + } = this.props; + + // Allow the consumer to control tab selection. + const selectedTab = externalSelectedTab || this.state.selectedTab + const { content } = selectedTab + + return ( +
+ + { + tabs.map((tab, index) => { + const { id, name, ...tabProps } = tab + const props = { + key: id !== undefined ? id : index, + ...tabProps, + onClick: () => this.onTabClick(tab), + isSelected: tab === selectedTab, + }; + + return {name}; + }) + } + + + {content} +
+ ) + } +} diff --git a/src/components/tabs/tabbed_content/tabbed_content.test.js b/src/components/tabs/tabbed_content/tabbed_content.test.js new file mode 100644 index 000000000000..3ddbf786203e --- /dev/null +++ b/src/components/tabs/tabbed_content/tabbed_content.test.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { render, mount } from 'enzyme'; +import sinon from 'sinon'; +import { requiredProps, findTestSubject } from '../../../test'; + +import { EuiTabbedContent } from './tabbed_content'; + +const elasticsearchTab = { + title: `Elasticsearch`, + content:

Elasticsearch content

, +}; + +const kibanaTab = { + title: `Kibana`, + 'data-test-subj': 'kibanaTab', + content:

Kibana content

, +}; + +const tabs = [ + elasticsearchTab, + kibanaTab, +]; + +describe('EuiTabbedContent', () => { + test('is rendered', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + describe('onTabClick', () => { + test('is called when a tab is clicked', () => { + const onTabClickHandler = sinon.stub(); + const component = mount(); + findTestSubject(component, 'kibanaTab').simulate('click'); + sinon.assert.calledOnce(onTabClickHandler); + }); + }); + + describe('selectedTab', () => { + test('is rendered', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + test('when not set, defaults to the first tab', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('initialSelectedTab', () => { + test('is rendered', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('size', () => { + test('is rendered', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + }); + }); +});