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

Commit

Permalink
fix(FocusTrapZone): Set focus into zone when inner content is lazy lo…
Browse files Browse the repository at this point in the history
…aded (#1505)

* fix(FocusTrapZone): Set focus into zone when inner content is lazy loaded

* Update PopupExample.shorthand.tsx

* Update PopupExample.shorthand.tsx

* update changelog

* Document lazy loaded content case.

* Update packages/react/test/specs/lib/FocusTrapZone-test.tsx

Co-Authored-By: Oleksandr Fediashov <alexander.mcgarret@gmail.com>

* update changelog

* small improvement
  • Loading branch information
sophieH29 authored Jun 25, 2019
1 parent b788cf4 commit 0a9e616
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Type `FontFaceStyle` was renamed to `FontFaceProps` @layershifter ([#1487](https://github.com/stardust-ui/react/pull/1487))
- Type `style` was renamed to `props` on `FontFace` @layershifter ([#1487](https://github.com/stardust-ui/react/pull/1487))
- Remove `boxShadowColor` variable for `Segment` component @Bugaa92 ([#1516](https://github.com/stardust-ui/react/pull/1516))
- Rename prop `forceFocusInsideTrap` to `forceFocusInsideTrapOnOutsideFocus` in `FocusTrapZone` @sophieH29 ([#1505](https://github.com/stardust-ui/react/pull/1505))

### Fixes
- Fix prop types of `Tooltip` component @kuzhelov ([#1499](https://github.com/stardust-ui/react/pull/1499))
Expand All @@ -29,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Fix `theme` types, remove duplication @kuzhelov ([#1508](https://github.com/stardust-ui/react/pull/1508))
- Fix `RadioGroup` first item should be tabbable by default when none of the items selected @sophieH29 ([#1515](https://github.com/stardust-ui/react/pull/1515))
- Export all accessibility behaviors @jurokapsiar ([#1538](https://github.com/stardust-ui/react/pull/1538))
- Fix `FocusTrapZone` sets focus into zone correctly for lazy loaded content @sophieH29 ([#1505](https://github.com/stardust-ui/react/pull/1505))

### Features
- Add 'poll' and 'to-do-list' icons to Teams theme @natashamayurshah ([#1498](https://github.com/stardust-ui/react/pull/1498))
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export interface PopupState {
* A Popup displays additional information on top of a page.
* @accessibility
* Do use popupFocusTrapBehavior if the focus needs to be trapped inside of the Popup.
* If Popup's content is lazy loaded and focus needs to be trapped inside - make sure to use state change to trigger componentDidUpdate,
* so the focus can be set correctly to the first tabbable element inside Popup or manually set focus to the element inside once content is loaded.
*/
export default class Popup extends AutoControlledComponent<PopupProps, PopupState> {
static displayName = 'Popup'
Expand Down
40 changes: 28 additions & 12 deletions packages/react/src/lib/accessibility/FocusZone/FocusTrapZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export class FocusTrapZone extends React.Component<FocusTrapZoneProps, {}> {
ariaLabelledBy: PropTypes.string,
isClickableOutsideFocusTrap: PropTypes.bool,
ignoreExternalFocusing: PropTypes.bool,
forceFocusInsideTrap: PropTypes.bool,
forceFocusInsideTrapOnOutsideFocus: PropTypes.bool,
forceFocusInsideTrapOnComponentUpdate: PropTypes.bool,
firstFocusableSelector: PropTypes.string,
disableFirstFocus: PropTypes.bool,
focusPreviouslyFocusedInnerElement: PropTypes.bool,
Expand All @@ -58,22 +59,24 @@ export class FocusTrapZone extends React.Component<FocusTrapZoneProps, {}> {

componentDidMount(): void {
FocusTrapZone._focusStack.push(this)
const { disableFirstFocus = false } = this.props

this._previouslyFocusedElementOutsideTrapZone = this._getPreviouslyFocusedElementOutsideTrapZone()
this._bringFocusIntoZone()
this._hideContentFromAccessibilityTree()
}

if (
!this._root.current.contains(this._previouslyFocusedElementOutsideTrapZone) &&
!disableFirstFocus
) {
this._findElementAndFocusAsync()
componentDidUpdate(): void {
if (!this.props.forceFocusInsideTrapOnComponentUpdate) {
return
}

this._hideContentFromAccessibilityTree()
const activeElement = document.activeElement as HTMLElement
// if after componentDidUpdate focus is not inside the focus trap, bring it back
if (!this._root.current.contains(activeElement)) {
this._bringFocusIntoZone()
}
}

render(): JSX.Element {
const { className, forceFocusInsideTrap, ariaLabelledBy } = this.props
const { className, forceFocusInsideTrapOnOutsideFocus, ariaLabelledBy } = this.props
const unhandledProps = getUnhandledProps(
{ handledProps: [..._.keys(FocusTrapZone.propTypes)] },
this.props,
Expand All @@ -93,7 +96,7 @@ export class FocusTrapZone extends React.Component<FocusTrapZoneProps, {}> {
{this.props.children}
</ElementType>

{forceFocusInsideTrap && (
{forceFocusInsideTrapOnOutsideFocus && (
<EventListener
capture
listener={this._handleOutsideFocus}
Expand Down Expand Up @@ -144,6 +147,19 @@ export class FocusTrapZone extends React.Component<FocusTrapZoneProps, {}> {
}
}

_bringFocusIntoZone = () => {
const { disableFirstFocus = false } = this.props

this._previouslyFocusedElementOutsideTrapZone = this._getPreviouslyFocusedElementOutsideTrapZone()

if (
!this._root.current.contains(this._previouslyFocusedElementOutsideTrapZone) &&
!disableFirstFocus
) {
this._findElementAndFocusAsync()
}
}

_findElementAndFocusAsync = () => {
if (!this._root.current) return
const { focusPreviouslyFocusedInnerElement, firstFocusableSelector } = this.props
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ export interface FocusTrapZoneProps extends React.HTMLAttributes<HTMLDivElement>
ignoreExternalFocusing?: boolean

/**
* Indicates whether focus trap zone should force focus inside the focus trap zone
* Indicates whether focus trap zone should force focus inside the trap zone when focus event occurs outside the zone.
*/
forceFocusInsideTrap?: boolean
forceFocusInsideTrapOnOutsideFocus?: boolean

/**
* Indicates whether focus trap zone should force focus inside the trap zone on component update.
*/
forceFocusInsideTrapOnComponentUpdate?: boolean

/**
* Indicates the selector for first focusable item. Only applies if focusPreviouslyFocusedInnerElement == false.
Expand Down
28 changes: 17 additions & 11 deletions packages/react/test/specs/lib/FocusTrapZone-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class FocusTrapZoneTestComponent extends React.Component<
render() {
return (
<div>
<FocusTrapZone forceFocusInsideTrap={true} isClickableOutsideFocusTrap={false}>
<FocusTrapZone forceFocusInsideTrapOnOutsideFocus isClickableOutsideFocusTrap={false}>
<button className={'a'} onClick={this._toggleFirst}>
a
</button>
Expand All @@ -40,12 +40,18 @@ class FocusTrapZoneTestComponent extends React.Component<
</FocusTrapZone>

{this.state.isShowingFirst && (
<FocusTrapZone forceFocusInsideTrap={false} isClickableOutsideFocusTrap={false}>
<FocusTrapZone
forceFocusInsideTrapOnOutsideFocus={false}
isClickableOutsideFocusTrap={false}
>
<FocusZone data-is-visible={true}>First</FocusZone>
</FocusTrapZone>
)}
{this.state.isShowingSecond && (
<FocusTrapZone forceFocusInsideTrap={false} isClickableOutsideFocusTrap={true}>
<FocusTrapZone
forceFocusInsideTrapOnOutsideFocus={false}
isClickableOutsideFocusTrap={true}
>
<FocusZone data-is-visible={true}>First</FocusZone>
</FocusTrapZone>
)}
Expand Down Expand Up @@ -100,7 +106,7 @@ describe('FocusTrapZone', () => {

const topLevelDiv = ReactTestUtils.renderIntoDocument<{}>(
<div onFocusCapture={_onFocus}>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusTrapZone forceFocusInsideTrapOnOutsideFocus={false}>
<FocusZone direction={FocusZoneDirection.horizontal} data-is-visible={true}>
<div data-is-visible={true}>
<button className="a">a</button>
Expand Down Expand Up @@ -161,7 +167,7 @@ describe('FocusTrapZone', () => {

const topLevelDiv = ReactTestUtils.renderIntoDocument<{}>(
<div onFocusCapture={_onFocus}>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusTrapZone forceFocusInsideTrapOnOutsideFocus={false}>
<div data-is-visible={true}>
<button className="x">x</button>
</div>
Expand Down Expand Up @@ -216,7 +222,7 @@ describe('FocusTrapZone', () => {
const topLevelDiv = ReactTestUtils.renderIntoDocument<{}>(
<div onFocusCapture={_onFocus}>
<button className={'z1'}>z1</button>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusTrapZone forceFocusInsideTrapOnOutsideFocus={false}>
<FocusZone direction={FocusZoneDirection.horizontal} data-is-visible={true}>
<button className={'a'}>a</button>
<button className={'b'}>b</button>
Expand Down Expand Up @@ -283,7 +289,7 @@ describe('FocusTrapZone', () => {
const topLevelDiv = ReactTestUtils.renderIntoDocument<{}>(
<div onFocusCapture={_onFocus}>
<button className={'z1'}>z1</button>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusTrapZone forceFocusInsideTrapOnOutsideFocus={false}>
<button className={'a'} tabIndex={-1}>
a
</button>
Expand Down Expand Up @@ -353,7 +359,7 @@ describe('FocusTrapZone', () => {
const topLevelDiv = ReactTestUtils.renderIntoDocument<{}>(
<div onFocusCapture={_onFocus}>
<FocusTrapZone
forceFocusInsideTrap={false}
forceFocusInsideTrapOnOutsideFocus={false}
focusPreviouslyFocusedInnerElement={focusPreviouslyFocusedInnerElement}
data-is-focusable={true}
ref={ftz => {
Expand Down Expand Up @@ -461,11 +467,11 @@ describe('FocusTrapZone', () => {

expect(focusTrapZoneFocusStack.length).toBe(2)
const baseFocusTrapZone = focusTrapZoneFocusStack[0]
expect(baseFocusTrapZone.props.forceFocusInsideTrap).toBe(true)
expect(baseFocusTrapZone.props.forceFocusInsideTrapOnOutsideFocus).toBe(true)
expect(baseFocusTrapZone.props.isClickableOutsideFocusTrap).toBe(false)

const firstFocusTrapZone = focusTrapZoneFocusStack[1]
expect(firstFocusTrapZone.props.forceFocusInsideTrap).toBe(false)
expect(firstFocusTrapZone.props.forceFocusInsideTrapOnOutsideFocus).toBe(false)
expect(firstFocusTrapZone.props.isClickableOutsideFocusTrap).toBe(false)

// There should be now 3 focus trap zones (base/first/second)
Expand All @@ -474,7 +480,7 @@ describe('FocusTrapZone', () => {
expect(focusTrapZoneFocusStack[0]).toBe(baseFocusTrapZone)
expect(focusTrapZoneFocusStack[1]).toBe(firstFocusTrapZone)
const secondFocusTrapZone = focusTrapZoneFocusStack[2]
expect(secondFocusTrapZone.props.forceFocusInsideTrap).toBe(false)
expect(secondFocusTrapZone.props.forceFocusInsideTrapOnOutsideFocus).toBe(false)
expect(secondFocusTrapZone.props.isClickableOutsideFocusTrap).toBe(true)

// we remove the middle one
Expand Down

0 comments on commit 0a9e616

Please sign in to comment.