Skip to content

Commit d14b8f2

Browse files
committed
Fix simpleDiff and tweak spatial navigation algorithms
1 parent f7aa91c commit d14b8f2

File tree

6 files changed

+164
-11
lines changed

6 files changed

+164
-11
lines changed

.changeset/mean-bananas-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plexinc/react-lightning": patch
3+
---
4+
5+
Fix simpleDiff to not error on nullish objects

.changeset/stupid-ideas-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plexinc/react-lightning": patch
3+
---
4+
5+
Tweak focus logic to match W3C spatial navigation spec

packages/react-lightning/src/utils/findClosestElement.spec.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it, suite } from 'vitest';
22
import { Direction } from '../focus/Direction';
33
import type { LightningElement, Rect } from '../types';
4-
import { findClosestElement } from './findClosestElement';
4+
import { findClosestElement, getOverlap } from './findClosestElement';
55

66
// First element is the source, second is the direction, third is the
77
// expected closest element or null if there is no closest element.
@@ -279,4 +279,60 @@ suite('findClosestElement', () => {
279279

280280
runTestsOnElements(elements, tests);
281281
});
282+
283+
describe('should return the correct element for a wide element in the middle', () => {
284+
/**
285+
* ┌─────────────────┐
286+
* │ ╔══════╗ │
287+
* │ ║ 1 ║ │
288+
* │ ╚══════╝ │
289+
* │ ╔═══════════╗ │
290+
* │ ║ 2 ║ │
291+
* │ ╚═══════════╝ │
292+
* │ ╔═══╗ │
293+
* │ ║ 3 ║ │
294+
* │ ╚═══╝ │
295+
* └─────────────────┘
296+
*/
297+
const elements = createLayout(500, 500, [
298+
{ height: 48, width: 243, x: 160, y: 24 },
299+
{ height: 96, width: 820, x: 160, y: 88 },
300+
{ height: 64, width: 544, x: 160, y: 264 },
301+
]);
302+
const tests: TestCases = [
303+
[1, Direction.Up, null],
304+
[1, Direction.Right, null],
305+
[1, Direction.Down, 2],
306+
[1, Direction.Left, null],
307+
[2, Direction.Up, 1],
308+
[2, Direction.Right, null],
309+
[2, Direction.Down, 3],
310+
[2, Direction.Left, null],
311+
[3, Direction.Up, 2],
312+
[3, Direction.Right, null],
313+
[3, Direction.Down, null],
314+
[3, Direction.Left, null],
315+
];
316+
317+
runTestsOnElements(elements, tests);
318+
});
319+
});
320+
321+
suite('getOverlap', () => {
322+
it('should return the correct overlap for two elements', () => {
323+
const a = { x: 0, y: 0, width: 100, height: 100, centerX: 50, centerY: 50 };
324+
const b = {
325+
x: 75,
326+
y: 75,
327+
width: 100,
328+
height: 100,
329+
centerX: 50,
330+
centerY: 50,
331+
};
332+
333+
expect(getOverlap(Direction.Up, a, b)).toEqual(0);
334+
expect(getOverlap(Direction.Right, a, b)).toEqual(25);
335+
expect(getOverlap(Direction.Down, a, b)).toEqual(25);
336+
expect(getOverlap(Direction.Left, a, b)).toEqual(0);
337+
});
282338
});

packages/react-lightning/src/utils/findClosestElement.ts

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,82 @@ function getDistance(
9191
return x * x + y * y;
9292
}
9393

