Skip to content

Commit

Permalink
Merge branch 'feature/lightbox' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
mrozilla committed Apr 20, 2020
2 parents e85dd49 + c1225a3 commit 29ae97b
Show file tree
Hide file tree
Showing 11 changed files with 1,609 additions and 450 deletions.
2 changes: 1 addition & 1 deletion gatsby-node.js
@@ -1,5 +1,5 @@
exports.onCreateWebpackConfig = require('./gatsby/onCreateWebpackConfig'); // aliases
exports.onCreateNode = require('./gatsby/onCreateNode'); // node transformations
exports.createPages = require('./gatsby/createPages'); // automatic pages

exports.createSchemaCustomization = require('./gatsby/createSchemaCustomization'); // schema customization
exports.createPages = require('./gatsby/createPages'); // automatic pages
37 changes: 19 additions & 18 deletions package.json
Expand Up @@ -21,31 +21,32 @@
"@mdx-js/mdx": "^1.5.8",
"@mdx-js/react": "^1.5.8",
"eslint-plugin-react-hooks": "^3.0.0",
"gatsby": "2.20.12",
"gatsby-plugin-google-analytics": "2.2.2",
"gatsby-plugin-manifest": "2.3.3",
"gatsby-plugin-mdx": "^1.1.4",
"gatsby-plugin-netlify": "2.2.1",
"gatsby-plugin-netlify-cms": "^4.2.2",
"gatsby-plugin-offline": "3.1.2",
"gatsby-plugin-react-helmet": "3.2.1",
"gatsby": "2.20.27",
"gatsby-plugin-google-analytics": "2.2.4",
"gatsby-plugin-manifest": "2.3.5",
"gatsby-plugin-mdx": "^1.1.9",
"gatsby-plugin-netlify": "2.2.3",
"gatsby-plugin-netlify-cms": "^4.2.4",
"gatsby-plugin-offline": "3.1.4",
"gatsby-plugin-react-helmet": "3.2.4",
"gatsby-plugin-robots-txt": "^1.5.0",
"gatsby-plugin-sharp": "2.5.3",
"gatsby-plugin-sitemap": "2.3.1",
"gatsby-plugin-styled-components": "3.2.1",
"gatsby-remark-images": "3.2.1",
"gatsby-plugin-sharp": "2.5.6",
"gatsby-plugin-sitemap": "2.3.5",
"gatsby-plugin-styled-components": "3.2.3",
"gatsby-remark-images": "3.2.4",
"gatsby-remark-relative-images": "^0.3.0",
"gatsby-source-filesystem": "2.2.2",
"gatsby-transformer-sharp": "^2.4.3",
"gatsby-source-filesystem": "2.2.4",
"gatsby-transformer-sharp": "^2.4.6",
"lodash": "^4.17.15",
"netlify-cms-app": "^2.12.3",
"netlify-cms-app": "^2.12.10",
"prism-react-renderer": "^1.0.2",
"prop-types": "^15.7.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-helmet": "^5.2.1",
"react-focus-lock": "^2.3.1",
"react-helmet": "^6.0.0",
"react-icons": "^3.9.0",
"styled-components": "^5.0.1",
"styled-components": "^5.1.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
Expand All @@ -56,7 +57,7 @@
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0"
}
}
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,
};
30 changes: 23 additions & 7 deletions src/components/multimedia/Img.js
Expand Up @@ -26,12 +26,19 @@ export const Picture = styled.picture`
align-items: center;
justify-content: center;
&::before {
content: '';
${({ ratio }) => {
if (ratio) {
return css`
&::before {
content: '';
display: block;
padding-bottom: ${({ ratio }) => ratio * 100}%;
}
display: block;
padding-bottom: ${ratio * 100}%;
}
`;
}
return null;
}}
${({ isLoaded }) => {
if (!isLoaded) {
Expand Down Expand Up @@ -92,7 +99,16 @@ export const StyledImg = styled.img`
// component
// ─────────────────────────────────────────────────────────────────────────────

export default function Img({ ratio, zoom, onLoad, onError, onMouseMove, ...rest }) {
export default function Img({
pictureProps,
ratio,
fit,
zoom,
onLoad,
onError,
onMouseMove,
...rest
}) {
const [isLoaded, setIsLoaded] = React.useState(false);

const handlers = {
Expand All @@ -118,7 +134,7 @@ export default function Img({ ratio, zoom, onLoad, onError, onMouseMove, ...rest
};

return (
<Picture ratio={ratio} isLoaded={isLoaded}>
<Picture {...pictureProps} ratio={ratio} isLoaded={isLoaded}>
<StyledImg {...rest} zoom={zoom} {...handlers} />
</Picture>
);
Expand Down
2 changes: 1 addition & 1 deletion src/containers/SEOContainer.js
Expand Up @@ -3,7 +3,7 @@
// ─────────────────────────────────────────────────────────────────────────────

import React from 'react';
import Helmet from 'react-helmet';
import { Helmet } from 'react-helmet';
import { graphql, useStaticQuery } from 'gatsby';

import { metaPropTypes } from '~utils';
Expand Down

0 comments on commit 29ae97b

Please sign in to comment.