Skip to content

Commit

Permalink
feat(project): add animation, grow and fade components
Browse files Browse the repository at this point in the history
  • Loading branch information
royschut committed Jun 19, 2021
1 parent 515188a commit 944c9cc
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 153 deletions.
56 changes: 56 additions & 0 deletions src/components/Animation/Animation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState, useEffect, ReactNode, useRef, CSSProperties } from 'react';

type Props = {
createStyle: (status: Status) => CSSProperties;
open?: boolean;
duration?: number;
delay?: number;
onOpenAnimationDone?: () => void;
onCloseAnimationEnd?: () => void;
children: ReactNode;
};

export type Status = 'opening' | 'open' | 'closing' | 'closed';

const Animation = ({
createStyle,
open = true,
duration = 250,
delay = 0,
onOpenAnimationDone,
onCloseAnimationEnd,
children,
}: Props): JSX.Element | null => {
const [status, setStatus] = useState<Status>('closed');
const seconds = duration / 1000;
const transition = `transform ${seconds}s ease-out`; // todo: -webkit-transform;

const timeout = useRef<NodeJS.Timeout>();
const timeout2 = useRef<NodeJS.Timeout>();

useEffect(() => {
if (timeout.current) clearTimeout(timeout.current);
if (timeout2.current) clearTimeout(timeout2.current);
if (open) {
timeout2.current = setTimeout(() => setStatus('opening'), delay);
timeout.current = setTimeout(() => {
setStatus('open');
onOpenAnimationDone && onOpenAnimationDone();
}, duration + delay);
} else {
timeout2.current = setTimeout(() => setStatus('closing'), delay);
timeout.current = setTimeout(() => {
setStatus('closed');
onCloseAnimationEnd && onCloseAnimationEnd();
}, duration + delay);
}
}, [duration, delay, transition, open, onOpenAnimationDone, onCloseAnimationEnd]);

if (!open && status === 'closed') {
return null;
}

return <div style={createStyle(status)}>{children}</div>;
};

export default Animation;
51 changes: 26 additions & 25 deletions src/components/Animation/Fade/Fade.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import React, { useState, useEffect, useCallback, ReactNode } from 'react';
import React, { CSSProperties, ReactNode } from 'react';

import Animation, { Status } from '../Animation';

type Props = {
open?: boolean;
duration?: number;
delay?: number;
onOpenAnimationDone?: () => void;
onCloseAnimationEnd?: () => void;
children: ReactNode;
};

type Status = 'opening' | 'open' | 'closing' | 'closed';

