Skip to content

Commit

Permalink
add lightbox component
Browse files Browse the repository at this point in the history
  • Loading branch information
mrozilla committed Apr 20, 2020
1 parent 017cf3c commit ded93a8
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/components/index.js
Expand Up @@ -4,8 +4,10 @@ export { default as Dots } from './interactive/Dots';
export { Details, Summary } from './interactive/Details';
export { Error } from './interactive/Error';
export { Fieldset } from './interactive/Fieldset';
export { Focusable } from './interactive/Focusable';
export { Form } from './interactive/Form';
export { default as Input } from './interactive/Input';
export { default as Lightbox } from './interactive/Lightbox';
export { default as Link } from './interactive/Link';
export { Radio } from './interactive/Radio';
export { default as TextInput } from './interactive/TextInput';
Expand Down
26 changes: 26 additions & 0 deletions src/components/interactive/Focusable.js
@@ -0,0 +1,26 @@
// ─────────────────────────────────────────────────────────────────────────────
// import
// ─────────────────────────────────────────────────────────────────────────────

import styled from 'styled-components';

// ─────────────────────────────────────────────────────────────────────────────
// component
// ─────────────────────────────────────────────────────────────────────────────

export const Focusable = styled.button`
-webkit-appearance: none;
border: none;
outline: none;
background-color: transparent;
position: absolute;
width: 100%;
height: 100%;
top: 0;
right: 0;
bottom: 0;
left: 0;
cursor: pointer;
`;
230 changes: 230 additions & 0 deletions src/components/interactive/Lightbox.js
@@ -0,0 +1,230 @@
// ─────────────────────────────────────────────────────────────────────────────
// import
// ─────────────────────────────────────────────────────────────────────────────

import React from 'react';
import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';
import { bool, arrayOf, string, func } from 'prop-types';

import { Section } from '~components/layout/Section';
import { P } from '~components/text/P';
import { Ul, Li } from '~components/text/List';
import { Button } from '~components/interactive/Button';
import { Focusable } from '~components/interactive/Focusable';
import { Icon } from '~components/multimedia/Icon';
import Img from '~components/multimedia/Img';

import { useEventListener, animation } from '~utils';

// ─────────────────────────────────────────────────────────────────────────────
// helpers
// ─────────────────────────────────────────────────────────────────────────────

const fadeUp = animation({
from: {
opacity: 0,
transform: 'translateY(1vh)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
properties: '500ms',
});

// ─────────────────────────────────────────────────────────────────────────────
// component
// ─────────────────────────────────────────────────────────────────────────────

export default function Lightbox({
images,
portalRoot,
isOpen,
isPreviews: defaultIsPreviews,
onClose,
}) {
const [currentIdx, setCurrentIdx] = React.useState(0);
const [isPreviews, setIsPreviews] = React.useState(defaultIsPreviews);

const closeButtonRef = React.useRef();

const handlePrevious = () => setCurrentIdx((prev) => (prev + images.length - 1) % images.length);
const handleNext = () => setCurrentIdx((prev) => (prev + 1) % images.length);

useEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
handlePrevious();
}
if (event.key === 'ArrowRight') {
event.preventDefault();
handleNext();
}
if (event.key === 'Escape') {
event.preventDefault();
onClose();
}
});

React.useEffect(() => {
if (isOpen) closeButtonRef.current.focus();
}, [isOpen]);

if (!isOpen) return null;

const header = (
<header
css={`
display: flex;
justify-content: space-between;
`}
>
<P
css={`
padding: 0 0 0 2rem;
align-self: center;
`}
>
{currentIdx + 1} / {images.length}
</P>
<menu>
<Button
aria-label="Toggle previews"
onClick={() => setIsPreviews((prev) => !prev)}
css={`
margin: 0 -2rem 0 0;
`}
>
<Icon icon="FaTh" />
</Button>
<Button ref={closeButtonRef} aria-label="Close" onClick={onClose}>
<Icon icon="FaTimes" />
</Button>
</menu>
</header>
);

const body = (
<div
css={`
max-height: 100%;
`}
>
<Img
src={images[currentIdx]}
alt="random image"
ratio={null}
pictureProps={{ style: { height: '100%' } }}
css={`
object-fit: contain;
background-color: transparent;
animation: ${fadeUp};
`}
/>
<Ul>
<Li
css={`
position: absolute;
align-self: center;
`}
>
<Button aria-label="Previous photo" onClick={handlePrevious}>
<Icon icon="FaArrowLeft" />
</Button>
</Li>
<Li
css={`
position: absolute;
align-self: center;
right: 0;
`}
>
<Button aria-label="Next photo" onClick={handleNext}>
<Icon icon="FaArrowRight" />
</Button>
</Li>
</Ul>
</div>
);

const footer = (
<footer
css={`
overflow-x: scroll;
`}
>
<Ul
css={`
grid-auto-flow: column;
grid-auto-columns: 14rem;
grid-gap: 1rem;
padding: 1rem;
`}
>
{images.map((src, i) => (
<Li
key={i} // eslint-disable-line react/no-array-index-key
css={`
position: relative;
cursor: pointer;
`}
>
<Img
src={src}
alt="random image"
pictureProps={{ style: { height: 'calc(100% - 2rem)' } }}
/>
<Focusable
css={`
&:hover,
&:focus {
box-shadow: 0 0 0 2px hsla(var(--hsl-primary), 1);
}
`}
onClick={() => setCurrentIdx(i)}
/>
</Li>
))}
</Ul>
</footer>
);

return ReactDOM.createPortal(
<FocusLock>
<Section
css={`
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: var(--z-index-modal);
display: grid;
grid-template-rows: 6rem 1fr ${isPreviews ? '16rem' : '6rem'};
background-color: hsla(var(--hsl-bg), 0.95);
`}
>
{header}
{body}
{isPreviews && footer}
</Section>
</FocusLock>,
document.querySelector(portalRoot),
);
}

Lightbox.propTypes = {
images: arrayOf(string).isRequired,
portalRoot: string,
isOpen: bool,
isPreviews: bool,
onClose: func.isRequired,
};

Lightbox.defaultProps = {
portalRoot: '#___gatsby',
isOpen: false,
isPreviews: true,
};
6 changes: 5 additions & 1 deletion src/utils/style/index.css
Expand Up @@ -12,8 +12,11 @@
--hsl-inverse: 0, 100%, 100%;
--color-inverse: hsl(var(--hsl-inverse));

--hsl-text: 200, 5%, 45%;
--hsl-text: 211, 5%, 45%;
--color-text: hsl(var(--hsl-text));

--hsl-dark: 211, 0%, 1%;
--color-dark: hsl(var--hsl-dark);

--hsl-success: 100, 70%, 60%;
--color-success: hsl(var(--hsl-success));
Expand Down Expand Up @@ -86,6 +89,7 @@ input,
select,
pre,
iframe,
menu,
hr,
h1,
h2,
Expand Down

0 comments on commit ded93a8

Please sign in to comment.