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": "@uifabric/utilities",
"comment": "Focus: Fix getPreviousElement to correctly walk across previous siblings if a potential child match was found",
"type": "patch"
}
],
"packageName": "@uifabric/utilities",
"email": "jspurlin@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* tslint:disable:no-unused-variable */
import * as React from 'react';
/* tslint:enable:no-unused-variable */

import * as ReactDOM from 'react-dom';
import * as ReactTestUtils from 'react-dom/test-utils';
import { KeyCodes } from '../../Utilities';

import { FocusZone, FocusZoneDirection } from '../FocusZone';
import { FocusTrapZone } from './FocusTrapZone';

describe('FocusTrapZone', () => {
let lastFocusedElement: HTMLElement | undefined;
function _onFocus(ev: any) {
lastFocusedElement = ev.target;
}

function setupElement(element: HTMLElement, {
clientRect,
isVisible = true
}: {
clientRect: {
top: number;
left: number;
bottom: number;
right: number;
};
isVisible?: boolean;
}): void {
element.getBoundingClientRect = () => ({
top: clientRect.top,
left: clientRect.left,
bottom: clientRect.bottom,
right: clientRect.right,
width: clientRect.right - clientRect.left,
height: clientRect.bottom - clientRect.top
});

element.setAttribute('data-is-visible', String(isVisible));

element.focus = () => ReactTestUtils.Simulate.focus(element);
}

beforeEach(() => {
lastFocusedElement = undefined;
});

it('can tab across FocusZones with different button structures', () => {
const component = ReactTestUtils.renderIntoDocument(
<div { ...{ onFocusCapture: _onFocus } }>
<FocusTrapZone forceFocusInsideTrap={ false }>
<FocusZone direction={ FocusZoneDirection.horizontal } data-is-visible={ true }>
<div data-is-visible={ true }>
<button className='a'>a</button>
</div>
<div data-is-visible={ true }>
<button className='b'>b</button>
</div>
<div data-is-visible={ true }>
<button className='c'>c</button>
</div>
</FocusZone>
<FocusZone direction={ FocusZoneDirection.horizontal } data-is-visible={ true }>
<div data-is-visible={ true }>
<div data-is-visible={ true }>
<button className='d'>d</button>
<button className='e'>e</button>
<button className='f'>f</button>
</div>
</div>
</FocusZone>
</FocusTrapZone>
</div>
);

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

const buttonA = focusTrapZone.querySelector('.a') as HTMLElement;
const buttonB = focusTrapZone.querySelector('.b') as HTMLElement;
const buttonC = focusTrapZone.querySelector('.c') as HTMLElement;
const buttonD = focusTrapZone.querySelector('.d') as HTMLElement;
const buttonE = focusTrapZone.querySelector('.e') as HTMLElement;
const buttonF = focusTrapZone.querySelector('.f') as HTMLElement;

// Assign bounding locations to buttons.
setupElement(buttonA, {
clientRect: {
top: 0,
bottom: 30,
left: 0,
right: 30
}
});

setupElement(buttonB, {
clientRect: {
top: 0,
bottom: 30,
left: 30,
right: 60
}
});

setupElement(buttonC, {
clientRect: {
top: 0,
bottom: 30,
left: 60,
right: 90
}
});

setupElement(buttonD, {
clientRect: {
top: 30,
bottom: 60,
left: 0,
right: 30
}
});

setupElement(buttonE, {
clientRect: {
top: 30,
bottom: 60,
left: 30,
right: 60
}
});

setupElement(buttonF, {
clientRect: {
top: 30,
bottom: 60,
left: 60,
right: 90
}
});

// Focus the first button.
ReactTestUtils.Simulate.focus(buttonA);
expect(lastFocusedElement).toBe(buttonA);

// Pressing shift + tab should go to d.
ReactTestUtils.Simulate.keyDown(buttonA, { which: KeyCodes.tab, shiftKey: true });
expect(lastFocusedElement).toBe(buttonD);

// Pressing tab should go to a.
ReactTestUtils.Simulate.keyDown(buttonD, { which: KeyCodes.tab });
expect(lastFocusedElement).toBe(buttonA);
});
});
21 changes: 19 additions & 2 deletions packages/utilities/src/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,25 @@ export function getPreviousElement(
allowFocusRoot,
tabbable);

if (childMatch && ((tabbable && (isElementTabbable(childMatch, true))) || !tabbable)) {
return childMatch;
if (childMatch) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible to write a unit test for this scenario?

Also, is there logic missing on "findNextElement"?

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.

getNextElement doesn't need to be updated, the logic is slightly different and doesn't check for tabindex when checking if an element is tabbable. There may be some other bug hidden in here from that difference, but it doesn't need this update

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.

and yes, I'll look at adding a unit test

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

cool ping me once you have a test to cover it (so we don't break it later!) and let's get it merged.

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.

@dzearing done, ut added. I also verified that this breaks without my fix and works with it :)

if ((tabbable && (isElementTabbable(childMatch, true))) || !tabbable) {
return childMatch;
} else {
// Check previous sibling of the child match.
const childMatchSiblingMatch = getPreviousElement(
rootElement,
childMatch.previousElementSibling as HTMLElement,
true,
true,
true,
includeElementsInFocusZones,
allowFocusRoot,
tabbable
);
if (childMatchSiblingMatch) {
return childMatchSiblingMatch;
}
}
}
}

Expand Down