Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Portal): take focus on mount and restore on unmount #1154

Merged
merged 1 commit into from
Jan 17, 2017
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
11 changes: 10 additions & 1 deletion src/addons/Portal/Portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ class Portal extends Component {
if (!this.state.open) return
debug('renderPortal()')

const { children, className } = this.props
const { children, className, closeOnTriggerBlur } = this.props

this.mountPortal()

Expand All @@ -363,6 +363,14 @@ class Portal extends Component {
)

this.portal = this.node.firstElementChild
// don't take focus away from portals that close on blur
if (!closeOnTriggerBlur) {
this.previousActiveElement = document.activeElement
this.portal.setAttribute('tabindex', '-1')
this.portal.style.outline = 'none'
// wait a tick for things like popups which need to calculate where the popup shows up
setTimeout(() => this.portal && this.portal.focus())
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to pull this side effect out of the render method if at all possible. I haven't the bandwidth at the moment to find a better place myself, but open to suggestions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Technically this code is being called by either componentDidMount or componentDidUpdate depending on when 'open' is set to true which is after the render method. Or are you saying you'd like me to make a named helper function that's called here? There's no later life cycle function I can use to trigger this behavior

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see, thanks for the clarification. From my initial pass I assumed this was called in a render() method itself. -1 for assumptions :)

}

this.portal.addEventListener('mouseleave', this.handlePortalMouseLeave)
this.portal.addEventListener('mouseenter', this.handlePortalMouseEnter)
Expand Down Expand Up @@ -397,6 +405,7 @@ class Portal extends Component {

ReactDOM.unmountComponentAtNode(this.node)
this.node.parentNode.removeChild(this.node)
if (this.previousActiveElement) this.previousActiveElement.focus()

this.portal.removeEventListener('mouseleave', this.handlePortalMouseLeave)
this.portal.removeEventListener('mouseenter', this.handlePortalMouseEnter)
Expand Down
43 changes: 43 additions & 0 deletions test/specs/addons/Portal/Portal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,4 +509,47 @@ describe('Portal', () => {
document.body.childElementCount.should.equal(0)
})
})

describe('focus', () => {
it('should take focus when mounted', (done) => {
attachTo = document.createElement('div')
document.body.appendChild(attachTo)
const opts = { attachTo }
const portal = wrapperMount(<Portal defaultOpen><p>Hi</p></Portal>, opts)
setTimeout(() => {
const portalNode = portal.node.node.firstElementChild
expect(document.activeElement).to.equal(portalNode)
expect(portalNode.getAttribute('tabindex')).to.equal('-1')
expect(portalNode.style.outline).to.equal('none')
done()
})
})
it('should not take focus when mounted on portals that closeOnTriggerBlur', (done) => {
attachTo = document.createElement('div')
document.body.appendChild(attachTo)
const opts = { attachTo }
const portal = wrapperMount(<Portal defaultOpen closeOnTriggerBlur><p>Hi</p></Portal>, opts)
setTimeout(() => {
const portalNode = portal.node.node.firstElementChild
expect(document.activeElement).to.not.equal(portalNode)
expect(portalNode.getAttribute('tabindex')).to.not.equal('-1')
expect(portalNode.style.outline).to.not.equal('none')
done()
})
})
it('should restore focus when unmounted', (done) => {
const activeElement = document.activeElement
attachTo = document.createElement('div')
document.body.appendChild(attachTo)
const opts = { attachTo }
const portal = wrapperMount(<Portal open><p>Hi</p></Portal>, opts)
setTimeout(() => {
portal.setProps({
open: false,
})
expect(document.activeElement).to.equal(activeElement)
done()
})
})
})
})