Skip to content

Commit

Permalink
Merge e109e4a into e4e9fb4
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinengle committed Sep 3, 2018
2 parents e4e9fb4 + e109e4a commit 7f9a12e
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 0 deletions.
103 changes: 103 additions & 0 deletions react/Tab.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import PropTypes from 'prop-types'
import React from 'react'
import {kill, pickRest} from '../lib/utils'
import Icon from './Icon'

// Tab
export const Tab = (props) => {
const [mods, {children, icon, index, title, ...rest}] = pickRest(props, ['active', 'disabled'])
return (
<div block='tab' mods={mods} {...rest}>
{!!title && <div block='tab' elem='title'>{title}</div>}
{!!icon && <div block='tab' elem='icon'><Icon k={icon} /></div>}
</div>
)
}

Tab.propTypes = {
active: PropTypes.bool,
children: PropTypes.any.isRequired,
disabled: PropTypes.bool,
icon: PropTypes.string,
onClick: PropTypes.func,
title: PropTypes.string
}

// Tabs
export class Tabs extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired
}

state = {
index: 0,
left: 0,
width: 149
}

tabs = null

componentDidMount () {
if (this.tabs && this.tabs.children) {
this.tabs.children[0].children[0].click()
}
}

handleSelect = (ev, index) => {
kill(ev)
const state = {index}

if (ev && ev.target) {
let el = ev.target
if (!el.classList.contains('tab__icon') && !el.classList.contains('tab__title')) {
el = el.parentNode
}
let rect = el.getBoundingClientRect()
state.left = rect.left
state.width = Math.abs(rect.right - rect.left)
}

this.setState(state)
}

render () {
const [mods, {children, ...rest}] = pickRest(this.props, [])
let tabs = children
if (!Array.isArray(tabs)) tabs = [tabs]

// Clone tabs and add props
let content = null
let hasIcon = false
tabs = tabs.map((t, i) => {
if (!hasIcon && !!t.props.icon) hasIcon = true
if (i === this.state.index) content = t.props.children
return React.cloneElement(t, {
active: i === this.state.index,
key: i,
onClick: t.props.disabled ? undefined : ev => this.handleSelect(ev, i)
})
})

// Setup bar style
const barStyle = {
left: this.state.left,
top: (hasIcon ? 70 : 50) - 1,
width: this.state.width
}

return (
<div block='tabs' mods={mods} {...rest}>
<div block='tabs' elem='list'>
<span block='tabs_list' elem='items' ref={i => { this.tabs = i }}>
{tabs}
<div block='tabs' elem='clear' />
</span>
</div>
<div block='tabs' elem='content'>{content}</div>
<div block='tabs' elem='bar' style={barStyle} />
</div>
)
}
}

export default {Tab, Tabs}
1 change: 1 addition & 0 deletions sass/_library.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@import 'inc/navbar';
@import 'inc/panel';
@import 'inc/reset';
@import 'inc/tab';
@import 'inc/text';
@import 'inc/tooltip';
@import 'inc/util';
76 changes: 76 additions & 0 deletions sass/inc/_tab.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

.tabs {
display: block;
margin: 0;
padding: 0;
position: relative;
text-align: center;

&__bar {
background-color: transparent;
border-bottom: 2px solid $primary;
height: 0px;
position: absolute;
top: 50px;
transition: left .2s ease;
-webkit-transition: left .2s ease;
}

&__clear { clear: both; }

&__content {
padding: 32px;
text-align: left;
width: 100%;
}

&__list {
background-color: $white;
border-radius: 5px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
position: relative;

&__items {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
}
}

.tab {
display: inline-block;

$self: &;

&--active {
#{$self}__title { font-weight: bold; }
}

&--disabled {
#{$self}__content { display: none; }

#{$self}__icon, #{$self}__title {
color: $secondary;
cursor: not-allowed;
}
}

&__icon {
cursor: pointer;
font-size: 24px;
height: 70px;
padding: 23px 63px;

&:hover { font-weight: bold; }
}

&__title {
cursor: pointer;
font-size: 16px;
line-height: 21px;
padding: 15px 42px;

&:hover { font-weight: bold; }
}
}
77 changes: 77 additions & 0 deletions test/Tab.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* global describe, it */
import React from 'react'
import expect from 'must'
import { mount, shallow } from 'enzyme'
import {Tab, Tabs} from '../react/Tab'

