diff --git a/packages/example/src/data/nav-items.yaml b/packages/example/src/data/nav-items.yaml index 3d7b4ab24..afbcba84f 100644 --- a/packages/example/src/data/nav-items.yaml +++ b/packages/example/src/data/nav-items.yaml @@ -54,3 +54,5 @@ path: /components/Tabs - title: Video path: /components/Video + - title: Image Gallery + path: /components/ImageGallery diff --git a/packages/example/src/pages/components/ImageGallery.mdx b/packages/example/src/pages/components/ImageGallery.mdx new file mode 100644 index 000000000..9c5df2a90 --- /dev/null +++ b/packages/example/src/pages/components/ImageGallery.mdx @@ -0,0 +1,131 @@ +--- +title: Image Gallery +--- + + + +A small scale example of the Image Gallery from the [IBM Design Language Gallery](https://www.ibm.com/design/language/gallery). + + + +## Image Gallery Example + +Click on an image to open the gallery. + + + + + +![IBM Design](/images/IBM_Design_landing.jpg) + + + + + +![IBM Cloud Logo](/images/IBM_Cloud_Logo.png) + + + + +![IBM Cloud Developers](/images/IBM_Cloud_Developers.jpg) + + + + +![IBM Cloud Data Center](/images/IBM_Cloud_Data_Center.jpg) + + + + +![IBM Cloud Interconnect](/images/IBM_Cloud_Interconnect.jpg) + + + + +![IBM Cloud Notebook](/images/IBM_Cloud_Notebook.jpg) + + + + +![IBM Cloud Platform Prototype](/images/IBM_Cloud_Platform_Prototype.gif) + + + + +![IBM Cloud Pictograms](/images/IBM_Cloud_Pictograms.gif) + + + + +![IBM Cloud Server](/images/IBM_Cloud_Server.png) + + + + +![IBM Cloud Think](/images/IBM_Cloud_Think_Keynote.jpg) + + + + + +## Markdown Code + +You can use the ImageGallery component in markdown by nesting your images inside the ImageGallery component and using the ImageGalleryImage component to define the image's location, title, alt tag, and the columns the image on the page will span at the medium and large breakpoints. There is no gallery view for mobile so the small breakpoint is not defined. + +Here's an example of how to use the ImageGallery and the ImageGalleryImage components in markdown. + +```markdown path=/components/ImageGallery.mdx src=https://gatsby-theme-carbon.now.sh + + + +![IBM Design](/images/IBM_Design_landing.jpg) + + + + + +![IBM Cloud Logo](/images/IBM_Cloud_Logo.png) + + + + +![IBM Cloud Developers](/images/IBM_Cloud_Developers.jpg) + + + + +![IBM Cloud Data Center](/images/IBM_Cloud_Data_Center.jpg) + + + + +![IBM Cloud Interconnect](/images/IBM_Cloud_Interconnect.jpg) + + + + +![IBM Cloud Notebook](/images/IBM_Cloud_Notebook.jpg) + + + + +![IBM Cloud Platform Prototype](/images/IBM_Cloud_Platform_Prototype.gif) + + + + +![IBM Cloud Pictograms](/images/IBM_Cloud_Pictograms.gif) + + + + +![IBM Cloud Server](/images/IBM_Cloud_Server.png) + + + + +![IBM Cloud Think](/images/IBM_Cloud_Think_Keynote.jpg) + + + +``` diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Data_Center.jpg b/packages/example/src/pages/components/images/IBM_Cloud_Data_Center.jpg new file mode 100644 index 000000000..ae54963ca Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Data_Center.jpg differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Developers.jpg b/packages/example/src/pages/components/images/IBM_Cloud_Developers.jpg new file mode 100644 index 000000000..051850857 Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Developers.jpg differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Interconnect.jpg b/packages/example/src/pages/components/images/IBM_Cloud_Interconnect.jpg new file mode 100644 index 000000000..3c3927d14 Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Interconnect.jpg differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Logo.png b/packages/example/src/pages/components/images/IBM_Cloud_Logo.png new file mode 100644 index 000000000..b275f04d3 Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Logo.png differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Notebook.jpg b/packages/example/src/pages/components/images/IBM_Cloud_Notebook.jpg new file mode 100644 index 000000000..e187f52f6 Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Notebook.jpg differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Pictograms.gif b/packages/example/src/pages/components/images/IBM_Cloud_Pictograms.gif new file mode 100644 index 000000000..ab744fa16 Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Pictograms.gif differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Platform_Prototype.gif b/packages/example/src/pages/components/images/IBM_Cloud_Platform_Prototype.gif new file mode 100644 index 000000000..559dd577e Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Platform_Prototype.gif differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Server.png b/packages/example/src/pages/components/images/IBM_Cloud_Server.png new file mode 100644 index 000000000..8e19f47bd Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Server.png differ diff --git a/packages/example/src/pages/components/images/IBM_Cloud_Think_Keynote.jpg b/packages/example/src/pages/components/images/IBM_Cloud_Think_Keynote.jpg new file mode 100644 index 000000000..6237d357e Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Cloud_Think_Keynote.jpg differ diff --git a/packages/example/src/pages/components/images/IBM_Design_landing.jpg b/packages/example/src/pages/components/images/IBM_Design_landing.jpg new file mode 100644 index 000000000..29abcc999 Binary files /dev/null and b/packages/example/src/pages/components/images/IBM_Design_landing.jpg differ diff --git a/packages/example/static/images/npm.svg b/packages/example/static/images/npm.svg new file mode 100644 index 000000000..cddc65602 --- /dev/null +++ b/packages/example/static/images/npm.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/packages/gatsby-theme-carbon/package.json b/packages/gatsby-theme-carbon/package.json index ed6aba807..580de6246 100644 --- a/packages/gatsby-theme-carbon/package.json +++ b/packages/gatsby-theme-carbon/package.json @@ -32,6 +32,7 @@ "classnames": "^2.2.6", "copy-to-clipboard": "^3.2.0", "emotion-theming": "^10.0.10", + "focus-trap-react": "^6.0.0", "gatsby-plugin-catch-links": "^2.0.15", "gatsby-plugin-emotion": "^4.0.7", "gatsby-plugin-manifest": "^2.1.1", diff --git a/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGallery.js b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGallery.js new file mode 100644 index 000000000..fbcb95ddd --- /dev/null +++ b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGallery.js @@ -0,0 +1,194 @@ +import React, { useState, useEffect, Children } from 'react'; +import ReactDOM from 'react-dom'; +import { breakpoints } from '@carbon/elements'; +import { ChevronRight32, ChevronLeft32, Close32 } from '@carbon/icons-react'; +import cx from 'classnames'; +import FocusTrap from 'focus-trap-react'; +import useMedia from 'use-media'; +import PropTypes from 'prop-types'; +import { Grid, Row, Column } from '../Grid'; +import { + galleryContainer, + inDialogGalleryContainer, + galleryGrid, + galleryRow, + navButtons, + closeButton, + icon, + navButtonsContainer, + firstRightNav, + rightNav, + leftNav, + addNoScroll, +} from './ImageGallery.module.scss'; + +function ImageGallery({ children }) { + const [portalsNode, updateNode] = useState(null); + const [isGalleryOpen, setIsGalleryOpen] = useState(false); + const [activeImageIndex, updateActiveImageIndex] = useState(null); + const childrenAsArray = Children.toArray(children); + const rightNavButton = cx({ + [rightNav]: true, + [firstRightNav]: activeImageIndex === 0, + [navButtons]: activeImageIndex > 0, + }); + const leftNavButton = cx([leftNav], [navButtons]); + const isMobile = useMedia({ maxWidth: breakpoints.md.width }); + + // Creates the node to go into the portalsNode state. + useEffect(() => { + const node = document.createElement('div'); + document.body.appendChild(node); + updateNode(node); + + return () => { + node.parentNode.removeChild(node); + }; + }, []); + + // Depending on if the gallery is open or not, this adds the addNoScroll class so the screen behind the modal doesn't scroll when opened. + useEffect(() => { + if (isGalleryOpen) { + document.body.classList.add(addNoScroll); + } + + return () => { + document.body.classList.remove(addNoScroll); + }; + }, [isGalleryOpen]); + + // Removes addNoScroll if view is shrunk to mobile view when the gallery is open + useEffect(() => { + if ( + (isMobile && document.body.classList.contains(addNoScroll)) || + !isGalleryOpen + ) { + document.body.classList.remove(addNoScroll); + } + + return () => { + if (!isMobile && isGalleryOpen) { + document.body.classList.add(addNoScroll); + } + }; + }, [isGalleryOpen, isMobile]); + + // Opens gallery if the breakpoint isn't mobile + function openGalleryForImage(index) { + return () => { + if (!isMobile) { + setIsGalleryOpen(true); + updateActiveImageIndex(index); + } + }; + } + + function closeGallery() { + setIsGalleryOpen(false); + updateActiveImageIndex(null); + } + + function selectNextImage() { + if (activeImageIndex + 1 < childrenAsArray.length) { + updateActiveImageIndex(activeImageIndex + 1); + } + } + + function selectPrevImage() { + if (activeImageIndex - 1 >= 0) { + updateActiveImageIndex(activeImageIndex - 1); + } + } + + function onKeyDown(event) { + if (event.key === 'Escape') { + closeGallery(); + return; + } + if (event.key === 'ArrowLeft') { + selectPrevImage(); + return; + } + if (event.key === 'ArrowRight') { + selectNextImage(); + } + } + + return ( + <> +
+ + {Children.map(children, (child, index) => + React.cloneElement(child, { + onClick: openGalleryForImage(index), + }) + )} + +
+ {portalsNode && + isGalleryOpen && + !isMobile && + ReactDOM.createPortal( + + {/* Because of FocusTrap, the key down events will propagate up removing the accessibility problem that would be created by having a keydown event listener on a non-interactive element. */} + {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +
+ + + + + + + + + {activeImageIndex - 1 >= 0 && ( + + )} + + + {React.cloneElement(childrenAsArray[activeImageIndex], { + isInDialog: true, + })} + + + {activeImageIndex + 1 < childrenAsArray.length && ( + + )} + + + +
+
, + portalsNode + )} + + ); +} + +ImageGallery.propTypes = { + children: PropTypes.arrayOf(PropTypes.element).isRequired, +}; + +export default ImageGallery; diff --git a/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGallery.module.scss b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGallery.module.scss new file mode 100644 index 000000000..5c42fea6c --- /dev/null +++ b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGallery.module.scss @@ -0,0 +1,162 @@ +$z-01: 1; +$z-02: 2; + +.add-no-scroll { + width: 100%; + height: 100%; + position: absolute; + overflow: hidden; +} + +.in-dialog-gallery-container { + position: absolute; + margin: 0; + z-index: 10000; + top: 0; + bottom: 0; + left: 0; + right: 0; + height: 100%; + width: 100%; + background: rgba(0, 0, 0, 0.9); + overflow: hidden; +} + +.gallery-grid { + height: 100%; + width: 100%; +} + +.gallery-row { + height: 100%; +} + +.nav-buttons-container { + width: 100%; + height: 100%; +} + +.nav-buttons { + height: 100%; + width: 100%; + outline: none; + background: none; + border: none; + padding: 0; + z-index: $z-01; + cursor: pointer; +} + +.left-nav { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.right-nav { + display: flex; + justify-content: flex-end; + align-items: center; +} + +.first-right-nav { + display: flex; + justify-content: flex-end; + align-items: center; + height: 100%; + width: 100%; + outline: none; + background: none; + border: none; + padding: 0; + z-index: $z-01; + cursor: pointer; +} + +.icon { + fill: $text-04; +} + +.close-button { + position: absolute; + right: 0; + width: 14.5%; + height: 4rem; + background: none; + border: none; + padding: 0; + z-index: $z-02; + outline: none; + cursor: pointer; +} + +.close-button svg { + position: absolute; + top: $spacing-09; + right: $spacing-07; +} + +.nav-buttons:focus svg { + outline: 2px solid $focus; + outline-offset: -2px; +} + +.close-button:focus svg { + outline: 2px solid $focus; + outline-offset: -2px; +} + +.in-dialog-gallery-container, +.gallery-container { + @include carbon--breakpoint('md') { + :global(.bx--col-lg-2) { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; + } + + :global(.bx--col-lg-3) { + flex: 0 0 25%; + max-width: 25%; + } + + :global(.bx--col-lg-4) { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + + :global(.bx--col-lg-5) { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; + } + + :global(.bx--col-lg-6) { + flex: 0 0 50%; + max-width: 50%; + } + + :global(.bx--col-lg-7) { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + + :global(.bx--col-lg-8) { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + + :global(.bx--col-lg-9) { + flex: 0 0 75%; + max-width: 75%; + } + + :global(.bx--col-lg-10) { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + + :global(.bx--col-lg-11) { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; + } + } +} diff --git a/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGalleryImage.js b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGalleryImage.js new file mode 100644 index 000000000..d4f284382 --- /dev/null +++ b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGalleryImage.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Column } from '../Grid'; +import { + imageButtonWrapper, + imageTitle, + imageInDialog, +} from './ImageGalleryImage.module.scss'; + +function ImageGalleryImage({ + title, + alt, + col, + isInDialog = false, + children, + ...rest +}) { + if (isInDialog) { + return ( + <> +

{title}

+
{children}
+ + ); + } + + return ( + +
+ +
+
+ ); +} + +ImageGalleryImage.propTypes = { + title: PropTypes.string, + alt: PropTypes.string.isRequired, + col: PropTypes.number, + isInDialog: PropTypes.bool, + children: PropTypes.object, +}; + +export default ImageGalleryImage; diff --git a/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGalleryImage.module.scss b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGalleryImage.module.scss new file mode 100644 index 000000000..4f4ee2210 --- /dev/null +++ b/packages/gatsby-theme-carbon/src/components/ImageGallery/ImageGalleryImage.module.scss @@ -0,0 +1,50 @@ +.image-button-wrapper { + border: 0; + background: none; + padding: 0; + outline: none; + width: 100%; +} + +.image-button-wrapper::-moz-focus-inner { + border: 0; +} + +.image-button-wrapper:focus { + outline: none; +} + +.image-title { + @include carbon--type-style('productive-heading-02'); + position: absolute; + display: block; + width: 100%; + align-self: flex-start; + left: $spacing-07; + color: $text-04; + margin-top: $spacing-09; + margin-left: $spacing-03; + user-select: none; +} + +.image-in-dialog { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + user-select: none; +} + +.image-in-dialog :global(.gatsby-resp-image-wrapper) { + width: 100%; +} + +.image-in-dialog img { + margin: $spacing-05 0; +} + +.image-button-wrapper:focus img { + outline: 2px solid $focus; + outline-offset: -2px; +} diff --git a/packages/gatsby-theme-carbon/src/components/ImageGallery/index.js b/packages/gatsby-theme-carbon/src/components/ImageGallery/index.js new file mode 100644 index 000000000..c51bc388f --- /dev/null +++ b/packages/gatsby-theme-carbon/src/components/ImageGallery/index.js @@ -0,0 +1,3 @@ +import ImageGallery from './ImageGallery'; + +export default ImageGallery; diff --git a/packages/gatsby-theme-carbon/src/components/MDXProvider/defaultComponents.js b/packages/gatsby-theme-carbon/src/components/MDXProvider/defaultComponents.js index 1618d2559..339977a7b 100644 --- a/packages/gatsby-theme-carbon/src/components/MDXProvider/defaultComponents.js +++ b/packages/gatsby-theme-carbon/src/components/MDXProvider/defaultComponents.js @@ -12,6 +12,8 @@ import ArticleCard from '../ArticleCard'; import Aside from '../Aside'; import FeatureCard from '../FeatureCard'; import ImageCard from '../ImageCard'; +import ImageGallery from '../ImageGallery'; +import ImageGalleryImage from '../ImageGallery/ImageGalleryImage'; import { Row, Column, Grid } from '../Grid'; import { AnchorLink, AnchorLinks } from '../AnchorLinks'; import { Tab, Tabs } from '../Tabs'; @@ -51,6 +53,8 @@ const components = { Aside, FeatureCard, ImageCard, + ImageGallery, + ImageGalleryImage, AnchorLink, AnchorLinks, Tab,