diff --git a/docs/src/examples/components/Portal/Types/PortalExample.tsx b/docs/src/examples/components/Portal/Types/PortalExample.tsx
new file mode 100644
index 0000000000..9417346c2b
--- /dev/null
+++ b/docs/src/examples/components/Portal/Types/PortalExample.tsx
@@ -0,0 +1,35 @@
+import React from 'react'
+
+import { Button, Portal } from 'stardust'
+
+class PortalExample extends React.Component {
+ render() {
+ return (
+ {
+ console.log('onClick outer')
+ }}
+ >
+ Open/close portal
+
+ }
+ >
+
+ portal popup
+
+
+ )
+ }
+}
+
+export default PortalExample
diff --git a/docs/src/examples/components/Portal/Types/index.tsx b/docs/src/examples/components/Portal/Types/index.tsx
new file mode 100644
index 0000000000..fcf17e2721
--- /dev/null
+++ b/docs/src/examples/components/Portal/Types/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
+
+const Types = () => (
+
+
+
+)
+
+export default Types
diff --git a/docs/src/examples/components/Portal/index.tsx b/docs/src/examples/components/Portal/index.tsx
new file mode 100644
index 0000000000..c3921d2e06
--- /dev/null
+++ b/docs/src/examples/components/Portal/index.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import Types from './Types'
+
+const PortalExamples = () => (
+
+
+
+)
+
+export default PortalExamples
diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx
new file mode 100644
index 0000000000..10b4ed316a
--- /dev/null
+++ b/src/components/Portal/Portal.tsx
@@ -0,0 +1,96 @@
+import React, { cloneElement } from 'react'
+import ReactDOM from 'react-dom'
+import PropTypes from 'prop-types'
+import { AutoControlledComponent, eventStack, makeDebugger } from '../../lib'
+
+const debug = makeDebugger('portal')
+
+class Portal extends AutoControlledComponent {
+ static propTypes = {
+ trigger: PropTypes.node,
+
+ open: PropTypes.bool,
+
+ defaultOpen: PropTypes.bool,
+ }
+
+ static autoControlledProps = ['open']
+
+ handleTriggerClick = () => {
+ debug('handleTriggerClick()')
+ this.props.trigger.props.onClick()
+ this.trySetState({ open: !this.state.open })
+ }
+
+ handlePortalMouseEnter = () => {
+ debug('handlePortalMouseEnter()')
+ }
+
+ componentDidMount() {
+ debug('componentDidMount()', this.state)
+ this.state.open ? this.createPortal() : this.destroyPortal()
+ }
+
+ componentDidUpdate() {
+ debug('componentDidUpdate()', this.state)
+ this.state.open ? this.createPortal() : this.destroyPortal()
+ }
+
+ componentWillUnmount() {
+ debug('componentWillUnmount()')
+ this.destroyPortal()
+ }
+
+ createPortal() {
+ if (this.state.portalEl) {
+ return
+ }
+ debug('creating portalEl')
+ const portalEl = document.createElement('div')
+ document.body.appendChild(portalEl)
+ eventStack.sub('mouseenter', this.handlePortalMouseEnter, {
+ target: portalEl,
+ })
+ this.setState({
+ portalEl,
+ })
+ }
+
+ destroyPortal() {
+ if (!this.state.portalEl) {
+ return
+ }
+ debug('destroying portalEl')
+ // TODO: unsubscribe from all events
+ const { portalEl } = this.state
+ portalEl.parentNode.removeChild(portalEl)
+ this.setState({ portalEl: undefined })
+ }
+
+ // To discuss:
+ // when to create rootNode? (it is required in render, componentWillMount is deprecated)
+ // should multiple portals share it? (how would mouseenter/mouseleave on portalEl work then?)
+ // when to destroy it (it is too early in componentWillUnmount)
+
+ render() {
+ const { trigger } = this.props
+ debug('render')
+
+ if (!trigger) {
+ return
+ }
+
+ return (
+
+ {cloneElement(trigger, {
+ onClick: this.handleTriggerClick,
+ })}
+ {this.state.open &&
+ this.state.portalEl &&
+ ReactDOM.createPortal(this.props.children, this.state.portalEl)}
+
+ )
+ }
+}
+
+export default Portal
diff --git a/src/components/Portal/index.ts b/src/components/Portal/index.ts
new file mode 100644
index 0000000000..0ec1d56187
--- /dev/null
+++ b/src/components/Portal/index.ts
@@ -0,0 +1 @@
+export { default } from './Portal'
diff --git a/src/index.ts b/src/index.ts
index 7ee1d5b091..c7ff917827 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,5 +4,6 @@ export { default as Divider } from './components/Divider'
export { default as Layout } from './components/Layout'
export { default as List } from './components/List'
export { ListItem } from './components/List'
+export { default as Portal } from './components/Portal'
export { default as Provider } from './components/Provider'
export { default as ProviderConsumer } from './components/Provider/ProviderConsumer'
diff --git a/src/lib/AutoControlledComponent.tsx b/src/lib/AutoControlledComponent.tsx
index 6cb644fce3..1958da57f3 100644
--- a/src/lib/AutoControlledComponent.tsx
+++ b/src/lib/AutoControlledComponent.tsx
@@ -191,7 +191,7 @@ export default class AutoControlledComponent extends Component {
* @param {object} maybeState State that corresponds to controlled props.
* @param {object} [state] Actual state, useful when you also need to setState.
*/
- trySetState = (maybeState, state) => {
+ trySetState = (maybeState, state?) => {
const { autoControlledProps } = this.constructor as any
if (process.env.NODE_ENV !== 'production') {
const { name } = this.constructor
diff --git a/src/lib/debug.tsx b/src/lib/debug.ts
similarity index 87%
rename from src/lib/debug.tsx
rename to src/lib/debug.ts
index f3427ebc9b..cb76a907bd 100644
--- a/src/lib/debug.tsx
+++ b/src/lib/debug.ts
@@ -11,7 +11,7 @@ if (isBrowser() && process.env.NODE_ENV !== 'production' && process.env.NODE_ENV
try {
DEBUG = window.localStorage.debug
} catch (e) {
- console.error('Semantic-UI-React could not enable debug.')
+ console.error('Stardust could not enable debug.')
console.error(e)
}
@@ -29,7 +29,7 @@ if (isBrowser() && process.env.NODE_ENV !== 'production' && process.env.NODE_ENV
* debug('Some message')
* @returns {Function}
*/
-export const makeDebugger = namespace => _debug(`semanticUIReact:${namespace}`)
+export const makeDebugger = namespace => _debug(`stardust:${namespace}`)
/**
* Default debugger, simple log.