Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<IComponentDemoPageProps, {}> {
public render() {
return (
Expand All @@ -34,6 +37,9 @@ export class FocusTrapZonePage extends React.Component<IComponentDemoPageProps,
<ExampleCard title='Simple Box with Clicking outside Trap Zone enabled' code={ FocusTrapZoneBoxClickExampleCode }>
<FocusTrapZoneBoxClickExample />
</ExampleCard>
<ExampleCard title='Multiple Nest FocusTrapZones' code={ FocusTrapZoneNestedExampleCode }>
<FocusTrapZoneNestedExample />
</ExampleCard>
</div>
}
propertiesTables={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
.ms-FocusTrapZoneBoxExample {
border: dashed 1px #ababab;
}

.ms-FocusTrapComponent {
border: black 2px solid;
padding: 5px;
}
Original file line number Diff line number Diff line change
@@ -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<IFocusTrapComponentProps, IFocusTrapComponentState> {

public refs: {
[key: string]: React.ReactInstance;
toggle: HTMLElement;
};

render() {
let contents = (
<div className='ms-FocusTrapComponent'>
<Button onClick={ this._onStringButtonClicked } >
{
this.props.name
}
</Button>
<Toggle
ref='toggle'
checked={ this.props.isActive }
onChanged={ this._onFocusTrapZoneToggleChanged }
label='Focus Trap Zone'
onText='On'
offText='Off' />
{
this.props.children
}
</div>
);

if (this.props.isActive) {
return (
<FocusTrapZone forceFocusInsideTrap={false}>
{
contents
}
</FocusTrapZone>
);
}
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<React.HTMLProps<HTMLDivElement>, IFocusTrapZoneNestedExampleState> {

constructor() {
super();

this.state = {
stateMap: {}
};
}

public render() {
return (
<div>
<FocusTrapComponent name={ 'One' } isActive={ !!this.state.stateMap['One']} setIsActive={ this._setIsActive } >
<FocusTrapComponent name={ 'Two' } isActive={ !!this.state.stateMap['Two']} setIsActive={ this._setIsActive } >
<FocusTrapComponent name={ 'Three' } isActive={ !!this.state.stateMap['Three']} setIsActive={ this._setIsActive } >
</FocusTrapComponent>
<FocusTrapComponent name={ 'Four' } isActive={ !!this.state.stateMap['Four']} setIsActive={ this._setIsActive } >
</FocusTrapComponent>
</FocusTrapComponent>
<FocusTrapComponent name={ 'Five' } isActive={ !!this.state.stateMap['Five']} setIsActive={ this._setIsActive } >
</FocusTrapComponent>
</FocusTrapComponent>
<Button onClick={ this._randomize }>Randomize</Button>
</div>
);
}

@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();
}

}


10 changes: 10 additions & 0 deletions common/changes/dagoff-focusTrapZone_2017-02-23-22-47.json
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ export class FocusTrapZone extends BaseComponent<IFocusTrapZoneProps, {}> 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;
Expand All @@ -39,6 +55,19 @@ export class FocusTrapZone extends BaseComponent<IFocusTrapZoneProps, {}> 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();
}
Expand Down Expand Up @@ -101,22 +130,26 @@ export class FocusTrapZone extends BaseComponent<IFocusTrapZoneProps, {}> 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();
}
}
}
}