From 9f2f73d337d3b24acc5e186cde4600d3e1b27c0c Mon Sep 17 00:00:00 2001 From: Miroslav Stastny Date: Mon, 25 Jun 2018 20:16:03 +0200 Subject: [PATCH 1/2] feat(Portal): component boilerplate, ReactDOM.createPortal() experiment --- .../components/Portal/Types/PortalExample.tsx | 35 ++++++++ .../components/Portal/Types/index.tsx | 15 ++++ docs/src/examples/components/Portal/index.tsx | 10 +++ src/components/Portal/Portal.tsx | 90 +++++++++++++++++++ src/components/Portal/index.ts | 1 + src/index.ts | 1 + 6 files changed, 152 insertions(+) create mode 100644 docs/src/examples/components/Portal/Types/PortalExample.tsx create mode 100644 docs/src/examples/components/Portal/Types/index.tsx create mode 100644 docs/src/examples/components/Portal/index.tsx create mode 100644 src/components/Portal/Portal.tsx create mode 100644 src/components/Portal/index.ts 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..c739a231f4 --- /dev/null +++ b/src/components/Portal/Portal.tsx @@ -0,0 +1,90 @@ +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.setState({ open: !this.state.open }) + } + + handlePortalMouseEnter = () => { + debug('handlePortalMouseEnter()') + } + + componentDidMount() { + debug('componentDidMount()', this.state) + if (this.state.open) { + this.createPortal() + } + } + + componentDidUpdate() { + debug('componentDidUpdate()', this.state) + if (this.state.open) { + this.createPortal() + } + } + + componentWillUnmount() { + debug('componentWillUnmount()') + } + + createPortal() { + if (this.state.portalEl) { + return + } + console.log('creating portalEl') + const portalEl = document.createElement('div') + document.body.appendChild(portalEl) + eventStack.sub('mouseenter', this.handlePortalMouseEnter, { + target: portalEl, + }) + this.setState({ + portalEl, + }) + } + + destroyPortal() {} + + // 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' From 25fd0e6c4c0211ed10b6a9db32ec2e9be08b0a8e Mon Sep 17 00:00:00 2001 From: Miroslav Stastny Date: Tue, 26 Jun 2018 06:50:00 +0200 Subject: [PATCH 2/2] feat(Portal): portal root element removed in componentWillUnmount --- src/components/Portal/Portal.tsx | 24 +++++++++++++++--------- src/lib/AutoControlledComponent.tsx | 2 +- src/lib/{debug.tsx => debug.ts} | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) rename src/lib/{debug.tsx => debug.ts} (87%) diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx index c739a231f4..10b4ed316a 100644 --- a/src/components/Portal/Portal.tsx +++ b/src/components/Portal/Portal.tsx @@ -19,7 +19,7 @@ class Portal extends AutoControlledComponent { handleTriggerClick = () => { debug('handleTriggerClick()') this.props.trigger.props.onClick() - this.setState({ open: !this.state.open }) + this.trySetState({ open: !this.state.open }) } handlePortalMouseEnter = () => { @@ -28,27 +28,24 @@ class Portal extends AutoControlledComponent { componentDidMount() { debug('componentDidMount()', this.state) - if (this.state.open) { - this.createPortal() - } + this.state.open ? this.createPortal() : this.destroyPortal() } componentDidUpdate() { debug('componentDidUpdate()', this.state) - if (this.state.open) { - this.createPortal() - } + this.state.open ? this.createPortal() : this.destroyPortal() } componentWillUnmount() { debug('componentWillUnmount()') + this.destroyPortal() } createPortal() { if (this.state.portalEl) { return } - console.log('creating portalEl') + debug('creating portalEl') const portalEl = document.createElement('div') document.body.appendChild(portalEl) eventStack.sub('mouseenter', this.handlePortalMouseEnter, { @@ -59,7 +56,16 @@ class Portal extends AutoControlledComponent { }) } - destroyPortal() {} + 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) 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.