From 27ebe46f6d4900d1bac5986f57770c93f2ab1287 Mon Sep 17 00:00:00 2001 From: Steve 'Cutter' Blades Date: Fri, 8 Jul 2022 16:09:59 -0500 Subject: [PATCH] fix: update to current react-overlays (#2217) Update react-overlays to resolve issues with StrictMode, and constrain popup to within Month container. #2186 --- package.json | 2 +- src/Month.js | 42 ++++++- src/PopOverlay.js | 95 +++++++++++++++ src/Popup.js | 217 +++++++++++++++++------------------ src/hooks/useClickOutside.js | 15 +++ yarn.lock | 38 +++--- 6 files changed, 269 insertions(+), 140 deletions(-) create mode 100644 src/PopOverlay.js create mode 100644 src/hooks/useClickOutside.js diff --git a/package.json b/package.json index 5f4f4e377..3d361a492 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.34", "prop-types": "^15.8.1", - "react-overlays": "^4.1.1", + "react-overlays": "^5.2.0", "uncontrollable": "^7.2.1" }, "bugs": { diff --git a/src/Month.js b/src/Month.js index 2bf573da9..997f48fd8 100644 --- a/src/Month.js +++ b/src/Month.js @@ -9,8 +9,9 @@ import { notify } from './utils/helpers' import getPosition from 'dom-helpers/position' import * as animationFrame from 'dom-helpers/animationFrame' -import Popup from './Popup' -import Overlay from 'react-overlays/Overlay' +/* import Popup from './Popup' +import Overlay from 'react-overlays/Overlay' */ +import PopOverlay from './PopOverlay' import DateContentRow from './DateContentRow' import Header from './Header' import DateHeader from './DateHeader' @@ -204,11 +205,40 @@ class MonthView extends React.Component { } renderOverlay() { - let overlay = (this.state && this.state.overlay) || {} - let { accessors, localizer, components, getters, selected, popupOffset } = - this.props + let overlay = this.state?.overlay ?? {} + let { + accessors, + localizer, + components, + getters, + selected, + popupOffset, + handleDragStart, + } = this.props + + const onHide = () => this.setState({ overlay: null }) return ( + + ) + + /* return ( )} - ) + ) */ } measureRowLimit() { diff --git a/src/PopOverlay.js b/src/PopOverlay.js new file mode 100644 index 000000000..3499bec62 --- /dev/null +++ b/src/PopOverlay.js @@ -0,0 +1,95 @@ +import React, { useRef } from 'react' +import PropTypes from 'prop-types' +import { Overlay } from 'react-overlays' +import Popup from './Popup' + +function CalOverlay({ + containerRef, + popupOffset = 5, + overlay, + accessors, + localizer, + components, + getters, + selected, + handleSelectEvent, + handleDoubleClickEvent, + handleKeyPressEvent, + handleDragStart, + onHide, + overlayDisplay, +}) { + const popperRef = useRef(null) + if (!overlay.position) return null + + let offset + if (!isNaN(popupOffset)) { + offset = { x: popupOffset, y: popupOffset } + } + + const { position, events, date, end } = overlay + return ( + + {({ props }) => ( + + )} + + ) +} + +const PopOverlay = React.forwardRef((props, ref) => ( + +)) + +PopOverlay.propTypes = { + popupOffset: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), + ]), + overlay: PropTypes.shape({ + position: PropTypes.object, + events: PropTypes.array, + date: PropTypes.instanceOf(Date), + end: PropTypes.instanceOf(Date), + }), + accessors: PropTypes.object.isRequired, + localizer: PropTypes.object.isRequired, + components: PropTypes.object.isRequired, + getters: PropTypes.object.isRequired, + selected: PropTypes.object, + handleSelectEvent: PropTypes.func, + handleDoubleClickEvent: PropTypes.func, + handleKeyPressEvent: PropTypes.func, + handleDragStart: PropTypes.func, + onHide: PropTypes.func, + overlayDisplay: PropTypes.func, +} + +export default PopOverlay diff --git a/src/Popup.js b/src/Popup.js index a19fb3cf7..5d14d31f2 100644 --- a/src/Popup.js +++ b/src/Popup.js @@ -1,136 +1,125 @@ +import React, { useLayoutEffect } from 'react' import PropTypes from 'prop-types' -import React from 'react' import getOffset from 'dom-helpers/offset' -import getScrollTop from 'dom-helpers/scrollTop' -import getScrollLeft from 'dom-helpers/scrollLeft' +import useClickOutside from './hooks/useClickOutside' import EventCell from './EventCell' import { isSelected } from './utils/selection' -class Popup extends React.Component { - componentDidMount() { - let { popupOffset = 5, popperRef } = this.props, - { top, left, width, height } = getOffset(popperRef.current), - viewBottom = window.innerHeight + getScrollTop(window), - viewRight = window.innerWidth + getScrollLeft(window), - bottom = top + height, - right = left + width - - if (bottom > viewBottom || right > viewRight) { - let topOffset, leftOffset - - if (bottom > viewBottom) - topOffset = bottom - viewBottom + (popupOffset.y || +popupOffset || 0) - if (right > viewRight) - leftOffset = right - viewRight + (popupOffset.x || +popupOffset || 0) +/** + * Changes to react-overlays cause issue with auto positioning, + * so we need to manually calculate the position of the popper, + * and constrain it to the Month container. + */ +function getPosition({ target, offset, container, box }) { + const { top, left, width, height } = getOffset(target) + const { + top: cTop, + left: cLeft, + width: cWidth, + height: cHeight, + } = getOffset(container) + const { width: bWidth, height: bHeight } = getOffset(box) + const viewBottom = cTop + cHeight + const viewRight = cLeft + cWidth + const bottom = top + bHeight + const right = left + bWidth + const { x, y } = offset + const topOffset = bottom > viewBottom ? top - bHeight - y : top + y + height + const leftOffset = right > viewRight ? left + x - bWidth + width : left + x - this.setState({ topOffset, leftOffset }) //eslint-disable-line - } + return { + topOffset, + leftOffset, } +} - render() { - let { - events, - selected, - getters, - accessors, - components, - onSelect, - onDoubleClick, - onKeyPress, - slotStart, - slotEnd, - localizer, - popperRef, - } = this.props - - let { width } = this.props.position, - topOffset = (this.state || {}).topOffset || 0, - leftOffset = (this.state || {}).leftOffset || 0 - - let style = { - top: -topOffset, - left: -leftOffset, - minWidth: width + width / 2, - } +function Pop({ + containerRef, + accessors, + getters, + selected, + components, + localizer, + position, + show, + events, + slotStart, + slotEnd, + onSelect, + onDoubleClick, + onKeyPress, + handleDragStart, + popperRef, + target, + offset, +}) { + useClickOutside({ ref: popperRef, callback: show }) + useLayoutEffect(() => { + const { topOffset, leftOffset } = getPosition({ + target, + offset, + container: containerRef.current, + box: popperRef.current, + }) + popperRef.current.style.top = `${topOffset}px` + popperRef.current.style.left = `${leftOffset}px` + }, [offset.x, offset.y, target]) - return ( -
-
- {localizer.format(slotStart, 'dayHeaderFormat')} -
- {events.map((event, idx) => ( - this.props.handleDragStart(event)} - onDragEnd={() => this.props.show()} - /> - ))} -
- ) + const { width } = position + const style = { + minWidth: width + width / 2, } + return ( +
+
+ {localizer.format(slotStart, 'dayHeaderFormat')} +
+ {events.map((event, idx) => ( + handleDragStart(event)} + onDragEnd={() => show()} + /> + ))} +
+ ) } +const Popup = React.forwardRef((props, ref) => ( + +)) Popup.propTypes = { - position: PropTypes.object, - popupOffset: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number, - }), - ]), - events: PropTypes.array, - selected: PropTypes.object, - accessors: PropTypes.object.isRequired, - components: PropTypes.object.isRequired, getters: PropTypes.object.isRequired, + selected: PropTypes.object, + components: PropTypes.object.isRequired, localizer: PropTypes.object.isRequired, + position: PropTypes.object.isRequired, + show: PropTypes.func.isRequired, + events: PropTypes.array.isRequired, + slotStart: PropTypes.instanceOf(Date).isRequired, + slotEnd: PropTypes.instanceOf(Date), onSelect: PropTypes.func, onDoubleClick: PropTypes.func, onKeyPress: PropTypes.func, handleDragStart: PropTypes.func, - show: PropTypes.func, - slotStart: PropTypes.instanceOf(Date), - slotEnd: PropTypes.number, - popperRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ current: PropTypes.Element }), - ]), + style: PropTypes.object, + offset: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), } - -/** - * The Overlay component, of react-overlays, creates a ref that is passed to the Popup, and - * requires proper ref forwarding to be used without error - */ -export default React.forwardRef((props, ref) => ( - -)) +export default Popup diff --git a/src/hooks/useClickOutside.js b/src/hooks/useClickOutside.js new file mode 100644 index 000000000..e25e0a651 --- /dev/null +++ b/src/hooks/useClickOutside.js @@ -0,0 +1,15 @@ +import { useEffect } from 'react' + +export default function useClickOutside({ ref, callback }) { + useEffect(() => { + const handleClickOutside = (e) => { + if (ref.current && !ref.current.contains(e.target)) { + callback() + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [ref, callback]) +} diff --git a/yarn.lock b/yarn.lock index f5e3b0701..84cf95982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1863,14 +1863,14 @@ pirates "^4.0.5" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6": +"@babel/runtime@^7.13.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== @@ -2803,15 +2803,15 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.5.3": - version "2.11.2" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" - integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== +"@popperjs/core@^2.8.6": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" + integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== -"@restart/hooks@^0.3.25": - version "0.3.27" - resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.3.27.tgz#91f356d66d4699a8cd8b3d008402708b6a9dc505" - integrity sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw== +"@restart/hooks@^0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39" + integrity sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A== dependencies: dequal "^2.0.2" @@ -13022,18 +13022,18 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-overlays@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-4.1.1.tgz#0060107cbe1c5171a744ccda3fbf0556d064bc5f" - integrity sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ== +react-overlays@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.2.0.tgz#e7ebfdfdfbebd9d23cafd0bc2cac1d08abf1535b" + integrity sha512-dKZR/w6qeAsW0z0aIlwq/5H/M6o5T4RSlPnqIKqYVJ++rjoPSFcVggPhDWno8awZQsuMMtkjuksTbE8vOY0s9g== dependencies: - "@babel/runtime" "^7.12.1" - "@popperjs/core" "^2.5.3" - "@restart/hooks" "^0.3.25" + "@babel/runtime" "^7.13.8" + "@popperjs/core" "^2.8.6" + "@restart/hooks" "^0.4.7" "@types/warning" "^3.0.0" dom-helpers "^5.2.0" prop-types "^15.7.2" - uncontrollable "^7.0.0" + uncontrollable "^7.2.1" warning "^4.0.3" react-refresh@^0.11.0: @@ -15065,7 +15065,7 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -uncontrollable@^7.0.0, uncontrollable@^7.2.1: +uncontrollable@^7.2.1: version "7.2.1" resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==