From 54d84618c14a705a9508199bcdc20431a600e8bc Mon Sep 17 00:00:00 2001 From: Tobias Date: Fri, 18 Jan 2019 16:40:29 +0100 Subject: [PATCH 1/2] feat: extend #tabs with enhanced function --- .../src/shared/inlineTags/Tabs.js | 7 + .../src/shared/parts/uilib/ItemWrapper.js | 12 +- .../src/uilib/components/examples/Tabs.txt | 12 + .../__snapshots__/FormStatus.test.js.snap | 2 +- .../dnb-ui-lib/src/components/tabs/Example.js | 37 ++- .../dnb-ui-lib/src/components/tabs/Tabs.js | 213 ++++++++++++++---- .../components/tabs/__tests__/Tabs.test.js | 44 +++- .../__tests__/__snapshots__/Tabs.test.js.snap | 33 ++- .../dnb-ui-lib/stories/componentExamples.js | 35 ++- 9 files changed, 318 insertions(+), 77 deletions(-) create mode 100644 packages/dnb-design-system-portal/src/shared/inlineTags/Tabs.js diff --git a/packages/dnb-design-system-portal/src/shared/inlineTags/Tabs.js b/packages/dnb-design-system-portal/src/shared/inlineTags/Tabs.js new file mode 100644 index 00000000000..32a3e6d39f9 --- /dev/null +++ b/packages/dnb-design-system-portal/src/shared/inlineTags/Tabs.js @@ -0,0 +1,7 @@ +/** + * Inline Tag + * + */ + +import { Tabs } from 'dnb-ui-lib/src' +export default Tabs diff --git a/packages/dnb-design-system-portal/src/shared/parts/uilib/ItemWrapper.js b/packages/dnb-design-system-portal/src/shared/parts/uilib/ItemWrapper.js index 1777f1e8097..15b524ce831 100644 --- a/packages/dnb-design-system-portal/src/shared/parts/uilib/ItemWrapper.js +++ b/packages/dnb-design-system-portal/src/shared/parts/uilib/ItemWrapper.js @@ -143,7 +143,7 @@ class ItemWrapper extends PureComponent { if (this.isActive('demo')) { tabsContent.push( - + {!hideTabs && }

Demos

