Skip to content

Commit

Permalink
Merge pull request #2388 from habx/feature/APP-31433
Browse files Browse the repository at this point in the history
APP-31433: Add `SidePanel` in Design System
  • Loading branch information
habxtech authored Sep 30, 2022
2 parents 976fbd2 + 26d48fd commit 32160b7
Show file tree
Hide file tree
Showing 7 changed files with 489 additions and 1 deletion.
22 changes: 22 additions & 0 deletions src/SidePanel/SidePanel.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Modal } from '@delangle/use-modal'
import * as React from 'react'

import { WithFloatingPanelBehavior } from '../withFloatingPanelBehavior'
import { WithTriggerElement } from '../withTriggerElement'

export interface SidePanelInnerProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title' | 'children'> {
modal: Modal
parentFloatingPanelRef: React.RefObject<HTMLElement> | null
title?: React.ReactNode
hideCloseIcon?: boolean
children?:
| React.ReactNode
| ((modal: Modal<HTMLDivElement>) => React.ReactNode)
}

export interface SidePanelProps
extends WithTriggerElement<
WithFloatingPanelBehavior<SidePanelInnerProps>,
HTMLDivElement
> {}
119 changes: 119 additions & 0 deletions src/SidePanel/SidePanel.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { render, within, act, fireEvent } from '@testing-library/react'
import * as React from 'react'
import sinon from 'sinon'

import { SidePanel } from './index'

jest.useFakeTimers()

describe('SidePanel component', () => {
describe('with react node children', () => {
it('should not render if not opened once', () => {
const { queryByTestId } = render(
<SidePanel onClose={() => null}>
<div data-testid="content">CONTENT</div>
</SidePanel>
)

const modalContainer = queryByTestId('modal-container')

expect(modalContainer).toBeNull()
})

it('should render if opened once', () => {
const { queryByTestId, getByTestId } = render(
<SidePanel
onClose={() => null}
triggerElement={
<button data-testid="modal-trigger-element">show</button>
}
>
<div data-testid="content">CONTENT</div>
</SidePanel>
)

fireEvent.click(getByTestId('modal-trigger-element'))
fireEvent.click(getByTestId('modal-overlay'))

const modalContainer = queryByTestId('modal-container') as HTMLElement

expect(modalContainer).toBeTruthy()
expect(within(modalContainer).queryByTestId('content')).toBeTruthy()
})

it('should not render if not opened once', () => {
const { queryByTestId } = render(
<SidePanel onClose={() => null}>
<div data-testid="content">CONTENT</div>
</SidePanel>
)

const modalContainer = queryByTestId('modal-container')

expect(modalContainer).toBeNull()
})
})

describe('with render props children', () => {
it('should not call children if modal is closed and has never been opened', () => {
const spyChildren = sinon.spy()

render(
<SidePanel onClose={() => null} open={false}>
{spyChildren}
</SidePanel>
)

expect(spyChildren.callCount).toEqual(0)
})

it('should have state = "opening" if modal is mounted with open=true"', () => {
const spyChildren = sinon.spy()

render(
<SidePanel onClose={() => null} open>
{spyChildren}
</SidePanel>
)

expect(spyChildren.lastCall.args[0].state).toEqual('opening')
})

it('should have state="opened" if opened for more than 1 second"', () => {
const spyChildren = sinon.spy()
render(
<SidePanel onClose={() => null} open>
{spyChildren}
</SidePanel>
)

act(() => {
jest.advanceTimersByTime(2000)
})

expect(spyChildren.lastCall.args[0].state).toEqual('opened')
})

it('should have state="closing" if open just switched to "false"', () => {
const spyChildren = sinon.spy()

const { rerender } = render(
<SidePanel onClose={() => null} open>
{spyChildren}
</SidePanel>
)

act(() => {
jest.advanceTimersByTime(1000)
})

rerender(
<SidePanel onClose={() => null} open={false}>
{spyChildren}
</SidePanel>
)

expect(spyChildren.lastCall.args[0].state).toEqual('closing')
})
})
})
113 changes: 113 additions & 0 deletions src/SidePanel/SidePanel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as React from 'react'
import styled from 'styled-components'

import { withGrid } from '../_storybook/withGrid'
import { ActionBar } from '../ActionBar'
import { Button } from '../Button'
import { HeaderBar } from '../HeaderBar'
import { TabsBar } from '../TabsBar'
import { TabsBarItem } from '../TabsBarItem'
import { Text } from '../Text'

import { SidePanel, SidePanelProps } from './index'

const Line = styled(Text)`
margin-bottom: 12px;
`

const TabItem = styled(TabsBarItem)`
font-size: 14px;
`

const Tabs = styled(HeaderBar).attrs({ small: true })`
padding-top: 12px;
`

const TabsBarContent: React.FunctionComponent = () => (
<React.Fragment>
<Tabs>
<TabsBar>
<TabItem active={true}>Section 1</TabItem>
</TabsBar>
</Tabs>
<Text>Section content</Text>
</React.Fragment>
)

