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

feat(portal): simple base implementation #144

Merged
merged 2 commits into from
Sep 7, 2018
Merged

Conversation

bmdalex
Copy link
Collaborator

@bmdalex bmdalex commented Aug 27, 2018

Portal

This PR will introduce Portal component, a component that allows you to render children outside their parent (for now all portals are rendered at the end of the body DOM element); a Portal comes with the following props:

  • onMount?: (props: IPortalProps) => void: Called when the portal is mounted on the DOM
  • onUnmount?: (props: IPortalProps) => void: Called when the portal is unmounted from the DOM
  • open?: boolean: Controls whether or not the portal is displayed
  • trigger?: JSX.Element: Element to be rendered in-place where the portal is defined
  • triggerRef?: (node: HTMLElement) => void: Called with a ref to the trigger node

and the following helper components used by Portal:

  • PortalInner with props:

    • onMount?: (props: IPortalProps) => void: Called when the portal is mounted on the DOM
    • onUnmount?: (props: IPortalProps) => void: Called when the portal is unmounted from the DOM
  • Ref with props:

    • innerRef?: (ref: HTMLElement) => void: Called with componentDidMount

TODO

  • Conformance test
  • Minimal doc site example
  • Stardust base theme
  • Teams Light theme
  • Teams Dark theme
  • Teams Contrast theme
  • Confirm RTL usage
  • W3 accessibility check
  • Stardust accessibility check
  • Update glossary props table
  • Update the CHANGELOG.md

Proposal

Basic portal

screen shot 2018-08-27 at 17 27 10

Renders a portal that is:

  • opened when clicking on the trigger, a component sent to the portal as a prop;
  • closed when clicking away or on the trigger (since it acts like a toggle)
<Portal
  content={<div>This is a basic portal</div>}
  trigger={<Button content={'Toggle portal'} onClick={this.handleClick} />}
/>

generates

<button>Toggle portal</button>

and at the end of body DOM element:

<body>
  ...
  <div>This is a basic portal</div>
</body>

Controlled portal

screen shot 2018-08-27 at 17 26 25

Renders a portal that is rendered or not depending on the value of open property

<Portal
  content={<div>This is a controlled portal</div>}
  open={true}
/>

generates

<body>
  ...
  <div>This is a controlled portal</div>
</body>

Toggle portal
at the end of body DOM element.

@bmdalex bmdalex mentioned this pull request Aug 27, 2018
11 tasks
@bmdalex bmdalex changed the title [WIP] feat(portal): simple base implementation feat(portal): simple base implementation Aug 27, 2018
@bmdalex bmdalex force-pushed the feat/portal-simple branch 2 times, most recently from b4d9467 to afa6847 Compare August 27, 2018 17:48
@codecov
Copy link

codecov bot commented Aug 27, 2018

Codecov Report

Merging #144 into master will increase coverage by 0.94%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #144      +/-   ##
==========================================
+ Coverage   89.23%   90.17%   +0.94%     
==========================================
  Files          50       55       +5     
  Lines         836      916      +80     
  Branches      130      142      +12     
==========================================
+ Hits          746      826      +80     
  Misses         86       86              
  Partials        4        4
Impacted Files Coverage Δ
src/index.ts 100% <100%> (ø) ⬆️
src/components/Portal/PortalInner.tsx 100% <100%> (ø)
src/components/Ref/index.ts 100% <100%> (ø)
src/components/Ref/Ref.tsx 100% <100%> (ø)
src/components/Portal/index.ts 100% <100%> (ø)
src/components/Portal/Portal.tsx 100% <100%> (ø)
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a6d7bcb...d0df7ed. Read the comment docs.

logCount: 0,
}

public render() {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should be typing public/private like this in React components. Even though method and other custom methods are publicly accessible, it is not idiomatic React to actually access them. You should never reach into a React component and call a method.

In other words, if we did type them, it would be more proper to consider all methods as private but I believe that would cause some other issues.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't agree with this and here are my reasons:

  1. The current pattern I see all over the project is to not specify any modifier for class members which means all members are public (class members are public by default in typescript).
    This is wrong and is one of the main reasons I'd recommend enforcing modifiers; developers often forget to specify a method is private since no modifier is required and this makes it private.

  2. render is a public method as it comes from React.Component or UIComponent, the classes we usually extend.

  3. Specifying modifiers makes it clear for developers coming from other environments where private is the default class member modifier (such as C#).

We had this discussion before and since it's occurring again I can create an RFC for enforcing modifiers if you think it's valuable.

@levithomason @kuzhelov @miroslavstastny @mnajdova what do you guys think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

since this is an example I'll remove the modifiers so that we make it jsx, but the points stand for actual typescript code

@@ -0,0 +1,65 @@
import * as React from 'react'
Copy link
Member

Choose a reason for hiding this comment

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

No typescript in examples please.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd rather allow both, the similar way as it is done on the Android Developer docs
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

then these should be jsx files, not tsx, right? :) I can do the change for now but it's confusing to see these tslint errors..

@@ -0,0 +1,48 @@
import * as PropTypes from 'prop-types'
import { invoke } from 'lodash'
Copy link
Member

Choose a reason for hiding this comment

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

Let's keep convention consistent in the repo, it makes tooling and programmatic updates easier to manage.

There is a babel-plugin-lodash which we use at SUIR to rewrite the import statements to cherry pick only used methods. We should either implement this plugin, or enforce cherry picking lodash methods, but, we should not mix both patterns without a guarantee of either one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok

}

public render() {
return createPortal(this.props.children, document.body)
Copy link
Member

Choose a reason for hiding this comment

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

Accessing document will fail in server side rendering. This should probably be a defaultProp instead that is safe guarded by a browser check first:

import { isBrowser } from '../../lib'

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

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

  return createPortal(children, context)
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made the change but I don't really understand it. Server side rendering? Where do we do that?

}

public componentDidMount() {
invoke(this.props, 'onMount', { ...this.props })
Copy link
Member

Choose a reason for hiding this comment

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

Since we're not adding / changing any props here, not sure that a shallow clone is required. We can save on the cycles by just passing this.props.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

made the change as suggested

log: [],
logCount: 0,
open: false,
}
Copy link
Member

Choose a reason for hiding this comment

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

In effort to minimize lines, this object can be single line:

state = { log: [], logCount: 0, open: false }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

const { log, logCount, open } = this.state
const content = open ? 'Close Portal' : 'Open Portal'

const controls = (
Copy link
Member

Choose a reason for hiding this comment

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

controls and portalContent can also be inlined into the render method to save some lines.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think my approach makes it more clear how to actually use the components since the controls and portalContent are lengthy and users can get confused; that's why I did it like this... Anyway, I made the change as suggested

Copy link
Member

@levithomason levithomason left a comment

Choose a reason for hiding this comment

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

Thanks for porting this. See individual comments for change request details.

@bmdalex bmdalex force-pushed the feat/portal-simple branch 5 times, most recently from a961ce4 to 83abf5b Compare September 4, 2018 09:01
@bmdalex bmdalex force-pushed the feat/portal-simple branch 3 times, most recently from 53987e1 to 4f1c8a1 Compare September 6, 2018 11:51
@bmdalex bmdalex merged commit a71db03 into master Sep 7, 2018
@bmdalex bmdalex deleted the feat/portal-simple branch September 7, 2018 16:22
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants