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

Commit de44332

Browse files
authored
feat(FocusZone): Enable RTL (#646)
* Pass isRtl in renderComponent * Fixes to focus zone when bidirectional and RTL * update changelog * Fix * Update changelog
1 parent efa3e89 commit de44332

File tree

5 files changed

+152
-4
lines changed

5 files changed

+152
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3030
- Export initial set of compose icons in Teams theme @joheredi ([#638](https://github.com/stardust-ui/react/pull/638))
3131
- Add and export compose icons in Teams theme @joheredi ([#639](https://github.com/stardust-ui/react/pull/639))
3232
- Add `menu` prop to `MenuItem` @mnajdova ([#539](https://github.com/stardust-ui/react/pull/539))
33+
- Enable RTL for `FocusZone` @sophieH29 ([#646](https://github.com/stardust-ui/react/pull/646))
3334

3435
<!--------------------------------[ v0.15.0 ]------------------------------- -->
3536
## [v0.15.0](https://github.com/stardust-ui/react/tree/v0.15.0) (2018-12-17)

src/lib/accessibility/FocusZone/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This is a list of changes made to this Stardust copy of FocusZone in comparison
1515
- Renamed `defaultActiveElement` to `defaultTabbableElement` and changed behavior:
1616
- Changed to query only descendants of the focus zone instead of the whole document, which enables to write simpler selectors. Note that we do not lose any functionality by this, because selecting elements outside of focus zone had no effect.
1717
- Changed not to call `this.focus()` on component mount (this was causing issues e.g., in docsite, where every change in source code would refocus the mounted component). Instead, you can now use a new property `shouldFocusOnMount`.
18+
- Enable RTL @sophieH29 ([#646](https://github.com/stardust-ui/react/pull/646))
1819

1920
- Add `shouldFocusFirstElementWhenReceivedFocus` prop, which forces focus to first element when container receives focus @sophieH29 ([#469](https://github.com/stardust-ui/react/pull/469))
2021
- Handle keyDownCapture based on `shouldHandleKeyDownCapture` prop @sophieH29 ([#563](https://github.com/stardust-ui/react/pull/563))

src/lib/accessibility/FocusZone/FocusZone.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -747,9 +747,20 @@ export class FocusZone extends React.Component<FocusZoneProps> implements IFocus
747747
this.props.isRtl,
748748
(activeRect: ClientRect, targetRect: ClientRect) => {
749749
let distance = -1
750+
let topBottomComparison
751+
752+
if (this.props.isRtl) {
753+
// When in RTL, this comparison should be the same as the one in moveFocusRight for LTR.
754+
// Going left at a leftmost rectangle will go down a line instead of up a line like in LTR.
755+
// This is important, because we want to be comparing the top of the target rect
756+
// with the bottom of the active rect.
757+
topBottomComparison = targetRect.top.toFixed(3) < activeRect.bottom.toFixed(3)
758+
} else {
759+
topBottomComparison = targetRect.bottom.toFixed(3) > activeRect.top.toFixed(3)
760+
}
750761

751762
if (
752-
targetRect.bottom > activeRect.top &&
763+
topBottomComparison &&
753764
targetRect.right <= activeRect.right &&
754765
this.props.direction !== FocusZoneDirection.vertical
755766
) {
@@ -775,9 +786,20 @@ export class FocusZone extends React.Component<FocusZoneProps> implements IFocus
775786
!this.props.isRtl,
776787
(activeRect: ClientRect, targetRect: ClientRect) => {
777788
let distance = -1
789+
let topBottomComparison
790+
791+
if (this.props.isRtl) {
792+
// When in RTL, this comparison should be the same as the one in moveFocusLeft for LTR.
793+
// Going right at a rightmost rectangle will go up a line instead of down a line like in LTR.
794+
// This is important, because we want to be comparing the bottom of the target rect
795+
// with the top of the active rect.
796+
topBottomComparison = targetRect.bottom.toFixed(3) > activeRect.top.toFixed(3)
797+
} else {
798+
topBottomComparison = targetRect.top.toFixed(3) < activeRect.bottom.toFixed(3)
799+
}
778800

779801
if (
780-
targetRect.top < activeRect.bottom &&
802+
topBottomComparison &&
781803
targetRect.left >= activeRect.left &&
782804
this.props.direction !== FocusZoneDirection.vertical
783805
) {

src/lib/renderComponent.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
AccessibilityDefinition,
2525
AccessibilityActionHandlers,
2626
FocusZoneMode,
27+
FocusZoneDefinition,
2728
} from './accessibility/types'
2829
import { defaultBehavior } from './accessibility'
2930
import getKeyDownHandlers from './getKeyDownHandlers'
@@ -100,11 +101,19 @@ function wrapInGenericFocusZone<
100101
)
101102
}
102103

103-
const renderWithFocusZone = (render, focusZoneDefinition, config, focusZoneRef): any => {
104+
const renderWithFocusZone = <P extends {}>(
105+
render: RenderComponentCallback<P>,
106+
focusZoneDefinition: FocusZoneDefinition,
107+
config: RenderResultConfig<P>,
108+
focusZoneRef: (focusZone: FocusZone) => void,
109+
): any => {
104110
if (focusZoneDefinition.mode === FocusZoneMode.Wrap) {
105111
return wrapInGenericFocusZone(
106112
FabricFocusZone,
107-
focusZoneDefinition.props,
113+
{
114+
...focusZoneDefinition.props,
115+
isRtl: config.rtl,
116+
},
108117
render(config),
109118
focusZoneRef,
110119
)
@@ -115,6 +124,7 @@ const renderWithFocusZone = (render, focusZoneDefinition, config, focusZoneRef):
115124
config.rest = { ...config.rest, ...focusZoneDefinition.props }
116125
config.rest.as = originalElementType
117126
config.rest.ref = focusZoneRef
127+
config.rest.isRtl = config.rtl
118128
}
119129
return render(config)
120130
}

test/specs/lib/FocusZone-test.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,4 +1173,118 @@ describe('FocusZone', () => {
11731173
// Focus goes to FocusZone container, which forces focus to first focusable element - buttonB
11741174
expect(lastFocusedElement).toBe(buttonB)
11751175
})
1176+
1177+
it('can use arrows bidirectionally in RTL', () => {
1178+
const component = ReactTestUtils.renderIntoDocument(
1179+
<div {...{ onFocusCapture: onFocus }}>
1180+
<FocusZone isRtl={true}>
1181+
<button className="a">a</button>
1182+
<button className="b">b</button>
1183+
<button className="c">c</button>
1184+
<button className="hidden">hidden</button>
1185+
<button className="d">d</button>
1186+
<button className="e">e</button>
1187+
</FocusZone>
1188+
</div>,
1189+
)
1190+
1191+
const focusZone = ReactDOM.findDOMNode(component as React.ReactInstance)!.firstChild as Element
1192+
const buttonA = focusZone.querySelector('.a') as HTMLElement
1193+
const buttonB = focusZone.querySelector('.b') as HTMLElement
1194+
const buttonC = focusZone.querySelector('.c') as HTMLElement
1195+
const hiddenButton = focusZone.querySelector('.hidden') as HTMLElement
1196+
const buttonD = focusZone.querySelector('.d') as HTMLElement
1197+
const buttonE = focusZone.querySelector('.e') as HTMLElement
1198+
1199+
// Set up a grid like so:
1200+
// B A
1201+
// hiddenButton C
1202+
// E D
1203+
//
1204+
// We will iterate from A to B, press down to skip hidden and go to C,
1205+
// down again to E, right to D, up to C, then back up to A.
1206+
setupElement(buttonA, {
1207+
clientRect: {
1208+
top: 0,
1209+
bottom: 20,
1210+
left: 30,
1211+
right: 0,
1212+
},
1213+
})
1214+
1215+
setupElement(buttonB, {
1216+
clientRect: {
1217+
top: 0,
1218+
bottom: 20,
1219+
left: 60,
1220+
right: 30,
1221+
},
1222+
})
1223+
1224+
setupElement(buttonC, {
1225+
clientRect: {
1226+
top: 20,
1227+
bottom: 40,
1228+
left: 20,
1229+
right: 0,
1230+
},
1231+
})
1232+
1233+
// hidden button should be ignored.
1234+
setupElement(hiddenButton, {
1235+
clientRect: {
1236+
top: 20,
1237+
bottom: 40,
1238+
left: 30,
1239+
right: 20,
1240+
},
1241+
isVisible: false,
1242+
})
1243+
1244+
setupElement(buttonD, {
1245+
clientRect: {
1246+
top: 40,
1247+
bottom: 60,
1248+
left: 25,
1249+
right: 0,
1250+
},
1251+
})
1252+
1253+
setupElement(buttonE, {
1254+
clientRect: {
1255+
top: 40,
1256+
bottom: 60,
1257+
left: 40,
1258+
right: 25,
1259+
},
1260+
})
1261+
1262+
// Focus the first button.
1263+
ReactTestUtils.Simulate.focus(buttonA)
1264+
expect(lastFocusedElement).toBe(buttonA)
1265+
1266+
// Pressing left should go to b.
1267+
ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowLeft })
1268+
expect(lastFocusedElement).toBe(buttonB)
1269+
1270+
// Pressing down should go to c.
1271+
ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown })
1272+
expect(lastFocusedElement).toBe(buttonC)
1273+
1274+
// Pressing down should go to e.
1275+
ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowDown })
1276+
expect(lastFocusedElement).toBe(buttonE)
1277+
1278+
// Pressing right should go to d.
1279+
ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowRight })
1280+
expect(lastFocusedElement).toBe(buttonD)
1281+
1282+
// Pressing up should go to c.
1283+
ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp })
1284+
expect(lastFocusedElement).toBe(buttonC)
1285+
1286+
// Pressing up should go to a.
1287+
ReactTestUtils.Simulate.keyDown(focusZone, { which: keyboardKey.ArrowUp })
1288+
expect(lastFocusedElement).toBe(buttonA)
1289+
})
11761290
})

0 commit comments

Comments
 (0)