Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RTL #96

Merged
merged 2 commits into from
Nov 15, 2023
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
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"no-nested-ternary": "off"
}
}
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ Also consider `role` and `aria-label` attributes. But that depends on the applic

The flag is ignored if `nativeMode` is set.

##### `rtl`: boolean (default: false)
This flag changes focus behavior for layouts in right-to-left (RTL) languages such as Arabic and Hebrew.

### `setKeyMap`
Method to set custom key codes (numbers) or key event names (strings) [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#non-printable_keys_function_keys). I.e. when the device key codes differ from a standard browser arrow key codes.
```jsx
Expand Down Expand Up @@ -443,6 +446,8 @@ Used to provide the `focusKey` of the current Focusable Container down the Tree
interface FocusableComponentLayout {
left: number; // absolute coordinate on the screen
top: number; // absolute coordinate on the screen
readonly right: number; // absolute coordinate on the screen
readonly bottom: number; // absolute coordinate on the screen
width: number;
height: number;
x: number; // relative to the parent DOM element
Expand Down
108 changes: 66 additions & 42 deletions src/SpatialNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import forOwn from 'lodash/forOwn';
import sortBy from 'lodash/sortBy';
import throttle from 'lodash/throttle';
import VisualDebugger from './VisualDebugger';
import WritingDirection from './WritingDirection';
import measureLayout, { getBoundingClientRect } from './measureLayout';

const DIRECTION_LEFT = 'left';
Expand Down Expand Up @@ -54,6 +55,8 @@ const THROTTLE_OPTIONS = {
export interface FocusableComponentLayout {
left: number;
top: number;
readonly right: number;
readonly bottom: number;
width: number;
height: number;
x: number;
Expand Down Expand Up @@ -136,10 +139,14 @@ export type BackwardsCompatibleKeyMap = {

export type KeyMap = { [index: string]: (string | number)[] };

const getChildClosestToOrigin = (children: FocusableComponent[]) => {
const getChildClosestToOrigin = (children: FocusableComponent[], writingDirection: WritingDirection) => {
const comparator = writingDirection === WritingDirection.LTR
? (({ layout }: FocusableComponent) => Math.abs(layout.left) + Math.abs(layout.top))
: (({ layout }: FocusableComponent) => Math.abs(window.innerWidth - layout.right) + Math.abs(layout.top));

const childrenClosestToOrigin = sortBy(
children,
({ layout }) => Math.abs(layout.left) + Math.abs(layout.top)
comparator,
);

return first(childrenClosestToOrigin);
Expand Down Expand Up @@ -227,31 +234,37 @@ class SpatialNavigationService {

private setFocusDebounced: DebouncedFunc<any>;

private writingDirection: WritingDirection;

/**
* Used to determine the coordinate that will be used to filter items that are over the "edge"
*/
static getCutoffCoordinate(
isVertical: boolean,
isIncremental: boolean,
isSibling: boolean,
layout: FocusableComponentLayout
layout: FocusableComponentLayout,
writingDirection: WritingDirection,
) {
const itemX = layout.left;
const itemY = layout.top;
const itemWidth = layout.width;
const itemHeight = layout.height;
const itemStart = isVertical
? layout.top
: writingDirection === WritingDirection.LTR
? layout.left
: layout.right;

const itemEnd = isVertical
? layout.bottom
: writingDirection === WritingDirection.LTR
? layout.right
: layout.left;

const coordinate = isVertical ? itemY : itemX;
const itemSize = isVertical ? itemHeight : itemWidth;

// eslint-disable-next-line no-nested-ternary
return isIncremental
? isSibling
? coordinate
: coordinate + itemSize
? itemStart
: itemEnd
: isSibling
? coordinate + itemSize
: coordinate;
? itemEnd
: itemStart;
}

/**
Expand All @@ -263,11 +276,6 @@ class SpatialNavigationService {
isSibling: boolean,
layout: FocusableComponentLayout
) {
const itemX = layout.left;
const itemY = layout.top;
const itemWidth = layout.width;
const itemHeight = layout.height;

const result = {
a: {
x: 0,
Expand All @@ -281,64 +289,64 @@ class SpatialNavigationService {

switch (direction) {
case DIRECTION_UP: {
const y = isSibling ? itemY + itemHeight : itemY;
const y = isSibling ? layout.bottom : layout.top;

result.a = {
x: itemX,
x: layout.left,
y
};

result.b = {
x: itemX + itemWidth,
x: layout.right,
y
};

break;
}

case DIRECTION_DOWN: {
const y = isSibling ? itemY : itemY + itemHeight;
const y = isSibling ? layout.top : layout.bottom;

result.a = {
x: itemX,
x: layout.left,
y
};

result.b = {
x: itemX + itemWidth,
x: layout.right,
y
};

break;
}

case DIRECTION_LEFT: {
const x = isSibling ? itemX + itemWidth : itemX;
const x = isSibling ? layout.right : layout.left;

result.a = {
x,
y: itemY
y: layout.top
};

result.b = {
x,
y: itemY + itemHeight
y: layout.bottom
};

break;
}

case DIRECTION_RIGHT: {
const x = isSibling ? itemX : itemX + itemWidth;
const x = isSibling ? layout.left : layout.right;

result.a = {
x,
y: itemY
y: layout.top
};

result.b = {
x,
y: itemY + itemHeight
y: layout.bottom
};

break;
Expand Down Expand Up @@ -537,6 +545,7 @@ class SpatialNavigationService {
this.throttleKeypresses = false;
this.useGetBoundingClientRect = false;
this.shouldFocusDOMNode = false;
this.writingDirection = WritingDirection.LTR;

this.pressedKeys = {};

Expand Down Expand Up @@ -581,14 +590,16 @@ class SpatialNavigationService {
throttle: throttleParam = 0,
throttleKeypresses = false,
useGetBoundingClientRect = false,
shouldFocusDOMNode = false
shouldFocusDOMNode = false,
rtl = false
} = {}) {
if (!this.enabled) {
this.enabled = true;
this.nativeMode = nativeMode;
this.throttleKeypresses = throttleKeypresses;
this.useGetBoundingClientRect = useGetBoundingClientRect;
this.shouldFocusDOMNode = shouldFocusDOMNode && !nativeMode;
this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR;

this.debug = debug;

Expand All @@ -598,7 +609,7 @@ class SpatialNavigationService {
}
this.bindEventHandlers();
if (visualDebug) {
this.visualDebugger = new VisualDebugger();
this.visualDebugger = new VisualDebugger(this.writingDirection);
this.startDrawLayouts();
}
}
Expand Down Expand Up @@ -885,7 +896,7 @@ class SpatialNavigationService {
const isVerticalDirection =
direction === DIRECTION_DOWN || direction === DIRECTION_UP;
const isIncrementalDirection =
direction === DIRECTION_DOWN || direction === DIRECTION_RIGHT;
direction === DIRECTION_DOWN || (this.writingDirection === WritingDirection.LTR ? direction === DIRECTION_RIGHT : direction === DIRECTION_LEFT);

this.log('smartNavigate', 'direction', direction);
this.log('smartNavigate', 'fromParentFocusKey', fromParentFocusKey);
Expand Down Expand Up @@ -927,7 +938,8 @@ class SpatialNavigationService {
isVerticalDirection,
isIncrementalDirection,
false,
layout
layout,
this.writingDirection,
);

/**
Expand All @@ -944,12 +956,22 @@ class SpatialNavigationService {
isVerticalDirection,
isIncrementalDirection,
true,
component.layout
component.layout,
this.writingDirection,
);

return isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate
: siblingCutoffCoordinate <= currentCutoffCoordinate;
return isVerticalDirection
? isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate // vertical next
: siblingCutoffCoordinate <= currentCutoffCoordinate // vertical previous
: this.writingDirection === WritingDirection.LTR
? isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate // horizontal LTR next
: siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal LTR previous
: isIncrementalDirection
? siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal RTL next
: siblingCutoffCoordinate >= currentCutoffCoordinate // horizontal RTL previous
;
}

return false;
Expand Down Expand Up @@ -1065,7 +1087,7 @@ class SpatialNavigationService {
*/
const sortedForceFocusableComponents = this.sortSiblingsByPriority(
forceFocusableComponents,
{ x: 0, y: 0, width: 0, height: 0, left: 0, top: 0, node: null },
{ x: 0, y: 0, width: 0, height: 0, left: 0, top: 0, right: 0, bottom: 0, node: null },
'down',
ROOT_FOCUS_KEY
);
Expand Down Expand Up @@ -1145,7 +1167,7 @@ class SpatialNavigationService {
* Otherwise, trying to focus something by coordinates
*/
children.forEach((component) => this.updateLayout(component.focusKey));
const { focusKey: childKey } = getChildClosestToOrigin(children);
const { focusKey: childKey } = getChildClosestToOrigin(children, this.writingDirection);

this.log('getNextFocusKey', 'childKey will be focused', childKey);

Expand Down Expand Up @@ -1207,6 +1229,8 @@ class SpatialNavigationService {
height: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,

/**
* Node ref is also duplicated in layout to be reported in onFocus callback
Expand Down
30 changes: 21 additions & 9 deletions src/VisualDebugger.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import WritingDirection from "./WritingDirection";

// We'll make VisualDebugger no-op for any environments lacking a DOM (e.g. SSR and React Native non-web platforms).
const hasDOM = typeof window !== 'undefined' && window.document;

Expand All @@ -7,6 +9,8 @@ const HEIGHT = hasDOM ? window.innerHeight : 0;
interface NodeLayout {
left: number;
top: number;
readonly right: number;
readonly bottom: number;
width: number;
height: number;
}
Expand All @@ -16,18 +20,22 @@ class VisualDebugger {

private layoutsCtx: CanvasRenderingContext2D;

constructor() {
private writingDirection: WritingDirection;

constructor(writingDirection: WritingDirection) {
if (hasDOM) {
this.debugCtx = VisualDebugger.createCanvas('sn-debug', '1010');
this.layoutsCtx = VisualDebugger.createCanvas('sn-layouts', '1000');
this.debugCtx = VisualDebugger.createCanvas('sn-debug', '1010', writingDirection);
this.layoutsCtx = VisualDebugger.createCanvas('sn-layouts', '1000', writingDirection);
this.writingDirection = writingDirection;
}
}

static createCanvas(id: string, zIndex: string) {
static createCanvas(id: string, zIndex: string, writingDirection: WritingDirection) {
const canvas: HTMLCanvasElement =
document.querySelector(`#${id}`) || document.createElement('canvas');

canvas.setAttribute('id', id);
canvas.setAttribute('dir', writingDirection === WritingDirection.LTR ? "ltr" : "rtl");

const ctx = canvas.getContext('2d');

Expand Down Expand Up @@ -73,16 +81,20 @@ class VisualDebugger {
);
this.layoutsCtx.font = '8px monospace';
this.layoutsCtx.fillStyle = 'red';
this.layoutsCtx.fillText(focusKey, layout.left, layout.top + 10);
this.layoutsCtx.fillText(parentFocusKey, layout.left, layout.top + 25);

const horizontalStartDirection = this.writingDirection === WritingDirection.LTR ? "left" : "right";
const horizontalStartCoordinate = layout[horizontalStartDirection];

this.layoutsCtx.fillText(focusKey, horizontalStartCoordinate, layout.top + 10);
this.layoutsCtx.fillText(parentFocusKey, horizontalStartCoordinate, layout.top + 25);
this.layoutsCtx.fillText(
`left: ${layout.left}`,
layout.left,
`${horizontalStartDirection}: ${horizontalStartCoordinate}`,
horizontalStartCoordinate,
layout.top + 40
);
this.layoutsCtx.fillText(
`top: ${layout.top}`,
layout.left,
horizontalStartCoordinate,
layout.top + 55
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/WritingDirection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum WritingDirection {
LTR,
RTL
};

export default WritingDirection;
Loading