@@ -152,7 +152,7 @@ class ItemWrapper extends PureComponent { {Additional && Additional.demo && ( )} -
+ ) } @@ -164,7 +164,7 @@ class ItemWrapper extends PureComponent { tabsUsed.push(tabs.find(({ key }) => key === 'info')) if (this.isActive('info')) { tabsContent.push( - +
{Additional && Additional.info && ( @@ -175,7 +175,7 @@ class ItemWrapper extends PureComponent { {ExampleCode} )} - + ) } } @@ -184,12 +184,12 @@ class ItemWrapper extends PureComponent { tabsUsed.push(tabs.find(({ key }) => key === 'code')) if (this.isActive('code')) { tabsContent.push( - + {Additional && Additional.code && ( )} - + ) } } diff --git a/packages/dnb-design-system-portal/src/uilib/components/examples/Tabs.txt b/packages/dnb-design-system-portal/src/uilib/components/examples/Tabs.txt index 5fb3af3f67b..1836b90e123 100644 --- a/packages/dnb-design-system-portal/src/uilib/components/examples/Tabs.txt +++ b/packages/dnb-design-system-portal/src/uilib/components/examples/Tabs.txt @@ -1,4 +1,16 @@ {exampleContent} + diff --git a/packages/dnb-ui-lib/src/components/tabs/Example.js b/packages/dnb-ui-lib/src/components/tabs/Example.js index db8f8f70fef..7d70ead0eff 100644 --- a/packages/dnb-ui-lib/src/components/tabs/Example.js +++ b/packages/dnb-ui-lib/src/components/tabs/Example.js @@ -32,7 +32,34 @@ class Example extends PureComponent {
{exampleContent} -

Left aligned tabs

+

+ Left aligned tabs, using both "data" property and content + object +

+
+
+ +

+ Left aligned tabs, using "data" property only +

+
+
+ + +

First

+
+ +

Second

+
+
+

+ Left aligned tabs, using React Components only +

First, - second: Focus me with next Tab key, - third: ( + first: () =>

First

, + second: () => Focus me with next Tab key, + third: () => (

Eros semper blandit tellus mollis primis quisque platea sollicitudin ipsum

), - fourth:

Fourth

+ fourth: () =>

Fourth

} const data = [ diff --git a/packages/dnb-ui-lib/src/components/tabs/Tabs.js b/packages/dnb-ui-lib/src/components/tabs/Tabs.js index d0c6e60fec8..90ac8ffdb30 100644 --- a/packages/dnb-ui-lib/src/components/tabs/Tabs.js +++ b/packages/dnb-ui-lib/src/components/tabs/Tabs.js @@ -3,7 +3,7 @@ * */ -import React, { Fragment, PureComponent } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import keycode from 'keycode' @@ -12,8 +12,6 @@ import { validateDOMAttributes, dispatchCustomElementEvent } from '../../shared/component-helper' -// import { pageFocus } from '../../shared/tools' -// import './style/dnb-tabs.scss' // no good solution to import the style here const renderProps = { render: null @@ -23,35 +21,52 @@ export const propTypes = { data: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf( - PropTypes.exact({ + PropTypes.shape({ title: PropTypes.string.isRequired, key: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool + }) + ), + PropTypes.objectOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + selected: PropTypes.bool, disabled: PropTypes.bool }) ) - ]).isRequired, + ]), label: PropTypes.string, selected_key: PropTypes.string, align: PropTypes.oneOf(['left', 'center', 'right']), use_hash: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + id: PropTypes.string, class: PropTypes.string, /** React props */ className: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + children: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.node, + PropTypes.func + ]), + // Web Component props render: PropTypes.func } export const defaultProps = { - data: [], + data: null, label: null, selected_key: null, align: 'left', use_hash: false, + id: null, class: null, + /** React props */ className: null, children: null, + // Web Component props ...renderProps } @@ -86,9 +101,52 @@ export default class Tabs extends PureComponent { static getData(props) { let res = [] - if (props.data) res = props.data - if (typeof res === 'string') - return res[0] === '[' ? JSON.parse(res) : [] + + // check if we have to use the children prop to prepare our data + const data = + !props.data && props.children ? props.children : props.data + + // if it is a React Component - collect data from Tabs.Tab component + if ( + Array.isArray(props.children) && + (typeof props.children[0] === 'function' || + React.isValidElement(props.children[0])) + ) { + res = props.children.reduce((acc, cur, i) => { + if (cur.props.title) { + const { key, ...rest } = cur.props + acc.push({ + key: key || `key${i}`, + content: cur, // can be a Node or a Function + ...rest + }) + } + return acc + }, []) + } + + // continue, while the children dident contain our data + if (!(res && res.length > 0)) { + // if data is array, it looks good! + if (props.data && Array.isArray(data)) { + res = data + + // it may be a json + } else if (typeof data === 'string') { + res = data[0] === '[' ? JSON.parse(data) : [] + + // but it may also be an object + } else if (data && typeof data === 'object') { + res = Object.entries(data).reduce((acc, [key, obj]) => { + acc.push({ + key, + ...obj + }) + return acc + }, []) + } + } + return res || [] } @@ -97,7 +155,14 @@ export default class Tabs extends PureComponent { this._id = props.id || `dnb-tabs-${Math.round(Math.random() * 999)}` // cause we need an id anyway const data = Tabs.getData(props) - const selected_key = props.selected_key || (data[0] && data[0].key) + + const selected_key = + props.selected_key || + data.reduce( + (acc, { selected, key }) => (selected ? key : acc), + null + ) || + (data[0] && data[0].key) this.state = { _listenForPropChanges: true, @@ -150,7 +215,7 @@ export default class Tabs extends PureComponent { const target = e.target.nodeName === 'SPAN' ? e.target.parentElement : e.target const selected_key = String(target.className).match( - /tab--([a-z0-9]+)/ + /tab--([-_a-z0-9]+)/i )[1] return this.openTab(selected_key) @@ -231,23 +296,50 @@ export default class Tabs extends PureComponent { renderContent() { const { children } = this.props - if (!children) { - return null - } const { selected_key } = this.state let content = null - if (typeof children === 'object' && children[selected_key]) { + // if content is provided as an object + if ( + children && + typeof children === 'object' && + children[selected_key] + ) { content = children[selected_key] - } else if (typeof children === 'function') { + + // if content is provided as a render prop + } else if (children && typeof children === 'function') { content = children.apply(this, [selected_key]) } + if (!content) { + let items = [] + + if (Array.isArray(this.state.data)) { + items = this.state.data + } else if (Array.isArray(children)) { + items = children + } + + // if content was provided as a React Component like "Tabs.Tab" + // - or the content was provided as a content prop i data + if (items) { + content = items + .filter(({ key }) => key && selected_key && key === selected_key) + .reduce((acc, { content }) => content || acc, null) + } + } + + if (typeof content === 'function') { + const Component = content + content = + } + return ( - + {content || Tab content not found} - + ) } @@ -268,32 +360,32 @@ export default class Tabs extends PureComponent { const content = this.renderContent() + // To have a reusable Component laster, do this like that const Tabs = () => { const tabs = this.state.data.map( ({ title, key, disabled = false }) => { const params = {} - if (this.isSelected(key)) + if (this.isSelected(key)) { params['aria-controls'] = `${this._id}-content-${key}` + } return ( - - - + ) } ) @@ -315,6 +407,8 @@ export default class Tabs extends PureComponent {
) } + + // To have a reusable Component laster, do this like that const TabsList = ({ children }) => (
) + // To have a reusable Component laster, do this like that const Wrapper = ({ children, ...rest }) => (
{children} @@ -333,6 +428,7 @@ export default class Tabs extends PureComponent {
) + // here we reuse the component, if it has a custom renderer if (typeof customRenderer === 'function') { return customRenderer({ Wrapper, TabsList, Tabs }) } @@ -347,14 +443,19 @@ export default class Tabs extends PureComponent { } } -class TabContent extends PureComponent { +class ContentWrapper extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, - selection_key: PropTypes.string.isRequired, + selected_key: PropTypes.string, children: PropTypes.node.isRequired } + static defaultProps = { + selected_key: null + } render() { - const { id, children, selection_key: key } = this.props + const { id, children } = this.props + const key = this.props.selected_key || this._reactInternalFiber.key + return (
+ first + second + + */ +class Tab extends PureComponent { + static propTypes = { + title: PropTypes.string.isRequired, // eslint-disable-line + selected: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // eslint-disable-line + disabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // eslint-disable-line + children: PropTypes.node.isRequired + } + static defaultProps = { + selected: null, + disabled: null + } + render() { + const { children } = this.props + return children + } +} + +Tabs.Tab = Tab +Tabs.ContentWrapper = ContentWrapper diff --git a/packages/dnb-ui-lib/src/components/tabs/__tests__/Tabs.test.js b/packages/dnb-ui-lib/src/components/tabs/__tests__/Tabs.test.js index 550c3a7c512..8c7ad418cd3 100644 --- a/packages/dnb-ui-lib/src/components/tabs/__tests__/Tabs.test.js +++ b/packages/dnb-ui-lib/src/components/tabs/__tests__/Tabs.test.js @@ -29,10 +29,15 @@ const tablistData = [ { title: 'Second', key: 'second' }, { title: 'Third', key: 'third' } ] -const tabContentData = { - first:

First

, - second:

Second

, - third:

Third

+const tablistDataWithContent = [ + { title: 'First', key: 'first', content:

First

}, // without function + { title: 'Second', key: 'second', content: () =>

Second

}, // with function + { title: 'Third', key: 'third', content: () =>

Third

} // with function +] +const contentWrapperData = { + first:

First

, // without function + second: () =>

Second

, // with function + third:

Third

// without function } describe('Tabs component', () => { @@ -42,7 +47,7 @@ describe('Tabs component', () => { data={tablistData} selected_key={startup_selected_key} > - {tabContentData} + {contentWrapperData} ) @@ -66,7 +71,7 @@ describe('TabList component', () => { data={tablistData} selected_key={startup_selected_key} > - {tabContentData} + {contentWrapperData} ) @@ -82,7 +87,7 @@ describe('TabList component', () => { Comp.find('div[role="tabpanel"]') .children() .html() - ).toBe(mount(tabContentData.third).html()) + ).toBe(mount(contentWrapperData.third).html()) }) }) @@ -93,11 +98,11 @@ describe('A single Tab component', () => { data={tablistData} selected_key={startup_selected_key} > - {tabContentData} + {contentWrapperData} ) - it('has to have the right content on a keydown "ArrowRight"', () => { + it('has to have a role="tab" attribute and a class="selcted"', () => { expect( Comp.find('button.tab--second') .instance() @@ -117,7 +122,26 @@ describe('A single Tab component', () => { Comp.find('div[role="tabpanel"]') .children() .html() - ).toBe(mount(tabContentData.third).html()) + ).toBe(mount(contentWrapperData.third).html()) + }) + + it('has to work with "data only" property containing a "content"', () => { + const Comp = mount() + expect(Comp.find('button.selected').exists()).toBe(true) + expect(Comp.find('div.dnb-tabs__content').text()).toBe('First') + }) + + it('has to work with "Tabs.Tab" as children Components', () => { + const Comp = mount( + + first + + second + + + ) + expect(Comp.find('button.selected').exists()).toBe(true) + expect(Comp.find('div.dnb-tabs__content').text()).toBe('second') }) }) diff --git a/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap b/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap index c16be10953b..f1c02544e98 100644 --- a/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap +++ b/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap @@ -11,6 +11,7 @@ exports[`Tabs component have to match snapshot 1`] = ` "class": "class", "className": "className", "data": "data", + "id": "id", "label": "label", "render": [Function], "selected_key": "selected_key", @@ -20,11 +21,22 @@ exports[`Tabs component have to match snapshot 1`] = ` } 1={ Object { - "displayName": "TabContent", + "displayName": "ContentWrapper", "props": Object { "children": "children", "id": "id", - "selection_key": "selection_key", + "selected_key": "selected_key", + }, + } + } + 2={ + Object { + "displayName": "Tab", + "props": Object { + "children": "children", + "disabled": "disabled", + "selected": "selected", + "title": "title", }, } } @@ -74,6 +86,7 @@ exports[`Tabs component have to match snapshot 1`] = ` className="dnb-tablink dnb-no-mouse-focus tab--first " disabled={false} id="id-tab-first" + key="tab--first" onClick={[Function]} role="tab" tabIndex="-1" @@ -96,6 +109,7 @@ exports[`Tabs component have to match snapshot 1`] = ` className="dnb-tablink dnb-no-mouse-focus tab--second selected" disabled={false} id="id-tab-second" + key="tab--second" onClick={[Function]} role="tab" tabIndex="-1" @@ -117,6 +131,7 @@ exports[`Tabs component have to match snapshot 1`] = ` className="dnb-tablink dnb-no-mouse-focus tab--third " disabled={false} id="id-tab-third" + key="tab--third" onClick={[Function]} role="tab" tabIndex="-1" @@ -137,9 +152,9 @@ exports[`Tabs component have to match snapshot 1`] = `
-
-

- Second -

+ +

+ Second +

+
-
+
diff --git a/packages/dnb-ui-lib/stories/componentExamples.js b/packages/dnb-ui-lib/stories/componentExamples.js index 4bd810a8adf..8ef1c4d0979 100644 --- a/packages/dnb-ui-lib/stories/componentExamples.js +++ b/packages/dnb-ui-lib/stories/componentExamples.js @@ -95,6 +95,11 @@ stories.push([ ) ]) +const tablistDataWithContent = [ + { title: 'First', key: 'first', content:

First

}, + { title: 'Second', key: 'second', content: () =>

Second

} +] + stories.push([ 'Tabs', () => ( @@ -104,6 +109,28 @@ stories.push([ {exampleTabsContent}
+ + + + +

First

}, + second: { title: 'Second', content: () =>

Second

} + }} + /> +
+ + + +

First

+
+ +

Second

+
+
+
) ]) @@ -175,15 +202,15 @@ stories.push([ ]) const exampleTabsContent = { - first:

First

, - second: Focus me with next Tab key, - third: ( + first: () =>

First

, + second: () => Focus me with next Tab key, + third: () => (

Eros semper blandit tellus mollis primis quisque platea sollicitudin ipsum

), - fourth:

Fourth

+ fourth: () =>

Fourth

} const tabsData = [ { title: 'First', key: 'first' }, From 6f63f9c9e626fa2ed97c64ebfdcbf6998ef251a9 Mon Sep 17 00:00:00 2001 From: Tobias Date: Fri, 18 Jan 2019 22:45:48 +0100 Subject: [PATCH 2/2] fix #tabs issue used from outside --- .../dnb-ui-lib/src/components/tabs/Tabs.js | 37 +++++++++++-------- .../__tests__/__snapshots__/Tabs.test.js.snap | 4 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/dnb-ui-lib/src/components/tabs/Tabs.js b/packages/dnb-ui-lib/src/components/tabs/Tabs.js index 90ac8ffdb30..78bc2ff6b96 100644 --- a/packages/dnb-ui-lib/src/components/tabs/Tabs.js +++ b/packages/dnb-ui-lib/src/components/tabs/Tabs.js @@ -351,15 +351,6 @@ export default class Tabs extends PureComponent { class: _className } = this.props - const params = { - className: classnames('dnb-tabs', className, _className) - } - - // also used for code markup simulation - validateDOMAttributes(this.props, params) - - const content = this.renderContent() - // To have a reusable Component laster, do this like that const Tabs = () => { const tabs = this.state.data.map( @@ -421,12 +412,26 @@ export default class Tabs extends PureComponent { ) // To have a reusable Component laster, do this like that - const Wrapper = ({ children, ...rest }) => ( -
- {children} - {content} -
- ) + const Wrapper = ({ children, isInside, ...rest }) => { + const params = { + className: classnames('dnb-tabs', className, _className) + } + + // also used for code markup simulation + validateDOMAttributes(this.props, params) + + // check if the Wrapper is used from "inside", else there have to be children + // from inside, there is the posibility that we got the content privded by the "data" prop + const content = + isInside || this.props.children ? this.renderContent() : null + + return ( +
+ {children} + {content} +
+ ) + } // here we reuse the component, if it has a custom renderer if (typeof customRenderer === 'function') { @@ -434,7 +439,7 @@ export default class Tabs extends PureComponent { } return ( - + diff --git a/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap b/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap index f1c02544e98..f99bd14534d 100644 --- a/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap +++ b/packages/dnb-ui-lib/src/components/tabs/__tests__/__snapshots__/Tabs.test.js.snap @@ -65,7 +65,9 @@ exports[`Tabs component have to match snapshot 1`] = ` selected_key="second" use_hash={false} > - +