1111( function ( ) {
1212
1313 // Indicates global variables for spatnav (starting position)
14- let spatNavManager = {
14+ const spatNavManager = {
1515 startingPosition : null ,
1616 useStandardName : true ,
1717 } ;
3232
3333 const ARROW_KEY_CODE = { 37 : 'left' , 38 : 'up' , 39 : 'right' , 40 : 'down' } ;
3434 const TAB_KEY_CODE = 9 ;
35+ let spatialNaviagtionKeyMode = 'ARROW' ;
3536
3637 function focusNavigationHeuristics ( ) {
3738
5354 * If arrow key pressed, get the next focusing element and send it to focusing controller
5455 */
5556 window . addEventListener ( 'keydown' , function ( e ) {
56- const spatnavPolyfillOff = window . spatnavPolyfillOff || ( parent && parent . spatnavPolyfillOff ) ;
57- if ( ! spatnavPolyfillOff && ! e . defaultPrevented ) {
57+ const currentKeyMode = ( parent && parent . __spatialNavigation__ . getKeyMode ( ) ) || window . __spatialNavigation__ . getKeyMode ( ) ;
58+ const eventTarget = document . activeElement ;
59+ const dir = ARROW_KEY_CODE [ e . keyCode ] ;
60+
61+ if ( e . keyCode === TAB_KEY_CODE )
62+ spatNavManager . startingPosition = null ;
63+
64+ if ( ! currentKeyMode ||
65+ ( currentKeyMode === 'NONE' ) ||
66+ ( ( currentKeyMode === 'SHIFTARROW' ) && ! e . shiftKey ) ||
67+ ( ( currentKeyMode === 'ARROW' ) && e . shiftKey ) )
68+ return ;
69+
70+ if ( ! e . defaultPrevented ) {
5871 let focusNavigableArrowKey = { 'left' : true , 'up' : true , 'right' : true , 'down' : true } ;
59- const eventTarget = document . activeElement ;
60- const dir = ARROW_KEY_CODE [ e . keyCode ] ;
6172
6273 // Edge case (text input, area) : Don't move focus, just navigate cursor in text area
6374 if ( ( eventTarget . nodeName === 'INPUT' ) || eventTarget . nodeName === 'TEXTAREA' )
7081 spatNavManager . startingPosition = null ;
7182 }
7283 }
73-
74- if ( e . keyCode === TAB_KEY_CODE )
75- spatNavManager . startingPosition = null ;
7684 } ) ;
7785
7886 /**
278286 }
279287
280288 /**
281- * Find the best candidate among focusable candidates within the container from the element
282- * reference: https://wicg.github.io/spatial-navigation/#js-api
289+ * Find the candidates among focusable candidates within the container from the element
283290 * @function for Element
284291 * @param {SpatialNavigationDirection } direction
285292 * @param {sequence<Node> } candidates
286293 * @param {<Node> } container
287294 * @returns {<Node> } the best candidate
288295 **/
289- function spatNavSearch ( dir , candidates , container ) {
290- // Let container be the nearest ancestor of eventTarget that is a spatnav container.
291- let targetElement = this ;
292- let bestCandidate = null ;
293-
296+ function spatNavCandidates ( element , dir , candidates , container ) {
297+ let targetElement = element ;
294298 // If the container is unknown, get the closest container from the element
295- container = container || this . getSpatnavContainer ( ) ;
299+ container = container || targetElement . getSpatnavContainer ( ) ;
296300
297301 // If the candidates is unknown, find candidates
298302 // 5-1
299- if ( ! candidates || candidates . length < 0 ) {
300- if ( ( isContainer ( this ) || this . nodeName === 'BODY' ) && ! ( this . nodeName === 'INPUT' ) ) {
301- if ( this . nodeName === 'IFRAME' )
302- targetElement = this . contentDocument . body ;
303+ if ( ! candidates || candidates . length <= 0 ) {
304+ if ( ( isContainer ( targetElement ) || targetElement . nodeName === 'BODY' ) && ! ( targetElement . nodeName === 'INPUT' ) ) {
305+ if ( targetElement . nodeName === 'IFRAME' )
306+ targetElement = targetElement . contentDocument . body ;
303307
304308 candidates = targetElement . focusableAreas ( ) ;
305309 }
310314 else {
311315 candidates = filteredCandidates ( targetElement , candidates , dir , container ) ;
312316 }
317+ return candidates ;
318+ }
319+
320+ /**
321+ * Find the best candidate among focusable candidates within the container from the element
322+ * reference: https://wicg.github.io/spatial-navigation/#js-api
323+ * @function for Element
324+ * @param {SpatialNavigationDirection } direction
325+ * @param {sequence<Node> } candidates
326+ * @param {<Node> } container
327+ * @returns {<Node> } the best candidate
328+ **/
329+ function spatNavSearch ( dir , candidates , container ) {
330+ // Let container be the nearest ancestor of eventTarget that is a spatnav container.
331+ const targetElement = this ;
332+ let bestCandidate = null ;
333+
334+ candidates = spatNavCandidates ( targetElement , dir , candidates , container ) ;
313335
314336 // Find the best candidate
315337 // 5
556578 }
557579
558580 function isCSSSpatNavContain ( el ) {
559- return ( readCssVar ( el , 'spatial-navigation-contain' ) == 'contain' ) ? true : false ;
581+ return readCssVar ( el , 'spatial-navigation-contain' ) == 'contain' ;
560582 }
561583
562584 /**
9841006 * @returns {Number } euclidian distance between two elements
9851007 **/
9861008 function getEntryAndExitPoints ( dir = 'down' , rect1 , rect2 ) {
987- let points = { entryPoint :[ 0 , 0 ] , exitPoint :[ 0 , 0 ] } ;
1009+ const points = { entryPoint :[ 0 , 0 ] , exitPoint :[ 0 , 0 ] } ;
9881010
9891011 // Set direction
9901012 switch ( dir ) {
11271149 focusNavigationHeuristics ( ) ;
11281150 } ) ;
11291151
1130- } ) ( window , document ) ;
1152+
1153+ function addNonStandardAPI ( ) {
1154+ function canScroll ( container , dir ) {
1155+ return ( isScrollable ( container , dir ) && ! isScrollBoundary ( container , dir ) ) ||
1156+ ( ! container . parentElement && ! isHTMLScrollBoundary ( container , dir ) ) ;
1157+ }
1158+
1159+
1160+ function findTarget ( findCandidate , element , dir ) {
1161+ let eventTarget = element ;
1162+ let bestNextTarget = null ;
1163+
1164+ // 4
1165+ if ( eventTarget === document || eventTarget === document . documentElement ) {
1166+ eventTarget = document . body || document . documentElement ;
1167+ }
1168+
1169+ // 5
1170+ // At this point, spatNavSearch can be applied.
1171+ // If startingPoint is either a scroll container or the document,
1172+ // find the best candidate within startingPoint
1173+ if ( ( isContainer ( eventTarget ) || eventTarget . nodeName === 'BODY' ) && ! ( eventTarget . nodeName === 'INPUT' ) ) {
1174+ if ( eventTarget . nodeName === 'IFRAME' )
1175+ eventTarget = eventTarget . contentDocument . body ;
1176+
1177+ const candidates = eventTarget . focusableAreas ( ) ;
1178+
1179+ // 5-2
1180+ if ( Array . isArray ( candidates ) && candidates . length > 0 ) {
1181+ if ( findCandidate ) {
1182+ return spatNavCandidates ( eventTarget , dir ) ;
1183+ } else {
1184+ bestNextTarget = eventTarget . spatNavSearch ( dir ) ;
1185+ return bestNextTarget ;
1186+ }
1187+ }
1188+ if ( canScroll ( eventTarget , dir ) ) {
1189+ if ( findCandidate ) {
1190+ return [ ] ;
1191+ } else {
1192+ bestNextTarget = eventTarget ;
1193+ return bestNextTarget ;
1194+ }
1195+ }
1196+ }
1197+
1198+ // 6
1199+ // Let container be the nearest ancestor of eventTarget
1200+ let container = eventTarget . getSpatnavContainer ( ) ;
1201+ let parentContainer = container . getSpatnavContainer ( ) ;
1202+
1203+ // When the container is the viewport of a browsing context
1204+ if ( ! parentContainer ) {
1205+ parentContainer = window . document . documentElement ;
1206+ // The container is IFRAME, so parentContainer will be retargeted to the document of the parent window
1207+ if ( window . location !== window . parent . location ) {
1208+ parentContainer = window . parent . document . documentElement ;
1209+ }
1210+ }
1211+
1212+ // 7
1213+ while ( parentContainer ) {
1214+ const candidates = filteredCandidates ( eventTarget , container . focusableAreas ( ) , dir , container ) ;
1215+
1216+ if ( Array . isArray ( candidates ) && candidates . length > 0 ) {
1217+ bestNextTarget = eventTarget . spatNavSearch ( dir , candidates , container ) ;
1218+ if ( bestNextTarget ) {
1219+ if ( findCandidate ) {
1220+ return spatNavCandidates ( eventTarget , dir , candidates , container ) ;
1221+ } else {
1222+ return bestNextTarget ;
1223+ }
1224+ }
1225+ }
1226+
1227+ // If there isn't any candidate and the best candidate among candidate:
1228+ // 1) Scroll or 2) Find candidates of the ancestor container
1229+ // 8 - if
1230+ else if ( canScroll ( container , dir ) ) {
1231+ if ( findCandidate ) {
1232+ return [ ] ;
1233+ } else {
1234+ bestNextTarget = eventTarget ;
1235+ return bestNextTarget ;
1236+ }
1237+ } else if ( container === document || container === document . documentElement ) {
1238+ container = window . document . documentElement ;
1239+
1240+ // The page is in an iframe
1241+ if ( window . location !== window . parent . location ) {
1242+
1243+ // eventTarget needs to be reset because the position of the element in the IFRAME
1244+ // is unuseful when the focus moves out of the iframe
1245+ eventTarget = window . frameElement ;
1246+ container = window . parent . document . documentElement ;
1247+ }
1248+ else if ( findCandidate ) {
1249+ return [ ] ;
1250+ } else {
1251+ return null ;
1252+ }
1253+
1254+ parentContainer = container . getSpatnavContainer ( ) ;
1255+ }
1256+ else {
1257+ // avoiding when spatnav container with tabindex=-1
1258+ if ( isFocusable ( container ) ) {
1259+ eventTarget = container ;
1260+ }
1261+
1262+ container = parentContainer ;
1263+ parentContainer = container . getSpatnavContainer ( ) ;
1264+ }
1265+ }
1266+
1267+ if ( ! parentContainer && container ) {
1268+ // Getting out from the current spatnav container
1269+ const candidates = filteredCandidates ( eventTarget , container . focusableAreas ( ) , dir , container ) ;
1270+
1271+ // 9
1272+ if ( Array . isArray ( candidates ) && candidates . length > 0 ) {
1273+ bestNextTarget = eventTarget . spatNavSearch ( dir , candidates , container ) ;
1274+
1275+ if ( bestNextTarget ) {
1276+ if ( findCandidate ) {
1277+ return spatNavCandidates ( eventTarget , dir , candidates , container ) ;
1278+ } else {
1279+ return bestNextTarget ;
1280+ }
1281+ }
1282+ }
1283+ }
1284+
1285+ if ( canScroll ( container , dir ) ) {
1286+ bestNextTarget = eventTarget ;
1287+ return bestNextTarget ;
1288+ }
1289+ }
1290+
1291+ window . __spatialNavigation__ = {
1292+ isContainer : isContainer ,
1293+ findCandidates : findTarget . bind ( null , true ) ,
1294+ findNextTarget : findTarget . bind ( null , false ) ,
1295+ getDistanceFromTarget : ( element , candidateElement , dir ) => {
1296+ if ( ( isContainer ( element ) || element . nodeName === 'BODY' ) && ! ( element . nodeName === 'INPUT' ) ) {
1297+ if ( element . focusableAreas ( ) . includes ( candidateElement ) ) {
1298+ return getInnerDistance ( element . getBoundingClientRect ( ) , candidateElement . getBoundingClientRect ( ) , dir ) ;
1299+ }
1300+ }
1301+ return getDistance ( element . getBoundingClientRect ( ) , candidateElement . getBoundingClientRect ( ) , dir ) ;
1302+ } ,
1303+
1304+ setKeyMode : ( option ) => {
1305+ if ( [ 'SHIFTARROW' , 'ARROW' , 'NONE' ] . includes ( option ) ) {
1306+ spatialNaviagtionKeyMode = option ;
1307+ } else {
1308+ spatialNaviagtionKeyMode = 'ARROW' ;
1309+ }
1310+ } ,
1311+
1312+ getKeyMode : ( ) => spatialNaviagtionKeyMode
1313+ } ;
1314+ }
1315+ addNonStandardAPI ( ) ;
1316+
1317+ } ) ( window , document ) ;
0 commit comments