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

feat(Tabs): Created Tabs component #93

Merged
merged 1 commit into from
Feb 18, 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
feat(Tabs): Created Tabs component
  • Loading branch information
Alex Suevalov committed Feb 18, 2019
commit 53789e6569881e6e485226fc1d2dda75e750feb8
12 changes: 12 additions & 0 deletions packages/forma-36-react-components/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
/// <reference path="./dist/components/Modal/Modal/Modal.d.ts" />
/// <reference path="./dist/components/Modal/ModalConfirm/ModalConfirm.d.ts" />
/// <reference path="./dist/components/Notification/index.d.ts" />
/// <reference path="./dist/components/Tabs/Tabs.d.ts" />
/// <reference path="./dist/components/Tabs/Tab.d.ts" />
/// <reference path="./dist/components/Tabs/TabPanel.d.ts" />

import * as React from 'react';

Expand Down Expand Up @@ -57,6 +60,9 @@ import FormComponent from './dist/components/Form/Form/Form';
import ModalComponent from './dist/components/Modal/Modal/Modal';
import ModalConfirmComponent from './dist/components/Modal/ModalConfirm/ModalConfirm';
import NotificationAPI from './dist/components/Notification/index';
import TabsComponent from './dist/components/Tabs/Tabs';
import TabComponent from './dist/components/Tabs/Tab';
import TabPanelComponent from './dist/components/Tabs/TabPanel';

export const Button: typeof ButtonComponent;
export const Spinner: typeof SpinnerComponent;
Expand Down Expand Up @@ -85,7 +91,13 @@ export const FieldGroup: typeof FieldGroupComponent;
export const Form: typeof FormComponent;
export const Modal: typeof ModalComponent;
export const ModalConfirm: typeof ModalConfirmComponent;
<<<<<<< HEAD
export const Notification: typeof NotificationAPI;
=======
export const Tabs: typeof TabsComponent;
export const Tab: typeof TabComponent;
export const TabPanel: typeof TabPanelComponent;
>>>>>>> feat(Tabs): Created Tabs component

