@bbc/tv-lrud-spatial
Advanced tools
Comparing version 0.0.10 to 0.0.11
108
lib/lrud.js
@@ -73,3 +73,3 @@ /** | ||
* @param {HTMLElement} scope The element to search inside of | ||
* @return {Array} Array of valid focusables inside the scope | ||
* @return {HTMLElement[]} Array of valid focusables inside the scope | ||
*/ | ||
@@ -83,6 +83,19 @@ const getFocusables = (scope) => { | ||
return toArray(scope.querySelectorAll(focusableSelector)) | ||
.filter(node => !ignoredElements.some(ignored => ignored == node || ignored.contains(node))); | ||
.filter(node => !ignoredElements.some(ignored => ignored == node || ignored.contains(node))) | ||
.filter(node => parseInt(node.getAttribute('tabIndex') || 0, 10) > -1); | ||
}; | ||
/** | ||
* Get all the focusable candidates inside `scope`, | ||
* including focusable containers | ||
* | ||
* @param {HTMLElement} scope The element to search inside of | ||
* @return {HTMLElement[]} Array of valid focusables inside the scope | ||
*/ | ||
const getAllFocusables = (scope) => [ | ||
...toArray(scope.querySelectorAll(focusableContainerSelector)).filter(container => getFocusables(container)?.length > 0), | ||
...getFocusables(scope) | ||
]; | ||
/** | ||
* Build an array of ancestor containers | ||
@@ -248,33 +261,41 @@ * | ||
/** | ||
* Get the closest spatial candidate | ||
* Sort the candidates ordered by distance to the elem, | ||
* and filter out invalid candidates. | ||
* | ||
* @param {HTMLElement[]} candidates A set of candidate elements to sort | ||
* @param {HTMLElement} elem The search origin | ||
* @param {HTMLElement[]} candidates An set of candidate elements to assess | ||
* @param {string} exitDir The direction in which we exited the elem (left, right, up, down) | ||
* @return {HTMLElement|null} The element that was spatially closest elem in candidates | ||
* @return {HTMLElement[]} The valid candidates, in order by distance | ||
*/ | ||
const getBestCandidate = (elem, candidates, exitDir) => { | ||
let bestCandidate = null; | ||
let bestDistance = Infinity; | ||
const sortValidCandidates = (candidates, elem, exitDir) => { | ||
const exitRect = elem.getBoundingClientRect(); | ||
const exitPoint = getMidpointForEdge(exitRect, exitDir); | ||
for (let i = 0; i < candidates.length; ++i) { | ||
const candidate = candidates[i]; | ||
return candidates.filter(candidate => { | ||
// Filter out candidates that are in the opposite direction or have no dimensions | ||
const entryRect = candidate.getBoundingClientRect(); | ||
// Bail if the candidate is in the opposite direction or has no dimensions | ||
const allowedOverlap = parseFloat(candidate.getAttribute('data-lrud-overlap-threshold')); | ||
if (!isValidCandidate(entryRect, exitDir, exitPoint, allowedOverlap)) continue; | ||
return isValidCandidate(entryRect, exitDir, exitPoint, allowedOverlap); | ||
}).map(candidate => { | ||
const entryRect = candidate.getBoundingClientRect(); | ||
const nearestPoint = getNearestPoint(exitPoint, exitDir, entryRect); | ||
const distance = getDistanceBetweenPoints(exitPoint, nearestPoint); | ||
return { | ||
candidate, | ||
distance | ||
}; | ||
}).sort((a, b) => a.distance - b.distance).map(({ candidate }) => candidate); | ||
}; | ||
if (bestDistance > distance) { | ||
bestDistance = distance; | ||
bestCandidate = candidate; | ||
} | ||
} | ||
/** | ||
* Get the first parent container that matches the focusable candidate selector | ||
* @param {HTMLElement} startingCandidate The starting candidate to get the parent container of | ||
* @return {HTMLElement} The container that matches or null | ||
*/ | ||
const getParentFocusableContainer = (startingCandidate) => { | ||
if (!startingCandidate) return null; | ||
do { | ||
startingCandidate = getParentContainer(startingCandidate); | ||
} while (startingCandidate && !matches(startingCandidate, focusableContainerSelector)); | ||
return bestCandidate; | ||
return startingCandidate; | ||
}; | ||
@@ -297,23 +318,23 @@ | ||
const parentContainer = getParentContainer(elem); | ||
if (parentContainer && matches(elem, focusableSelector)) { | ||
parentContainer.setAttribute('data-focus', elem.id); | ||
getParentFocusableContainer(parentContainer)?.setAttribute('data-focus', elem.id); | ||
} | ||
let bestCandidate; | ||
let focusableCandidates = []; | ||
// Get all siblings within a prioritised container | ||
if (parentContainer?.getAttribute('data-lrud-prioritise-children') !== 'false' && scope.contains(parentContainer)) { | ||
const focusableSiblings = getFocusables(parentContainer); | ||
bestCandidate = getBestCandidate(elem, focusableSiblings, exitDir); | ||
const focusableSiblings = getAllFocusables(parentContainer); | ||
focusableCandidates = sortValidCandidates(focusableSiblings, elem, exitDir); | ||
} | ||
if (!bestCandidate) { | ||
const focusableCandidates = [ | ||
...getFocusables(scope), | ||
...toArray(scope.querySelectorAll(focusableContainerSelector)).filter(container => getFocusables(container)?.length > 0 && container !== parentContainer) | ||
]; | ||
bestCandidate = getBestCandidate(elem, focusableCandidates, exitDir); | ||
if (focusableCandidates.length === 0) { | ||
const candidates = getAllFocusables(scope); | ||
focusableCandidates = sortValidCandidates(candidates, elem, exitDir); | ||
} | ||
if (bestCandidate) { | ||
const isBestCandidateAContainer = matches(bestCandidate, containerSelector); | ||
const candidateContainer = isBestCandidateAContainer ? bestCandidate : getParentContainer(bestCandidate); | ||
for (const candidate of focusableCandidates) { | ||
const candidateIsContainer = matches(candidate, containerSelector); | ||
const candidateContainer = candidateIsContainer ? candidate : getParentContainer(candidate); | ||
@@ -324,9 +345,5 @@ const isCurrentContainer = candidateContainer === parentContainer; | ||
const candidateActiveChild = candidateContainer?.getAttribute('data-focus'); | ||
parentContainer?.setAttribute('data-focus', elem.id); | ||
candidateContainer?.setAttribute('data-focus', bestCandidate.id); | ||
if (!isCurrentContainer && (!isNestedContainer || isBestCandidateAContainer)) { | ||
if (!isCurrentContainer && (!isNestedContainer || candidateIsContainer)) { | ||
const blockedExitDirs = getBlockedExitDirs(parentContainer, candidateContainer); | ||
if (blockedExitDirs.indexOf(exitDir) > -1) return; | ||
if (blockedExitDirs.indexOf(exitDir) > -1) continue; | ||
@@ -336,5 +353,6 @@ if (candidateContainer && !isAnscestorContainer) { | ||
// are already nested in | ||
const lastActiveChild = document.getElementById(candidateActiveChild); | ||
const lastActiveChild = document.getElementById(candidateContainer?.getAttribute('data-focus')); | ||
const newFocus = lastActiveChild || getFocusables(candidateContainer)?.[0]; | ||
getParentFocusableContainer(candidateContainer)?.setAttribute('data-focus', newFocus?.id); | ||
candidateContainer?.setAttribute('data-focus', newFocus?.id); | ||
@@ -344,5 +362,9 @@ return newFocus; | ||
} | ||
if (!candidateIsContainer) { | ||
getParentFocusableContainer(candidateContainer)?.setAttribute('data-focus', candidate.id); | ||
candidateContainer?.setAttribute('data-focus', candidate.id); | ||
} | ||
return candidate; | ||
} | ||
return bestCandidate; | ||
}; | ||
@@ -349,0 +371,0 @@ |
@@ -1,1 +0,3 @@ | ||
"use strict";exports.__esModule=true;exports.getNextFocus=void 0;var focusableSelector="[tabindex], a, input, button";var containerSelector="nav, section, .lrud-container";var focusableContainerSelector="[data-lrud-consider-container-distance]";var ignoredClass="lrud-ignore";var matches=function matches(element,selectors){var fn=Element.prototype.matches||Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector||function(s){var matches=(this.document||this.ownerDocument).querySelectorAll(s),i=matches.length;while(--i>=0&&matches.item(i)!==this){}return i>-1};return fn.call(element,selectors)};var toArray=function toArray(nodeList){return Array.prototype.slice.call(nodeList)};var getParentContainer=function getParentContainer(elem){if(!elem.parentElement||elem.parentElement.tagName==="BODY"){return null}else if(matches(elem.parentElement,containerSelector)){return elem.parentElement}return getParentContainer(elem.parentElement)};var getFocusables=function getFocusables(scope){if(!scope)return[];var ignoredElements=toArray(scope.querySelectorAll("."+ignoredClass));if(scope.className.indexOf(ignoredClass)>-1)ignoredElements.push(scope);return toArray(scope.querySelectorAll(focusableSelector)).filter(function(node){return!ignoredElements.some(function(ignored){return ignored==node||ignored.contains(node)})})};var collectContainers=function collectContainers(initialContainer){if(!initialContainer)return[];var acc=[initialContainer];var cur=initialContainer;while(cur){cur=getParentContainer(cur);if(cur)acc.push(cur)}return acc};var getMidpointForEdge=function getMidpointForEdge(rect,dir){switch(dir){case"left":return{x:rect.left,y:(rect.top+rect.bottom)/2};case"right":return{x:rect.right,y:(rect.top+rect.bottom)/2};case"up":return{x:(rect.left+rect.right)/2,y:rect.top};case"down":return{x:(rect.left+rect.right)/2,y:rect.bottom};}};var getNearestPoint=function getNearestPoint(point,dir,rect){if(dir==="left"||dir==="right"){var x=dir==="left"?rect.right:rect.left;if(point.y<rect.top)return{x:x,y:rect.top};if(point.y>rect.bottom)return{x:x,y:rect.bottom};return{x:x,y:point.y}}else if(dir==="up"||dir==="down"){var y=dir==="up"?rect.bottom:rect.top;if(point.x<rect.left)return{x:rect.left,y:y};if(point.x>rect.right)return{x:rect.right,y:y};return{x:point.x,y:y}}};var getDistanceBetweenPoints=function getDistanceBetweenPoints(a,b){return Math.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2))};var isBelow=function isBelow(a,b){return a.y>b.y};var isRight=function isRight(a,b){return a.x>b.x};var getBlockedExitDirs=function getBlockedExitDirs(container,candidateContainer){var currentAncestorContainers=collectContainers(container);var candidateAncestorContainers=collectContainers(candidateContainer);for(var i=0;i<candidateAncestorContainers.length;i++){var commonCandidate=candidateAncestorContainers[i];var spliceIndex=currentAncestorContainers.indexOf(commonCandidate);if(spliceIndex>-1){currentAncestorContainers.splice(spliceIndex);break}}return currentAncestorContainers.reduce(function(acc,cur){var dirs=((cur===null||cur===void 0?void 0:cur.getAttribute("data-block-exit"))||"").split(" ");return acc.concat(dirs)},[])};var isValidCandidate=function isValidCandidate(entryRect,exitDir,exitPoint,entryWeighting){if(entryRect.width===0&&entryRect.height===0)return false;if(!entryWeighting&&entryWeighting!=0)entryWeighting=0.3;var weightedEntryPoint={x:entryRect.left+entryRect.width*(exitDir==="left"?1-entryWeighting:exitDir==="right"?entryWeighting:0.5),y:entryRect.top+entryRect.height*(exitDir==="up"?1-entryWeighting:exitDir==="down"?entryWeighting:0.5)};if(exitDir==="left"&&isRight(exitPoint,weightedEntryPoint)||exitDir==="right"&&isRight(weightedEntryPoint,exitPoint)||exitDir==="up"&&isBelow(exitPoint,weightedEntryPoint)||exitDir==="down"&&isBelow(weightedEntryPoint,exitPoint))return true;return false};var getBestCandidate=function getBestCandidate(elem,candidates,exitDir){var bestCandidate=null;var bestDistance=Infinity;var exitRect=elem.getBoundingClientRect();var exitPoint=getMidpointForEdge(exitRect,exitDir);for(var i=0;i<candidates.length;++i){var candidate=candidates[i];var entryRect=candidate.getBoundingClientRect();var allowedOverlap=parseFloat(candidate.getAttribute("data-lrud-overlap-threshold"));if(!isValidCandidate(entryRect,exitDir,exitPoint,allowedOverlap))continue;var nearestPoint=getNearestPoint(exitPoint,exitDir,entryRect);var distance=getDistanceBetweenPoints(exitPoint,nearestPoint);if(bestDistance>distance){bestDistance=distance;bestCandidate=candidate}}return bestCandidate};var getNextFocus=function getNextFocus(elem,keyOrKeyCode,scope){var _getFocusables;if(!scope||!scope.querySelector)scope=document.body;if(!elem)return(_getFocusables=getFocusables(scope))===null||_getFocusables===void 0?void 0:_getFocusables[0];var exitDir=_keyMap[keyOrKeyCode];var parentContainer=getParentContainer(elem);var bestCandidate;if((parentContainer===null||parentContainer===void 0?void 0:parentContainer.getAttribute("data-lrud-prioritise-children"))!=="false"&&scope.contains(parentContainer)){var focusableSiblings=getFocusables(parentContainer);bestCandidate=getBestCandidate(elem,focusableSiblings,exitDir)}if(!bestCandidate){var focusableCandidates=[].concat(getFocusables(scope),toArray(scope.querySelectorAll(focusableContainerSelector)).filter(function(container){var _getFocusables2;return((_getFocusables2=getFocusables(container))===null||_getFocusables2===void 0?void 0:_getFocusables2.length)>0&&container!==parentContainer}));bestCandidate=getBestCandidate(elem,focusableCandidates,exitDir)}if(bestCandidate){var isBestCandidateAContainer=matches(bestCandidate,containerSelector);var candidateContainer=isBestCandidateAContainer?bestCandidate:getParentContainer(bestCandidate);var isCurrentContainer=candidateContainer===parentContainer;var isNestedContainer=parentContainer===null||parentContainer===void 0?void 0:parentContainer.contains(candidateContainer);var isAnscestorContainer=candidateContainer===null||candidateContainer===void 0?void 0:candidateContainer.contains(parentContainer);var candidateActiveChild=candidateContainer===null||candidateContainer===void 0?void 0:candidateContainer.getAttribute("data-focus");parentContainer===null||parentContainer===void 0?void 0:parentContainer.setAttribute("data-focus",elem.id);candidateContainer===null||candidateContainer===void 0?void 0:candidateContainer.setAttribute("data-focus",bestCandidate.id);if(!isCurrentContainer&&(!isNestedContainer||isBestCandidateAContainer)){var blockedExitDirs=getBlockedExitDirs(parentContainer,candidateContainer);if(blockedExitDirs.indexOf(exitDir)>-1)return;if(candidateContainer&&!isAnscestorContainer){var _getFocusables3;var lastActiveChild=document.getElementById(candidateActiveChild);var newFocus=lastActiveChild||((_getFocusables3=getFocusables(candidateContainer))===null||_getFocusables3===void 0?void 0:_getFocusables3[0]);candidateContainer===null||candidateContainer===void 0?void 0:candidateContainer.setAttribute("data-focus",newFocus===null||newFocus===void 0?void 0:newFocus.id);return newFocus}}}return bestCandidate};exports.getNextFocus=getNextFocus;var _left="left",_right="right",_up="up",_down="down";var _keyMap={4:_left,21:_left,37:_left,214:_left,205:_left,218:_left,5:_right,22:_right,39:_right,213:_right,206:_right,217:_right,29460:_up,19:_up,38:_up,211:_up,203:_up,215:_up,29461:_down,20:_down,40:_down,212:_down,204:_down,216:_down,"ArrowLeft":_left,"ArrowRight":_right,"ArrowUp":_up,"ArrowDown":_down}; | ||
"use strict";exports.__esModule=true;exports.getNextFocus=void 0;function _createForOfIteratorHelper(o,allowArrayLike){var it=typeof Symbol!=="undefined"&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=_unsupportedIterableToArray(o))||allowArrayLike&&o&&typeof o.length==="number"){if(it)o=it;var i=0;var F=function F(){};return{s:F,n:function n(){if(i>=o.length)return{done:true};return{done:false,value:o[i++]}},e:function e(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var normalCompletion=true,didErr=false,err;return{s:function s(){it=it.call(o)},n:function n(){var step=it.next();normalCompletion=step.done;return step},e:function e(_e2){didErr=true;err=_e2},f:function f(){try{if(!normalCompletion&&it["return"]!=null)it["return"]()}finally{if(didErr)throw err}}}}function _unsupportedIterableToArray(o,minLen){if(!o)return;if(typeof o==="string")return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);if(n==="Object"&&o.constructor)n=o.constructor.name;if(n==="Map"||n==="Set")return Array.from(o);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}function _arrayLikeToArray(arr,len){if(len==null||len>arr.length)len=arr.length;for(var i=0,arr2=new Array(len);i<len;i++)arr2[i]=arr[i];return arr2}var focusableSelector="[tabindex], a, input, button";var containerSelector="nav, section, .lrud-container";var focusableContainerSelector="[data-lrud-consider-container-distance]";var ignoredClass="lrud-ignore";var matches=function matches(element,selectors){var fn=Element.prototype.matches||Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector||function(s){var matches=(this.document||this.ownerDocument).querySelectorAll(s),i=matches.length;while(--i>=0&&matches.item(i)!==this){}return i>-1};return fn.call(element,selectors)};var toArray=function toArray(nodeList){return Array.prototype.slice.call(nodeList)};var getParentContainer=function getParentContainer(elem){if(!elem.parentElement||elem.parentElement.tagName==="BODY"){return null}else if(matches(elem.parentElement,containerSelector)){return elem.parentElement}return getParentContainer(elem.parentElement)};var getFocusables=function getFocusables(scope){if(!scope)return[];var ignoredElements=toArray(scope.querySelectorAll("."+ignoredClass));if(scope.className.indexOf(ignoredClass)>-1)ignoredElements.push(scope);return toArray(scope.querySelectorAll(focusableSelector)).filter(function(node){return!ignoredElements.some(function(ignored){return ignored==node||ignored.contains(node)})}).filter(function(node){return parseInt(node.getAttribute("tabIndex")||0,10)>-1})};var getAllFocusables=function getAllFocusables(scope){return[].concat(toArray(scope.querySelectorAll(focusableContainerSelector)).filter(function(container){var _getFocusables;return((_getFocusables=getFocusables(container))===null||_getFocusables===void 0?void 0:_getFocusables.length)>0}),getFocusables(scope))};var collectContainers=function collectContainers(initialContainer){if(!initialContainer)return[];var acc=[initialContainer];var cur=initialContainer;while(cur){cur=getParentContainer(cur);if(cur)acc.push(cur)}return acc};var getMidpointForEdge=function getMidpointForEdge(rect,dir){switch(dir){case"left":return{x:rect.left,y:(rect.top+rect.bottom)/2};case"right":return{x:rect.right,y:(rect.top+rect.bottom)/2};case"up":return{x:(rect.left+rect.right)/2,y:rect.top};case"down":return{x:(rect.left+rect.right)/2,y:rect.bottom}}};var getNearestPoint=function getNearestPoint(point,dir,rect){if(dir==="left"||dir==="right"){var x=dir==="left"?rect.right:rect.left;if(point.y<rect.top)return{x:x,y:rect.top};if(point.y>rect.bottom)return{x:x,y:rect.bottom};return{x:x,y:point.y}}else if(dir==="up"||dir==="down"){var y=dir==="up"?rect.bottom:rect.top;if(point.x<rect.left)return{x:rect.left,y:y};if(point.x>rect.right)return{x:rect.right,y:y};return{x:point.x,y:y}}};var getDistanceBetweenPoints=function getDistanceBetweenPoints(a,b){return Math.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2))};var isBelow=function isBelow(a,b){return a.y>b.y};var isRight=function isRight(a,b){return a.x>b.x};var getBlockedExitDirs=function getBlockedExitDirs(container,candidateContainer){var currentAncestorContainers=collectContainers(container);var candidateAncestorContainers=collectContainers(candidateContainer);for(var i=0;i<candidateAncestorContainers.length;i++){var commonCandidate=candidateAncestorContainers[i];var spliceIndex=currentAncestorContainers.indexOf(commonCandidate);if(spliceIndex>-1){currentAncestorContainers.splice(spliceIndex);break}}return currentAncestorContainers.reduce(function(acc,cur){var dirs=((cur===null||cur===void 0?void 0:cur.getAttribute("data-block-exit"))||"").split(" ");return acc.concat(dirs)},[])};var isValidCandidate=function isValidCandidate(entryRect,exitDir,exitPoint,entryWeighting){if(entryRect.width===0&&entryRect.height===0)return false;if(!entryWeighting&&entryWeighting!=0)entryWeighting=0.3;var weightedEntryPoint={x:entryRect.left+entryRect.width*(exitDir==="left"?1-entryWeighting:exitDir==="right"?entryWeighting:0.5),y:entryRect.top+entryRect.height*(exitDir==="up"?1-entryWeighting:exitDir==="down"?entryWeighting:0.5)};if(exitDir==="left"&&isRight(exitPoint,weightedEntryPoint)||exitDir==="right"&&isRight(weightedEntryPoint,exitPoint)||exitDir==="up"&&isBelow(exitPoint,weightedEntryPoint)||exitDir==="down"&&isBelow(weightedEntryPoint,exitPoint))return true;return false};var sortValidCandidates=function sortValidCandidates(candidates,elem,exitDir){var exitRect=elem.getBoundingClientRect();var exitPoint=getMidpointForEdge(exitRect,exitDir);return candidates.filter(function(candidate){var entryRect=candidate.getBoundingClientRect();var allowedOverlap=parseFloat(candidate.getAttribute("data-lrud-overlap-threshold"));return isValidCandidate(entryRect,exitDir,exitPoint,allowedOverlap)}).map(function(candidate){var entryRect=candidate.getBoundingClientRect();var nearestPoint=getNearestPoint(exitPoint,exitDir,entryRect);var distance=getDistanceBetweenPoints(exitPoint,nearestPoint);return{candidate:candidate,distance:distance}}).sort(function(a,b){return a.distance-b.distance}).map(function(_ref){var candidate=_ref.candidate;return candidate})};var getParentFocusableContainer=function getParentFocusableContainer(startingCandidate){if(!startingCandidate)return null;do{startingCandidate=getParentContainer(startingCandidate)}while(startingCandidate&&!matches(startingCandidate,focusableContainerSelector));return startingCandidate};var getNextFocus=exports.getNextFocus=function getNextFocus(elem,keyOrKeyCode,scope){var _getFocusables2;if(!scope||!scope.querySelector)scope=document.body;if(!elem)return(_getFocusables2=getFocusables(scope))===null||_getFocusables2===void 0?void 0:_getFocusables2[0];var exitDir=_keyMap[keyOrKeyCode];var parentContainer=getParentContainer(elem);if(parentContainer&&matches(elem,focusableSelector)){var _getParentFocusableCo;parentContainer.setAttribute("data-focus",elem.id);(_getParentFocusableCo=getParentFocusableContainer(parentContainer))===null||_getParentFocusableCo===void 0||_getParentFocusableCo.setAttribute("data-focus",elem.id)}var focusableCandidates=[];if((parentContainer===null||parentContainer===void 0?void 0:parentContainer.getAttribute("data-lrud-prioritise-children"))!=="false"&&scope.contains(parentContainer)){var focusableSiblings=getAllFocusables(parentContainer);focusableCandidates=sortValidCandidates(focusableSiblings,elem,exitDir)}if(focusableCandidates.length===0){var candidates=getAllFocusables(scope);focusableCandidates=sortValidCandidates(candidates,elem,exitDir)}var _iterator=_createForOfIteratorHelper(focusableCandidates),_step;try{for(_iterator.s();!(_step=_iterator.n()).done;){var candidate=_step.value;var candidateIsContainer=matches(candidate,containerSelector);var candidateContainer=candidateIsContainer?candidate:getParentContainer(candidate);var isCurrentContainer=candidateContainer===parentContainer;var isNestedContainer=parentContainer===null||parentContainer===void 0?void 0:parentContainer.contains(candidateContainer);var isAnscestorContainer=candidateContainer===null||candidateContainer===void 0?void 0:candidateContainer.contains(parentContainer);if(!isCurrentContainer&&(!isNestedContainer||candidateIsContainer)){var blockedExitDirs=getBlockedExitDirs(parentContainer,candidateContainer);if(blockedExitDirs.indexOf(exitDir)>-1)continue;if(candidateContainer&&!isAnscestorContainer){var _getFocusables3,_getParentFocusableCo2;var lastActiveChild=document.getElementById(candidateContainer===null||candidateContainer===void 0?void 0:candidateContainer.getAttribute("data-focus"));var newFocus=lastActiveChild||((_getFocusables3=getFocusables(candidateContainer))===null||_getFocusables3===void 0?void 0:_getFocusables3[0]);(_getParentFocusableCo2=getParentFocusableContainer(candidateContainer))===null||_getParentFocusableCo2===void 0||_getParentFocusableCo2.setAttribute("data-focus",newFocus===null||newFocus===void 0?void 0:newFocus.id);candidateContainer===null||candidateContainer===void 0||candidateContainer.setAttribute("data-focus",newFocus===null||newFocus===void 0?void 0:newFocus.id);return newFocus}}if(!candidateIsContainer){var _getParentFocusableCo3;(_getParentFocusableCo3=getParentFocusableContainer(candidateContainer))===null||_getParentFocusableCo3===void 0||_getParentFocusableCo3.setAttribute("data-focus",candidate.id);candidateContainer===null||candidateContainer===void 0||candidateContainer.setAttribute("data-focus",candidate.id)}return candidate}}catch(err){_iterator.e(err)}finally{_iterator.f()}};var _left="left",_right="right",_up="up",_down="down";var _keyMap={4:_left,21:_left,37:_left,214:_left,205:_left,218:_left,5:_right,22:_right,39:_right,213:_right,206:_right,217:_right,29460:_up,19:_up,38:_up,211:_up,203:_up,215:_up,29461:_down,20:_down,40:_down,212:_down,204:_down,216:_down,"ArrowLeft":_left,"ArrowRight":_right,"ArrowUp":_up,"ArrowDown":_down}; | ||
//# sourceMappingURL=lrud.min.js.map |
{ | ||
"name": "@bbc/tv-lrud-spatial", | ||
"version": "0.0.10", | ||
"version": "0.0.11", | ||
"description": "Spatial navigation library", | ||
@@ -11,3 +11,3 @@ "main": "lib/lrud.min.js", | ||
"test": "jest", | ||
"build": "rm -f lib/lrud.min.js && babel lib --out-file lib/lrud.min.js", | ||
"build": "rm -f lib/lrud.min.js && babel lib --source-maps true --out-file lib/lrud.min.js", | ||
"server": "node ./test/server.js", | ||
@@ -39,6 +39,6 @@ "lint": "eslint .", | ||
"dependencies": { | ||
"@babel/cli": "^7.16.8", | ||
"@babel/core": "^7.16.12", | ||
"@babel/preset-env": "^7.12.1" | ||
"@babel/cli": "^7.23.9", | ||
"@babel/core": "^7.23.9", | ||
"@babel/preset-env": "^7.23.9" | ||
} | ||
} |
@@ -35,3 +35,3 @@ <p align="center"> | ||
- `[tabindex]` | ||
- `[tabindex]` (for tabindex >= 0) | ||
- `a` | ||
@@ -45,2 +45,4 @@ - `button` | ||
Focusables with a `tabindex="-1"` attribute will be skipped over, however any focusable inside any parent with `tabindex="-1"` will still be considered focusable. | ||
### Focusable Overlap | ||
@@ -52,2 +54,4 @@ | ||
Please also note that LRUD does not consider the Z Axis, which can cause surprising results with elements that are overlapped in this way, including in the case of full screen overlays on existing UIs. The above attribute can help alleviate this issue. | ||
## Containers | ||
@@ -66,4 +70,2 @@ | ||
Adding attribute `data-lrud-consider-container-distance` on a container will make LRUD also measure distances to that container's boundary, as well as its children. If the container is the closest of all the possible candidates assessed, LRUD will return one of its children - even if they are not necessarily the spatially closest focusable. The above container focus logic will still be used, so if the container has a previous focus state that will be the returned element. This allows for layouts where moving between containers is the desired behaviour, but their individual elements may not be in the correct positions for that. By default LRUD will only consider focusables when measuring, and ignores container positions. | ||
### Block exits | ||
@@ -82,2 +84,11 @@ | ||
### Focusable Containers | ||
By default, LRUD only measures the distances to focusables and does not consider where their container boundaries are. Adding the attribute `data-lrud-consider-container-distance` to a container will include it in the distance calculations, as well as its children. | ||
If the container is the closest out of all the possible focusables assessed, LRUD will return one of its children - even if they are not necessarily the spatially closest focusable. | ||
The above container focus logic will still be used, and moving into a focusable container will move to its last focused child if there was one, at any level of container depth inside it. | ||
## How does it work? | ||
@@ -128,6 +139,4 @@ | ||
> Remember to terminate this process before running the tests again! | ||
## Contact | ||
[TVOpenSource@bbc.co.uk](mailto:TVOpenSource@bbc.co.uk) - we aim to respond to emails within a week. |
@@ -11,4 +11,12 @@ const puppeteer = require('puppeteer'); | ||
const getParentContainerDataFocus = (id) => page.evaluate((id) => document.getElementById(id).parentElement.getAttribute('data-focus'), id); | ||
beforeAll(async () => { | ||
await server.listen(); | ||
try { | ||
await server.listen(); | ||
} catch (ex) { | ||
if (ex.code !== 'EADDRINUSE') { | ||
throw ex; | ||
} | ||
} | ||
browser = await puppeteer.launch({ | ||
@@ -25,9 +33,11 @@ defaultViewport: {width: 1280, height: 800} | ||
afterEach(async () => { | ||
await page.close(); | ||
await page?.close(); | ||
}); | ||
afterAll(async () => { | ||
await context.close(); | ||
await browser.close(); | ||
await server.close(); | ||
await context?.close(); | ||
await browser?.close(); | ||
try { | ||
await server.close(); | ||
} catch { /* empty */ } | ||
}); | ||
@@ -261,3 +271,3 @@ | ||
* Elements with the `lrud-ignore` class, or inside a parent with the `lrud-ignore` class, should | ||
* be not be considered focusable candidates | ||
* not be considered focusable candidates | ||
* | ||
@@ -287,2 +297,36 @@ */ | ||
describe('Unreachable foucsable elements', () => { | ||
/* | ||
* Elements with a tabindex of -1 should not be considered focusable candidates. | ||
* Elements inside a parent with tabindex -1 should be considered focusable candidates. | ||
* | ||
*/ | ||
it('should ignore tabindex -1 items as possible candidates and move past them', async () => { | ||
await page.goto(`${testPath}/unreachable.html`); | ||
await page.waitForFunction('document.activeElement'); | ||
await page.keyboard.press('ArrowRight'); | ||
await page.keyboard.press('ArrowRight'); | ||
const result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-4'); | ||
}); | ||
it('should not ignore visible items inside tabindex -1 containers', async () => { | ||
await page.goto(`${testPath}/unreachable.html`); | ||
await page.waitForFunction('document.activeElement'); | ||
await page.keyboard.press('ArrowDown'); | ||
const result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-6'); | ||
await page.keyboard.press('ArrowDown'); | ||
const nextResult = await page.evaluate(() => document.activeElement.id); | ||
expect(nextResult).toEqual('item-7'); | ||
}); | ||
}); | ||
describe('Page with 11 candidates, with varying sizes', () => { | ||
@@ -419,2 +463,12 @@ it('should focus on candidate 6 when right, down is pressed', async () => { | ||
}); | ||
it('should move to the second best candidate if first was blocked', async () => { | ||
await page.goto(`${testPath}/3c-h-6f-blocked-exit.html`); | ||
await page.evaluate(() => document.getElementById('item-6').focus()); | ||
await page.keyboard.press('ArrowUp'); | ||
const result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-1'); | ||
}); | ||
}); | ||
@@ -649,2 +703,48 @@ | ||
}); | ||
it('should not set data-focus attributes to a container ID', async () => { | ||
await page.goto(`${testPath}/focusable-container-with-empty-space.html`); | ||
// makes section-2 the best candidate, although it will later be ignored | ||
await page.evaluate(() => document.getElementById('section-2').setAttribute('data-lrud-overlap-threshold', 1)); | ||
await page.evaluate(() => document.getElementById('item-6').focus()); | ||
await page.keyboard.press('ArrowLeft'); | ||
const result = await page.evaluate(() => document.activeElement.id); | ||
expect(await page.evaluate(() => document.getElementById('section-2').getAttribute('data-focus'))).toBe('item-6'); | ||
expect(result).toEqual('item-6'); | ||
}); | ||
it('should remember the last active child of a focusable container', async () => { | ||
await page.goto(`${testPath}/nested-focusable-containers.html`); | ||
await page.waitForFunction('document.activeElement'); | ||
await page.evaluate(() => document.getElementById('item-3').focus()); | ||
await page.keyboard.press('ArrowRight'); | ||
let result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-4'); | ||
await page.evaluate(() => document.getElementById('item-9').focus()); | ||
await page.keyboard.press('ArrowUp'); | ||
result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-4'); | ||
await page.evaluate(() => document.getElementById('item-5').focus()); | ||
await page.keyboard.press('ArrowUp'); | ||
result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-1'); | ||
await page.keyboard.press('ArrowDown'); | ||
result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-5'); | ||
}); | ||
it('should prioritise focusable containers that are the same distance away as a regular focusable', async () => { | ||
await page.goto(`${testPath}/nested-focusable-containers.html`); | ||
await page.waitForFunction('document.activeElement'); | ||
await page.evaluate(() => document.getElementById('item-11').focus()); | ||
await page.keyboard.press('ArrowDown'); | ||
let result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-12'); | ||
await page.evaluate(() => document.getElementById('item-9').focus()); | ||
await page.keyboard.press('ArrowDown'); | ||
result = await page.evaluate(() => document.activeElement.id); | ||
expect(result).toEqual('item-12'); | ||
}); | ||
}); | ||
@@ -689,3 +789,2 @@ | ||
it('should only store the last active child ID if the child is not inside another container', async () => { | ||
const getParentContainerDataFocus = (id) => page.evaluate((id) => document.getElementById(id).parentElement.getAttribute('data-focus'), id); | ||
await page.goto(`${testPath}/4c-v-5f-nested.html`); | ||
@@ -708,3 +807,12 @@ await page.evaluate(() => document.getElementById('item-1').focus()); | ||
}); | ||
it('does not update the new active child ID if the exit was blocked', async () => { | ||
await page.goto(`${testPath}/3c-h-6f-blocked-exit.html`); | ||
await page.evaluate(() => document.getElementById('item-2').focus()); | ||
await page.keyboard.press('ArrowDown'); | ||
expect(await getParentContainerDataFocus('item-2')).toEqual('item-2'); | ||
expect(await page.evaluate(() => document.activeElement.id)).toEqual('item-2'); | ||
expect(await getParentContainerDataFocus('item-3')).toEqual(null); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
107057
38
1246
137
Updated@babel/cli@^7.23.9
Updated@babel/core@^7.23.9
Updated@babel/preset-env@^7.23.9