import { FocusZoneDirection, FocusZoneTabbableElements, IS_FOCUSABLE_ATTRIBUTE } from '@fluentui/accessibility'; import { FocusZone } from '@fluentui/react-bindings'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as ReactTestUtils from 'react-dom/test-utils'; import { getCode, keyboardKey } from '@fluentui/keyboard-key'; describe('FocusZone', () => { let lastFocusedElement: HTMLElement | undefined; let host: HTMLElement; function onFocus(ev: any): void { lastFocusedElement = ev.target; } function setupElement( element: HTMLElement, { clientRect, isVisible = true, }: { clientRect: { top: number; left: number; bottom: number; right: number; }; isVisible?: boolean; }, ): void { // @ts-ignore 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; }); afterEach(() => { if (host) { ReactDOM.unmountComponentAtNode(host); (host as any) = undefined; } }); it('can use arrows vertically', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const buttonB = focusZone.querySelector('#b') as HTMLElement; const buttonC = focusZone.querySelector('#c') as HTMLElement; // Assign bounding locations to buttons. setupElement(buttonA, { clientRect: { top: 0, bottom: 30, left: 0, right: 100, }, }); setupElement(buttonB, { clientRect: { top: 30, bottom: 60, left: 0, right: 100, }, }); setupElement(buttonC, { clientRect: { top: 60, bottom: 90, left: 0, right: 100, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); // Pressing down should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonB); // Pressing down should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonC); // Pressing down should stay on c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonC); // Pressing up should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonB); // Pressing up should go to a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonA); // Pressing up should stay on a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonA); // Click on c to focus it. ReactTestUtils.Simulate.focus(buttonC); expect(lastFocusedElement).toBe(buttonC); // Pressing up should move to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonB); // Test that pressing horizontal buttons don't move focus. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonB); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonB); // Press home should go to the first target. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.Home }); expect(lastFocusedElement).toBe(buttonA); // Press end should go to the last target. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.End }); expect(lastFocusedElement).toBe(buttonC); }); it('can ignore arrowing if default is prevented', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const buttonB = focusZone.querySelector('#b') as HTMLElement; // Assign bounding locations to buttons. setupElement(buttonA, { clientRect: { top: 0, bottom: 30, left: 0, right: 100, }, }); setupElement(buttonB, { clientRect: { top: 30, bottom: 60, left: 0, right: 100, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); // Pressing down should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown, isDefaultPrevented: () => true, } as any); expect(lastFocusedElement).toBe(buttonA); }); it('can use arrows horizontally', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const buttonB = focusZone.querySelector('#b') as HTMLElement; const buttonC = focusZone.querySelector('#c') as HTMLElement; // Assign bounding locations to buttons. setupElement(buttonA, { clientRect: { top: 0, bottom: 100, left: 0, right: 30, }, }); setupElement(buttonB, { clientRect: { top: 0, bottom: 100, left: 30, right: 60, }, }); setupElement(buttonC, { clientRect: { top: 0, bottom: 100, left: 60, right: 90, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); // Pressing right should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonB); // Pressing right should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); // Pressing right should stay on c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); // Pressing left should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonB); // Pressing left should go to a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonA); // Pressing left should stay on a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonA); // Click on c to focus it. ReactTestUtils.Simulate.focus(buttonC); expect(lastFocusedElement).toBe(buttonC); // Pressing left should move to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonB); // Test that pressing vertical buttons don't move focus. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonB); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonB); // Press home should go to the first target. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.Home }); expect(lastFocusedElement).toBe(buttonA); // // Press end should go to the last target. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.End }); expect(lastFocusedElement).toBe(buttonC); }); it('can use arrows bidirectionally', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!.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 hiddenButton = focusZone.querySelector('#hidden') as HTMLElement; const buttonD = focusZone.querySelector('#d') as HTMLElement; const buttonE = focusZone.querySelector('#e') as HTMLElement; // Set up a grid like so: // A B // C hiddenButton // D E // // We will iterate from A to B, press down to skip hidden and go to C, // down again to E, left to D, then back up to A. setupElement(buttonA, { clientRect: { top: 0, bottom: 20, left: 0, right: 30, }, }); setupElement(buttonB, { clientRect: { top: 0, bottom: 20, left: 20, right: 40, }, }); setupElement(buttonC, { clientRect: { top: 20, bottom: 40, left: 0, right: 20, }, }); // hidden button should be ignored. setupElement(hiddenButton, { clientRect: { top: 20, bottom: 40, left: 2, right: 40, }, isVisible: false, }); setupElement(buttonD, { clientRect: { top: 40, bottom: 60, left: 0, right: 20, }, }); setupElement(buttonE, { clientRect: { top: 40, bottom: 60, left: 20, right: 40, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); // Pressing right should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonB); // Pressing down should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonC); // Pressing left should go to d. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonD); // Pressing down should go to e. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonE); // Pressing up should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonC); // Pressing up should go to a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonA); }); it('can use arrows bidirectionally by following DOM order', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const buttonB = focusZone.querySelector('#b') as HTMLElement; const buttonC = focusZone.querySelector('#c') as HTMLElement; // Assign bounding locations to buttons. setupElement(buttonA, { clientRect: { top: 0, bottom: 30, left: 0, right: 100, }, }); setupElement(buttonB, { clientRect: { top: 30, bottom: 60, left: 0, right: 100, }, }); setupElement(buttonC, { clientRect: { top: 60, bottom: 90, left: 0, right: 100, }, }); // Pressing down/right arrow keys moves focus to the next focusable item. // Pressing up/left arrow keys moves focus to the previous focusable item. // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); // Pressing down should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonB); // Pressing right should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); // Pressing down should stay on c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonC); // Pressing up should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonB); // Pressing left should go to a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonA); // Pressing left should stay on a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonA); // Click on c to focus it. ReactTestUtils.Simulate.focus(buttonC); expect(lastFocusedElement).toBe(buttonC); // Pressing up should move to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonB); // Pressing left should move to a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonA); // Pressing right should move to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonB); // Press home should go to the first target. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.Home }); expect(lastFocusedElement).toBe(buttonA); // Press end should go to the last target. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.End }); expect(lastFocusedElement).toBe(buttonC); }); it('can reset alignment on mouse down', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!.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: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonA); // Pressing left should stay on a. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(buttonA); // Pressing right should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonB); // Pressing down should go to d. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonD); // Mousing down on a should reset alignment to a. ReactTestUtils.Simulate.mouseDown(focusZone, { target: buttonA }); // Pressing down should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonC); }); it('correctly skips data-not-focusable elements', () => { const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const buttonB = focusZone.querySelector('#b') as HTMLElement; const buttonC = focusZone.querySelector('#c') as HTMLElement; // Set up a grid like so: // A B // C hiddenButton // D E // // We will iterate from A to B, press down to skip hidden and go to C, // down again to E, left to D, then back up to A. 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, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); // Pressing right should go to b. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown }); expect(lastFocusedElement).toBe(buttonC); // Pressing down should go to c. ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp }); expect(lastFocusedElement).toBe(buttonA); }); it('skips subzone elements until manually entered', () => { const shouldEnterInnerZone = (e: React.KeyboardEvent): boolean => getCode(e) === keyboardKey.Enter; const isFocusableProperty = { [IS_FOCUSABLE_ATTRIBUTE]: true }; const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const divB = focusZone.querySelector('#b') as HTMLElement; const buttonC = focusZone.querySelector('#c') as HTMLElement; const buttonB = focusZone.querySelector('#bsub') as HTMLElement; setupElement(buttonA, { clientRect: { top: 0, bottom: 20, left: 0, right: 20, }, }); setupElement(divB, { clientRect: { top: 0, bottom: 20, left: 20, right: 40, }, }); setupElement(buttonB, { clientRect: { top: 5, bottom: 15, left: 25, right: 35, }, }); setupElement(buttonC, { clientRect: { top: 0, bottom: 20, left: 40, right: 60, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(divB); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(divB); ReactTestUtils.Simulate.keyDown(divB, { which: keyboardKey.Enter }); expect(lastFocusedElement).toBe(buttonB); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(divB); }); it('skips child focusZone elements until manually entered', () => { const shouldEnterInnerZone = (e: React.KeyboardEvent): boolean => getCode(e) === keyboardKey.Enter; const isFocusableProperty = { [IS_FOCUSABLE_ATTRIBUTE]: true }; const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
, ); const focusZone = ReactDOM.findDOMNode(component)!.firstChild as Element; const buttonA = focusZone.querySelector('#a') as HTMLElement; const divB = focusZone.querySelector('#b') as HTMLElement; const buttonC = focusZone.querySelector('#c') as HTMLElement; const buttonB = focusZone.querySelector('#bsub') as HTMLElement; setupElement(buttonA, { clientRect: { top: 0, bottom: 20, left: 0, right: 20, }, }); setupElement(divB, { clientRect: { top: 0, bottom: 20, left: 20, right: 40, }, }); setupElement(buttonB, { clientRect: { top: 5, bottom: 15, left: 25, right: 35, }, }); setupElement(buttonC, { clientRect: { top: 0, bottom: 20, left: 40, right: 60, }, }); // Focus the first button. ReactTestUtils.Simulate.focus(buttonA); expect(lastFocusedElement).toBe(buttonA); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(divB); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(divB); ReactTestUtils.Simulate.keyDown(divB, { which: keyboardKey.Enter }); expect(lastFocusedElement).toBe(buttonB); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight }); expect(lastFocusedElement).toBe(buttonC); ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft }); expect(lastFocusedElement).toBe(divB); }); it('Focus first tabbable element, when active element is dynamically disabled', () => { let focusZone: FocusZone | null = null; let buttonA: any; let buttonB: any; const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(