export interface CopyButtonProps {
extraClassNames?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { storiesOf, StoryDecorator } from '@storybook/react';
import { text, boolean } from '@storybook/addon-knobs';
import { StateDecorator, Store } from '@sambego/storybook-state';
import { host } from 'storybook-host';
Expand All @@ -20,8 +20,7 @@ storiesOf('Components|CheckboxField', module)
cropMarks: false,
}),
)
// @ts-ignore
.addDecorator(StateDecorator(store))
.addDecorator(StateDecorator(store) as StoryDecorator)
.add(
'default',
withInfo()(() => (
Expand Down
86 changes: 86 additions & 0 deletions packages/forma-36-react-components/src/components/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { Component, CSSProperties } from 'react';
import classNames from 'classnames';

const styles = require('./Tabs.css');

interface TabProps {
id: string;
onSelect?: Function;
selected?: boolean;
href?: string;
target?: string;
disabled?: boolean;
tabIndex?: number;
style?: CSSProperties;
extraClassNames?: string;
testId?: string;
children: React.ReactNode;
}

export class Tab extends Component<TabProps> {
static defaultProps = {
onSelect: () => {},
onKeyPress: () => {},
selected: false,
disabled: false,
testId: 'cf-ui-tab',
tabIndex: 0,
};

onClick = () => {
this.props.onSelect(this.props.id);
};

onKeyPress = e => {
if (e.key === 'Enter') {
this.props.onSelect(this.props.id);
e.preventDefault();
}
};

render() {
const {
id,
disabled,
extraClassNames,
href,
style,
testId,
selected,
children,
tabIndex,
} = this.props;
let elementProps = {
className: classNames(
styles.Tab,
{
[styles['Tab__selected']]: selected,
},
extraClassNames,
),
onClick: this.onClick,
onKeyPress: this.onKeyPress,
style: style,
'data-test-id': testId,
tabIndex,
};

if (disabled) {
elementProps['aria-disabled'] = true;
}
if (href) {
elementProps['href'] = href;
if (selected) {
elementProps['aria-current'] = 'page';
}
return <a {...elementProps}>{children}</a>;
} else {
elementProps['aria-selected'] = selected;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is clever :)

elementProps['role'] = 'tab';
elementProps['aria-controls'] = id;
return <div {...elementProps}>{children}</div>;
}
}
}

export default Tab;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { Component } from 'react';

interface TabPanelProps
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need this? Just curious.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because API allows using (with ...rest) all HTML properties that you would normally use with <div />

id: string;
extraClassNames?: string;
testId?: string;
children: React.ReactNode;
}

export class TabPanel extends Component<TabPanelProps> {
static defaultProps = {
testId: 'cf-ui-tab-panel',
};

render() {
const { testId, extraClassNames, children, id, ...rest } = this.props;
return (
<div
{...rest}
id={id}
role="tabpanel"
data-test-id={testId}
className={extraClassNames}
>
{children}
</div>
);
}
}

export default TabPanel;
57 changes: 57 additions & 0 deletions packages/forma-36-react-components/src/components/Tabs/Tabs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@import 'settings/colors';
@import 'settings/typography';
@import 'settings/transitions';
@import 'settings/dimensions';

.Tabs {
display: flex;
}

.Tabs .Tab {
margin-right: var(--spacing-l);
}

.Tab {
white-space: nowrap;
color: var(--color-text-dark);
position: relative;
cursor: pointer;
padding: 0 var(--spacing-s);
height: 56px;
line-height: 56px;
font-size: var(--font-size-m);
font-family: var(--font-stack-primary);
font-weight: var(--font-weight-normal);
outline: none;
text-decoration: none;
}

.Tabs .Tab:last-child {
margin-right: 0;
}

.Tab__selected {
font-weight: var(--font-weight-demi-bold);
}

.Tab:before {
content: '';
position: absolute;
background: var(--color-primary);
opacity: 0;
bottom: 0;
left: 0;
right: 0;
height: 3px;
}

.Tab:hover:before,
.Tab:focus:before {
opacity: 0.5;
}

.Tab__selected:hover:before,
.Tab__selected:focus:before,
.Tab__selected:before {
opacity: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import { storiesOf, StoryDecorator } from '@storybook/react';
import { StateDecorator, Store } from '@sambego/storybook-state';
import { action } from '@storybook/addon-actions';
import { host } from 'storybook-host';
import { text } from '@storybook/addon-knobs';
import { withInfo } from '@storybook/addon-info';

import Tabs from './Tabs';
import Tab from './Tab';
import TabPanel from './TabPanel';

const store = new Store({
selected: 'first',
});

storiesOf('Components|Tabs', module)
.addDecorator((story, context) => withInfo()(story)(context))
.addDecorator(
host({
align: 'center middle',
cropMarks: false,
}),
)
.addDecorator(StateDecorator(store) as StoryDecorator)
.add('default', () => (
<div>
<Tabs extraClassNames={text('extraClassNames', '')}>
<Tab
id="first"
selected={store.state.selected === 'first'}
onSelect={id => {
action('onSelect')(id);
store.set({ selected: id });
}}
>
First
</Tab>
<Tab
id="second"
selected={store.state.selected === 'second'}
onSelect={id => {
action('onSelect')(id);
store.set({ selected: id });
}}
>
Second
</Tab>
<Tab
id="third"
selected={store.state.selected === 'third'}
onSelect={id => {
action('onSelect')(id);
store.set({ selected: id });
}}
>
Third
</Tab>
</Tabs>
{store.state.selected === 'first' && (
<TabPanel id="first">content first tab</TabPanel>
)}
{store.state.selected === 'second' && (
<TabPanel id="second">content second tab</TabPanel>
)}
{store.state.selected === 'third' && (
<TabPanel id="third">content third tab</TabPanel>
)}
</div>
))
.add('as navigation', () => (
<Tabs role="navigation" extraClassNames={text('extraClassNames', '')}>
<Tab
id="first"
href="https://contentful.com"
selected={store.state.selected === 'first'}
onSelect={id => {
action('onSelect')(id);
store.set({ selected: id });
}}
>
First
</Tab>
<Tab
id="second"
href="https://contentful.com"
selected={store.state.selected === 'second'}
onSelect={id => {
action('onSelect')(id);
store.set({ selected: id });
}}
>
Second
</Tab>
<Tab
id="third"
href="https://contentful.com"
selected={store.state.selected === 'third'}
onSelect={id => {
action('onSelect')(id);
store.set({ selected: id });
}}
>
Third
</Tab>
</Tabs>
));
Loading