const wrapper = shallow(<Tab title='Test'>Testing</Tab>)
const wrapper2 = shallow(<Tab disabled icon='home'>Testing</Tab>)

describe('<Tab />', () => {
it('renders as a div', () => {
expect(wrapper.is('div')).to.be.true()
})

it('renders title element', () => {
expect(wrapper.find('.tab__title')).to.have.length(1)
})

it('renders icon element', () => {
expect(wrapper2.find('.tab__icon')).to.have.length(1)
})

it('renders disabled element', () => {
const wrapper4 = shallow(
<Tabs>
<Tab title='A'>Test 1</Tab>
<Tab disabled title='B'>Test 2</Tab>
</Tabs>
)
expect(wrapper4.childAt(0).childAt(0).childAt(1).dive().hasClass('tab--disabled')).to.be.true()
})
})

const wrapper3 = shallow(
<Tabs>
<Tab title='Test'>Test 1</Tab>
<Tab icon='home'>Test 2</Tab>
</Tabs>
)

describe('<Tabs />', () => {
it('renders as a div', () => {
expect(wrapper3.is('div')).to.be.true()
})

it('renders as tabs', () => {
expect(wrapper3.hasClass('tabs')).to.be.true()
expect(wrapper3.find('.tabs__list')).to.have.length(1)
})

it('renders first tab as active', () => {
expect(wrapper3.childAt(0).childAt(0).childAt(0).dive().hasClass('tab--active')).to.be.true()
})

it('changes second tab to active on click', () => {
wrapper3.childAt(0).childAt(0).childAt(1).dive().simulate('click')
expect(wrapper3.childAt(0).childAt(0).childAt(1).dive().hasClass('tab--active')).to.be.true()
})

it('mounts the tree and selects first tab', () => {
const wrapper4 = mount(
<Tabs>
<Tab title='A'>Test 1</Tab>
<Tab icon='home'>Test 2</Tab>
</Tabs>
)
wrapper4.mount().childAt(0).childAt(0).childAt(0).childAt(1).childAt(0).simulate('click')
expect(wrapper4.mount().childAt(0).childAt(0).childAt(0).childAt(1).hasClass('tab--active'))
wrapper4.unmount()
})

it('converts single tab into tab array', () => {
const wrapper4 = mount(<Tabs><Tab title='Test'>Test 1</Tab></Tabs>)
expect(!!wrapper4).to.be.true()
wrapper4.unmount()
})
})
2 changes: 2 additions & 0 deletions todo/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ModalDemo from './Section/ModalDemo'
import {Navbar, NavbarLink} from '../react/Navbar'
import PanelDemo from './Section/PanelDemo'
import SelectInputDemo from './Section/SelectInputDemo'
import TabDemo from './Section/TabDemo'
import TextInputDemo from './Section/TextInputDemo'
import TooltipDemo from './Section/TooltipDemo'

Expand Down Expand Up @@ -55,6 +56,7 @@ class App extends React.Component {
<TextInputDemo />
<SelectInputDemo />
<MenuDemo />
<TabDemo />
</div>
<div style={{height: 100}} />
</div>
Expand Down
34 changes: 34 additions & 0 deletions todo/Section/TabDemo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react'

import {Tabs, Tab} from '../../react/Tab'

const TabDemo = () => (
<div>
<h3>Tab</h3>
<Tabs>
<Tab active title='Item One'>
Alpha
</Tab>
<Tab title='Item Two'>
Bravo
</Tab>
<Tab disabled title='Disabled'>
Charlie
</Tab>
</Tabs>
<br />
<Tabs>
<Tab active icon='home'>
Alpha
</Tab>
<Tab icon='check'>
Bravo
</Tab>
<Tab disabled icon='envelope'>
Charlie
</Tab>
</Tabs>
</div>
)

export default TabDemo

0 comments on commit 7f9a12e

Please sign in to comment.