-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2388 from habx/feature/APP-31433
APP-31433: Add `SidePanel` in Design System
- Loading branch information
Showing
7 changed files
with
489 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
> {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')}; | ||
} | ||
} | ||
` |
Oops, something went wrong.