const ScrollableContent: React.FunctionComponent = () => (
<React.Fragment>
{Array.from({ length: 20 }, (_, index) => (
<Line>Element n°{index + 1}</Line>
))}
</React.Fragment>
)

const ScrollableContentWithActionBar: React.FunctionComponent = () => (
<div>
{Array.from({ length: 20 }, (_, index) => (
<Line>Item {index + 1}</Line>
))}
<ActionBar>
<Button ghost>Cancel</Button>
<Button>Validate</Button>
</ActionBar>
</div>
)

const GRID_PROPS = {
triggerElement: <Button outline>Open</Button>,
}

const GRID_LINES = [{ title: 'Regular' }]

const GRID_ITEMS = [
{
props: {
title: 'Example',
children: <TabsBarContent />,
},
label: 'Short with tab bar and title',
},
{
props: {
title: 'Item list',
children: <ScrollableContent />,
},
label: 'Scrollable',
},
{
props: {
title: 'Item list',
children: <ScrollableContentWithActionBar />,
},
label: 'Scrollable with action bar',
},
]

const Grid = withGrid<SidePanelProps>({
props: GRID_PROPS,
lines: GRID_LINES,
items: GRID_ITEMS,
itemHorizontalSpace: 36,
})(SidePanel)

export default {
title: 'Modals/SidePanel',
component: SidePanel,
}

export const basic = (props: SidePanelProps) => (
<SidePanel
triggerElement={<Button outline>Open Side Panel</Button>}
{...props}
/>
)

basic.parameters = {
design: {
type: 'figma',
url: 'https://www.figma.com/file/LfGEUbovutcTpygwzrfTYbl5/Desktop-components?node-id=62%3A2',
},
}

export const gallery = () => <Grid />
141 changes: 141 additions & 0 deletions src/SidePanel/SidePanel.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import styled, { keyframes } from 'styled-components'

import { zIndex } from '../_internal/theme/zIndex'
import { ActionBarContent } from '../ActionBar/ActionBar.style'
import { animations } from '../animations'
import { breakpoints } from '../breakpoints'
import { Layout } from '../Layout'
import { mixins } from '../mixins'
import { theme } from '../theme'

const FADE_IN = keyframes`
from {
background-color: transparent;
}
to {
background-color: rgba(50, 50, 50, 0.7);
}
`

export const SidePanelContainer = styled(Layout)`
position: absolute;
top: 0;
bottom: 0;
right: 0;
display: flex;
gap: 24px;
flex-direction: column;
overflow: auto;
overflow: overlay;
padding: 36px;
box-shadow: ${theme.shadow('regular')};
z-index: ${zIndex.modals};
--modal-width: 480px;
@media (${breakpoints.above.phone}) {
width: var(--modal-width);
max-width: calc(100vw - 48px);
}
@media (${breakpoints.below.phone}) {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
}
`

export const SidePanelOverlay = styled.div`
@media (${breakpoints.below.phone}) {
background-color: transparent;
&[data-state='opening'] {
& ${SidePanelContainer} {
animation: ${animations('emergeSlantFromTop')};
}
}
&[data-state='closing'] {
& ${SidePanelContainer} {
animation: ${animations('diveSlant')};
}
}
}
@media (${breakpoints.above.phone}) {
&[data-state='opening'] {
animation: ${FADE_IN} var(--modal-animation-duration) linear 0ms;
& ${SidePanelContainer} {
animation: ${animations('emergeSlantFromBottom')};
}
}
&[data-state='closing'] {
animation: ${FADE_IN} var(--modal-animation-duration) linear 0ms reverse;
pointer-events: none;
background-color: transparent;
& ${SidePanelContainer} {
animation: ${animations('diveSlant')};
}
}
}
&[data-state='closed'] {
opacity: 0;
pointer-events: none;
}
`

export const HeaderBarContainer = styled.div`
display: flex;
${mixins.removeLayoutPadding({ top: true, right: true, left: true })};
${mixins.addLayoutPadding({ top: true, right: true, left: true })};
@media (${breakpoints.above.phone}) {
align-items: baseline;
justify-content: space-between;
& > *:not(:last-child) {
margin-right: 12px;
}
}
@media (${breakpoints.below.phone}) {
flex-direction: column-reverse;
& > *:not(:first-child) {
margin-bottom: 12px;
}
}
`

export const SidePanelContent = styled.div`
flex: 1 1 100%;
min-height: 0;
display: flex;
z-index: ${zIndex.modals};
`

export const SidePanelScrollableContent = styled.div`
flex: 1;
display: flex;
gap: 24px;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
@media (${breakpoints.below.phone}) {
padding-bottom: 72px;
& ${ActionBarContent}:not([data-count='0']):not([data-count='1']) {
box-shadow: ${theme.shadow('low')};
}
}
`
Loading

0 comments on commit 32160b7

Please sign in to comment.