Skip to content

Commit

Permalink
Calculate container corners for validate candidates (#25)
Browse files Browse the repository at this point in the history
* filter out candiates parent containers for focusable condidates
Co-authored-by: Murray Coghill <Murreey@users.noreply.github.com>

* use a container corners to measure the shortest distance when finding a valid candidate & the position of the container

* add test case

* add check for not parent container back

* update jsdoc for signature update

* Move calculation down to where using
  • Loading branch information
abbigrundy committed Aug 14, 2023
1 parent eeed08a commit dfac028
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 25 deletions.
49 changes: 28 additions & 21 deletions lib/lrud.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,20 +218,27 @@ const getBlockedExitDirs = (container, candidateContainer) => {
*
* @param {Object} entryRect An object representing the rectangle of the item we're moving to
* @param {String} exitDir The direction we're moving in
* @param {Object} entryPoint The first point of the candidate
* @param {Object} exitPoint The first point of the item we're leaving
* @return {Booelan} true if candidate is in the correct dir, false if not
*/
const isValidCandidate = (entryRect, exitDir, entryPoint, exitPoint) => {
const isValidCandidate = (entryRect, exitDir, exitPoint) => {
if (entryRect.width === 0 && entryRect.height === 0) return false;

const corners = [
{ x: entryRect.left, y: entryRect.top },
{ x: entryRect.right, y: entryRect.top },
{ x: entryRect.right, y: entryRect.bottom },
{ x: entryRect.left, y: entryRect.bottom }
];

if (
entryRect.width === 0 && entryRect.height === 0 ||
exitDir === 'left' && isRight(entryPoint, exitPoint) ||
exitDir === 'right' && isRight(exitPoint, entryPoint) ||
exitDir === 'up' && isBelow(entryPoint, exitPoint) ||
exitDir === 'down' && isBelow(exitPoint, entryPoint)
) return false;

return true;
exitDir === 'left' && corners.some((corner) => isRight(exitPoint, corner)) ||
exitDir === 'right' && corners.some((corner) => isRight(corner, exitPoint)) ||
exitDir === 'up' && corners.some((corner) => isBelow(exitPoint, corner)) ||
exitDir === 'down' && corners.some((corner) => isBelow(corner, exitPoint))
) return true;

return false;
};

const getBestCandidate = (elem, candidates, exitDir) => {
Expand All @@ -244,10 +251,10 @@ const getBestCandidate = (elem, candidates, exitDir) => {
const candidate = candidates[i];
const entryRect = candidate.getBoundingClientRect();

const nearestPoint = getNearestPoint(exitPoint, exitDir, entryRect);
// Bail if the candidate is in the opposite direction or has no dimensions
if (!isValidCandidate(entryRect, exitDir, nearestPoint, exitPoint)) continue;
if (!isValidCandidate(entryRect, exitDir, exitPoint)) continue;

const nearestPoint = getNearestPoint(exitPoint, exitDir, entryRect);
const distance = getDistanceBetweenPoints(exitPoint, nearestPoint);

if (bestDistance > distance) {
Expand All @@ -272,20 +279,20 @@ export const getNextFocus = (elem, keyCode, scope) => {
const exitDir = _keyMap[keyCode];

// Get parent focus container
const container = getParentContainer(elem);
const parentContainer = getParentContainer(elem);

let bestCandidate;

// Get all siblings within a prioritised container
if (container?.getAttribute('data-lrud-prioritise-children') !== 'false' && scope.contains(container)) {
const focusableSiblings = getFocusables(container);
if (parentContainer?.getAttribute('data-lrud-prioritise-children') !== 'false' && scope.contains(parentContainer)) {
const focusableSiblings = getFocusables(parentContainer);
bestCandidate = getBestCandidate(elem, focusableSiblings, exitDir);
}

if (!bestCandidate) {
const focusableCandidates = [
...getFocusables(scope),
...toArray(scope.querySelectorAll(focusableContainerSelector)).filter(container => getFocusables(container)?.length > 0)
...toArray(scope.querySelectorAll(focusableContainerSelector)).filter(container => getFocusables(container)?.length > 0 && container !== parentContainer)
];

bestCandidate = getBestCandidate(elem, focusableCandidates, exitDir);
Expand All @@ -295,18 +302,18 @@ export const getNextFocus = (elem, keyCode, scope) => {
const isBestCandidateAContainer = matches(bestCandidate, containerSelector);
const candidateContainer = isBestCandidateAContainer ? bestCandidate : getParentContainer(bestCandidate);

const isCurrentContainer = candidateContainer === container;
const isNestedContainer = container?.contains(candidateContainer);
const isAnscestorContainer = candidateContainer?.contains(container);
const isCurrentContainer = candidateContainer === parentContainer;
const isNestedContainer = parentContainer?.contains(candidateContainer);
const isAnscestorContainer = candidateContainer?.contains(parentContainer);

if (!isCurrentContainer && (!isNestedContainer || isBestCandidateAContainer)) {
const blockedExitDirs = getBlockedExitDirs(container, candidateContainer);
const blockedExitDirs = getBlockedExitDirs(parentContainer, candidateContainer);
if (blockedExitDirs.indexOf(exitDir) > -1) return;

if (candidateContainer && !isAnscestorContainer) {
// Ignore active child behaviour when moving into a container that we
// are already nested in
if (elem.id) container?.setAttribute('data-focus', elem.id);
if (elem.id) parentContainer?.setAttribute('data-focus', elem.id);

const lastActiveChild = document.getElementById(candidateContainer.getAttribute('data-focus'));

Expand Down
15 changes: 11 additions & 4 deletions test/lrud.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,25 +472,32 @@ describe('LRUD spatial', () => {
});

describe('Page with small or close items', () => {
it('should not skip items with far away corners', async () => {
beforeEach(async () => {
await page.goto(`${testPath}/tiled-items.html`);
await page.evaluate(() => document.getElementById('item-48').focus());
await page.waitForFunction('document.activeElement');
});

it('should not skip items with far away corners', async () => {
await page.evaluate(() => document.getElementById('item-48').focus());
await page.keyboard.press('ArrowUp');
expect(await page.evaluate(() => document.activeElement.id)).toEqual('item-41');
await page.keyboard.press('ArrowDown');
expect(await page.evaluate(() => document.activeElement.id)).toEqual('item-48');
});

it('should go to the most central item when there are multiple below the exit edge', async () => {
await page.goto(`${testPath}/tiled-items.html`);
await page.waitForFunction('document.activeElement');
await page.keyboard.press('ArrowRight');
await page.keyboard.press('ArrowRight');
expect(await page.evaluate(() => document.activeElement.id)).toEqual('item-3');
await page.keyboard.press('ArrowDown');
expect(await page.evaluate(() => document.activeElement.id)).toEqual('item-11');
});

it('should consider items in the direction you want to move even if some of that items area is in the opposite direction', async () => {
await page.evaluate(() => document.getElementById('item-22').focus());
await page.keyboard.press('ArrowRight');
expect(await page.evaluate(() => document.activeElement.id)).toEqual('item-15');
});
});

describe('Scope', () => {
Expand Down

0 comments on commit dfac028

Please sign in to comment.