94+
/**
95+
* Calculate the shortest distance between two elements based on the W3C CSS
96+
* Spatial Navigation spec: https://drafts.csswg.org/css-nav-1/#heuristics
97+
*/
98+
function calculateShortestDistance(
99+
direction: Direction,
100+
source: Dimensions,
101+
target: Dimensions,
102+
): number | null {
103+
const euclidean = getDistance(direction, source, target);
104+
const displacement = getDisplacement(direction, source, target);
105+
const alignment = getAlignment(direction, source, target);
106+
107+
if (euclidean === null) {
108+
return null;
109+
}
110+
111+
return (
112+
euclidean +
113+
displacement -
114+
alignment -
115+
Math.sqrt(getOverlap(direction, source, target))
116+
);
117+
}
118+
119+
function getAlignment(
120+
direction: Direction,
121+
source: Dimensions,
122+
target: Dimensions,
123+
): number {
124+
const isHorizontal = direction & Direction.Horizontal;
125+
const bias =
126+
getOverlap(direction, source, target) /
127+
(isHorizontal ? source.width : source.height);
128+
129+
return bias * 5;
130+
}
131+
132+
function getDisplacement(
133+
direction: Direction,
134+
{ width: w1, height: h1, centerX: cx1, centerY: cy1 }: Dimensions,
135+
{ centerX: cx2, centerY: cy2 }: Dimensions,
136+
) {
137+
const isHorizontal = direction & Direction.Horizontal;
138+
const distance = isHorizontal ? cy2 - cy1 : cx2 - cx1;
139+
const bias = isHorizontal ? h1 / 2 : w1 / 2;
140+
const weight = isHorizontal ? 30 : 2;
141+
142+
return (distance + bias) * weight;
143+
}
144+
145+
export function getOverlap(
146+
direction: Direction,
147+
{ x: x1, y: y1, width: w1, height: h1 }: Dimensions,
148+
{ x: x2, y: y2, width: w2, height: h2 }: Dimensions,
149+
): number {
150+
let length = 0;
151+
152+
switch (direction) {
153+
case Direction.Up:
154+
length = y1 > y2 && y1 < y2 + h2 ? y2 + h2 - y1 : 0;
155+
break;
156+
case Direction.Right:
157+
length = x1 + w1 > x2 && x1 + w1 < x2 + w2 ? x1 + w1 - x2 : 0;
158+
break;
159+
case Direction.Down:
160+
length = y1 + w1 > y2 && y1 + w1 < y2 + h2 ? y1 + h1 - y2 : 0;
161+
break;
162+
case Direction.Left:
163+
length = x1 > x2 && x1 < x2 + w2 ? x2 + w2 - x1 : 0;
164+
break;
165+
}
166+
167+
return length;
168+
}
169+
94170
function isOverlap(
95171
{ x: x1, y: y1, width: w1, height: h1 }: Dimensions,
96172
{ x: x2, y: y2, width: w2, height: h2 }: Dimensions,
@@ -132,7 +208,7 @@ export function findClosestElement(
132208
otherDimensions,
133209
);
134210

135-
const distance = getDistance(
211+
const distance = calculateShortestDistance(
136212
direction,
137213
sourceDimensions,
138214
otherDimensions,
@@ -154,14 +230,6 @@ export function findClosestElement(
154230
}
155231
}
156232

157-
if (closest.length === 0) {
158-
return null;
159-
}
160-
161-
if (closest.length === 1) {
162-
return closest[0] as LightningElement;
163-
}
164-
165233
// If we have multiple elements with the same closeness, then try to pick the
166234
// next element in the render tree. This may not be the same order as the
167235
// `elementsToCheck` array.

packages/react-lightning/src/utils/simpleDiff.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,23 @@ describe('simpleDiff', () => {
5252
const result = simpleDiff(obj1, obj2);
5353
expect(result).toEqual({ children: [obj2] });
5454
});
55+
56+
it('should handle null and undefined values', () => {
57+
type User = {
58+
name: string;
59+
dob?: { year: number; month: number; day: number } | null;
60+
};
61+
const obj1: User = { name: 'John', dob: null };
62+
const obj2: User = { name: 'John', dob: { year: 1920, month: 2, day: 2 } };
63+
64+
const result = simpleDiff(obj1, obj2);
65+
66+
expect(result).toEqual({
67+
dob: {
68+
day: 2,
69+
month: 2,
70+
year: 1920,
71+
},
72+
});
73+
});
5574
});

packages/react-lightning/src/utils/simpleDiff.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function areEqualObjects<T extends object>(
4141
second: T,
4242
visited: Set<unknown>,
4343
): boolean {
44-
if ((first == null && second == null) || typeof first !== 'object') {
44+
if (first == null || second == null || typeof first !== 'object') {
4545
return first === second;
4646
}
4747

0 commit comments

Comments
 (0)