diff --git a/lib/lrud.js b/lib/lrud.js index c5fbf36..4630fa8 100644 --- a/lib/lrud.js +++ b/lib/lrud.js @@ -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) => { @@ -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) { @@ -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); @@ -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')); diff --git a/test/lrud.test.js b/test/lrud.test.js index d5e937b..830d4ed 100644 --- a/test/lrud.test.js +++ b/test/lrud.test.js @@ -472,10 +472,13 @@ 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'); @@ -483,14 +486,18 @@ describe('LRUD spatial', () => { }); 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', () => {