From 53789e6569881e6e485226fc1d2dda75e750feb8 Mon Sep 17 00:00:00 2001 From: Alex Suevalov Date: Thu, 14 Feb 2019 15:52:55 +0100 Subject: [PATCH] feat(Tabs): Created Tabs component --- packages/forma-36-react-components/index.d.ts | 12 + .../CheckboxField/CheckboxField.stories.tsx | 5 +- .../src/components/Tabs/Tab.tsx | 86 ++++++ .../src/components/Tabs/TabPanel.tsx | 35 +++ .../src/components/Tabs/Tabs.css | 57 ++++ .../src/components/Tabs/Tabs.stories.tsx | 107 +++++++ .../src/components/Tabs/Tabs.test.tsx | 72 +++++ .../src/components/Tabs/Tabs.tsx | 47 +++ .../Tabs/__snapshots__/Tabs.test.tsx.snap | 198 +++++++++++++ .../src/components/Tabs/index.ts | 1 + .../forma-36-react-components/src/index.js | 268 +++++++++--------- 11 files changed, 754 insertions(+), 134 deletions(-) create mode 100644 packages/forma-36-react-components/src/components/Tabs/Tab.tsx create mode 100644 packages/forma-36-react-components/src/components/Tabs/TabPanel.tsx create mode 100644 packages/forma-36-react-components/src/components/Tabs/Tabs.css create mode 100644 packages/forma-36-react-components/src/components/Tabs/Tabs.stories.tsx create mode 100644 packages/forma-36-react-components/src/components/Tabs/Tabs.test.tsx create mode 100644 packages/forma-36-react-components/src/components/Tabs/Tabs.tsx create mode 100644 packages/forma-36-react-components/src/components/Tabs/__snapshots__/Tabs.test.tsx.snap create mode 100644 packages/forma-36-react-components/src/components/Tabs/index.ts diff --git a/packages/forma-36-react-components/index.d.ts b/packages/forma-36-react-components/index.d.ts index b2aa1895fa..22b3952733 100644 --- a/packages/forma-36-react-components/index.d.ts +++ b/packages/forma-36-react-components/index.d.ts @@ -26,6 +26,9 @@ /// /// /// +/// +/// +/// import * as React from 'react'; @@ -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; @@ -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; diff --git a/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.stories.tsx b/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.stories.tsx index 9ac101c9b2..1c1187cf08 100644 --- a/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.stories.tsx +++ b/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.stories.tsx @@ -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'; @@ -20,8 +20,7 @@ storiesOf('Components|CheckboxField', module) cropMarks: false, }), ) - // @ts-ignore - .addDecorator(StateDecorator(store)) + .addDecorator(StateDecorator(store) as StoryDecorator) .add( 'default', withInfo()(() => ( diff --git a/packages/forma-36-react-components/src/components/Tabs/Tab.tsx b/packages/forma-36-react-components/src/components/Tabs/Tab.tsx new file mode 100644 index 0000000000..cffe3a450e --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/Tab.tsx @@ -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 { + 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 {children}; + } else { + elementProps['aria-selected'] = selected; + elementProps['role'] = 'tab'; + elementProps['aria-controls'] = id; + return
{children}
; + } + } +} + +export default Tab; diff --git a/packages/forma-36-react-components/src/components/Tabs/TabPanel.tsx b/packages/forma-36-react-components/src/components/Tabs/TabPanel.tsx new file mode 100644 index 0000000000..23e22c9365 --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/TabPanel.tsx @@ -0,0 +1,35 @@ +import React, { Component } from 'react'; + +interface TabPanelProps + extends React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + > { + id: string; + extraClassNames?: string; + testId?: string; + children: React.ReactNode; +} + +export class TabPanel extends Component { + static defaultProps = { + testId: 'cf-ui-tab-panel', + }; + + render() { + const { testId, extraClassNames, children, id, ...rest } = this.props; + return ( +
+ {children} +
+ ); + } +} + +export default TabPanel; diff --git a/packages/forma-36-react-components/src/components/Tabs/Tabs.css b/packages/forma-36-react-components/src/components/Tabs/Tabs.css new file mode 100644 index 0000000000..6c7ef87667 --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/Tabs.css @@ -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; +} diff --git a/packages/forma-36-react-components/src/components/Tabs/Tabs.stories.tsx b/packages/forma-36-react-components/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 0000000000..ad49b4a9fc --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/Tabs.stories.tsx @@ -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', () => ( +
+ + { + action('onSelect')(id); + store.set({ selected: id }); + }} + > + First + + { + action('onSelect')(id); + store.set({ selected: id }); + }} + > + Second + + { + action('onSelect')(id); + store.set({ selected: id }); + }} + > + Third + + + {store.state.selected === 'first' && ( + content first tab + )} + {store.state.selected === 'second' && ( + content second tab + )} + {store.state.selected === 'third' && ( + content third tab + )} +
+ )) + .add('as navigation', () => ( + + { + action('onSelect')(id); + store.set({ selected: id }); + }} + > + First + + { + action('onSelect')(id); + store.set({ selected: id }); + }} + > + Second + + { + action('onSelect')(id); + store.set({ selected: id }); + }} + > + Third + + + )); diff --git a/packages/forma-36-react-components/src/components/Tabs/Tabs.test.tsx b/packages/forma-36-react-components/src/components/Tabs/Tabs.test.tsx new file mode 100644 index 0000000000..c9c88ca052 --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/Tabs.test.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { axe } from 'jest-axe'; +import Tabs from './Tabs'; +import Tab from './Tab'; +import TabPanel from './TabPanel'; + +it('renders the component', () => { + const output = mount( + + + First + Second + Third + + content first panel + content second panel + content third panel + , + ); + + expect(output).toMatchSnapshot(); +}); + +it('renders the component with role=navigation and an additional class name', () => { + const output = mount( + + + First + + + Second + + + Third + + , + ); + + expect(output).toMatchSnapshot(); +}); + +it('has no a11y issues', async () => { + const output = mount( +
+ + First + Second + Third + + content first panel + content second panel + content third panel +
, + ).html(); + + const output2 = mount( + + + First + + + Second + + + Third + + , + ).html(); + expect(await axe(output)).toHaveNoViolations(); + expect(await axe(output2)).toHaveNoViolations(); +}); diff --git a/packages/forma-36-react-components/src/components/Tabs/Tabs.tsx b/packages/forma-36-react-components/src/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..47216bfe13 --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/Tabs.tsx @@ -0,0 +1,47 @@ +import React, { Component, CSSProperties } from 'react'; +import cn from 'classnames'; + +const styles = require('./Tabs.css'); + +export interface TabsProps { + extraClassNames?: string; + children?: React.ReactNode; + testId?: string; + role?: 'navigation' | 'tablist'; + style?: CSSProperties; +} + +export class Tabs extends Component { + static defaultProps = { + testId: 'cf-ui-tabs', + role: 'tablist', + }; + + render() { + const { extraClassNames, children, testId, role, style } = this.props; + + const classNames = cn(styles.Tabs, extraClassNames); + + const elementProps = { + 'data-test-id': testId, + className: classNames, + style, + }; + + if (role === 'navigation') { + return ( + + ); + } + + return ( +
+ {children} +
+ ); + } +} + +export default Tabs; diff --git a/packages/forma-36-react-components/src/components/Tabs/__snapshots__/Tabs.test.tsx.snap b/packages/forma-36-react-components/src/components/Tabs/__snapshots__/Tabs.test.tsx.snap new file mode 100644 index 0000000000..5f81624d14 --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/__snapshots__/Tabs.test.tsx.snap @@ -0,0 +1,198 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders the component 1`] = ` +Array [ + +
+ +
+ First +
+
+ +
+ Second +
+
+ +
+ Third +
+
+
+
, + +
+ content first panel +
+
, + +
+ content second panel +
+
, + +
+ content third panel +
+
, +] +`; + +exports[`renders the component with role=navigation and an additional class name 1`] = ` + + + +`; diff --git a/packages/forma-36-react-components/src/components/Tabs/index.ts b/packages/forma-36-react-components/src/components/Tabs/index.ts new file mode 100644 index 0000000000..bc6749b1b2 --- /dev/null +++ b/packages/forma-36-react-components/src/components/Tabs/index.ts @@ -0,0 +1 @@ +export { default } from './Tabs'; diff --git a/packages/forma-36-react-components/src/index.js b/packages/forma-36-react-components/src/index.js index a29e8fcd1d..59b258aa4a 100644 --- a/packages/forma-36-react-components/src/index.js +++ b/packages/forma-36-react-components/src/index.js @@ -1,131 +1,137 @@ -import TextLink from './components/TextLink/TextLink'; -import Icon from './components/Icon/Icon'; -import Pill from './components/Pill/Pill'; -import HelpText from './components/HelpText/HelpText'; -import FormLabel from './components/FormLabel/FormLabel'; -import ValidationMessage from './components/ValidationMessage/ValidationMessage'; -import TextInput from './components/TextInput/TextInput'; -import TextField from './components/TextField/TextField'; -import Textarea from './components/Textarea/Textarea'; -import CopyButton from './components/CopyButton/CopyButton'; -import Card from './components/Card/Card'; -import Tooltip from './components/Tooltip/Tooltip'; -import ReferenceCard from './components/Card/ReferenceCard'; -import IconButton from './components/IconButton/IconButton'; -import CheckboxField from './components/CheckboxField/CheckboxField'; -import Checkbox from './components/Checkbox/Checkbox'; -import TabFocusTrap from './components/TabFocusTrap/TabFocusTrap'; -import Spinner from './components/Spinner/Spinner'; -import Button from './components/Button/Button'; -import EditorToolbar from './components/EditorToolbar/EditorToolbar'; -import EditorToolbarButton from './components/EditorToolbar/EditorToolbarButton'; -import Dropdown from './components/Dropdown/Dropdown'; -import DropdownListItem from './components/Dropdown/DropdownListItem'; -import DropdownList from './components/Dropdown/DropdownList'; -import EditorToolbarDivider from './components/EditorToolbar/EditorToolbarDivider'; -import SelectField from './components/SelectField'; -import Select from './components/Select/Select'; -import Option from './components/Select/Option'; -import InlineReferenceCard from './components/Card/InlineReferenceCard'; -import Illustration from './components/Illustration'; -import Table from './components/Table/Table'; -import TableBody from './components/Table/TableBody'; -import TableCell from './components/Table/TableCell'; -import TableHead from './components/Table/TableHead'; -import TableRow from './components/Table/TableRow'; -import LineChart from './components/LineChart/LineChart'; -import ToggleButton from './components/ToggleButton/ToggleButton'; -import AssetCard from './components/Card/AssetCard'; -import Asset from './components/Asset'; -import Tag from './components/Tag'; -import Heading from './components/Typography/Heading'; -import InViewport from './components/InViewport'; -import Modal from './components/Modal/Modal'; -import ModalConfirm from './components/Modal/ModalConfirm'; -import FieldGroup from './components/Form/FieldGroup'; -import Form from './components/Form/Form'; -import Note from './components/Note'; -import Notification from './components/Notification'; -import ControlledInput from './components/ControlledInput'; -import ControlledInputField from './components/ControlledInputField'; -import RadioButtonField from './components/RadioButtonField'; -import Subheading from './components/Typography/Subheading'; -import SectionHeading from './components/Typography/SectionHeading'; -import Paragraph from './components/Typography/Paragraph'; -import DisplayText from './components/Typography/DisplayText'; -import List from './components/List/List'; -import ListItem from './components/List/ListItem'; -import SkeletonBodyText from './components/Skeleton/SkeletonBodyText'; -import SkeletonContainer from './components/Skeleton/SkeletonContainer'; -import SkeletonDisplayText from './components/Skeleton/SkeletonDisplayText'; -import SkeletonText from './components/Skeleton/SkeletonText'; -import SkeletonImage from './components/Skeleton/SkeletonImage'; -// -- Add imports above this line (required by plopfile.js) -- -// The above line is used as a insert marker when autogenerating components with `yarn add-component` - -module.exports = { - TextLink, - Icon, - Pill, - HelpText, - FormLabel, - ValidationMessage, - TextInput, - TextField, - Textarea, - CopyButton, - Card, - Tooltip, - ReferenceCard, - IconButton, - Illustration, - CheckboxField, - Checkbox, - TabFocusTrap, - Spinner, - Button, - EditorToolbar, - EditorToolbarButton, - Dropdown, - DropdownList, - DropdownListItem, - EditorToolbarDivider, - Select, - SelectField, - Option, - InlineReferenceCard, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - LineChart, - ToggleButton, - AssetCard, - Asset, - Tag, - Heading, - InViewport, - Modal, - ModalConfirm, - FieldGroup, - Form, - Note, - Notification, - ControlledInput, - ControlledInputField, - RadioButtonField, - Subheading, - SectionHeading, - Paragraph, - DisplayText, - List, - ListItem, - SkeletonBodyText, - SkeletonContainer, - SkeletonDisplayText, - SkeletonText, - SkeletonImage, - // -- Add exports above this line (required by plopfile.js) -- - // The above line is used as a insert marker when autogenerating components with `yarn add-component` -}; +import TextLink from './components/TextLink/TextLink'; +import Icon from './components/Icon/Icon'; +import Pill from './components/Pill/Pill'; +import HelpText from './components/HelpText/HelpText'; +import FormLabel from './components/FormLabel/FormLabel'; +import ValidationMessage from './components/ValidationMessage/ValidationMessage'; +import TextInput from './components/TextInput/TextInput'; +import TextField from './components/TextField/TextField'; +import Textarea from './components/Textarea/Textarea'; +import CopyButton from './components/CopyButton/CopyButton'; +import Card from './components/Card/Card'; +import Tooltip from './components/Tooltip/Tooltip'; +import ReferenceCard from './components/Card/ReferenceCard'; +import IconButton from './components/IconButton/IconButton'; +import CheckboxField from './components/CheckboxField/CheckboxField'; +import Checkbox from './components/Checkbox/Checkbox'; +import TabFocusTrap from './components/TabFocusTrap/TabFocusTrap'; +import Spinner from './components/Spinner/Spinner'; +import Button from './components/Button/Button'; +import EditorToolbar from './components/EditorToolbar/EditorToolbar'; +import EditorToolbarButton from './components/EditorToolbar/EditorToolbarButton'; +import Dropdown from './components/Dropdown/Dropdown'; +import DropdownListItem from './components/Dropdown/DropdownListItem'; +import DropdownList from './components/Dropdown/DropdownList'; +import EditorToolbarDivider from './components/EditorToolbar/EditorToolbarDivider'; +import SelectField from './components/SelectField'; +import Select from './components/Select/Select'; +import Option from './components/Select/Option'; +import InlineReferenceCard from './components/Card/InlineReferenceCard'; +import Illustration from './components/Illustration'; +import Table from './components/Table/Table'; +import TableBody from './components/Table/TableBody'; +import TableCell from './components/Table/TableCell'; +import TableHead from './components/Table/TableHead'; +import TableRow from './components/Table/TableRow'; +import LineChart from './components/LineChart/LineChart'; +import ToggleButton from './components/ToggleButton/ToggleButton'; +import AssetCard from './components/Card/AssetCard'; +import Asset from './components/Asset'; +import Tag from './components/Tag'; +import Heading from './components/Typography/Heading'; +import InViewport from './components/InViewport'; +import Modal from './components/Modal/Modal'; +import ModalConfirm from './components/Modal/ModalConfirm'; +import FieldGroup from './components/Form/FieldGroup'; +import Form from './components/Form/Form'; +import Note from './components/Note'; +import Notification from './components/Notification'; +import ControlledInput from './components/ControlledInput'; +import ControlledInputField from './components/ControlledInputField'; +import RadioButtonField from './components/RadioButtonField'; +import Subheading from './components/Typography/Subheading'; +import SectionHeading from './components/Typography/SectionHeading'; +import Paragraph from './components/Typography/Paragraph'; +import DisplayText from './components/Typography/DisplayText'; +import List from './components/List/List'; +import ListItem from './components/List/ListItem'; +import SkeletonBodyText from './components/Skeleton/SkeletonBodyText'; +import SkeletonContainer from './components/Skeleton/SkeletonContainer'; +import SkeletonDisplayText from './components/Skeleton/SkeletonDisplayText'; +import SkeletonText from './components/Skeleton/SkeletonText'; +import SkeletonImage from './components/Skeleton/SkeletonImage'; +import Tabs from './components/Tabs'; +import Tab from './components/Tabs/Tab'; +import TabPanel from './components/Tabs/TabPanel'; +// -- Add imports above this line (required by plopfile.js) -- +// The above line is used as a insert marker when autogenerating components with `yarn add-component` + +module.exports = { + TextLink, + Icon, + Pill, + HelpText, + FormLabel, + ValidationMessage, + TextInput, + TextField, + Textarea, + CopyButton, + Card, + Tooltip, + ReferenceCard, + IconButton, + Illustration, + CheckboxField, + Checkbox, + TabFocusTrap, + Spinner, + Button, + EditorToolbar, + EditorToolbarButton, + Dropdown, + DropdownList, + DropdownListItem, + EditorToolbarDivider, + Select, + SelectField, + Option, + InlineReferenceCard, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + LineChart, + ToggleButton, + AssetCard, + Asset, + Tag, + Heading, + InViewport, + Modal, + ModalConfirm, + FieldGroup, + Form, + Note, + Notification, + ControlledInput, + ControlledInputField, + RadioButtonField, + Subheading, + SectionHeading, + Paragraph, + DisplayText, + List, + ListItem, + SkeletonBodyText, + SkeletonContainer, + SkeletonDisplayText, + SkeletonText, + SkeletonImage, + Tabs, + Tab, + TabPanel, + // -- Add exports above this line (required by plopfile.js) -- + // The above line is used as a insert marker when autogenerating components with `yarn add-component` +};