diff --git a/package.json b/package.json index bacfe1e9c3..43e745d67e 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ }, "dependencies": { "@babel/runtime": "^7.3.4", + "@popperjs/core": "^2.4.4", "body-scroll-lock": "^2.5.8", "classnames": "^2.2.5", "date-fns": "^1.28.5", @@ -143,6 +144,7 @@ "react-hot-loader": "^4.3.11", "react-markdown": "^4.0.8", "react-pdf": "^4.0.5", + "react-popper": "^2.2.3", "react-select": "2.2.0", "react-swipeable-views": "0.13.3" }, diff --git a/react/ActionMenu/Readme.md b/react/ActionMenu/Readme.md index f7fe9993a3..cae813632c 100644 --- a/react/ActionMenu/Readme.md +++ b/react/ActionMenu/Readme.md @@ -91,9 +91,10 @@ const hideMenu = () => setState({ menuDisplayed: false }); ``` -### Placement +### Placement on desktop -The `placement` and `anchorElRef` prop can be used to control the placement of the menu on desktop. `anchorElRef` should be a ref to a DOM element and not a react component. +You can pass a reference to a custom DOM element through the `anchorElRef` prop to attach the menu to that element. +We use [popper.js](https://popper.js.org/docs/v2/) under the hood. You can use the `popperOptions` prop to pass options to the popper.js instance. This lets you control things like placement relative to the anchor, positioning strategies and more — refer to the popper doc for all the details. ``` import ActionMenu, { ActionMenuItem } from 'cozy-ui/transpiled/react/ActionMenu'; @@ -114,112 +115,9 @@ const anchorRef = React.createRef(); {state.menuDisplayed && }>Item 1 } ``` - -### ActionMenu inside custom fixed elements - -The ActionMenu component is rendered at the root of the DOM tree using a [Portal](https://reactjs.org/docs/portals.html) and a fixed z-index. Since Modals for example have a higher z-index than ActionMenus, an ActionMenu _inside_ a Modal would be displayed behind it, instead of over it. To fix this problem, the Popper instance -is given a reference to the Modal DOM node for its `container` option via a special -React context. - -Any z-indexed element that will display action menus should use this technique to -provide a ref to a node inside this stacking context (CSS context, not React -context). - -``` -import { useRef, useState, useCallback } from 'react' -import PopperContainerContext from '../PopperContainerContext' -import ActionMenu, { ActionMenuItem, ActionMenuHeader } from './index'; -import Icon from '../Icon'; - -const MyActionMenu = ({ anchorRef, buttonRef, onClose }) => { - return - }>Item 1 - -} - -const MyFixedElement = () => { - const ref = useRef() - const [shown, setShown] = useState(isTesting()) - const buttonRef = useRef() - const show = useCallback(() => setShown(true), [setShown]) - const hide = useCallback(() => setShown(false), [setShown]) - return -
-
- - {shown ? : null} -
- -}; - -initialState = { shown: isTesting() }; - -<> - - { state.shown && } - -``` - -### ActionMenu within modals - -The above technique is used in Modals, nothing is required from the developer -for ActionMenus inside modals to work correctly. - -``` -import { useState, useRef } from 'react' -import ActionMenu, { ActionMenuItem, ActionMenuHeader } from './index'; -import Icon from '../Icon'; -import Modal, { ModalDescription } from '../Modal'; -import Filename from '../Filename'; - -initialState = { modalDisplayed: isTesting() }; - -const showModal = () => setState({ modalDisplayed: true }); -const hideModal = () => setState({ modalDisplayed: false }); - - -const ExampleModalContent = () => { - const [menuDisplayed, setMenuDisplayed] = useState(isTesting()) - const showMenu = () => setMenuDisplayed(true); - const hideMenu = () => setMenuDisplayed(false); - return ( - - - {menuDisplayed && - - - - - alert('click')}left={}>Item 1 - } - - ) -} - -
- - {state.modalDisplayed && - - - - } -
-``` - -### preventOverflow - -Set `preventOverflow` to `true` to keep the ActionMenu visible on desktop, even if `anchorElRef` is outside the viewport. diff --git a/react/ActionMenu/index.jsx b/react/ActionMenu/index.jsx index 5829ceaf6e..d83e216645 100644 --- a/react/ActionMenu/index.jsx +++ b/react/ActionMenu/index.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext } from 'react' +import React from 'react' import PropTypes from 'prop-types' import cx from 'classnames' import ClickAwayListener from '@material-ui/core/ClickAwayListener' @@ -6,54 +6,65 @@ import styles from './styles.styl' import { Media, Bd, Img } from '../Media' import BottomDrawer from '../BottomDrawer' import withBreakpoints from '../helpers/withBreakpoints' -import Popper from '@material-ui/core/Popper' import { getCssVariableValue } from '../utils/color' -import PopperContainerContext from '../PopperContainerContext' import Radio from '../Radio' import { spacingProp } from '../Stack' +import { usePopper } from 'react-popper' +import createDepreciationLogger from '../helpers/createDepreciationLogger' const ActionMenuWrapper = ({ inline, onClose, anchorElRef, - containerElRef, + popperOptions, placement, preventOverflow, children }) => { - const getElementFromRefCallback = ref => - useCallback(() => { - return ref ? ref.current : undefined - }, [ref]) - - const popperContainerRef = useContext(PopperContainerContext) - const getAnchorElement = getElementFromRefCallback(anchorElRef) - - const containerRef = popperContainerRef || containerElRef - const getContainerElement = getElementFromRefCallback(containerRef) - const normalOverflowModifiers = { - preventOverflow: { enabled: false }, - hide: { enabled: false } + const [popperElement, setPopperElement] = React.useState(null) + const referenceElement = anchorElRef ? anchorElRef.current : null + + const normalOverflowModifiers = [ + { + name: 'preventOverflow', + enabled: false + }, + { + name: 'hide', + enabled: false + } + ] + const options = popperOptions || { + placement, + modifiers: preventOverflow ? undefined : normalOverflowModifiers } + const { styles, attributes } = usePopper( + referenceElement, + popperElement, + options + ) + return inline ? ( - {children} - +
) : ( {children} ) } +const logDepecratedPlacement = createDepreciationLogger() +const logDepecratedOverflow = createDepreciationLogger() +const logDepecratedContainer = createDepreciationLogger() + const ActionMenu = ({ children, autoclose, @@ -61,10 +72,24 @@ const ActionMenu = ({ onClose, placement, preventOverflow, + popperOptions, anchorElRef, containerElRef, breakpoints: { isMobile } }) => { + if (placement) + logDepecratedPlacement( + ' is deprecated, use instead' + ) + if (preventOverflow) + logDepecratedOverflow( + ' is deprecated, use instead' + ) + if (containerElRef) + logDepecratedContainer( + ' is not needed anymore, it can be removed.' + ) + const shouldDisplayInline = !isMobile const containerRef = React.createRef() return ( @@ -77,9 +102,9 @@ const ActionMenu = ({ onClose={onClose} inline={shouldDisplayInline} anchorElRef={anchorElRef || containerRef} - containerElRef={containerElRef} placement={placement} preventOverflow={preventOverflow} + popperOptions={popperOptions} >
- -
- +
+ +
- {/** The popper ref is on a sibling node and not on a parent so that its ref - gets filled in before we render the content of the modal. When the ref - is on a parent of the modal, the ref is not filled yet when we render - the content of the modal */} -
-
- {closable && ( - + )} + {title && } + {description && ( + {description} + )} + {children} + {(primaryText && primaryAction) || + (secondaryText && secondaryAction) ? ( + + - )} - {title && } - {description && ( - {description} - )} - {children} - {(primaryText && primaryAction) || - (secondaryText && secondaryAction) ? ( - - - - ) : null} -
+ + ) : null}
- -
- - +
+
+
+ ) } } diff --git a/react/PopperContainerContext.js b/react/PopperContainerContext.js deleted file mode 100644 index ec35e422ae..0000000000 --- a/react/PopperContainerContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react' - -export default createContext() diff --git a/react/__snapshots__/examples.spec.jsx.snap b/react/__snapshots__/examples.spec.jsx.snap index c8449f1e4b..69b74307d6 100644 --- a/react/__snapshots__/examples.spec.jsx.snap +++ b/react/__snapshots__/examples.spec.jsx.snap @@ -6,27 +6,29 @@ exports[`ActionMenu should render examples: ActionMenu 1`] = `
-
-
-
- -
-
Item 1
-
- -
-
-
-
-
Item 2
-
-
-
- -
-
-
Item 3
-
Descriptive text to elaborate on what item 3 does.
+
+
+
+
+ +
+
Item 1
+
+ +
+
+
+
+
Item 2
+
+
+
+ +
+
+
Item 3
+
Descriptive text to elaborate on what item 3 does.
+
@@ -41,24 +43,26 @@ exports[`ActionMenu should render examples: ActionMenu 2`] = `
-
-
-
- -
-
Item 1
-
-
-
- -
-
Item 2
-
-
-
- -
-
Item 3
+
+
+
+
+ +
+
Item 1
+
+
+
+ +
+
Item 2
+
+
+
+ +
+
Item 3
+
@@ -72,12 +76,14 @@ exports[`ActionMenu should render examples: ActionMenu 3`] = `
-
-
-
- -
-
Item 1
+
+
+
+
+ +
+
Item 1
+
@@ -91,30 +97,14 @@ exports[`ActionMenu should render examples: ActionMenu 4`] = `
-
-
-
- -
-
Item 1
-
-
-
-
-
" -`; - -exports[`ActionMenu should render examples: ActionMenu 5`] = ` -"
-
-
-
-
-
-
- -
-
Item 1
+
+
+
+
+ +
+
Item 1
+
@@ -122,12 +112,6 @@ exports[`ActionMenu should render examples: ActionMenu 5`] = `
" `; -exports[`ActionMenu should render examples: ActionMenu 6`] = ` -"
-
-
" -`; - exports[`AppTitle should render examples: AppTitle 1`] = ` "

Drive

diff --git a/yarn.lock b/yarn.lock index 578e3b512c..529881d1ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1691,6 +1691,11 @@ dependencies: assertion-error "^1.0.2" +"@popperjs/core@^2.4.4": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398" + integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg== + "@semantic-release/changelog@3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-3.0.6.tgz#9d68d68bf732cbba1034c028bb6720091f783b2a" @@ -13863,6 +13868,11 @@ react-event-listener@^0.6.0, react-event-listener@^0.6.2: prop-types "^15.6.0" warning "^4.0.1" +react-fast-compare@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + react-group@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/react-group/-/react-group-3.0.2.tgz#cb31d0fae255111e1dace5b5f9abb9deefc7b36e" @@ -13939,6 +13949,14 @@ react-pdf@^4.0.5: pdfjs-dist "2.1.266" prop-types "^15.6.2" +react-popper@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.3.tgz#33d425fa6975d4bd54d9acd64897a89d904b9d97" + integrity sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-redux@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" @@ -17166,7 +17184,7 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" -warning@^4.0.1: +warning@^4.0.1, warning@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==