Skip to content
This repository has been archived by the owner on Aug 21, 2023. It is now read-only.

Commit

Permalink
HS-App wrapper & decorator (#531)
Browse files Browse the repository at this point in the history
* Create a replication of the HsApp layout to wrap any story with. Replicate the HSApp nav. Add decorator

* Add tests for HsApp component
  • Loading branch information
plbabin authored and Jon Quach committed Mar 22, 2019
1 parent ec90917 commit f0693f9
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .storybook/storybook.css
Expand Up @@ -8,6 +8,10 @@ body {
margin: 20px;
}

body.with-hsapp-wrapper {
margin: 0;
}

.with-aktiv {
--BlueConfigGlobalFontFamily: 'Aktiv Grotesk', sans-serif !important;
}
125 changes: 125 additions & 0 deletions src/components/HsApp/HsApp.css.js
@@ -0,0 +1,125 @@
import styled from '../styled'
import baseStyles from '../../../src/styles/resets/baseStyles.css.js'
import { getColor } from '../../../src/styles/utilities/color'
import Dropdown from '../Dropdown/DropdownV2'
import Avatar from '../Avatar'

export const config = {
headerHeight: '54px',
}

export const HsAppUI = styled('div')`
${baseStyles};
`

export const AppLayoutUI = styled('div')`
align-items: stretch;
display: flex;
justify-content: space-between;
`
export const AppContainerUI = styled('div')`
align-items: stretch;
box-sizing: border-box;
display: flex;
flex: 1;
justify-content: space-between;
min-height: calc((100vh) - (${config.headerHeight}));
width: 100%;
`

export const HeaderUI = styled('div')`
background: ${getColor('blue.700')};
padding: 7px 10px 7px 4px;
position: relative;
height: ${config.headerHeight};
color: ${getColor('blue.300')};
display: flex;
align-items: center;
`

export const LogoUI = styled('span')`
padding: 0 10px 0 16px;
color: ${getColor('blue.300')};
&:hover {
cursor: pointer;
color: white;
}
`

export const SecIconUI = styled('span')`
padding: 0 18px;
color: ${getColor('blue.300')};
&.less-padding {
padding-right: 10px;
}
&:hover {
cursor: pointer;
color: white;
}
`

export const SecondaryNavUI = styled('span')`
margin-left: auto;
display: flex;
align-items: center;
`

export const NavUI = styled('span')`
display: flex;
flew-direction: row;
`

export const AvatarUI = styled(Avatar)`
margin: 0 10px;
`

export const DropdownTriggerUI = styled('span')`
display: inline-flex;
align-items: center;
height: 40px;
padding: 0 10px 0 17px;
color: ${getColor('blue.300')};
&:hover {
color: white;
}
.c-Icon {
top: 2px;
margin-left: 2px;
}
`

export const DropdownUI = styled(Dropdown)`
.is-open ${DropdownTriggerUI} {
color: white;
}
`

export const SidenavUI = styled('div')`
background: ${getColor('grey.300')};
width: 250px;
border-right: 1px solid ${getColor('grey.500')};
height: 100%;
`

export const ContentUI = styled('div')`
box-shadow: -1px 0 0 #d6dde3, 1px 0 0 #d6dde3, 0 1px 0 #d6dde3;
box-sizing: border-box;
flex: 1;
flex-basis: auto;
max-width: 100%;
min-width: 0;
position: relative;
z-index: 2;
background: #f9fafa;
/* background: ${getColor('grey.400')}; */
padding: 20px;
`

export const InnerContentUI = styled('div')`
background: #fff;
padding: 20px;
`
67 changes: 67 additions & 0 deletions src/components/HsApp/HsApp.tsx
@@ -0,0 +1,67 @@
import * as React from 'react'

import {
HsAppUI,
ContentUI,
InnerContentUI,
AppLayoutUI,
AppContainerUI,
} from './HsApp.css'
import Nav from './Nav'
import Sidenav from './Sidenav'

export interface Props {
children?: any
withInnerWrapper?: boolean
sidenavComponent?: any
contentComponent?: any
navComponent?: any
}

class HsApp extends React.PureComponent<Props> {
static defaultProps = {
withInnerWrapper: true,
sidenavComponent: null,
navComponent: null,
contentComponent: null,
}

static Nav = Nav
static Sidenav = Sidenav

componentDidMount() {
document.body.classList.add('with-hsapp-wrapper')
}

componentWillUnmount() {
document.body.classList.remove('with-hsapp-wrapper')
}

renderChildren() {
const { children, withInnerWrapper } = this.props
if (withInnerWrapper) {
return <InnerContentUI>{children}</InnerContentUI>
}
return children
}

render() {
const { sidenavComponent, contentComponent, navComponent } = this.props
return (
<HsAppUI className="c-HsApp">
{navComponent ? navComponent : <HsApp.Nav />}
<AppLayoutUI>
<AppContainerUI>
{sidenavComponent ? sidenavComponent : <HsApp.Sidenav />}
{contentComponent ? (
contentComponent
) : (
<ContentUI>{this.renderChildren()}</ContentUI>
)}
</AppContainerUI>
</AppLayoutUI>
</HsAppUI>
)
}
}
export default HsApp
9 changes: 9 additions & 0 deletions src/components/HsApp/HsApp.utils.ts
@@ -0,0 +1,9 @@
export const COMPONENT_KEY: {
HsApp: string
Nav: string
Sidenav: string
} = {
HsApp: 'HsApp',
Nav: 'PageNav',
Sidenav: 'PageSidenav',
}
64 changes: 64 additions & 0 deletions src/components/HsApp/Nav.tsx
@@ -0,0 +1,64 @@
import * as React from 'react'
import {
HeaderUI,
LogoUI,
NavUI,
DropdownTriggerUI,
DropdownUI,
SecIconUI,
SecondaryNavUI,
AvatarUI,
} from './HsApp.css'
import Icon from '../Icon'
import Text from '../Text'
import { createSpec, faker } from '@helpscout/helix'

const ItemSpec = createSpec({
id: faker.random.uuid(),
label: faker.company.companyName(),
value: faker.company.companyName(),
})

export interface Props {}

class Nav extends React.PureComponent<Props> {
renderDropdowns() {
return ['Mailboxes', 'Docs', 'Reports', 'Manage'].map(d => (
<DropdownUI
key={d}
items={ItemSpec.generate(8)}
trigger={
<DropdownTriggerUI>
<Text size="14">{d}</Text>{' '}
<Icon name="caret-down" inline size="16" />
</DropdownTriggerUI>
}
/>
))
}
render() {
return (
<HeaderUI>
<LogoUI>
<Icon name="hs-logo" size="20" />
</LogoUI>
<NavUI>{this.renderDropdowns()}</NavUI>
<SecondaryNavUI>
<SecIconUI>
<Icon name="bell" size="20" />
</SecIconUI>
<SecIconUI className="less-padding">
<Icon name="buoy" size="20" withCaret />
</SecIconUI>
<SecIconUI>
<Icon name="search" size="20" />
</SecIconUI>
</SecondaryNavUI>

<AvatarUI shape="rounded" size="xs" />
</HeaderUI>
)
}
}

export default Nav
22 changes: 22 additions & 0 deletions src/components/HsApp/README.md
@@ -0,0 +1,22 @@
# HsApp

This component wrap an any other component with the Helpscout App layout

## Example

```jsx
<HsApp>
<Page>
<Page.Card>content</Page.Card>
</Page>
</HsApp>
```

## Props

| Prop | Type | Description |
| ---------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| withInnerWrapper | `bool` | When active, the content will be wrapped with a white box, otherwise it will be the default HS App background color. |
| sidenavComponent | `any` | Overwrite the sidebar with this component |
| navComponent | `any` | Overwrite the top navigation with this component |
| contentComponent | `any` | Overwrite the actual content with this component |
12 changes: 12 additions & 0 deletions src/components/HsApp/Sidenav.tsx
@@ -0,0 +1,12 @@
import * as React from 'react'
import { SidenavUI } from './HsApp.css'

export interface Props {}

class Sidenav extends React.PureComponent<Props> {
render() {
return <SidenavUI />
}
}

export default Sidenav
71 changes: 71 additions & 0 deletions src/components/HsApp/__tests__/HsApp.test.js
@@ -0,0 +1,71 @@
import React from 'react'
import { mount } from 'enzyme'
import HsApp from '../HsApp'

import { InnerContentUI } from '../HsApp.css'

describe('ClassName', () => {
test('Has default className', () => {
const wrapper = mount(<HsApp />)
expect(wrapper.getDOMNode().classList.contains('c-HsApp')).toBe(true)
})

test('Adds/removes className on document.body when mounting/unmounting', () => {
const wrapper = mount(<HsApp />)
expect(document.body.classList.contains('with-hsapp-wrapper')).toBe(true)
wrapper.unmount()
expect(document.body.classList.contains('with-hsapp-wrapper')).toBe(false)
})
})

describe('Rendering', () => {
test('Renders inner wrapper by default', () => {
const wrapper = mount(<HsApp />)
expect(wrapper.find(InnerContentUI).length).toBe(1)
})

test('Does not render inner wrapper', () => {
const wrapper = mount(<HsApp withInnerWrapper={false} />)
expect(wrapper.find(InnerContentUI).length).toBe(0)
})

test('Renders custom nav', () => {
const CustomNav = props => {
return <span className="customNav" />
}
const wrapper = mount(<HsApp navComponent={<CustomNav />} />)
const HsAppNav = HsApp.Nav
expect(wrapper.find(HsAppNav).length).toBe(0)
expect(wrapper.find('.customNav').length).toBe(1)
})

test('Renders custom sidenav', () => {
const CustomSidenav = props => {
return <span className="customSidenav" />
}
const wrapper = mount(<HsApp sidenavComponent={<CustomSidenav />} />)
const HsAppSidenav = HsApp.Sidenav
expect(wrapper.find(HsAppSidenav).length).toBe(0)
expect(wrapper.find('.customSidenav').length).toBe(1)
})

test('Renders custom content', () => {
const CustomContent = props => {
return <span className="customContent" />
}
const wrapper = mount(<HsApp contentComponent={<CustomContent />} />)
expect(wrapper.find('.customContent').length).toBe(1)
})

test('Renders children', () => {
const ChildrenComponent = props => {
return <span className="children" />
}
const wrapper = mount(
<HsApp>
<ChildrenComponent />
</HsApp>
)
expect(wrapper.find('.children').length).toBe(1)
})
})
8 changes: 8 additions & 0 deletions src/components/HsApp/index.ts
@@ -0,0 +1,8 @@
import { propConnect } from '../PropProvider'
import HsApp from './HsApp'
import { COMPONENT_KEY } from './HsApp.utils'

HsApp.Nav = propConnect(COMPONENT_KEY.Nav)(HsApp.Nav)
HsApp.Sidenav = propConnect(COMPONENT_KEY.Sidenav)(HsApp.Sidenav)

export default propConnect(COMPONENT_KEY.HsApp)(HsApp)

0 comments on commit f0693f9

Please sign in to comment.