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.