Skip to content

Commit 812c5e6

Browse files
author
Jihye Hong
authored
Merge pull request w3c#110 from jeonghee27/non-standard-api
Make some non-standard APIs
2 parents bf547e8 + d49aa9a commit 812c5e6

File tree

2 files changed

+213
-24
lines changed

2 files changed

+213
-24
lines changed

demo/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
function polyfilEnableChanged() {
4747
let polyfilEnableSwitch = document.getElementById('polyfill-enable-switch');
4848
let polyfilStateDiv = document.getElementById('polyfill-state');
49-
window.spatnavPolyfillOff = !polyfilEnableSwitch.checked;
49+
50+
// toggle spatialNavigation
51+
window.__spatialNavigation__.setKeyMode(window.__spatialNavigation__.getKeyMode()==='NONE'? '':'NONE');
5052

5153
if(polyfilEnableSwitch.checked) {
5254
polyfilStateDiv.classList.remove('deactivate');

polyfill/spatnav-heuristic.js

Lines changed: 210 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
(function () {
1212

1313
// Indicates global variables for spatnav (starting position)
14-
let spatNavManager = {
14+
const spatNavManager = {
1515
startingPosition: null,
1616
useStandardName: true,
1717
};
@@ -32,6 +32,7 @@
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

@@ -53,11 +54,21 @@
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')
@@ -70,9 +81,6 @@
7081
spatNavManager.startingPosition = null;
7182
}
7283
}
73-
74-
if (e.keyCode === TAB_KEY_CODE)
75-
spatNavManager.startingPosition = null;
7684
});
7785

7886
/**
@@ -278,28 +286,24 @@
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
}
@@ -310,6 +314,24 @@
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
@@ -556,7 +578,7 @@
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
/**
@@ -984,7 +1006,7 @@
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) {
@@ -1127,4 +1149,169 @@
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

Comments
 (0)