Skip to content

Commit

Permalink
feat(client): add Swipeout component
Browse files Browse the repository at this point in the history
  • Loading branch information
Jozwiaczek committed Sep 3, 2021
1 parent 61d97cc commit 8c21c80
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 20 deletions.
3 changes: 0 additions & 3 deletions packages/client/src/elements/AppBar/AppBar.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,5 @@ export const AppBarPageWrapper = styled.div(
${down(breakpoints.md)} {
padding: 40px 20px;
}
${down(breakpoints.xs)} {
padding: 40px 16px;
}
`,
);
67 changes: 67 additions & 0 deletions packages/client/src/elements/Swipeout/Swipeout.stories.tsx
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,
};
59 changes: 59 additions & 0 deletions packages/client/src/elements/Swipeout/Swipeout.styled.ts
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;
}
`,
);
31 changes: 31 additions & 0 deletions packages/client/src/elements/Swipeout/Swipeout.types.d.ts
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;
}
114 changes: 114 additions & 0 deletions packages/client/src/elements/Swipeout/index.tsx
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;
1 change: 1 addition & 0 deletions packages/client/src/elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export { default as Link } from './Link';
export { CardList, DetailedList, HistoryCompactList } from './lists';
export { default as Portal } from './Portal';
export { default as Snackbar } from './Snackbar';
export { default as Swipeout } from './Swipeout';
export { default as ToggleSlider } from './ToggleSlider';
export { default as Tooltip } from './Tooltip';
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { getRecordIconCircleColors } from './HistoryCompactList.utils';

export const ListCard = styled(Card)(
({ theme: { down, breakpoints } }) => css`
padding: 18px 16px;
padding: 18px 0 18px 16px;
display: flex;
width: 100%;
flex-direction: column;
row-gap: 32px;
overflow: hidden;
${down(breakpoints.xxs)} {
padding: 12px 10px;
padding: 12px 0 12px 10px;
}
`,
);
Expand All @@ -24,11 +26,15 @@ export const DayLabel = styled.h4`
margin-bottom: 16px;
`;

export const RecordRow = styled.div`
display: flex;
align-items: center;
column-gap: 8px;
`;
export const RecordRow = styled.div<{ width: number }>(
({ width }) => css`
display: flex;
align-items: center;
column-gap: 8px;
height: 56px;
width: ${width + 1}px;
`,
);

export const RecordIconCircle = styled.div<RecordIconCircleProps>(
({ theme: { down, breakpoints }, firstRecord, event }) => css`
Expand Down Expand Up @@ -56,7 +62,6 @@ export const SingleDayRecordsWrapper = styled.div(
({ theme: { palette } }) => css`
display: flex;
flex-direction: column;
row-gap: ${RECORD_ICON_CIRCLE_SIZE};
${RecordIconCircle}:before {
content: '';
Expand Down
Loading

0 comments on commit 8c21c80

Please sign in to comment.