diff --git a/package-lock.json b/package-lock.json index e12395b4f3..8b40122ed9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6717,6 +6717,11 @@ } } }, + "@popperjs/core": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.4.tgz", + "integrity": "sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==" + }, "@reach/router": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz", @@ -28840,8 +28845,7 @@ "react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", - "dev": true + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-helmet-async": { "version": "1.0.6", @@ -28943,19 +28947,23 @@ "prop-types": "^15.6.2" } }, - "react-popper": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", - "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", - "dev": true, + "react-popper-2": { + "version": "npm:react-popper@2.2.3", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.3.tgz", + "integrity": "sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw==", "requires": { - "@babel/runtime": "^7.1.2", - "create-react-context": "^0.3.0", - "deep-equal": "^1.1.1", - "popper.js": "^1.14.4", - "prop-types": "^15.6.1", - "typed-styles": "^0.0.7", + "react-fast-compare": "^3.0.1", "warning": "^4.0.2" + } + }, + "react-popper-tooltip": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz", + "integrity": "sha512-04A2f24GhyyMicKvg/koIOQ5BzlrRbKiAgP6L+Pdj1MVX3yJ1NeZ8+EidndQsbejFT55oW1b++wg2Z8KlAyhfQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.9.2", + "react-popper": "^1.3.7" }, "dependencies": { "deep-equal": { @@ -28977,19 +28985,24 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true + }, + "react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "dev": true, + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } } } }, - "react-popper-tooltip": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz", - "integrity": "sha512-04A2f24GhyyMicKvg/koIOQ5BzlrRbKiAgP6L+Pdj1MVX3yJ1NeZ8+EidndQsbejFT55oW1b++wg2Z8KlAyhfQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "react-popper": "^1.3.7" - } - }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -29104,6 +29117,17 @@ "refractor": "^2.4.1" } }, + "react-test-renderer": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz", + "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + } + }, "react-textarea-autosize": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.2.0.tgz", @@ -29132,6 +29156,15 @@ } } }, + "react-use-css-breakpoints": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-use-css-breakpoints/-/react-use-css-breakpoints-1.0.3.tgz", + "integrity": "sha512-l1rg/gknC8bDELa3lW3Ph0tTTbn1J31gUx+pskbS1OxSEh/7aEQeT37ZB5IPfmDediriROSPERx0XKq7UEfC0w==", + "requires": { + "react": "^16.10.2", + "react-test-renderer": "^16.10.2" + } + }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -34348,7 +34381,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } diff --git a/package.json b/package.json index 54570bee5f..0ac6f02af5 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@fortawesome/react-fontawesome": "^0.1.0", "@mozillareality/easing-functions": "^0.1.1", "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", + "@popperjs/core": "^2.4.4", "aframe": "github:mozillareality/aframe#hubs/master", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", @@ -101,8 +102,10 @@ "react-infinite-scroller": "^1.2.2", "react-intl": "^2.4.0", "react-linkify": "^0.2.2", + "react-popper-2": "npm:react-popper@^2.2.3", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", + "react-use-css-breakpoints": "^1.0.3", "screenfull": "^4.0.1", "semver": "^7.3.2", "three": "github:mozillareality/three.js#hubs/master", diff --git a/src/react-components/popover/Popover.js b/src/react-components/popover/Popover.js new file mode 100644 index 0000000000..ddbc37ff81 --- /dev/null +++ b/src/react-components/popover/Popover.js @@ -0,0 +1,116 @@ +import React, { useState, useCallback, useEffect } from "react"; +// Note react-popper-2 is just an alias to react-popper@2.2.3 because storybook is depending on an old version. +// https://github.com/storybookjs/storybook/issues/10982 +import { usePopper } from "react-popper-2"; +import styles from "./Popover.scss"; +import { createPortal } from "react-dom"; +import PropTypes from "prop-types"; +import { useCssBreakpoints } from "react-use-css-breakpoints"; +import classNames from "classnames"; +import { ReactComponent as CloseIcon } from "../icons/Close.svg"; +import { ReactComponent as PopoverArrow } from "./PopoverArrow.svg"; + +export function Popover({ content: Content, children, title, placement, initiallyVisible }) { + const [visible, setVisible] = useState(initiallyVisible); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [arrowElement, setArrowElement] = useState(null); + const { + styles: { popper: popperStyles, arrow: arrowStyles }, + attributes + } = usePopper(referenceElement, popperElement, { + placement, + modifiers: [{ name: "arrow", options: { element: arrowElement } }, { name: "offset", options: { offset: [0, 16] } }] + }); + const breakpoint = useCssBreakpoints(); + const fullscreen = breakpoint === "sm"; + const closePopover = useCallback(() => setVisible(false), [setVisible]); + const togglePopover = useCallback(() => setVisible(visible => !visible), [setVisible]); + + useEffect( + () => { + const onClick = e => { + if ( + (referenceElement && referenceElement.contains(e.target)) || + (popperElement && popperElement.contains(e.target)) + ) { + return; + } + + setVisible(false); + }; + + if (visible) { + window.addEventListener("click", onClick); + } + + return () => { + window.removeEventListener("click", onClick); + }; + }, + [visible, popperElement, referenceElement] + ); + + useEffect( + () => { + if (visible && fullscreen) { + document.body.classList.add(styles.fullscreenBody); + } else { + document.body.classList.remove(styles.fullscreenBody); + } + + return () => { + document.body.classList.remove(styles.fullscreenBody); + }; + }, + [fullscreen, visible] + ); + + return ( + <> + {children({ + togglePopover, + popoverVisible: visible, + triggerRef: setReferenceElement + })} + {visible && + createPortal( +