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 4 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 @@ -28,6 +28,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `headerAction` slot to the `Dialog` component @mnajdova ([#1617](https://github.com/stardust-ui/react/pull/1617))
- Add `Slider` component @Bugaa92 ([#1559](https://github.com/stardust-ui/react/pull/1559))
- Add `tooltipAsLabelBehavior` accessibility behavior for `Tooltip` @sophieH29 ([#1635](https://github.com/stardust-ui/react/pull/1635))
- 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 4 arrow keys.',
sophieH29 marked this conversation as resolved.
Show resolved Hide resolved
'Use `gridHorizontalBehavior` for horizontal keyboard navigation with 4 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
2 changes: 1 addition & 1 deletion packages/react/src/components/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface GridProps
ContentComponentProps<React.ReactNode | React.ReactNode[]> {
/**
* Accessibility behavior if overridden by the user.
* @available gridBehavior
* @available gridBehavior, gridHorizontalBehavior
* */
accessibility?: Accessibility

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 4 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 4 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 @@ -44,7 +44,7 @@ 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 @@ -173,4 +173,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