Skip to content

Commit

Permalink
Merge pull request #1432 from cozy/container-ref
Browse files Browse the repository at this point in the history
fix: Remove need for containerElRef
ptbrowne authored Apr 10, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents a2ba03b + 93b6f98 commit ee56a5c
Showing 5 changed files with 164 additions and 76 deletions.
85 changes: 69 additions & 16 deletions react/ActionMenu/Readme.md
Original file line number Diff line number Diff line change
@@ -109,46 +109,99 @@ const anchorRef = React.createRef();
</div>
```

### ActionMenu within other components and containerElRef
### 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 will be displayed behind it, instead of over it.
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.

In that case, we can use the `containerElRef` prop to provide a reference to an element where ActionMenu will be rendered. Here is an example for the most common case, an ActionMenu inside a Modal:
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 <ActionMenu
anchorElRef={anchorRef}
placement="bottom-end"
buttonRef={buttonRef}
onClose={onClose}>
<ActionMenuItem onClick={onClose} left={<Icon icon='file' />}>Item 1</ActionMenuItem>
</ActionMenu>
}
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 <PopperContainerContext.Provider value={ref}>
<div style={{ position: 'fixed', height: 100, width: 100, bottom: 0, top: 0, margin: 'auto', left: 0, right: 0, zIndex: 1 }}>
<div ref={ref} />
<button onClick={show} ref={buttonRef}>Open action menu</button>
{shown ? <MyActionMenu onClose={hide} buttonRef={buttonRef}/> : null}
</div>
</PopperContainerContext.Provider>
};
initialState = { shown: isTesting() };
<>
<button onClick={() => setState({ shown: !state.shown })}>
Toggle custom fixed element
</button>
{ state.shown && <MyFixedElement /> }
</>
```

