Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(Popup): add mountNode and mountDocument #1288

Merged
merged 7 commits into from
May 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### BREAKING CHANGES
- Rename `context` prop to `mountNode` in `PortalInner` @layershifter ([#1288](https://github.com/stardust-ui/react/pull/1288))

### Features

- Add default child a11y behavior to `Menu` related behaviors @silviuavram ([#1282](https://github.com/stardust-ui/react/pull/1282))
- `Ref` component extracted to a `@stardust-ui/react-component-ref` @layershifter ([#1281](https://github.com/stardust-ui/react/pull/1281))
- added `isRefObject()`, `toRefObject()` utils for React refs @layershifter ([#1281](https://github.com/stardust-ui/react/pull/1281))
- Add new callings icons in Teams theme @codepretty ([#1264](https://github.com/stardust-ui/react/pull/1264))
- Add `mountNode` and `mountDocument` props to allow proper multi-window rendering ([#1288](https://github.com/stardust-ui/react/pull/1288))

<!--------------------------------[ v0.29.1 ]------------------------------- -->
## [v0.29.1](https://github.com/stardust-ui/react/tree/v0.29.1) (2019-05-01)
Expand Down
38 changes: 24 additions & 14 deletions packages/react/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { documentRef, EventListener } from '@stardust-ui/react-component-event-listener'
import { EventListener } from '@stardust-ui/react-component-event-listener'
import { NodeRef, Unstable_NestingAuto } from '@stardust-ui/react-component-nesting-registry'
import { handleRef, Ref } from '@stardust-ui/react-component-ref'
import { handleRef, toRefObject, Ref } from '@stardust-ui/react-component-ref'
import * as customPropTypes from '@stardust-ui/react-proptypes'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
Expand Down Expand Up @@ -78,6 +78,12 @@ export interface PopupProps
/** Whether the Popup should be rendered inline with the trigger or in the body. */
inline?: boolean

/** Existing document the popup should add listeners. */
mountDocument?: Document

/** Existing element the popup should be bound to. */
mountNode?: HTMLElement

/** Delay in ms for the mouse leave event, before the popup will be closed. */
mouseLeaveDelay?: number

Expand Down Expand Up @@ -165,6 +171,8 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
defaultOpen: PropTypes.bool,
defaultTarget: PropTypes.any,
inline: PropTypes.bool,
mountDocument: PropTypes.object,
mountNode: customPropTypes.domNode,
mouseLeaveDelay: PropTypes.number,
on: PropTypes.oneOfType([
PropTypes.oneOf(['hover', 'click', 'focus']),
Expand All @@ -184,15 +192,15 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
static defaultProps: PopupProps = {
accessibility: popupBehavior,
align: 'start',
mountDocument: isBrowser() ? document : null,
mountNode: isBrowser() ? document.body : null,
position: 'above',
on: 'click',
mouseLeaveDelay: 500,
}

static autoControlledProps = ['open', 'target']

static isBrowserContext = isBrowser()

triggerDomElement = null
// focusable element which has triggered Popup, can be either triggerDomElement or the element inside it
triggerFocusableDomElement = null
Expand Down Expand Up @@ -224,17 +232,15 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
rtl,
accessibility,
}: RenderResultConfig<PopupProps>): React.ReactNode {
const { inline } = this.props
const popupContent = this.renderPopupContent(classes.popup, rtl, accessibility)
const { inline, mountNode } = this.props
const { open } = this.state
const popupContent = open && this.renderPopupContent(classes.popup, rtl, accessibility)

return (
<>
{this.renderTrigger(accessibility)}

{this.state.open &&
Popup.isBrowserContext &&
popupContent &&
(inline ? popupContent : ReactDOM.createPortal(popupContent, document.body))}
{open &&
(inline ? popupContent : mountNode && ReactDOM.createPortal(popupContent, mountNode))}
</>
)
}
Expand Down Expand Up @@ -449,8 +455,9 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
style: popupPlacementStyles,
}: PopperChildrenProps,
) => {
const { content: propsContent, renderContent, contentRef, pointing } = this.props
const { content: propsContent, renderContent, contentRef, mountDocument, pointing } = this.props
const content = renderContent ? renderContent(scheduleUpdate) : propsContent
const documentRef = toRefObject(mountDocument)

const popupWrapperAttributes = {
...(rtl && { dir: 'rtl' }),
Expand Down Expand Up @@ -560,8 +567,11 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
* Can be either trigger DOM element itself or the element inside it.
*/
updateTriggerFocusableDomElement() {
this.triggerFocusableDomElement = this.triggerDomElement.contains(document.activeElement)
? document.activeElement
const { mountDocument } = this.props
const activeElement = mountDocument.activeElement

this.triggerFocusableDomElement = this.triggerDomElement.contains(activeElement)
? activeElement
: this.triggerDomElement
}
}
20 changes: 10 additions & 10 deletions packages/react/src/components/Portal/PortalInner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ReactProps } from '../../types'

export interface PortalInnerProps extends ChildrenComponentProps {
/** Existing element the portal should be bound to. */
context?: HTMLElement
mountNode?: HTMLElement

/**
* Called when the portal is mounted on the DOM
Expand All @@ -28,7 +28,7 @@ export interface PortalInnerProps extends ChildrenComponentProps {
* An inner component that allows you to render children outside their parent.
*/
class PortalInner extends React.Component<ReactProps<PortalInnerProps>> {
public static propTypes = {
static propTypes = {
...commonPropTypes.createCommon({
accessibility: false,
animated: false,
Expand All @@ -37,27 +37,27 @@ class PortalInner extends React.Component<ReactProps<PortalInnerProps>> {
content: false,
styled: false,
}),
context: PropTypes.object,
mountNode: PropTypes.object,
onMount: PropTypes.func,
onUnmount: PropTypes.func,
}

public static defaultProps = {
context: isBrowser() ? document.body : null,
static defaultProps = {
mountNode: isBrowser() ? document.body : null,
}

public componentDidMount() {
componentDidMount() {
_.invoke(this.props, 'onMount', this.props)
}

public componentWillUnmount() {
componentWillUnmount() {
_.invoke(this.props, 'onUnmount', this.props)
}

public render() {
const { children, context } = this.props
render() {
const { children, mountNode } = this.props

return context && ReactDOM.createPortal(children, context)
return mountNode && ReactDOM.createPortal(children, mountNode)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,29 @@ const mountPortalInner = (props: PortalInnerProps) =>
describe('PortalInner', () => {
describe('render', () => {
it('calls react createPortal', () => {
const context = document.createElement('div')
const comp = mountPortalInner({ context })
const mountNode = document.createElement('div')
const comp = mountPortalInner({ mountNode })

expect(context.contains(comp.getDOMNode())).toBeTruthy()
expect(mountNode.contains(comp.getDOMNode())).toBeTruthy()
})
})

describe('onMount', () => {
it('called when mounting', () => {
const handlerSpy = jest.fn()
mountPortalInner({ onMount: handlerSpy })
const onMount = jest.fn()
mountPortalInner({ onMount })

expect(handlerSpy).toHaveBeenCalledTimes(1)
expect(onMount).toHaveBeenCalledTimes(1)
})
})

describe('onUnmount', () => {
it('is called only once when unmounting', () => {
const handlerSpy = jest.fn()
const wrapper = mountPortalInner({ onUnmount: handlerSpy })
const onUnmount = jest.fn()
const wrapper = mountPortalInner({ onUnmount })
wrapper.unmount()

expect(handlerSpy).toHaveBeenCalledTimes(1)
expect(onUnmount).toHaveBeenCalledTimes(1)
})
})
})