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
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "Focus Zone: Add support for tab to skip selection elements",
"type": "minor"
}
],
"packageName": "office-ui-fabric-react",
"email": "chiechan@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { IContextualMenuProps, IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.types';
import { DirectionalHint } from '../../common/DirectionalHint';
import { FocusZone, FocusZoneDirection, IFocusZoneProps } from '../../FocusZone';
import { FocusZone, FocusZoneDirection, IFocusZoneProps, FocusZoneTabbableElements } from '../../FocusZone';
import {
IMenuItemClassNames,
IContextualMenuClassNames,
Expand Down Expand Up @@ -319,7 +319,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
{...this._adjustedFocusZoneProps }
className={ this._classNames.root }
isCircularNavigation={ true }
allowTabKey={ true }
handleTabKey={ FocusZoneTabbableElements.all }
>
<ul
role='presentation'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as ReactTestUtils from 'react-dom/test-utils';
import { KeyCodes } from '../../Utilities';

import { FocusZone } from './FocusZone';
import { FocusZoneDirection } from './FocusZone.types';
import { FocusZoneDirection, FocusZoneTabbableElements } from './FocusZone.types';

describe('FocusZone', () => {
let lastFocusedElement: HTMLElement | undefined;
Expand Down Expand Up @@ -1048,7 +1048,7 @@ describe('FocusZone', () => {
const tabDownListener = jest.fn();
const component = ReactTestUtils.renderIntoDocument(
<div { ...{ onFocusCapture: _onFocus, onKeyDown: tabDownListener } }>
<FocusZone {...{ allowTabKey: true, isCircularNavigation: true }}>
<FocusZone {...{ handleTabKey: FocusZoneTabbableElements.all, isCircularNavigation: true }}>
<button className='a'>a</button>
<button className='b'>b</button>
<button className='c'>c</button>
Expand Down Expand Up @@ -1160,4 +1160,62 @@ describe('FocusZone', () => {
const onKeyDownEvent = tabDownListener.mock.calls[0][0];
expect(onKeyDownEvent.which).toBe(KeyCodes.tab);
});

it('should stay in input box with arrow keys and exit with tab', () => {
const tabDownListener = jest.fn();
const component = ReactTestUtils.renderIntoDocument(
<div { ...{ onFocusCapture: _onFocus, onKeyDown: tabDownListener } }>
<FocusZone {...{ handleTabKey: FocusZoneTabbableElements.inputOnly, isCircularNavigation: false }}>
<input type='text' className='a' />
<button className='b'>b</button>
</FocusZone>
</div >
);

const focusZone = ReactDOM.findDOMNode(component as React.ReactInstance).firstChild as Element;

const inputA = focusZone.querySelector('.a') as HTMLElement;
const buttonB = focusZone.querySelector('.b') as HTMLElement;

setupElement(inputA, {
clientRect: {
top: 0,
bottom: 20,
left: 20,
right: 40
}
});

setupElement(buttonB, {
clientRect: {
top: 0,
bottom: 20,
left: 20,
right: 40
}
});

// InputA should be focused.
inputA.focus();
expect(lastFocusedElement).toBe(inputA);

// When we hit right/left on the arrow key, we don't want to be able to leave focus on an input
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right });
expect(lastFocusedElement).toBe(inputA);

expect(inputA.tabIndex).toBe(0);
expect(buttonB.tabIndex).toBe(-1);

// Pressing tab will be the only way for us to exit the focus zone
ReactTestUtils.Simulate.keyDown(inputA, { which: KeyCodes.tab });
expect(lastFocusedElement).toBe(buttonB);
expect(inputA.tabIndex).toBe(-1);
expect(buttonB.tabIndex).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import {
FocusZoneDirection,
FocusZoneTabbableElements,
IFocusZone,
IFocusZoneProps
} from './FocusZone.types';
Expand Down Expand Up @@ -63,17 +64,25 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
private _focusAlignment: IPoint;
private _isInnerZone: boolean;

/** Used to allow us to move to next focusable element even when we're focusing on a input element when pressing tab */
private _processingTabKey: boolean;

constructor(props: IFocusZoneProps) {
super(props);

this._warnDeprecations({ rootProps: undefined });
this._warnDeprecations({
rootProps: undefined,
'allowTabKey': 'handleTabKey'
});

this._id = getId('FocusZone');

this._focusAlignment = {
left: 0,
top: 0
};

this._processingTabKey = false;
}

public componentDidMount() {
Expand Down Expand Up @@ -361,21 +370,20 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
return;

case KeyCodes.tab:
if (this.props.allowTabKey) {
if (this.props.allowTabKey ||
this.props.handleTabKey === FocusZoneTabbableElements.all ||
(this.props.handleTabKey === FocusZoneTabbableElements.inputOnly &&
this._isElementInput(ev.target as HTMLElement))) {
let focusChanged = false;
this._processingTabKey = true;
if (direction === FocusZoneDirection.vertical ||
!this._shouldWrapFocus(this._activeElement as HTMLElement, NO_HORIZONTAL_WRAP)) {
if (ev.shiftKey) {
this._moveFocusUp();
} else {
this._moveFocusDown();
}
break;
focusChanged = ev.shiftKey ? this._moveFocusUp() : this._moveFocusDown();
} else if (direction === FocusZoneDirection.horizontal || direction === FocusZoneDirection.bidirectional) {
if (ev.shiftKey) {
this._moveFocusLeft();
} else {
this._moveFocusRight();
}
focusChanged = ev.shiftKey ? this._moveFocusLeft() : this._moveFocusRight();
}
this._processingTabKey = false;
if (focusChanged) {
break;
}
}
Expand Down Expand Up @@ -646,7 +654,7 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
return distance;
},
undefined /*ev*/,
(shouldWrap || !getRTL())
shouldWrap
)) {
this._setFocusAlignment(this._activeElement as HTMLElement, true, false);
return true;
Expand Down Expand Up @@ -674,7 +682,7 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
return distance;
},
undefined /*ev*/,
(shouldWrap || getRTL())
shouldWrap
)) {
this._setFocusAlignment(this._activeElement as HTMLElement, true, false);
return true;
Expand Down Expand Up @@ -787,7 +795,9 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
}

private _shouldInputLoseFocus(element: HTMLInputElement, isForward?: boolean) {
if (element &&
// If a tab was used, we want to focus on the next element.
if (!this._processingTabKey &&
element &&
element.type &&
ALLOWED_INPUT_TYPES.indexOf(element.type.toLowerCase()) > -1) {
const selectionStart = element.selectionStart;
Expand All @@ -799,9 +809,11 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
// 1. There is range selected.
// 2. When selection start is larger than 0 and it is backward.
// 3. when selection start is not the end of lenght and it is forward.
// 4. We press any of the arrow keys when our handleTabKey isn't none or undefined (only losing focus if we hit tab)
if (isRangeSelected ||
(selectionStart > 0 && !isForward) ||
(selectionStart !== inputValue.length && isForward)) {
(selectionStart !== inputValue.length && isForward) ||
!!this.props.handleTabKey) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,18 @@ export interface IFocusZoneProps extends React.HTMLAttributes<HTMLElement | Focu
* Allows tab key to be handled to tab through a list of items in the focus zone,
* an unfortunate side effect is that users will not be able to tab out of the focus zone
* and have to hit escape or some other key.
* @deprecated Use 'handleTabKey' instead.
*
*/
allowTabKey?: boolean;

/**
* Allows tab key to be handled to tab through a list of items in the focus zone,
* an unfortunate side effect is that users will not be able to tab out of the focus zone
* and have to hit escape or some other key.
*/
handleTabKey?: FocusZoneTabbableElements;

/**
* Whether the to check for data-no-horizontal-wrap or data-no-vertical-wrap attributes
* when determining how to move focus
Expand All @@ -119,6 +128,18 @@ export interface IFocusZoneProps extends React.HTMLAttributes<HTMLElement | Focu
checkForNoWrap?: boolean;
}

export const enum FocusZoneTabbableElements {

/** Tabbing is not allowed */
none = 0,

/** All tabbing action is allowed */
all = 1,

/** Tabbing is allowed only on input elements */
inputOnly = 2
}

export enum FocusZoneDirection {
/** Only react to up/down arrows. */
vertical = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
import { FocusZonePhotosExample } from './examples/FocusZone.Photos.Example';
import { FocusZoneListExample } from './examples/FocusZone.List.Example';
import { FocusZoneDisabledExample } from './examples/FocusZone.Disabled.Example';
import { FocusZoneTabbableExample } from './examples/FocusZone.Tabbable.Example';

const FocusZonePhotosExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/FocusZone/examples/FocusZone.Photos.Example.tsx') as string;
const FocusZoneListExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/FocusZone/examples/FocusZone.List.Example.tsx') as string;
const FocusZoneDisabledExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/FocusZone/examples/FocusZone.Disabled.Example.tsx') as string;
const FocusZoneTabbableCode = require('!raw-loader!office-ui-fabric-react/src/components/FocusZone/examples/FocusZone.Tabbable.Example.tsx') as string;

export class FocusZonePage extends React.Component<IComponentDemoPageProps, {}> {
public render() {
Expand All @@ -30,6 +32,9 @@ export class FocusZonePage extends React.Component<IComponentDemoPageProps, {}>
<ExampleCard title='Disabled FocusZone' code={ FocusZoneDisabledExampleCode }>
<FocusZoneDisabledExample />
</ExampleCard>
<ExampleCard title='Tabbable FocusZone' code={ FocusZoneTabbableCode }>
<FocusZoneTabbableExample />
</ExampleCard>
</div>
}
propertiesTables={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:global {
.ms-FocusZoneTabbableExample .ms-Row {
display: block;
margin: 5px;
}

.ms-FocusZoneTabbableExample-textField {
display: inline-block;
width: 300px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* tslint:disable:no-unused-variable */
import * as React from 'react';
/* tslint:enable:no-unused-variable */

import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { FocusZone, FocusZoneDirection, FocusZoneTabbableElements } from 'office-ui-fabric-react/lib/FocusZone';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import './FocusZone.Tabbable.Example.scss';

export const FocusZoneTabbableExample = () => (
<div className='ms-FocusZoneTabbableExample'>
<div className='ms-Row'>
<FocusZone direction={ FocusZoneDirection.horizontal } handleTabKey={ FocusZoneTabbableElements.all } isCircularNavigation={ true }>
<span>Circular Tabbable FocusZone: </span>
<DefaultButton>Button 1</DefaultButton>
<DefaultButton>Button 2</DefaultButton>
<TextField value='FocusZone TextField' className='ms-FocusZoneTabbableExample-textField' />
<DefaultButton>Button 3</DefaultButton>
</FocusZone>
</div>
<div className='ms-Row'>
<FocusZone direction={ FocusZoneDirection.horizontal } handleTabKey={ FocusZoneTabbableElements.inputOnly } isCircularNavigation={ false }>
<span>Input Only FocusZone: </span>
<DefaultButton>Button 1</DefaultButton>
<DefaultButton>Button 2</DefaultButton>
<TextField value='FocusZone TextField' className='ms-FocusZoneTabbableExample-textField' />
<DefaultButton>Button 3</DefaultButton>
</FocusZone>
</div>
</div>
);