-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(client): add Swipeout component
- Loading branch information
1 parent
61d97cc
commit 8c21c80
Showing
9 changed files
with
323 additions
and
20 deletions.
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
67 changes: 67 additions & 0 deletions
67
packages/client/src/elements/Swipeout/Swipeout.stories.tsx
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,67 @@ | ||
import { Meta, Story } from '@storybook/react/types-6-0'; | ||
import React from 'react'; | ||
import styled, { css } from 'styled-components'; | ||
|
||
import Swipeout from '.'; | ||
import { SwipeoutProps } from './Swipeout.types'; | ||
|
||
export default { | ||
title: 'Elements/Swipeout', | ||
component: Swipeout, | ||
} as Meta; | ||
|
||
const MockContainer = styled.div( | ||
({ theme: { palette } }) => css` | ||
width: 320px; | ||
height: 200px; | ||
background: ${palette.background.paper}; | ||
display: flex; | ||
align-items: center; | ||
overflow: hidden; | ||
`, | ||
); | ||
|
||
const MockRow = styled.div( | ||
({ theme: { palette } }) => css` | ||
width: 320px; | ||
height: 50px; | ||
background: ${palette.primary.dark}; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
color: ${palette.text.light}; | ||
`, | ||
); | ||
|
||
const Template: Story<SwipeoutProps> = (args) => ( | ||
<MockContainer> | ||
<Swipeout {...args}> | ||
<MockRow>⬅ Swipe left</MockRow> | ||
</Swipeout> | ||
</MockContainer> | ||
); | ||
|
||
export const Default = Template.bind({}); | ||
Default.args = { | ||
right: [ | ||
{ | ||
order: 1, | ||
component: 'Edit', | ||
onPress: () => console.log('Edit button pressed'), | ||
borderRadius: '12px 0 0 12px', | ||
background: 'transparent', | ||
}, | ||
{ | ||
order: 2, | ||
component: 'Delete', | ||
onPress: () => console.log('Delete button pressed'), | ||
background: 'red', | ||
color: 'text-light', | ||
}, | ||
], | ||
onOpen: () => console.log('onOpen'), | ||
onClose: () => console.log('onClose'), | ||
onSwipe: () => console.log('onSwipe'), | ||
autoClose: true, | ||
disabled: false, | ||
}; |
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,59 @@ | ||
import styled, { css } from 'styled-components'; | ||
|
||
import { getCssColor } from '../../utils'; | ||
import { ContentProps, SwipeoutActionButtonProps, WrapperProps } from './Swipeout.types'; | ||
|
||
export const Wrapper = styled.div<WrapperProps>( | ||
({ isDragging }) => css` | ||
display: flex; | ||
justify-content: space-between; | ||
width: 100%; | ||
.react-draggable { | ||
${!isDragging && | ||
css` | ||
transition: transform 350ms cubic-bezier(0.33, 1, 0.68, 1); | ||
`} | ||
} | ||
`, | ||
); | ||
|
||
export const Content = styled.div<ContentProps>( | ||
({ isDragging }) => css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
:hover { | ||
${isDragging | ||
? css` | ||
cursor: grabbing; | ||
` | ||
: css` | ||
cursor: grab; | ||
`} | ||
} | ||
`, | ||
); | ||
|
||
export const ActionsContainer = styled.div` | ||
height: 100%; | ||
display: flex; | ||
`; | ||
|
||
export const ActionButton = styled.button<SwipeoutActionButtonProps>( | ||
({ theme, borderRadius, background, color }) => css` | ||
height: 100%; | ||
min-width: 100px; | ||
border: none; | ||
background: ${getCssColor({ color: background ?? 'background-default', theme })}; | ||
color: ${getCssColor({ color: color ?? 'text-primary', theme })}; | ||
border-radius: ${borderRadius ?? 0}; | ||
:hover { | ||
cursor: pointer; | ||
} | ||
:disabled { | ||
cursor: not-allowed; | ||
} | ||
`, | ||
); |
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,31 @@ | ||
import { ButtonHTMLAttributes, ReactNode } from 'react'; | ||
|
||
interface SwipeoutActionButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { | ||
color?: string; | ||
background?: string; | ||
borderRadius?: string; | ||
} | ||
|
||
interface SwipeoutAction extends SwipeoutActionButtonProps { | ||
order: number; | ||
component: ReactNode | string; | ||
onPress: () => void; | ||
} | ||
|
||
interface SwipeoutProps { | ||
children: ReactNode; | ||
right: Array<SwipeoutAction>; | ||
onOpen?: () => void; | ||
onSwipe?: () => void; | ||
onClose?: () => void; | ||
autoClose?: boolean; | ||
disabled?: boolean; | ||
} | ||
|
||
interface WrapperProps { | ||
isDragging: boolean; | ||
} | ||
|
||
interface ContentProps { | ||
isDragging: boolean; | ||
} |
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,114 @@ | ||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; | ||
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; | ||
|
||
import { useOnClickOutside } from '../../hooks'; | ||
import { ActionButton, ActionsContainer, Content, Wrapper } from './Swipeout.styled'; | ||
import { SwipeoutProps } from './Swipeout.types'; | ||
|
||
const Swipeout = ({ | ||
children, | ||
disabled, | ||
onClose, | ||
autoClose, | ||
right, | ||
onOpen, | ||
onSwipe, | ||
}: SwipeoutProps) => { | ||
const outerContainerRef = useRef<HTMLDivElement>(null); | ||
const draggableElementRef = useRef(null); | ||
const actionsContainerRef = useRef<HTMLDivElement>(null); | ||
const [slideXPosition, setSlideXPosition] = useState(0); | ||
const [actionsContainerWidth, setActionsContainerWidth] = useState(100); | ||
const [isOpen, setIsOpen] = useState(false); | ||
const [isDragging, setIsDragging] = useState(false); | ||
|
||
useLayoutEffect(() => { | ||
if (!actionsContainerRef.current) { | ||
return; | ||
} | ||
setActionsContainerWidth(actionsContainerRef.current.clientWidth); | ||
}, []); | ||
|
||
const close = useCallback(() => { | ||
if (!isOpen) { | ||
return; | ||
} | ||
|
||
setIsOpen(false); | ||
onClose && onClose(); | ||
setSlideXPosition(0); | ||
}, [isOpen, onClose]); | ||
|
||
useOnClickOutside(outerContainerRef, close); | ||
|
||
const handleDragStop = useCallback( | ||
(event: DraggableEvent, { x }: DraggableData) => { | ||
setIsDragging(false); | ||
if (x > 0) { | ||
return; | ||
} | ||
|
||
const OPEN_CLOSE_MARGIN = actionsContainerWidth / 4; | ||
|
||
if (Math.abs(x) < actionsContainerWidth - OPEN_CLOSE_MARGIN && isOpen) { | ||
close(); | ||
} | ||
|
||
if (Math.abs(x) >= OPEN_CLOSE_MARGIN && !isOpen) { | ||
setIsOpen(true); | ||
setSlideXPosition(-actionsContainerWidth); | ||
onOpen && onOpen(); | ||
} | ||
}, | ||
[actionsContainerWidth, close, isOpen, onOpen], | ||
); | ||
|
||
const handleDragStart = useCallback(() => { | ||
setIsDragging(true); | ||
onSwipe && onSwipe(); | ||
}, [onSwipe]); | ||
|
||
const onActionButtonClick = useCallback( | ||
(onClickCb?: () => void) => () => { | ||
onClickCb && onClickCb(); | ||
autoClose && close(); | ||
}, | ||
[autoClose, close], | ||
); | ||
|
||
const sortedRightActions = useMemo(() => right.sort((a, b) => a.order - b.order), [right]); | ||
|
||
return ( | ||
<Wrapper data-testid="swipeout" ref={outerContainerRef} isDragging={isDragging}> | ||
<Draggable | ||
axis="x" | ||
disabled={disabled} | ||
nodeRef={draggableElementRef} | ||
onStart={handleDragStart} | ||
onStop={handleDragStop} | ||
bounds={{ right: 0, left: -actionsContainerWidth }} | ||
position={{ x: slideXPosition, y: 0 }} | ||
> | ||
<Content ref={draggableElementRef} isDragging={isDragging}> | ||
{children} | ||
<ActionsContainer ref={actionsContainerRef}> | ||
{sortedRightActions.map(({ order, component, onPress, ...buttonProps }) => ( | ||
<ActionButton | ||
{...buttonProps} | ||
key={order} | ||
type="button" | ||
onClick={onActionButtonClick(onPress)} | ||
> | ||
{component} | ||
</ActionButton> | ||
))} | ||
</ActionsContainer> | ||
</Content> | ||
</Draggable> | ||
</Wrapper> | ||
); | ||
}; | ||
|
||
Swipeout.displayName = 'Swipeout'; | ||
|
||
export default Swipeout; |
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
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
Oops, something went wrong.