Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Fixes
- Fix for cropped rounded corners in `Menu` component @Bugaa92 ([#360](https://github.com/stardust-ui/react/pull/360))

### Features
- Add `target` prop to `Popup` @kuzhelog ([#356](https://github.com/stardust-ui/react/pull/356))

<!--------------------------------[ v0.9.1 ]------------------------------- -->
## [v0.9.1](https://github.com/stardust-ui/react/tree/v0.9.1) (2018-10-11)
[Compare changes](https://github.com/stardust-ui/react/compare/v0.9.0...v0.9.1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import { Popup, Divider, Icon, Text, Grid } from '@stardust-ui/react'
import { findDOMNode } from 'react-dom'

class PopupExample extends React.Component {
ref = React.createRef<any>()
state = { popupTarget: undefined }

componentDidMount() {
this.setState({ popupTarget: findDOMNode(this.ref.current) })
}

render() {
return (
<Grid columns="auto 1fr">
{/* CUSTOM DOM ELEMENT is used as target for Popup */}
<Popup
target={this.state.popupTarget}
trigger={<Icon name="question" circular bordered styles={{ cursor: 'pointer' }} />}
content="well, yes, I am just a garbish text ¯\_(ツ)_/¯"
position="below"
/>

<div style={{ marginLeft: 10 }}>
<Text>Could you guess what does this text means? :)</Text>
<Divider />
<Text ref={this.ref}>
"To the lascivious looking-glass I, that love's majesty to strut before a want love's
majesto, to the souls of York."
</Text>
</div>
</Grid>
)
}
}

export default PopupExample
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'
import { Popup, Divider, Icon, Text, Grid } from '@stardust-ui/react'
import { findDOMNode } from 'react-dom'

class PopupExample extends React.Component {
ref = React.createRef<any>()
state = { popupTarget: undefined }

componentDidMount() {
this.setState({ popupTarget: findDOMNode(this.ref.current) })
}

render() {
return (
<Grid columns="auto 1fr">
{/* CUSTOM DOM ELEMENT is used as target for Popup */}
<Popup
target={this.state.popupTarget}
content="well, yes, I am just a garbish text ¯\_(ツ)_/¯"
position="below"
>
<Icon name="question" circular bordered styles={{ cursor: 'pointer' }} />
</Popup>

<div style={{ marginLeft: 10 }}>
<Text>Could you guess what does this text means? :)</Text>
<Divider />
<Text ref={this.ref}>
"To the lascivious looking-glass I, that love's majesty to strut before a want love's
majesto, to the souls of York."
</Text>
</div>
</Grid>
)
}
}

export default PopupExample
5 changes: 5 additions & 0 deletions docs/src/examples/components/Popup/Types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const Types = () => (
description="Use 'content' prop of the Popup to set whether Popup content should be rendered with the default wrapper."
examplePath="components/Popup/Types/PopupContentWrapperExample"
/>
<ComponentExample
title="Custom Target"
description="By default Popup uses trigger element as the one it is displayed for, but it is possible to provide any DOM element as popup's target."
examplePath="components/Popup/Types/PopupCustomTargetExample"
/>
</ExampleSection>
)

Expand Down
55 changes: 37 additions & 18 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ export interface IPopupProps {
open?: boolean
onOpenChange?: ComponentEventHandler<IPopupProps>
position?: Position
target?: HTMLElement
defaultTarget?: HTMLElement
trigger?: JSX.Element
}

export interface IPopupState {
open: boolean
triggerRef: HTMLElement
target: HTMLElement
}

/**
Expand Down Expand Up @@ -85,6 +87,9 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
/** Initial value for 'open'. */
defaultOpen: PropTypes.bool,

/** Initial value for 'target'. */
defaultTarget: PropTypes.any,

/** Defines whether popup is displayed. */
open: PropTypes.bool,

Expand All @@ -98,13 +103,17 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
/**
* Position for the popup. Position has higher priority than align. If position is vertical ('above' | 'below')
* and align is also vertical ('top' | 'bottom') or if both position and align are horizontal ('before' | 'after'
* and 'start' | 'end' respectively), we ignore the value set for align and make it 'center'.
* This is the mechanism we chose for dealing with mismatched prop values.
* and 'start' | 'end' respectively), then provided value for 'align' will be ignored and 'center' will be used instead.
*/
position: PropTypes.oneOf(POSITIONS),

/**
* DOM element that should be used as popup's target - instead of 'trigger' element that is used by default.
*/
target: PropTypes.any,

/** Element to be rendered in-place where the popup is defined. */
trigger: PropTypes.node,
trigger: PropTypes.any,
}

public static defaultProps: IPopupProps = {
Expand All @@ -113,33 +122,47 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
position: 'above',
}

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

private static isBrowserContext = isBrowser()

protected actionHandlers: AccessibilityActionHandlers = {
toggle: e => this.trySetOpen(!this.state.open, e, true),
closeAndFocusTrigger: e => {
this.trySetOpen(false, e, true)
_.invoke(this.state.triggerRef, 'focus')
_.invoke(this.state.target, 'focus')
},
}

public state = { triggerRef: undefined, open: false }
public state = { target: undefined, open: false }

public renderComponent({
rtl,
accessibility,
}: IRenderResultConfig<IPopupProps>): React.ReactNode {
const { children, trigger } = this.props
const popupContent = this.renderPopupContent(rtl, accessibility)

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

{this.state.open &&
Popup.isBrowserContext &&
popupContent &&
createPortal(popupContent, document.body)}
</>
)
}

private renderTrigger(accessibility) {
const { children, trigger } = this.props
const triggerElement = childrenExist(children) ? children : (trigger as any)

return (
<>
triggerElement && (
<Ref
innerRef={domNode => {
this.setState({ triggerRef: domNode })
this.trySetState({ target: domNode })
}}
>
{React.cloneElement(triggerElement, {
Expand All @@ -151,25 +174,21 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
...accessibility.keyHandlers.trigger,
})}
</Ref>

{this.state.open &&
Popup.isBrowserContext &&
createPortal(this.renderPopupContent(rtl, accessibility), document.body)}
</>
)
)
}

private renderPopupContent(rtl: boolean, accessibility: IAccessibilityBehavior): JSX.Element {
const { align, position } = this.props
const triggerRef = this.state.triggerRef
const { target } = this.state

const placement = computePopupPlacement({ align, position, rtl })

return (
triggerRef && (
target && (
<Popper
placement={placement}
referenceElement={triggerRef}
referenceElement={target}
children={this.renderPopperChildren.bind(this, rtl, accessibility)}
/>
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/Ref/Ref.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export default class Ref extends Component<IRefProps> {
}

render() {
return Children.only(this.props.children)
return this.props.children && Children.only(this.props.children)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwise, in case if children is not provided for the component, this will throw the error and fails rendering process for the whole tree - while, in contrast, if we will handle it this way, then for innerRef callback null will be validly provided as captured DOM element (as there are no elements rendered)

}
}