diff --git a/gatsby-node.js b/gatsby-node.js
index a5f9e8e7..0627daa6 100644
--- a/gatsby-node.js
+++ b/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
diff --git a/package.json b/package.json
index ee3a6961..95d8afe3 100644
--- a/package.json
+++ b/package.json
@@ -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": {
@@ -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"
}
}
diff --git a/src/components/index.js b/src/components/index.js
index ea025dce..d72f1ba3 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -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';
diff --git a/src/components/interactive/Focusable.js b/src/components/interactive/Focusable.js
new file mode 100644
index 00000000..7c3057cb
--- /dev/null
+++ b/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;
+`;
diff --git a/src/components/interactive/Lightbox.js b/src/components/interactive/Lightbox.js
new file mode 100644
index 00000000..c5f92126
--- /dev/null
+++ b/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 = (
+
+ );
+
+ const body = (
+
+
+
+
+ );
+
+ const footer = (
+
+ );
+
+ return ReactDOM.createPortal(
+
+
+ {header}
+ {body}
+ {isPreviews && footer}
+
+ ,
+ 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,
+};
diff --git a/src/components/multimedia/Img.js b/src/components/multimedia/Img.js
index 0264455d..8ad4ae6a 100644
--- a/src/components/multimedia/Img.js
+++ b/src/components/multimedia/Img.js
@@ -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) {
@@ -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 = {
@@ -118,7 +134,7 @@ export default function Img({ ratio, zoom, onLoad, onError, onMouseMove, ...rest
};
return (
-