### 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(), menuDisplayed: isTesting() };
initialState = { modalDisplayed: isTesting() };
const showModal = () => setState({ modalDisplayed: true });
const hideModal = () => setState({ modalDisplayed: false });
const showMenu = () => setState({ menuDisplayed: true });
const hideMenu = () => setState({ menuDisplayed: false });
const insideModalRef = React.createRef();
<div>
<button onClick={showModal}>Show modal</button>
{state.modalDisplayed &&
<Modal dismissAction={hideModal}>
const ExampleModalContent = () => {
const [menuDisplayed, setMenuDisplayed] = useState(isTesting())
const showMenu = () => setMenuDisplayed(true);
const hideMenu = () => setMenuDisplayed(false);
return (
<ModalDescription>
<div ref={insideModalRef}>
<button onClick={showMenu}>Show action menu</button>
</div>
{state.menuDisplayed &&
{menuDisplayed &&
<ActionMenu
containerElRef={insideModalRef}
onClose={hideMenu}>
<ActionMenuHeader>
<Filename icon="file" filename="my_awesome_paper" extension=".pdf" />
</ActionMenuHeader>
<ActionMenuItem onClick={() => alert('click')}left={<Icon icon='file' />}>Item 1</ActionMenuItem>
</ActionMenu>}
</ModalDescription>
)
}
<div>
<button onClick={showModal}>Show modal</button>
{state.modalDisplayed &&
<Modal dismissAction={hideModal}>
<ExampleModalContent />
</Modal>
}
</div>
9 changes: 7 additions & 2 deletions react/ActionMenu/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'
import React, { useCallback, useContext } from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
@@ -8,6 +8,8 @@ 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'

const ActionMenuWrapper = ({
inline,
onClose,
@@ -22,8 +24,11 @@ const ActionMenuWrapper = ({
return ref ? ref.current : undefined
}, [ref])

const popperContainerRef = useContext(PopperContainerContext)
const getAnchorElement = getElementFromRefCallback(anchorElRef)
const getContainerElement = getElementFromRefCallback(containerElRef)

const containerRef = popperContainerRef || containerElRef
const getContainerElement = getElementFromRefCallback(containerRef)
const normalOverflowModifiers = {
preventOverflow: { enabled: false },
hide: { enabled: false }
125 changes: 67 additions & 58 deletions react/Modal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react'
import React, { Component, createRef } from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'
import Overlay from '../Overlay'
@@ -14,6 +14,7 @@ import ModalFooter from './ModalFooter'
import ModalButtons from './ModalButtons'
import AnimatedContentHeader from './AnimatedContentHeader'
import ModalBackButton from './ModalBackButton'
import PopperContainerContext from '../PopperContainerContext'

const ModalDescription = ModalContent

@@ -88,73 +89,81 @@ class Modal extends Component {
} = this.props
const { titleID } = this
const style = Object.assign({}, height && { height }, width && { width })
const modalPopperRef = createRef()
return (
<Portal into={into}>
<div className={cx(styles['c-modal-container'], containerClassName)}>
<Overlay
onEscape={closable ? dismissAction : undefined}
className={overlayClassName}
>
<div
className={cx(
styles['c-modal-wrapper'],
{
[styles['c-modal-wrapper--fullscreen']]: mobileFullscreen
},
wrapperClassName
)}
onClick={closable ? this.handleOutsideClick : undefined}
<PopperContainerContext.Provider value={modalPopperRef}>
<Portal into={into}>
<div className={cx(styles['c-modal-container'], containerClassName)}>
<Overlay
onEscape={closable ? dismissAction : undefined}
className={overlayClassName}
>
<div
className={cx(
styles['c-modal'],
styles[`c-modal--${size}`],
styles['c-modal-wrapper'],
{
[styles['c-modal--overflowHidden']]: overflowHidden,
[styles[`c-modal--${spacing}-spacing`]]: spacing,
[styles['c-modal--fullscreen']]: mobileFullscreen,
[styles['c-modal--closable']]: closable
[styles['c-modal-wrapper--fullscreen']]: mobileFullscreen
},
className
wrapperClassName
)}
style={style}
role="dialog"
aria-modal="true"
aria-labelledby={title ? titleID : null}
{...restProps}
onClick={closable ? this.handleOutsideClick : undefined}
>
{closable && (
<ModalCross
className={cx(closeBtnClassName, {
[styles['c-modal-close--notitle']]: !title
})}
onClick={dismissAction}
color={closeBtnColor}
/>
)}
{title && <ModalHeader title={title} id={titleID} />}
{description && (
<ModalDescription>{description}</ModalDescription>
)}
{children}
{(primaryText && primaryAction) ||
(secondaryText && secondaryAction) ? (
<ModalFooter>
<ModalButtons
primaryText={primaryText}
primaryAction={primaryAction}
primaryType={primaryType}
secondaryText={secondaryText}
secondaryAction={secondaryAction}
secondaryType={secondaryType}
{/** 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 */}
<div ref={modalPopperRef} />
<div
className={cx(
styles['c-modal'],
styles[`c-modal--${size}`],
{
[styles['c-modal--overflowHidden']]: overflowHidden,
[styles[`c-modal--${spacing}-spacing`]]: spacing,
[styles['c-modal--fullscreen']]: mobileFullscreen,
[styles['c-modal--closable']]: closable
},
className
)}
style={style}
role="dialog"
aria-modal="true"
aria-labelledby={title ? titleID : null}
{...restProps}
>
{closable && (
<ModalCross
className={cx(closeBtnClassName, {
[styles['c-modal-close--notitle']]: !title
})}
onClick={dismissAction}
color={closeBtnColor}
/>
</ModalFooter>
) : null}
)}
{title && <ModalHeader title={title} id={titleID} />}
{description && (
<ModalDescription>{description}</ModalDescription>
)}
{children}
{(primaryText && primaryAction) ||
(secondaryText && secondaryAction) ? (
<ModalFooter>
<ModalButtons
primaryText={primaryText}
primaryAction={primaryAction}
primaryType={primaryType}
secondaryText={secondaryText}
secondaryAction={secondaryAction}
secondaryType={secondaryType}
/>
</ModalFooter>
) : null}
</div>
</div>
</div>
</Overlay>
</div>
</Portal>
</Overlay>
</div>
</Portal>
</PopperContainerContext.Provider>
)
}
}
3 changes: 3 additions & 0 deletions react/PopperContainerContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react'

export default createContext()
18 changes: 18 additions & 0 deletions react/__snapshots__/examples.spec.jsx.snap
Original file line number Diff line number Diff line change
@@ -101,6 +101,24 @@ exports[`ActionMenu should render examples: ActionMenu 4`] = `
`;

exports[`ActionMenu should render examples: ActionMenu 5`] = `
"<div><button>Toggle custom fixed element</button>
<div style=\\"position: fixed; height: 100px; width: 100px; bottom: 0px; top: 0px; margin: auto; left: 0px; right: 0px; z-index: 1;\\">
<div></div><button>Open action menu</button>
<div>
<div class=\\"styles__c-actionmenu___22Fp1 styles__c-actionmenu--inline___1SXZa\\">
<div class=\\"styles__media___cSJMp styles__c-actionmenu-item___gODqd\\">
<div class=\\"styles__img___3SHpG u-mh-1\\"><svg class=\\"styles__icon___23x3R\\" width=\\"16\\" height=\\"16\\">
<use xlink:href=\\"#file\\"></use>
</svg></div>
<div class=\\"styles__bd___1Uv-F u-mr-1\\">Item 1</div>
</div>
</div>
</div>
</div>
</div>"
`;

exports[`ActionMenu should render examples: ActionMenu 6`] = `
"<div>
<div><button>Show modal</button></div>
</div>"

0 comments on commit ee56a5c

Please sign in to comment.