diff --git a/src/components/Nav/Nav.css.ts b/src/components/Nav/Nav.css.ts
index 0f6e5c2a7..bb7ab0031 100644
--- a/src/components/Nav/Nav.css.ts
+++ b/src/components/Nav/Nav.css.ts
@@ -6,6 +6,7 @@ import baseStyles from '../../styles/resets/baseStyles.css'
export const config = {
indicatorTransition: 'opacity 100ms ease',
+ padding: '10px',
}
export const NavUI = styled('nav', { pure: false })`
@@ -24,7 +25,7 @@ export const ListUI = styled('ul', { pure: false })`
`
export const ItemUI = styled('li', { pure: false })`
- padding: 0 10px;
+ padding: 0;
transform: translateZ(0);
&.is-disabled {
@@ -36,6 +37,7 @@ export const ItemUI = styled('li', { pure: false })`
color: ${getColor('charcoal.200')};
display: block;
text-decoration: none !important;
+ padding: 0 ${config.padding};
&:hover {
color: ${getColor('charcoal.300')};
@@ -81,13 +83,13 @@ export const ErrorWrapperUI = styled(Flexy.Item)`
export const IndicatorUI = styled('div')`
background: ${getColor('blue.500')};
border-radius: 9999px;
- bottom: 0;
+ bottom: -1px;
height: 2px;
opacity: 0;
left: 0;
+ right: 0;
position: absolute;
transition: ${config.indicatorTransition};
- width: 100%;
will-change: opacity;
${({ isActive }) =>
diff --git a/src/components/TabBar/README.md b/src/components/TabBar/README.md
new file mode 100644
index 000000000..746678c96
--- /dev/null
+++ b/src/components/TabBar/README.md
@@ -0,0 +1,28 @@
+# TabBar
+
+This component is provides a Tab-like component with support for [react-router](https://github.com/ReactTraining/react-router). The [TabBar.Item](./Item.md) component is a simple link to the [Nav.Item](../Nav/docs/Item.md) component.
+
+TabBar is a wrapper containing a [Toolbar](../Toolbar/README.md), a [Nav](../Nav/README.md) and a secondary (left or right aligned) content placeholder.
+
+## Example
+
+```jsx
+
+
+ Home
+
+
+ About
+
+
+ Contact
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| ---------- | -------- | ------- | --------------------------------------------------------------------------------------------------- |
+| className | `string` | | The className of the component. |
+| secContent | `any` | | A right or left aligned placeholder that will be render inside the toolbar as the secondary content |
diff --git a/src/components/TabBar/TabBar.css.ts b/src/components/TabBar/TabBar.css.ts
new file mode 100644
index 000000000..5220ec332
--- /dev/null
+++ b/src/components/TabBar/TabBar.css.ts
@@ -0,0 +1,69 @@
+import styled from '../styled'
+import Toolbar from '../Toolbar'
+
+import { getColor } from '../../styles/utilities/color'
+import baseStyles from '../../styles/resets/baseStyles.css'
+
+const getAlignment = align => {
+ switch (align) {
+ case 'center':
+ return 'center'
+ default:
+ return 'flex-start'
+ }
+}
+
+const getDirection = align => {
+ switch (align) {
+ case 'right':
+ return 'row-reverse'
+ default:
+ return 'row'
+ }
+}
+
+export const TabBarUI = styled('nav')`
+ --BlueConfigGlobalFontSize: 14px;
+ ${baseStyles};
+ display: flex;
+ margin: 0 auto;
+
+ .c-Toolbar {
+ justify-content: ${props => getAlignment(props.align)};
+ flex-direction: ${props => getDirection(props.align)};
+ }
+
+ .c-ToolbarWrapper {
+ width: 100%;
+ }
+`
+
+export const SecContentUI = styled(Toolbar.Item)`
+ font-size: 14px;
+ color: ${getColor('charcoal.200')};
+ display: flex;
+ align-items: center;
+ margin-bottom: calc(8px * -1);
+
+ &.is-defaultItem {
+ margin-left: ${props => (props.align !== 'right' ? 'auto' : '0')};
+ margin-right: ${props => (props.align === 'right' ? 'auto' : '0')};
+ }
+
+ b,
+ strong {
+ font-weight: 500;
+ color: ${getColor('charcoal.600')};
+ }
+`
+
+// adjust margin to have to active bar hover the toolbar border
+export const ToolbarUI = styled(Toolbar)`
+ width: 100%;
+ padding-left: 0;
+ padding-right: 0;
+
+ &.is-placement-top.is-size-sm .c-Nav {
+ /* margin-bottom: calc(9px * -1); */
+ }
+`
diff --git a/src/components/TabBar/TabBar.tsx b/src/components/TabBar/TabBar.tsx
new file mode 100644
index 000000000..028aabc1d
--- /dev/null
+++ b/src/components/TabBar/TabBar.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react'
+import propConnect from '../PropProvider/propConnect'
+import getValidProps from '@helpscout/react-utils/dist/getValidProps'
+import Nav from '../Nav'
+import Toolbar from '../Toolbar'
+import { classNames } from '../../utilities/classNames'
+import { noop } from '../../utilities/other'
+import { TabBarUI, SecContentUI, ToolbarUI } from './TabBar.css'
+import { COMPONENT_KEY } from './TabBar.utils'
+
+export interface Props {
+ className?: string
+ children?: any
+ innerRef: (node: HTMLElement) => void
+ secContent?: any
+ align?: 'left' | 'center' | 'right'
+}
+
+export class TabBar extends React.Component {
+ static className = 'c-TabBar'
+ static defaultProps = {
+ innerRef: noop,
+ align: 'left',
+ }
+
+ static Item = Nav.Item
+
+ getClassName() {
+ const { className } = this.props
+ return classNames(TabBar.className, className)
+ }
+
+ render() {
+ const { children, innerRef, secContent, align, ...rest } = this.props
+
+ return (
+
+
+
+
+
+ {secContent && (
+ {secContent}
+ )}
+
+
+ )
+ }
+}
+
+const PropConnectedComponent = propConnect(COMPONENT_KEY, { pure: false })(
+ TabBar
+)
+
+export default PropConnectedComponent
diff --git a/src/components/TabBar/TabBar.utils.ts b/src/components/TabBar/TabBar.utils.ts
new file mode 100644
index 000000000..d9c1a42e9
--- /dev/null
+++ b/src/components/TabBar/TabBar.utils.ts
@@ -0,0 +1 @@
+export const COMPONENT_KEY = 'TabBar'
diff --git a/src/components/TabBar/__tests__/TabBar.test.tsx b/src/components/TabBar/__tests__/TabBar.test.tsx
new file mode 100644
index 000000000..083e37a35
--- /dev/null
+++ b/src/components/TabBar/__tests__/TabBar.test.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react'
+import { mount, render } from 'enzyme'
+import { TabBar } from '../TabBar'
+import { SecContentUI, TabBarUI } from '../TabBar.css'
+import Item from '../../Nav/Nav.Item'
+import Toolbar from '../../Toolbar'
+
+describe('className', () => {
+ test('Has default className', () => {
+ const wrapper = render()
+
+ expect(wrapper.hasClass('c-TabBar')).toBeTruthy()
+ })
+
+ test('Can render custom className', () => {
+ const customClassName = 'blue'
+ const wrapper = render()
+
+ expect(wrapper.hasClass(customClassName)).toBeTruthy()
+ })
+})
+
+describe('Sub-components', () => {
+ test('Has Item sub-component', () => {
+ expect(TabBar.Item).toBe(Item)
+ })
+})
+
+describe('Render', () => {
+ test('Has a Toolbar component', () => {
+ const wrapper = mount()
+ expect(wrapper.find(Toolbar).length).toBeTruthy()
+ })
+})
+
+describe('Align', () => {
+ test('Sets align prop to TabBarUI', () => {
+ const align = 'right'
+ const wrapper = mount()
+ expect(wrapper.find(TabBarUI).prop('align')).toBe(align)
+ })
+
+ test('Sets align prop to childrens', () => {
+ const align = 'right'
+ const wrapper = mount()
+ expect(wrapper.find(TabBarUI).prop('align')).toBe(align)
+ expect(wrapper.find(SecContentUI).prop('align')).toBe(align)
+ })
+})
+
+describe('Secondary content', () => {
+ test('Renders nothing if prop is empty', () => {
+ const wrapper = mount()
+ expect(wrapper.find(SecContentUI).length).toBeFalsy()
+ })
+ test('Renders secondary content text', () => {
+ const text = 'sec content text'
+ const wrapper = mount()
+ expect(wrapper.find(SecContentUI).text()).toBe(text)
+ })
+
+ test('Renders secondary content child', () => {
+ const text = 'this is a test'
+ const node = {text}
+ const wrapper = mount()
+ expect(wrapper.find(SecContentUI).text()).toBe(text)
+ })
+})
diff --git a/src/components/TabBar/docs/TabBar.md b/src/components/TabBar/docs/TabBar.md
new file mode 100644
index 000000000..0c3622ab4
--- /dev/null
+++ b/src/components/TabBar/docs/TabBar.md
@@ -0,0 +1,25 @@
+# TabBar
+
+This component is provides navigation interactions with support for [react-router](https://github.com/ReactTraining/react-router). The [TabBar.Item](./Item.md) component is a simple link to the [Nav.Item](../Nav/docs/Item.md) component.
+
+## Example
+
+```jsx
+
+
+ Home
+
+
+ About
+
+
+ Contact
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| --------- | -------- | ------- | ------------------------------- |
+| className | `string` | | The className of the component. |
diff --git a/src/components/TabBar/index.ts b/src/components/TabBar/index.ts
new file mode 100644
index 000000000..58ee39610
--- /dev/null
+++ b/src/components/TabBar/index.ts
@@ -0,0 +1,3 @@
+import TabBar from './TabBar'
+
+export default TabBar
diff --git a/src/components/index.js b/src/components/index.js
index 68b1dd51d..ce26fa2e6 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -107,6 +107,7 @@ export { default as Text } from './Text'
export { default as ThemeProvider } from './ThemeProvider'
export { default as Timeline } from './Timeline'
export { default as Timestamp } from './Timestamp'
+export { default as TabBar } from './TabBar'
export { default as Toolbar } from './Toolbar'
export { default as Tooltip } from './Tooltip'
export { default as Truncate } from './Truncate'
diff --git a/stories/TabBar.stories.js b/stories/TabBar.stories.js
new file mode 100644
index 000000000..e88aca71c
--- /dev/null
+++ b/stories/TabBar.stories.js
@@ -0,0 +1,126 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { TabBar } from '../src/index.js'
+import Dropdown from '../src/components/Dropdown/V2/'
+import Button from '../src/components/Button'
+
+import { MemoryRouter as Router, Route } from 'react-router-dom'
+import { createSpec, faker } from '@helpscout/helix'
+
+import { withAktiv, withHsApp } from './utils'
+
+import {
+ withKnobs,
+ boolean,
+ number,
+ text,
+ select,
+} from '@storybook/addon-knobs'
+
+const ItemSpec = createSpec({
+ id: faker.random.uuid(),
+ label: faker.name.firstName(),
+ value: faker.name.firstName(),
+})
+
+const routerDecorator = storyFn => {
+ return {storyFn()}
+}
+
+const stories = storiesOf('TabBar', module)
+stories.addDecorator(withKnobs)
+stories.addDecorator(withAktiv)
+stories.addDecorator(routerDecorator)
+
+const renderTabBarItem = () => {
+ const itemHome = text('itemHomeText', 'Home')
+ const itemOne = text('itemOneText', 'One')
+ const itemTwo = text('itemTwoText', 'Two')
+ const itemThree = text('itemThreeText', 'Three')
+ const itemThreeError = text('itemThreeError', 'Something went wrong')
+ const itemFour = text('itemFourText', 'Four')
+ const itemFourDisabled = boolean('itemFourDisabled', true)
+
+ return [
+
+ {itemHome}
+ ,
+
+ {itemOne}
+ ,
+
+ {itemTwo}
+ ,
+
+ {itemThree}
+ ,
+
+ {itemFour}
+ ,
+ ]
+}
+
+const dropdownContent = (
+
+ Dropdown
+
+ }
+ />
+)
+
+stories.add('default', () => {renderTabBarItem()})
+
+stories.add('center align', () => (
+ {renderTabBarItem()}
+))
+
+stories.add('right align', () => (
+ {renderTabBarItem()}
+))
+
+stories.add('with secondary content', () => {
+ const secContent = (
+
+ 13,456 items
+
+ )
+ return {renderTabBarItem()}
+})
+
+stories.add('right align with secondary content', () => {
+ const secContent = (
+
+ 13,456 items
+
+ )
+ return (
+
+ {renderTabBarItem()}
+
+ )
+})
+
+stories.add('with secondary content dropdown', () => {
+ return {renderTabBarItem()}
+})
+
+const storiesHsApp = storiesOf('TabBar/HS App', module)
+storiesHsApp.addDecorator(withKnobs)
+storiesHsApp.addDecorator(withHsApp)
+storiesHsApp.addDecorator(routerDecorator)
+storiesHsApp.add('with secondary content', () => {
+ const secContent = (
+
+ 13,456 items
+
+ )
+ return {renderTabBarItem()}
+})
+
+storiesHsApp.add('with secondary dropdown', () => {
+ return {renderTabBarItem()}
+})