Skip to content

Commit

Permalink
feat: react-tabs-provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Federico Zivolo committed Jan 24, 2019
1 parent ac2ddd3 commit a3200f4
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 10 deletions.
15 changes: 15 additions & 0 deletions packages/react-tabs-provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
This package provides a set of React components that make it easy to
handle tab based navigation user interfaces.

It supports both fully controlled and stateful approaches to better fit
the consumer requirements.

#### Installation

```bash
npm install --save @quid/react-tabs-provider

# or

yarn add @quid/react-tabs-provider
```
26 changes: 26 additions & 0 deletions packages/react-tabs-provider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@quid/react-tabs-provider",
"version": "1.0.0",
"description": "render-props based component to handle tab navigation",
"main": "dist/index.js",
"main:umd": "dist/index.umd.js",
"module": "dist/index.es.js",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/quid/ui-framework.git"
},
"scripts": {
"start": "microbundle watch",
"prepare": "microbundle build --jsx React.createElement && flow-copy-source --ignore '{__mocks__/*,*.test}.js' src dist",
"test": "cd ../.. && yarn test --testPathPattern packages/react-tabs-provider"
},
"devDependencies": {
"flow-copy-source": "^2.0.2",
"microbundle": "^0.8.3",
"react": "^16.0.0"
},
"peerDependencies": {
"react": "15||16"
}
}
15 changes: 15 additions & 0 deletions packages/react-tabs-provider/src/TabList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow
import * as React from 'react';
import { Context, type ContextState } from './Tabs';

type Props = {
children: ContextState => React.Node,
};

export default class TabList extends React.Component<Props> {
render() {
return this.props.children(this.context);
}

static contextType = Context;
}
22 changes: 22 additions & 0 deletions packages/react-tabs-provider/src/TabPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @flow
import * as React from 'react';
import { Context } from './Tabs';

type Props = {
name: string,
children: ({ name: string }) => React.Node,
};

export default class TabPanel extends React.Component<Props> {
componentDidMount() {
this.context.register(this.props.name);
}

render() {
return this.props.name === this.context.active
? this.props.children({ name: this.props.name })
: null;
}

static contextType = Context;
}
81 changes: 81 additions & 0 deletions packages/react-tabs-provider/src/Tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// @flow
import * as React from 'react';

export type ContextState = {
active: ?string,
select: string => void,
register: string => void,
};

const noop = () => {};

// prettier-ignore
export const Context = React.createContext/*:: <ContextState> */({
active: '',
select: noop,
register: noop,
});

type Props = {
children: ({ active: ?string }) => React.Node,
defaultActive?: string,
active?: string,
onSelect?: string => void,
};

type State = {
registeredTabs: Array<string>,
active: ?string,
register: string => void,
select: string => void,
};

export default class Tabs extends React.Component<Props, State> {
state = {
registeredTabs: [],
active: this.props.defaultActive,
register: this.register,
select: this.select,
};

register = (name: string) =>
this.setState(({ registeredTabs }) => ({
registeredTabs: [...registeredTabs, name],
}));

select = (name: string) =>
this.props.onSelect != null
? this.props.onSelect(name)
: this.setState({
active: name,
});

render() {
const {
children,
active: controlledActive,
onSelect: controlledOnSelect,
} = this.props;
const { active: uncontrolledActive, registeredTabs } = this.state;

if (
(controlledActive != null && controlledOnSelect == null) ||
(controlledOnSelect != null && controlledActive == null)
) {
// eslint-disable-next-line no-console
console.error(
'Warning: TabsProvider.Tabs can be used either as a controlled or stateful component, ' +
'when used as controlled component, make sure to define both `value` and `onSelect` properties to it.'
);
}
const active = controlledActive || uncontrolledActive || registeredTabs[0];

return (
<Context.Provider
value={{ active, register: this.register, select: this.select }}
>
{children({ active })}
</Context.Provider>
);
}
}
18 changes: 18 additions & 0 deletions packages/react-tabs-provider/src/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders a TabList with interactable properties 1`] = `
<x-wrapper>
<button
active="active"
type="button"
/>
</x-wrapper>
`;

exports[`renders an active TabPanel 1`] = `
<x-wrapper>
foobar
</x-wrapper>
`;

exports[`renders an inactive TabPanel 1`] = `<x-wrapper />`;
10 changes: 10 additions & 0 deletions packages/react-tabs-provider/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @flow
export { default as Tabs } from './Tabs';
export { default as TabList } from './TabList';
export { default as TabPanel } from './TabPanel';

/**
* @component
* @visibleName Usage example
*/
export default () => null;
110 changes: 110 additions & 0 deletions packages/react-tabs-provider/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
This first example showcases the "stateful" or "uncontrolled" usage.

We just have to define an optional `defaultActive` property to specify
which tab we want to select by default, and then all the state management
will be handled by the component automatically.

```js
const { Tabs, TabList, TabPanel } = require('.');

<Tabs defaultActive="b">
{({ active }) => (
<>
<TabList>
{({ select, active }) => (
<>
<Button
onClick={() => select('a')}
importance={active === 'a' ? 'primary' : 'secondary'}
>
Tab A
</Button>{' '}
<Button
onClick={() => select('b')}
importance={active === 'b' ? 'primary' : 'secondary'}
>
Tab B
</Button>{' '}
<Button
onClick={() => select('c')}
importance={active === 'c' ? 'primary' : 'secondary'}
>
Tab C
</Button>
</>
)}
</TabList>
<div>
<TabPanel name="a">
{({ name }) => `Content of tab ${name.toUpperCase()}`}
</TabPanel>
<TabPanel name="b">
{({ name }) => `Content of tab ${name.toUpperCase()}`}
</TabPanel>
<TabPanel name="c">
{({ name }) => `Content of tab ${name.toUpperCase()}`}
</TabPanel>
</div>
</>
)}
</Tabs>;
```

Example of fully controlled usage, in this case, the state of the component
is controlled by the properties we provide it.

Specifically, we want to provide the `active` property, which defines the
currently active tab, and the `onSelect` callback that gets called anytime
the user changes tab.

```js
initialState = { active: 'b' };
const { Tabs, TabList, TabPanel } = require('.');
<>
<Button onClick={() => setState({ active: 'a' })}>Reset to A</Button>

<hr />

<Tabs active={state.active} onSelect={active => setState({ active })}>
{({ active }) => (
<>
<TabList>
{({ select, active }) => (
<React.Fragment>
<Button
onClick={() => select('a')}
importance={active === 'a' ? 'primary' : 'secondary'}
>
Tab A
</Button>{' '}
<Button
onClick={() => select('b')}
importance={active === 'b' ? 'primary' : 'secondary'}
>
Tab B
</Button>{' '}
<Button
onClick={() => select('c')}
importance={active === 'c' ? 'primary' : 'secondary'}
>
Tab C
</Button>
</React.Fragment>
)}
</TabList>
<div>
<TabPanel name="a">
{({ name }) => `Content of tab ${name.toUpperCase()}`}
</TabPanel>
<TabPanel name="b">
{({ name }) => `Content of tab ${name.toUpperCase()}`}
</TabPanel>
<TabPanel name="c">
{({ name }) => `Content of tab ${name.toUpperCase()}`}
</TabPanel>
</div>
</>
)}
</Tabs>
</>;
```
Loading

0 comments on commit a3200f4

Please sign in to comment.