const Fade = ({ open = true, duration = 250, children }: Props): JSX.Element | null => {
const [status, setStatus] = useState<Status>('closed');
const Fade = ({
open = true,
duration = 250,
delay = 0,
onOpenAnimationDone,
onCloseAnimationEnd,
children,
}: Props): JSX.Element | null => {
const seconds = duration / 1000;
const transition = `opacity ${seconds}s ease`;

const prepareClose = useCallback(() => {
if (open) {
setStatus('closing');
setTimeout(() => setStatus('closed'), duration);
}
}, [duration, open]);
const transition = `opacity ${seconds}s ease-in-out`;

useEffect(() => {
if (open) {
setTimeout(() => setStatus('opening'), 10);
setTimeout(() => setStatus('open'), duration - 10);
} else {
setStatus('closing');
setTimeout(() => setStatus('closed'), duration);
}
}, [duration, transition, open, prepareClose]);
const createStyle = (status: Status): CSSProperties => ({ transition, opacity: status === 'opening' || status === 'open' ? 1 : 0 });

return (
<div style={{ display: status === 'closed' ? 'none' : '' }}>
<div style={{ opacity: status === 'opening' || status === 'open' ? 1 : 0, transition }}>{children}</div>
</div>
<Animation
createStyle={(status: Status) => createStyle(status)}
open={open}
duration={duration}
delay={delay}
onOpenAnimationDone={onOpenAnimationDone}
onCloseAnimationEnd={onCloseAnimationEnd}
>
{children}
</Animation>
);
};

Expand Down
43 changes: 43 additions & 0 deletions src/components/Animation/Grow/Grow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { ReactNode, CSSProperties } from 'react';

import Animation, { Status } from '../Animation';

type Props = {
open?: boolean;
duration?: number;
delay?: number;
onOpenAnimationDone?: () => void;
onCloseAnimationEnd?: () => void;
children: ReactNode;
};

const Grow = ({
open = true,
duration = 250,
delay = 0,
onOpenAnimationDone,
onCloseAnimationEnd,
children,
}: Props): JSX.Element | null => {
const seconds = duration / 1000;
const transition = `transform ${seconds}s ease-out`; // todo: -webkit-transform;
const createStyle = (status: Status): CSSProperties => ({
transition,
transform: status === 'opening' || status === 'open' ? 'scale(1)' : 'scale(0.1)',
});

return (
<Animation
createStyle={(status: Status) => createStyle(status)}
open={open}
duration={duration}
delay={delay}
onOpenAnimationDone={onOpenAnimationDone}
onCloseAnimationEnd={onCloseAnimationEnd}
>
{children}
</Animation>
);
};

export default Grow;
18 changes: 0 additions & 18 deletions src/components/Modal/Modal.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@
left: 0px;
top: 0px;
z-index: 1;
&.hidden {
display: none;
}
}
.backdrop {
width: inherit;
height: inherit;
background: rgba(0, 0, 0, 0.6);
opacity: 0;
transition: opacity 0.25s ease;
&.open {
opacity: 1;
}
}
.modalContainer {
position: absolute;
Expand All @@ -38,29 +30,19 @@
width: 80vw;
height: calc(80vw / 16 * 9);
background-color: rgba(0, 0, 0, 0.9);
transition: transform 0.15s ease-out, -webkit-transform 0.15s ease-out;
transform: scale(0);
@include responsive.mobile-only() {
width: 100vw;
height: calc(100vw / 16 * 9);
}
&.open {
transform: scale(1);
}
}
.modal {
position: relative;
width: 80vw;
height: calc(80vw / 16 * 9);
opacity: 0;
transition: opacity 0.25s ease;
@include responsive.mobile-only() {
width: 100vw;
height: calc(100vw / 16 * 9);
}
&.open {
opacity: 1;
}
}
.close {
position: absolute;
Expand Down
65 changes: 27 additions & 38 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { ReactFragment, useEffect, useState, useCallback } from 'react';
import React, { ReactFragment, useState } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';

import IconButton from '../IconButton/IconButton';
import Close from '../../icons/Close';
// import Fade from '../Animation/Fade/Fade';
import Fade from '../Animation/Fade/Fade';
import Grow from '../Animation/Grow/Grow';

import styles from './Modal.module.scss';

Expand All @@ -15,49 +16,37 @@ type Props = {
children: ReactFragment;
};

type Status = 'opening' | 'open' | 'closing' | 'closed';

const Modal: React.FC<Props> = ({ open, onClose, closeButtonVisible = true, children }: Props) => {
const { t } = useTranslation('common');
const [status, setStatus] = useState<Status>('closed');

const prepareClose = useCallback(() => {
if (open) {
setStatus('closing');
setTimeout(() => setStatus('closed'), 150);
document.body.style.overflowY = '';
document.removeEventListener('keydown', (event: KeyboardEvent) => event.keyCode === 27 && prepareClose());
onClose();
}
}, [open, onClose]);

useEffect(() => {
if (open) {
setTimeout(() => setStatus('opening'), 10); // wait for overlay to appear
setTimeout(() => setStatus('open'), 250);

document.body.style.overflowY = 'hidden';
document.addEventListener('keydown', (event: KeyboardEvent) => event.keyCode === 27 && prepareClose());
}
}, [open, prepareClose]);
const [doRender, setDoRender] = useState<boolean>(false);

return (
<div className={classNames(styles.overlay, { [styles.hidden]: !open && status !== 'closing' })} onClick={prepareClose}>
<div className={classNames(styles.backdrop, { [styles.open]: status === 'opening' || status === 'open' })} />
<div className={classNames(styles.modalContainer)}>
<div className={classNames(styles.modalBackground, { [styles.open]: status === 'opening' || status === 'open' })} />
<div className={classNames(styles.modal, { [styles.open]: status === 'open' })} onClick={(event) => event.stopPropagation()}>
{children}
<IconButton
onClick={prepareClose}
aria-label={t('close_modal')}
className={classNames(styles.close, { [styles.hidden]: !closeButtonVisible })}
<Fade open={open} duration={300}>
<div className={classNames(styles.overlay)} onClick={onClose}>
<div className={classNames(styles.backdrop)} />
<div className={classNames(styles.modalContainer)}>
<Grow
open={open}
delay={100}
duration={200}
onOpenAnimationDone={() => setDoRender(true)}
onCloseAnimationEnd={() => setDoRender(false)}
>
<Close />
</IconButton>
<div className={classNames(styles.modalBackground)} />
<div className={classNames(styles.modal)} onClick={(event) => event.stopPropagation()}>
{doRender && children}
<IconButton
onClick={onClose}
aria-label={t('close_modal')}
className={classNames(styles.close, { [styles.hidden]: !closeButtonVisible })}
>
<Close />
</IconButton>
</div>
</Grow>
</div>
</div>
</div>
</Fade>
);
};

Expand Down
65 changes: 35 additions & 30 deletions src/components/Modal/__snapshots__/Modal.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,51 @@
exports[`<Modal> renders and matches snapshot 1`] = `
<div>
<div
class="overlay hidden"
style="transition: opacity 0.3s ease-in-out; opacity: 0;"
>
<div
class="backdrop"
/>
<div
class="modalContainer"
class="overlay"
>
<div
class="modalBackground"
class="backdrop"
/>
<div
class="modal"
class="modalContainer"
>
<p>
Test modal
</p>
<div
aria-label="close_modal"
class="iconButton close"
role="button"
tabindex="0"
style="transition: transform 0.2s ease-out; transform: scale(0.1);"
>
<svg
aria-hidden="true"
class="icon"
focusable="false"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
<div
class="modalBackground"
/>
<div
class="modal"
>
<g>
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</g>
</svg>
<div
aria-label="close_modal"
class="iconButton close"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
class="icon"
focusable="false"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</g>
</svg>
</div>
</div>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 944c9cc

Please sign in to comment.