diff --git a/common/changes/@uifabric/utilities/jspurlin-FocusZoneAddWrapProps_2018-02-13-16-53.json b/common/changes/@uifabric/utilities/jspurlin-FocusZoneAddWrapProps_2018-02-13-16-53.json new file mode 100644 index 0000000000000..2574a45378c67 --- /dev/null +++ b/common/changes/@uifabric/utilities/jspurlin-FocusZoneAddWrapProps_2018-02-13-16-53.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/utilities", + "comment": "Focus/DOM: add the ability to find if an element (or any ancestor) contains a given attribute. Also, add a shouldrWapFocus function to the focus utility (which leverages the fild element attribute just described) which returns true if the given no wrap data attribute (data-no-vertical-wrap OR data-no-horizontal-wrap) exists and is set to true", + "type": "minor" + } + ], + "packageName": "@uifabric/utilities", + "email": "jspurlin@microsoft.com" +} diff --git a/common/changes/office-ui-fabric-react/jspurlin-FocusZoneAddWrapProps_2018-02-13-16-53.json b/common/changes/office-ui-fabric-react/jspurlin-FocusZoneAddWrapProps_2018-02-13-16-53.json new file mode 100644 index 0000000000000..61a72b95ce1ba --- /dev/null +++ b/common/changes/office-ui-fabric-react/jspurlin-FocusZoneAddWrapProps_2018-02-13-16-53.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "FocusZone: Add the ability turn off directional wrapping on sections of a FocusZone", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "jspurlin@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx index 57d37f382db25..435a4f8d7f78d 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx @@ -84,7 +84,7 @@ describe('ContextualMenu', () => { items={ items } isSubMenu={ true } onDismiss={ onDismissSpy } - arrowDirection={ FocusZoneDirection.horizontal } + focusZoneProps={ { direction: FocusZoneDirection.horizontal } } /> ); @@ -110,7 +110,7 @@ describe('ContextualMenu', () => { items={ items } isSubMenu={ true } onDismiss={ onDismissSpy } - arrowDirection={ FocusZoneDirection.horizontal } + focusZoneProps={ { direction: FocusZoneDirection.bidirectional } } /> ); diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 65f4ac27d065e..34f04a285937c 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { IContextualMenuProps, IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.types'; import { DirectionalHint } from '../../common/DirectionalHint'; -import { FocusZone, FocusZoneDirection } from '../../FocusZone'; +import { FocusZone, FocusZoneDirection, IFocusZoneProps } from '../../FocusZone'; import { IMenuItemClassNames, IContextualMenuClassNames, @@ -25,7 +25,8 @@ import { customizable, getFirstFocusable, getLastFocusable, - css + css, + shouldWrapFocus } from '../../Utilities'; import { withResponsiveMode, ResponsiveMode } from '../../utilities/decorators/withResponsiveMode'; import { Callout } from '../../Callout'; @@ -36,6 +37,7 @@ import { import { VerticalDivider } from '../../Divider'; + export interface IContextualMenuState { expandedMenuItemKey?: string; dismissedMenuItemKey?: string; @@ -109,8 +111,7 @@ export class ContextualMenu extends BaseComponent { title } } { (items && items.length) ? ( @@ -340,6 +347,19 @@ export class ContextualMenu extends BaseComponent; } @@ -678,12 +698,10 @@ export class ContextualMenu extends BaseComponent) { - const submenuCloseKey = getRTL() ? KeyCodes.right : KeyCodes.left; - - if (ev.which === KeyCodes.escape - || ev.altKey - || ev.metaKey - || (ev.which === submenuCloseKey && this.props.isSubMenu && this.props.arrowDirection === FocusZoneDirection.vertical)) { + if (ev.which === KeyCodes.escape || + ev.altKey || + ev.metaKey || + this._shouldCloseSubMenu(ev)) { // When a user presses escape, we will try to refocus the previous focused element. this._isFocusingPreviousElement = true; ev.preventDefault(); @@ -692,6 +710,21 @@ export class ContextualMenu extends BaseComponent): boolean { + const submenuCloseKey = getRTL() ? KeyCodes.right : KeyCodes.left; + + if (ev.which !== submenuCloseKey || !this.props.isSubMenu) { + return false; + } + + return this._adjustedFocusZoneProps.direction === FocusZoneDirection.vertical || + (!!this._adjustedFocusZoneProps.checkForNoWrap && !shouldWrapFocus(ev.target as HTMLElement, 'data-no-horizontal-wrap')); + } + @autobind private _onMenuKeyDown(ev: React.KeyboardEvent) { if (ev.which === KeyCodes.escape || ev.altKey || ev.metaKey) { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts index ad1289a135fcf..7e6ebb098f4cd 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { ContextualMenu } from './ContextualMenu'; import { DirectionalHint } from '../../common/DirectionalHint'; -import { FocusZoneDirection } from '../../FocusZone'; +import { FocusZoneDirection, IFocusZoneProps } from '../../FocusZone'; import { IIconProps } from '../Icon/Icon.types'; import { ICalloutProps } from '../../Callout'; import { ITheme, IStyle } from '../../Styling'; @@ -169,7 +169,7 @@ export interface IContextualMenuProps extends React.Props, IWith /** * Direction for arrow navigation of the ContextualMenu. Should only be specified if using custom-rendered menu items. - * @default FocusZoneDirection.vertical + * @deprecated Use focusZoneProps instead */ arrowDirection?: FocusZoneDirection; @@ -219,6 +219,14 @@ export interface IContextualMenuProps extends React.Props, IWith /** Method to call when trying to render a submenu. */ onRenderSubMenu?: IRenderFunction; + + /** + * Props to pass down to the FocusZone. + * NOTE: the default FocusZoneDirection will be used unless a direction + * is specified in the focusZoneProps (even if other focusZoneProps are defined) + * @default {direction: FocusZoneDirection.vertical} + */ + focusZoneProps?: IFocusZoneProps; } export interface IContextualMenuItem { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx index 1281e932a2d14..109736bdae362 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx @@ -9,6 +9,7 @@ import { ContextualMenuBasicExample } from './examples/ContextualMenu.Basic.Exam import { ContextualMenuIconExample } from './examples/ContextualMenu.Icon.Example'; import { ContextualMenuSectionExample } from './examples/ContextualMenu.Section.Example'; import { ContextualMenuSubmenuExample } from './examples/ContextualMenu.Submenu.Example'; +import { ContextualMenuCustomizationWithNoWrapExample } from './examples/ContextualMenu.CustomizationWithNoWrap.Example'; import { ContextualMenuCheckmarksExample } from './examples/ContextualMenu.Checkmarks.Example'; import { ContextualMenuDirectionalExample } from './examples/ContextualMenu.Directional.Example'; import { ContextualMenuCustomizationExample } from './examples/ContextualMenu.Customization.Example'; @@ -20,6 +21,7 @@ const ContextualMenuBasicExampleCode = require('!raw-loader!office-ui-fabric-rea const ContextualMenuIconExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Icon.Example.tsx') as string; const ContextualMenuSectionExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Section.Example.tsx') as string; const ContextualMenuSubmenuExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Submenu.Example.tsx') as string; +const ContextualMenuCustomizationWithNoWrapExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.CustomizationWithNoWrap.Example.tsx') as string; const ContextualMenuCheckmarksExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Checkmarks.Example.tsx') as string; const ContextualMenuDirectionalExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Directional.Example.tsx') as string; const ContextualMenuCustomizationExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Customization.Example.tsx') as string; @@ -76,6 +78,12 @@ export class ContextualMenuPage extends React.Component + + + { + public render() { + return ( + + ); + } + + @autobind + private _renderCharmMenuItem(item: any, dismissMenu: () => void) { + return ( + + ); + } + + private _renderCategoriesList(item: any) { + return ( +
    +
  • + { item.categoryList.map((category: any) => + + ) } +
  • +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.test.tsx b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.test.tsx index ce4ef11a8c89e..df4efe0a93dc1 100644 --- a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.test.tsx +++ b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.test.tsx @@ -354,6 +354,340 @@ describe('FocusZone', () => { expect(lastFocusedElement).toBe(buttonA); }); + it('can use arrows bidirectionally with data-no-vertical-wrap', () => { + const component = ReactTestUtils.renderIntoDocument( +
+ + + + + + +
+ ); + + const focusZone = ReactDOM.findDOMNode(component as React.ReactInstance).firstChild as Element; + const buttonA = focusZone.querySelector('.a') as HTMLElement; + const buttonB = focusZone.querySelector('.b') as HTMLElement; + const buttonC = focusZone.querySelector('.c') as HTMLElement; + const buttonD = focusZone.querySelector('.d') as HTMLElement; + + // Set up a grid like so: + // A B + // C D + setupElement(buttonA, { + clientRect: { + top: 0, + bottom: 20, + left: 0, + right: 20 + } + }); + + setupElement(buttonB, { + clientRect: { + top: 0, + bottom: 20, + left: 20, + right: 40 + } + }); + + setupElement(buttonC, { + clientRect: { + top: 20, + bottom: 40, + left: 0, + right: 20 + } + }); + + setupElement(buttonD, { + clientRect: { + top: 20, + bottom: 40, + left: 20, + right: 40 + } + }); + + // Focus the first button. + ReactTestUtils.Simulate.focus(buttonA); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing up should stay on a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing left should stay on a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing right should go to b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing down should go to d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing down stay on d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing right stay on d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing left should go to c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing down should stay on c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing left should go to b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing up should stay on b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing right should go to c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonC); + }); + + it('can use arrows bidirectionally with data-no-horizontal-wrap', () => { + const component = ReactTestUtils.renderIntoDocument( +
+ + + + + + +
+ ); + + const focusZone = ReactDOM.findDOMNode(component as React.ReactInstance).firstChild as Element; + const buttonA = focusZone.querySelector('.a') as HTMLElement; + const buttonB = focusZone.querySelector('.b') as HTMLElement; + const buttonC = focusZone.querySelector('.c') as HTMLElement; + const buttonD = focusZone.querySelector('.d') as HTMLElement; + + // Set up a grid like so: + // A B + // C D + setupElement(buttonA, { + clientRect: { + top: 0, + bottom: 20, + left: 0, + right: 20 + } + }); + + setupElement(buttonB, { + clientRect: { + top: 0, + bottom: 20, + left: 20, + right: 40 + } + }); + + setupElement(buttonC, { + clientRect: { + top: 20, + bottom: 40, + left: 0, + right: 20 + } + }); + + setupElement(buttonD, { + clientRect: { + top: 20, + bottom: 40, + left: 20, + right: 40 + } + }); + + // Focus the first button. + ReactTestUtils.Simulate.focus(buttonA); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing up should stay on a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing left should stay on a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing right should go to b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing right should stay on b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing down should go to d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing down stay on d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing right stay on d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing left should go to c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing left should stay on c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing down should go to d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing left should go to c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing left should go to a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing left should go to b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing up should go to a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonA); + }); + + it('can use arrows bidirectionally with data-no-horizontal-wrap and data-no-vertical-wrap', () => { + const component = ReactTestUtils.renderIntoDocument( +
+ + + + + + +
+ ); + + const focusZone = ReactDOM.findDOMNode(component as React.ReactInstance).firstChild as Element; + const buttonA = focusZone.querySelector('.a') as HTMLElement; + const buttonB = focusZone.querySelector('.b') as HTMLElement; + const buttonC = focusZone.querySelector('.c') as HTMLElement; + const buttonD = focusZone.querySelector('.d') as HTMLElement; + + // Set up a grid like so: + // A B + // C D + setupElement(buttonA, { + clientRect: { + top: 0, + bottom: 20, + left: 0, + right: 20 + } + }); + + setupElement(buttonB, { + clientRect: { + top: 0, + bottom: 20, + left: 20, + right: 40 + } + }); + + setupElement(buttonC, { + clientRect: { + top: 20, + bottom: 40, + left: 0, + right: 20 + } + }); + + setupElement(buttonD, { + clientRect: { + top: 20, + bottom: 40, + left: 20, + right: 40 + } + }); + + // Focus the first button. + ReactTestUtils.Simulate.focus(buttonA); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing up should stay on a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing left should stay on a. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonA); + + // Pressing right should go to b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing right should stay on b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing up should stay on b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonB); + + // Pressing down should go to d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing down stay on d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing right stay on d. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.right }); + expect(lastFocusedElement).toBe(buttonD); + + // Pressing left should go to c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing left should stay on c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.left }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing down should stay on c. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.down }); + expect(lastFocusedElement).toBe(buttonC); + + // Pressing left should go to b. + ReactTestUtils.Simulate.keyDown(focusZone, { which: KeyCodes.up }); + expect(lastFocusedElement).toBe(buttonA); + }); + it('correctly skips data-not-focusable elements', () => { const component = ReactTestUtils.renderIntoDocument(
diff --git a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx index 81831cf00a808..0ed756889a895 100644 --- a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx +++ b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx @@ -22,13 +22,18 @@ import { getWindow, isElementFocusZone, isElementFocusSubZone, - isElementTabbable + isElementTabbable, + shouldWrapFocus } from '../../Utilities'; const IS_FOCUSABLE_ATTRIBUTE = 'data-is-focusable'; const IS_ENTER_DISABLED_ATTRIBUTE = 'data-disable-click-on-enter'; const FOCUSZONE_ID_ATTRIBUTE = 'data-focuszone-id'; const TABINDEX = 'tabindex'; +const NO_VERTICAL_WRAP = 'data-no-vertical-wrap'; +const NO_HORIZONTAL_WRAP = 'data-no-horizontal-wrap'; +const LARGE_DISTANCE_FROM_CENTER = 999999999; +const LARGE_NEGATIVE_DISTANCE_FROM_CENTER = -999999999; const _allInstances: { [key: string]: FocusZone @@ -357,7 +362,8 @@ export class FocusZone extends BaseComponent implements IFo case KeyCodes.tab: if (this.props.allowTabKey) { - if (direction === FocusZoneDirection.vertical) { + if (direction === FocusZoneDirection.vertical || + !this._shouldWrapFocus(this._activeElement as HTMLElement, NO_HORIZONTAL_WRAP)) { if (ev.shiftKey) { this._moveFocusUp(); } else { @@ -474,7 +480,8 @@ export class FocusZone extends BaseComponent implements IFo private _moveFocus( isForward: boolean, getDistanceFromCenter: (activeRect: ClientRect, targetRect: ClientRect) => number, - ev?: Event): boolean { + ev?: Event, + useDefaultWrap: boolean = true): boolean { let element = this._activeElement; let candidateDistance = -1; @@ -495,9 +502,7 @@ export class FocusZone extends BaseComponent implements IFo const activeRect = isBidirectional ? element.getBoundingClientRect() : null; do { - element = (isForward ? - getNextElement(this._root, element) : - getPreviousElement(this._root, element)) as HTMLElement; + element = (isForward ? getNextElement(this._root, element) : getPreviousElement(this._root, element)) as HTMLElement; if (isBidirectional) { if (element) { @@ -529,7 +534,7 @@ export class FocusZone extends BaseComponent implements IFo if (candidateElement && candidateElement !== this._activeElement) { changedFocus = true; this.focusElement(candidateElement); - } else if (this.props.isCircularNavigation) { + } else if (this.props.isCircularNavigation && useDefaultWrap) { if (isForward) { return this.focusElement(getNextElement(this._root, this._root.firstElementChild as HTMLElement, true) as HTMLElement); } else { @@ -554,7 +559,11 @@ export class FocusZone extends BaseComponent implements IFo const activeRectBottom = Math.floor(activeRect.bottom); if (targetRectTop < activeRectBottom) { - return 999999999; + if (!this._shouldWrapFocus(this._activeElement as HTMLElement, NO_VERTICAL_WRAP)) { + return LARGE_NEGATIVE_DISTANCE_FROM_CENTER; + } + + return LARGE_DISTANCE_FROM_CENTER; } if ((targetTop === -1 && targetRectTop >= activeRectBottom) || @@ -591,7 +600,10 @@ export class FocusZone extends BaseComponent implements IFo const activeRectTop = Math.floor(activeRect.top); if (targetRectBottom > activeRectTop) { - return 999999999; + if (!this._shouldWrapFocus(this._activeElement as HTMLElement, NO_VERTICAL_WRAP)) { + return LARGE_NEGATIVE_DISTANCE_FROM_CENTER; + } + return LARGE_DISTANCE_FROM_CENTER; } if ((targetTop === -1 && targetRectBottom <= activeRectTop) || @@ -614,6 +626,7 @@ export class FocusZone extends BaseComponent implements IFo } private _moveFocusLeft(): boolean { + const shouldWrap = this._shouldWrapFocus(this._activeElement as HTMLElement, NO_HORIZONTAL_WRAP); if (this._moveFocus(getRTL(), (activeRect: ClientRect, targetRect: ClientRect) => { let distance = -1; @@ -624,10 +637,17 @@ export class FocusZone extends BaseComponent implements IFo ) { distance = activeRect.right - targetRect.right; + } else { + if (!shouldWrap) { + distance = LARGE_NEGATIVE_DISTANCE_FROM_CENTER; + } } return distance; - })) { + }, + undefined /*ev*/, + (shouldWrap || !getRTL()) + )) { this._setFocusAlignment(this._activeElement as HTMLElement, true, false); return true; } @@ -636,6 +656,7 @@ export class FocusZone extends BaseComponent implements IFo } private _moveFocusRight(): boolean { + const shouldWrap = this._shouldWrapFocus(this._activeElement as HTMLElement, NO_HORIZONTAL_WRAP); if (this._moveFocus(!getRTL(), (activeRect: ClientRect, targetRect: ClientRect) => { let distance = -1; @@ -646,10 +667,15 @@ export class FocusZone extends BaseComponent implements IFo ) { distance = targetRect.left - activeRect.left; + } else if (!shouldWrap) { + distance = LARGE_NEGATIVE_DISTANCE_FROM_CENTER; } return distance; - })) { + }, + undefined /*ev*/, + (shouldWrap || getRTL()) + )) { this._setFocusAlignment(this._activeElement as HTMLElement, true, false); return true; } @@ -782,4 +808,8 @@ export class FocusZone extends BaseComponent implements IFo return true; } + + private _shouldWrapFocus(element: HTMLElement, noWrapDataAttribute: 'data-no-vertical-wrap' | 'data-no-horizontal-wrap'): boolean { + return !!this.props.checkForNoWrap ? shouldWrapFocus(element, noWrapDataAttribute) : true; + } } diff --git a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts index 300bb3b20aa33..64af0ab337413 100644 --- a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts +++ b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts @@ -110,6 +110,13 @@ export interface IFocusZoneProps extends React.HTMLAttributes boolean): HTMLElement | null { + if (!element || element === document.body) { + return null; + } + + return matchFunction(element) ? element : findElementRecursive(getParent(element), matchFunction); +} + +/** + * Determines if an element, or any of its ancestors, contian the given attribute + * @param element - element to start searching at + * @param attribute - the attribute to search for + * @returns the value of the first instance found + */ +export function elementContainsAttribute(element: HTMLElement, attribute: string): string | null { + let elementMatch = findElementRecursive(element, (testElement: HTMLElement) => testElement.hasAttribute(attribute)); + return elementMatch && elementMatch.getAttribute(attribute); +} + /** * Determines whether or not an element has the virtual hierarchy extension. * diff --git a/packages/utilities/src/focus.ts b/packages/utilities/src/focus.ts index 071bfde4ad2e6..73a0d6f8992e0 100644 --- a/packages/utilities/src/focus.ts +++ b/packages/utilities/src/focus.ts @@ -1,6 +1,6 @@ /* tslint:disable:no-string-literal */ -import { elementContains, getDocument } from './dom'; +import { elementContains, getDocument, elementContainsAttribute } from './dom'; const IS_FOCUSABLE_ATTRIBUTE = 'data-is-focusable'; const IS_VISIBLE_ATTRIBUTE = 'data-is-visible'; @@ -316,3 +316,14 @@ export function doesElementContainFocus(element: HTMLElement): boolean { } return false; } + +/** + * Determines if an, or any of its ancestors, sepcificies that it doesn't want focus to wrap + * @param element - element to start searching from + * @param noWrapDataAttribute - the no wrap data attribute to match (either) + * @returns true if focus should wrap, false otherwise + */ +export function shouldWrapFocus(element: HTMLElement, noWrapDataAttribute: 'data-no-vertical-wrap' | 'data-no-horizontal-wrap'): boolean { + + return elementContainsAttribute(element, noWrapDataAttribute) === 'true' ? false : true; +} diff --git a/scripts/package.json b/scripts/package.json index 8e5639e2f79d3..1349d0e56e187 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -49,7 +49,7 @@ "bundlesize": [ { "path": "../apps/test-bundle-button/dist/main.min.js", - "maxSize": "43.6 kB" + "maxSize": "43.7 kB" } ] -} +} \ No newline at end of file