diff --git a/apps/fabric-examples/src/pages/FocusTrapZonePage/FocusTrapZonePage.tsx b/apps/fabric-examples/src/pages/FocusTrapZonePage/FocusTrapZonePage.tsx index 63adce4797a75..16ea5b0d3d76a 100644 --- a/apps/fabric-examples/src/pages/FocusTrapZonePage/FocusTrapZonePage.tsx +++ b/apps/fabric-examples/src/pages/FocusTrapZonePage/FocusTrapZonePage.tsx @@ -17,6 +17,9 @@ let FocusTrapZoneBoxExampleWithFocusableItemCode = require('./examples/FocusTrap import FocusTrapZoneBoxClickExample from './examples/FocusTrapZone.Box.Click.Example'; let FocusTrapZoneBoxClickExampleCode = require('./examples/FocusTrapZone.Box.Click.Example.tsx') as string; +import FocusTrapZoneNestedExample from './examples/FocusTrapZone.Nested.Example'; +let FocusTrapZoneNestedExampleCode = require('./examples/FocusTrapZone.Nested.Example.tsx') as string; + export class FocusTrapZonePage extends React.Component { public render() { return ( @@ -34,6 +37,9 @@ export class FocusTrapZonePage extends React.Component + + + } propertiesTables={ diff --git a/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Box.Example.scss b/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Box.Example.scss index 09b48341653ef..1bd0f36c5abe5 100644 --- a/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Box.Example.scss +++ b/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Box.Example.scss @@ -3,3 +3,8 @@ .ms-FocusTrapZoneBoxExample { border: dashed 1px #ababab; } + +.ms-FocusTrapComponent { + border: black 2px solid; + padding: 5px; +} \ No newline at end of file diff --git a/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Nested.Example.tsx b/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Nested.Example.tsx new file mode 100644 index 0000000000000..9e73cbb0bfe00 --- /dev/null +++ b/apps/fabric-examples/src/pages/FocusTrapZonePage/examples/FocusTrapZone.Nested.Example.tsx @@ -0,0 +1,126 @@ +/* tslint:disable:no-unused-variable */ +import * as React from 'react'; +/* tslint:enable:no-unused-variable */ + +import * as ReactDOM from 'react-dom'; +import { Button } from 'office-ui-fabric-react/lib/Button'; +import { FocusTrapZone } from 'office-ui-fabric-react/lib/FocusTrapZone'; +import { Link } from 'office-ui-fabric-react/lib/Link'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; +import { autobind } from 'office-ui-fabric-react/lib/Utilities'; +import './FocusTrapZone.Box.Example.scss'; + +interface IFocusTrapComponentProps { + name: string; + isActive: boolean; + setIsActive: (name: string, isActive: boolean) => void; +} + +interface IFocusTrapComponentState { +} + +class FocusTrapComponent extends React.Component { + + public refs: { + [key: string]: React.ReactInstance; + toggle: HTMLElement; + }; + + render() { + let contents = ( +
+ + + { + this.props.children + } +
+ ); + + if (this.props.isActive) { + return ( + + { + contents + } + + ); + } + return contents; + } + + @autobind + private _onStringButtonClicked() { + console.log(this.props.name); + } + + @autobind + private _onFocusTrapZoneToggleChanged(isChecked: boolean) { + this.props.setIsActive(this.props.name, isChecked); + } + +} + +export interface IFocusTrapZoneNestedExampleState { + stateMap: any; +} + +const NAMES: string[] = ['One', 'Two', 'Three', 'Four', 'Five']; + +export default class FocusTrapZoneNestedExample extends React.Component, IFocusTrapZoneNestedExampleState> { + + constructor() { + super(); + + this.state = { + stateMap: {} + }; + } + + public render() { + return ( +
+ + + + + + + + + + + +
+ ); + } + + @autobind + private _setIsActive(name: string, isActive: boolean): void { + this.state.stateMap[name] = isActive; + this.forceUpdate(); + } + + @autobind + private _randomize(): void { + for (let i in NAMES) { + let newVal: boolean = Math.random() < .5 ? false : true; + this.state.stateMap[NAMES[i]] = newVal; + } + this.forceUpdate(); + } + +} + + diff --git a/common/changes/dagoff-focusTrapZone_2017-02-23-22-47.json b/common/changes/dagoff-focusTrapZone_2017-02-23-22-47.json new file mode 100644 index 0000000000000..ea94a8a365f75 --- /dev/null +++ b/common/changes/dagoff-focusTrapZone_2017-02-23-22-47.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "FocusTrapZone: Fixed a scenario where multiple instances would fight over focus.", + "type": "patch" + } + ], + "email": "dagoff@microsoft.com" +} diff --git a/packages/office-ui-fabric-react/src/components/FocusTrapZone/FocusTrapZone.tsx b/packages/office-ui-fabric-react/src/components/FocusTrapZone/FocusTrapZone.tsx index 5828b0f668eb5..1102e9accc915 100644 --- a/packages/office-ui-fabric-react/src/components/FocusTrapZone/FocusTrapZone.tsx +++ b/packages/office-ui-fabric-react/src/components/FocusTrapZone/FocusTrapZone.tsx @@ -20,6 +20,22 @@ export class FocusTrapZone extends BaseComponent implem }; private _previouslyFocusedElement: HTMLElement; + private static _focusStack: FocusTrapZone[] = []; + private _isInFocusStack: boolean = false; + private static _clickStack: FocusTrapZone[] = []; + private _isInClickStack: boolean = false; + + public componentWillMount() { + let { isClickableOutsideFocusTrap = false, forceFocusInsideTrap = true } = this.props; + if (forceFocusInsideTrap) { + this._isInFocusStack = true; + FocusTrapZone._focusStack.push(this); + } + if (!isClickableOutsideFocusTrap) { + this._isInClickStack = true; + FocusTrapZone._clickStack.push(this); + } + } public componentDidMount() { let { elementToFocusOnDismiss, isClickableOutsideFocusTrap = false, forceFocusInsideTrap = true } = this.props; @@ -39,6 +55,19 @@ export class FocusTrapZone extends BaseComponent implem public componentWillUnmount() { let { ignoreExternalFocusing } = this.props; + this._events.dispose(); + if (this._isInFocusStack || this._isInClickStack) { + let filter = (value: FocusTrapZone) => { + return this !== value; + }; + if (this._isInFocusStack) { + FocusTrapZone._focusStack = FocusTrapZone._focusStack.filter(filter); + } + if (this._isInClickStack) { + FocusTrapZone._clickStack = FocusTrapZone._clickStack.filter(filter); + } + } + if (!ignoreExternalFocusing && this._previouslyFocusedElement) { this._previouslyFocusedElement.focus(); } @@ -101,22 +130,26 @@ export class FocusTrapZone extends BaseComponent implem } private _forceFocusInTrap(ev: FocusEvent) { - const focusedElement = document.activeElement as HTMLElement; - - if (!elementContains(this.refs.root, focusedElement)) { - this.focus(); - ev.preventDefault(); - ev.stopPropagation(); + if (FocusTrapZone._focusStack.length && this === FocusTrapZone._focusStack[FocusTrapZone._focusStack.length - 1]) { + const focusedElement = document.activeElement as HTMLElement; + + if (!elementContains(this.refs.root, focusedElement)) { + this.focus(); + ev.preventDefault(); + ev.stopPropagation(); + } } } private _forceClickInTrap(ev: MouseEvent) { - const clickedElement = ev.target as HTMLElement; - - if (clickedElement && !elementContains(this.refs.root, clickedElement)) { - this.focus(); - ev.preventDefault(); - ev.stopPropagation(); + if (FocusTrapZone._clickStack.length && this === FocusTrapZone._clickStack[FocusTrapZone._clickStack.length - 1]) { + const clickedElement = ev.target as HTMLElement; + + if (clickedElement && !elementContains(this.refs.root, clickedElement)) { + this.focus(); + ev.preventDefault(); + ev.stopPropagation(); + } } } } \ No newline at end of file