Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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 }
tabPermission={ 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 {...{ tabPermission: FocusZoneTabbableElements.all, isCircularNavigation: true }}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should add a new test case for inputOnly

<button className='a'>a</button>
<button className='b'>b</button>
<button className='c'>c</button>
Expand Down
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,6 +64,9 @@ 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);

Expand All @@ -74,6 +78,8 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
left: 0,
top: 0
};

this._processingTabKey = false;
}

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

case KeyCodes.tab:
if (this.props.allowTabKey) {
if (this.props.tabPermission === FocusZoneTabbableElements.all
|| (this.props.tabPermission === FocusZoneTabbableElements.inputOnly && this._isElementInput(ev.target as HTMLElement))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

put the || at the end of the previous line, it makes the if statement more readable

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is the code now only breaking if focusChanged?

}
}
Expand Down Expand Up @@ -646,7 +649,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 +677,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 +790,13 @@ export class FocusZone extends BaseComponent<IFocusZoneProps, {}> implements IFo
}

private _shouldInputLoseFocus(element: HTMLInputElement, isForward?: boolean) {
if (element &&
if (this.props.tabPermission === FocusZoneTabbableElements.inputOnly && !this._processingTabKey) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You don't know that the element is an input at this point, this check should be moved down to the if statement below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You don't know if the element is an input element at this point. This needs to be moved down to the if statement below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe we should. The element we pass in is of type: HTMLInputElement

However, I think it would make more sense to add it with the rest of the cases, so I'll move it down.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inputs can be of different types, that's why the check below is set up the way that it is

return false;
}

// 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export interface IFocusZoneProps extends React.HTMLAttributes<HTMLElement | Focu
* 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.
*/
allowTabKey?: boolean;
tabPermission?: FocusZoneTabbableElements;

@jspurlin jspurlin Feb 22, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's go ahead and deprecate allowTabKey instead of removing it. Remove all references to it (setting the value to tabPermission if it is passed in), put at @deprecated in the jsdoc comment, and add a _warnDeprecations for that prop referencing tabPermission

@jspurlin jspurlin Feb 22, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider updating the prop name, maybe handleTabKey


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

export enum FocusZoneTabbableElements {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

export const enum ... it will help keep size down.


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

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you update the prop name to something like handleTabKey the enum name and string values should be updated... for the values maybe never = 0, always= 1, 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 } tabPermission={ 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 } tabPermission={ 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>
);