Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(FocusZone): add bidirectional navigation following DOM order #1647

Merged
merged 6 commits into from
Jul 19, 2019
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add outline version of `menu` icon and `files-visio` icon to Teams theme @notandrew ([#1623](https://github.com/stardust-ui/react/pull/1623))
- Add `amethyst` color to the Teams theme color palette @mnajdova ([#1650](https://github.com/stardust-ui/react/pull/1650))
- Add `image-unavailable` icon to Teams Theme @joheredi ([#1633](https://github.com/stardust-ui/react/pull/1633))
- Add bidirectional navigation following DOM in `FocusZone` @sophieH29 ([#1637](https://github.com/stardust-ui/react/pull/1647))

### Fixes
- Fix `ChatMessage`'s focus border overlays `actionMenu` in Teams theme @mnajdova ([#1637](https://github.com/stardust-ui/react/pull/1637))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import * as React from 'react'
import ComponentBestPractices from 'docs/src/components/ComponentBestPractices'

const doList = [
'Use Grid behavior for bidirectional keyboard navigation. Use appropriate ARIA role for the grid and actionable components inside of it.',
'Use `gridBehavior` for bidirectional keyboard navigation with arrow keys.',
'Use `gridHorizontalBehavior` for horizontal keyboard navigation with arrow keys.',
'Use appropriate ARIA role for the grid and actionable components inside of it when keyboard navigation provided.',
]

const dontList = ["Don't use grid component as a replacement for table."]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import * as React from 'react'
import { Grid, Image, Button, gridBehavior } from '@stardust-ui/react'
import {
Grid,
Image,
Button,
Text,
Label,
gridBehavior,
gridHorizontalBehavior,
} from '@stardust-ui/react'
import * as _ from 'lodash'

const imageNames = [
Expand Down Expand Up @@ -51,13 +59,25 @@ const renderImageButtons = () => {

const GridExample = () => (
<div>
Grid with images, which are not natively focusable elements. Set 'data-is-focusable=true' to
each item to make grid items focusable and navigable.
<Text size="medium">
Grid with images, which are not natively focusable elements. Set{' '}
<Label>data-is-focusable=true</Label> to each item to make grid items focusable and navigable.
Use <Label>gridBehavior</Label> to provide arrow key navigation in 4 directions.
</Text>
<Grid accessibility={gridBehavior} columns="7" content={renderImages()} />
<br />
Grid with images, wrapped with buttons, which are natively focusable elements. No need to add
'data-is-focusable'='true'.
<Text size="medium">
Grid with buttons images, which are natively focusable elements. <b>No need</b> to add{' '}
<Label>data-is-focusable=true</Label>
</Text>
<Grid accessibility={gridBehavior} columns="7" content={renderImageButtons()} />
<br />
<Text size="medium">
Grid with buttons images, which are natively focusable elements. Use{' '}
<Label>gridHorizontalBehavior</Label> to provide horizontal navigation within Grid with 4
arrow keys.
</Text>
<Grid accessibility={gridHorizontalBehavior} columns="7" content={renderImageButtons()} />
</div>
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import * as React from 'react'
import { Grid, Image, Button, gridBehavior } from '@stardust-ui/react'
import {
Grid,
Image,
Button,
Text,
Label,
gridBehavior,
gridHorizontalBehavior,
} from '@stardust-ui/react'
import * as _ from 'lodash'

const imageNames = [
Expand Down Expand Up @@ -56,17 +64,31 @@ const gridStyles = {

const GridExample = () => (
<div>
Grid with images, which are not natively focusable elements. Set 'data-is-focusable=true' to
each item to make grid items focusable and navigable.
<Text size="medium">
Grid with images, which are not natively focusable elements. Set{' '}
<Label>data-is-focusable=true</Label> to each item to make grid items focusable and navigable.
Use <Label>gridBehavior</Label> to provide arrow key navigation in 4 directions.
</Text>
<Grid accessibility={gridBehavior} styles={gridStyles} columns="7">
{renderImages()}
</Grid>
<br />
Grid with images, wrapped with button components, which are natively focusable elements. No need
to add 'data-is-focusable'='true'
<Text size="medium">
Grid with buttons images, which are natively focusable elements. <b>No need</b> to add{' '}
<Label>data-is-focusable=true</Label>
</Text>
<Grid accessibility={gridBehavior} styles={gridStyles} columns="7">
{renderImageButtons()}
</Grid>
<br />
<Text size="medium">
Grid with buttons images, which are natively focusable elements. Use{' '}
<Label>gridHorizontalBehavior</Label> to provide horizontal navigation within Grid with 4
arrow keys.
</Text>
<Grid accessibility={gridHorizontalBehavior} styles={gridStyles} columns="7">
{renderImageButtons()}
</Grid>
</div>
)

Expand Down
2 changes: 1 addition & 1 deletion docs/src/examples/components/Grid/Variations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Variations = () => (
/>
<ComponentExample
title="Navigable with keyboard arrow buttons"
description="Use a Grid accessibility behavior, so Grid items can be keyboard navigable by adding 'data-is-focusable=true' attribute to each item. This attribute can be skipped if the Grid items are natively focusable elements, like buttons, anchors etc."
description="Choose between Grid's accessibility behaviors to provide needed keyboard navigation. Add 'data-is-focusable=true' attribute to grid items which aren't natively focusable but should be keyboard navigable."
examplePath="components/Grid/Variations/GridExampleKeyboardNavigable"
/>
</ExampleSection>
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/components/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { Accessibility } from '../../lib/accessibility/types'
export interface GridProps extends UIComponentProps, ChildrenComponentProps, ContentComponentProps {
/**
* Accessibility behavior if overridden by the user.
* @available gridBehavior
*/
* @available gridBehavior, gridHorizontalBehavior
* */
accessibility?: Accessibility

/** The columns of the grid with a space-separated list of values. The values represent the track size, and the space between them represents the grid line. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Accessibility, FocusZoneMode } from '../../types'
import { FocusZoneDirection } from '../../FocusZone'

/**
* @description
* Provides navigation between focusable children of Grid component with arrow keys in 4 directions.
*
* @specification
* Embeds component into FocusZone.
* Provides arrow key navigation in bidirectional direction.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Accessibility, FocusZoneMode } from '../../types'
import { FocusZoneDirection } from '../../FocusZone'

/**
* @description
* Provides navigation between focusable children of Grid component with arrow keys in horizontal direction (based on DOM order).
* Right/Down arrow keys move to next item, Up/Left arrow keys to previous item. Right and Left arrow keys are switched in RTL mode.
*
* @specification
* Embeds component into FocusZone.
* Provides arrow key navigation in bidirectionalDomOrder direction.
*/
const gridHorizontalBehavior: Accessibility = () => ({
attributes: {},
focusZone: {
mode: FocusZoneMode.Embed,
props: {
direction: FocusZoneDirection.bidirectionalDomOrder,
},
},
})

export default gridHorizontalBehavior
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import tabBehavior from './tabBehavior'
* @specification
* Adds role 'tablist' to 'root' slot.
* Embeds component into FocusZone.
* Provides arrow key navigation in bidirectional direction.
* Provides arrow key navigation in bidirectionalDomOrder direction.
* When component's container element receives focus, focus will be set to the default focusable child element of the component.
*/
const tabListBehavior: Accessibility = () => ({
Expand All @@ -22,7 +22,7 @@ const tabListBehavior: Accessibility = () => ({
mode: FocusZoneMode.Embed,
props: {
shouldFocusInnerElementWhenReceivedFocus: true,
direction: FocusZoneDirection.bidirectional,
direction: FocusZoneDirection.bidirectionalDomOrder,
},
},
childBehaviors: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import menuItemAsToolbarButtonBehavior from './menuItemAsToolbarButtonBehavior'
* @specification
* Adds role 'toolbar' to 'root' slot.
* Embeds component into FocusZone.
* Provides arrow key navigation in bidirectional direction.
* Provides arrow key navigation in bidirectionalDomOrder direction.
* When component's container element receives focus, focus will be set to the default focusable child element of the component.
*/
const menuAsToolbarBehavior: Accessibility = () => ({
Expand All @@ -22,7 +22,7 @@ const menuAsToolbarBehavior: Accessibility = () => ({
mode: FocusZoneMode.Embed,
props: {
shouldFocusInnerElementWhenReceivedFocus: true,
direction: FocusZoneDirection.bidirectional,
direction: FocusZoneDirection.bidirectionalDomOrder,
},
},
childBehaviors: {
Expand Down
8 changes: 5 additions & 3 deletions packages/react/src/lib/accessibility/FocusZone/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a list of changes made to this Stardust copy of FocusZone in comparison with the original [Fabric FocusZone @ 0f567e05952c6b50c691df2fb72d100b5e525d9e](https://github.com/OfficeDev/office-ui-fabric-react/blob/0f567e05952c6b50c691df2fb72d100b5e525d9e/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx).

### fixes
### Fixes
- With `defaultTabbableElement` prop set tab indexes are not updated accordingly ([#342](https://github.com/stardust-ui/react/pull/342))
- Remove unused prop `componentRef` ([#397](https://github.com/stardust-ui/react/pull/397))
- Fix `defaultTabbableElement` prop to be as a function ([#450](https://github.com/stardust-ui/react/pull/450))
Expand All @@ -22,9 +22,11 @@ This is a list of changes made to this Stardust copy of FocusZone in comparison
- Enable RTL @sophieH29 ([#646](https://github.com/stardust-ui/react/pull/646))

- Add `shouldFocusFirstElementWhenReceivedFocus` prop, which forces focus to first element when container receives focus @sophieH29 ([#469](https://github.com/stardust-ui/react/pull/469))
- Handle keyDownCapture based on `shouldHandleKeyDownCapture` prop @sophieH29 ([#563](https://github.com/stardust-ui/react/pull/563))
- Handle keyDownCapture based on `shouldHandleKeyDownCapture` prop @sophieH29 ([#563](https://github.com/stardust-ui/react/pull/563))
- Add `bidirectionalDomOrder` direction allowing arrow keys navigation following DOM order @sophieH29 ([#1637](https://github.com/stardust-ui/react/pull/1647))


### feat(FocusZone): Implement FocusZone into renderComponent [#116](https://github.com/stardust-ui/react/pull/116)
#### feat(FocusZone): Implement FocusZone into renderComponent [#116](https://github.com/stardust-ui/react/pull/116)
- Prettier and linting fixes, e.g., removing semicolons, removing underscores from private methods.
- Moved `IS_FOCUSABLE_ATTRIBUTE` and others to `focusUtilities.ts`.
- Added prop types, default props, and handled props.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export interface FocusZoneProps extends React.HTMLAttributes<HTMLElement | Focus

/**
* Defines which arrows to react to.
* It has next options: horizontal, vertical, bidirectional.
* It has next options: horizontal, vertical, bidirectional, bidirectionalDomOrder.
* @default FocusZoneDirection.bidirectional
*/
direction?: FocusZoneDirection

Expand Down Expand Up @@ -169,4 +170,7 @@ export enum FocusZoneDirection {

/** React to all arrows. */
bidirectional = 2,

/** React to all arrows. Navigate next item on right/down arrow keys and previous - left/up arrow keys. Vice versa in RTL mode. */
bidirectionalDomOrder = 3,
}
1 change: 1 addition & 0 deletions packages/react/src/lib/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export { default as popupBehavior } from './Behaviors/Popup/popupBehavior'
export { default as chatBehavior } from './Behaviors/Chat/chatBehavior'
export { default as chatMessageBehavior } from './Behaviors/Chat/chatMessageBehavior'
export { default as gridBehavior } from './Behaviors/Grid/gridBehavior'
export { default as gridHorizontalBehavior } from './Behaviors/Grid/gridHorizontalBehavior'
export { default as treeBehavior } from './Behaviors/Tree/treeBehavior'
export { default as treeItemBehavior } from './Behaviors/Tree/treeItemBehavior'
export { default as treeTitleBehavior } from './Behaviors/Tree/treeTitleBehavior'
Expand Down
2 changes: 2 additions & 0 deletions packages/react/test/specs/behaviors/behavior-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
treeItemBehavior,
subtreeBehavior,
gridBehavior,
gridHorizontalBehavior,
statusBehavior,
alertWarningBehavior,
accordionBehavior,
Expand Down Expand Up @@ -88,6 +89,7 @@ testHelper.addBehavior('treeBehavior', treeBehavior)
testHelper.addBehavior('treeItemBehavior', treeItemBehavior)
testHelper.addBehavior('subtreeBehavior', subtreeBehavior)
testHelper.addBehavior('gridBehavior', gridBehavior)
testHelper.addBehavior('gridHorizontalBehavior', gridHorizontalBehavior)
testHelper.addBehavior('dialogBehavior', dialogBehavior)
testHelper.addBehavior('statusBehavior', statusBehavior)
testHelper.addBehavior('alertWarningBehavior', alertWarningBehavior)
Expand Down
8 changes: 8 additions & 0 deletions packages/react/test/specs/behaviors/testDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ definitions.push({
},
})

definitions.push({
regexp: /arrow key navigation in bidirectionalDomOrder direction/g,
testMethod: (parameters: TestMethod) => {
const actualFocusZoneHorizontal = parameters.behavior({}).focusZone
expect(actualFocusZoneHorizontal.props.direction).toBe(FocusZoneDirection.bidirectionalDomOrder)
},
})

definitions.push({
regexp: /Keyboard navigation is circular/g,
testMethod: (parameters: TestMethod) => {
Expand Down
101 changes: 101 additions & 0 deletions packages/react/test/specs/lib/FocusZone-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,107 @@ describe('FocusZone', () => {
expect(lastFocusedElement).toBe(buttonA)
})

it('can use arrows bidirectionally by following DOM order', () => {
const component = ReactTestUtils.renderIntoDocument<{}, React.Component>(
<div {...{ onFocusCapture: onFocus }}>
<FocusZone direction={FocusZoneDirection.bidirectionalDomOrder}>
<button id="a">a</button>
<button id="b">b</button>
<button id="c">c</button>
</FocusZone>
</div>,
)

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>(
<div {...{ onFocusCapture: onFocus }}>
Expand Down