Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for open shadow dom nodes #224

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 144 additions & 38 deletions polyfill/spatial-navigation-polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
*/
window.addEventListener('keydown', (e) => {
const currentKeyMode = (parent && parent.__spatialNavigation__.keyMode) || window.__spatialNavigation__.keyMode;
const eventTarget = document.activeElement;
const eventTarget = getActiveElement();
const dir = ARROW_KEY_CODE[e.keyCode];

if (e.keyCode === TAB_KEY_CODE) {
Expand Down Expand Up @@ -138,6 +138,111 @@
});
}

/**
* Gets the parent element for a node. If the node is a shadow dom element then the host is returned.
* @function getParent
* @param element {Node} - The element to get the parent for
* @returns {Node} The parent element
*/
function getParentElement(element) {
return element.parentElement || element.host || (element.parentNode && element.parentNode.host);
}

/**
* Gets the children for a node. If the node is a shadow dom element then the shadowRoot's children are returned.
* @function getChildren
* @param element {Node} - The element to get the children for
* @returns {Node} The parent element
*/
function getChildren(element) {
if (element.shadowRoot) {
return element.shadowRoot.children;
}
return element.children;
}

/**
* Gets the child element count for a node. If the node is shadow dom element then the shadowRoot's child count is returned.
* @function getChildElementCount
* @param element {Node} - The element to get the children for
* @returns {Node} The parent element
*/
function getChildElementCount(element) {
if (element.shadowRoot) {
return element.shadowRoot.childElementCount;
}
return element.childElementCount;
}

/**
* Checks if an element contains another element. If the element is a shadow dom element, the shadow root is checked.
* @function elementContains
* @param element {Node} - The element to search through
* @param n {Node} - The element search for
* @returns {Boolean}
*/
function elementContains(element, n) {
if (n === null) {
return false;
}
let el = element;
if (element.shadowRoot) {
if (element.shadowRoot.mode !== 'open') {
return false;
}
el = element.shadowRoot;
}
let ok = el.contains(n);
if (ok) {
return ok;
}
while (true) {
n = n.getRootNode({
composed: false
});
ok = el.contains(n);
if (ok || !n.host) {
return ok;
}
if (el.contains(n.host)) {
return true;
}
n = n.host;
}
}

/**
* Gets the active element. It will search recusively through shadow doms
* @function getActiveElement
* @returns {Node} The active element
*/
function getActiveElement() {
let ac = document.activeElement;
while (ac && ac.shadowRoot && ac.shadowRoot.activeElement){
ac = ac.shadowRoot.activeElement
}
return ac;
}


/**
* Gets element from point within another element. It will search recusively through shadow doms
* @param element {Node} - The containing element to search through
* @param x {Number} - The x coordinate
* @param y {Number} - The y coordinate
* @returns {Node} The active element
*/
function getElementFromPoint(element, x, y) {
let el = element.elementFromPoint(x, y);
if (el && el.shadowRoot && element !== el.shadowRoot) {
return getElementFromPoint(el.shadowRoot, x, y);
}
if (el && el.host) {
return getElementFromPoint(el.host, x, y);
}
return el;
}

/**
* Enable the author to trigger spatial navigation programmatically, as if the user had done so manually.
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-window-navigate}
Expand All @@ -156,7 +261,7 @@
// 2 Optional step, UA defined starting point
if (startingPoint) {
// if there is a starting point, set eventTarget as the element from position for getting the spatnav container
elementFromPosition = document.elementFromPoint(startingPoint.x, startingPoint.y);
elementFromPosition = getElementFromPoint(document, startingPoint.x, startingPoint.y);

// Use starting point if the starting point isn't inside the focusable element (but not container)
// * Starting point is meaningfull when:
Expand Down Expand Up @@ -192,8 +297,9 @@
let bestInsideCandidate = null;

// 5-2
if ((document.activeElement === searchOrigin) ||
(document.activeElement === document.body) && (searchOrigin === document.documentElement)) {
let activeElement = getActiveElement();
if ((activeElement === searchOrigin) ||
(activeElement === document.body) && (searchOrigin === document.documentElement)) {
if (getCSSSpatNavAction(eventTarget) === 'scroll') {
if (scrollingController(eventTarget, dir)) return;
} else if (getCSSSpatNavAction(eventTarget) === 'focus') {
Expand All @@ -212,7 +318,7 @@
// 6
// Let container be the nearest ancestor of eventTarget
container = eventTarget.getSpatialNavigationContainer();
let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;
let parentContainer = (getParentElement(container)) ? container.getSpatialNavigationContainer() : null;

// When the container is the viewport of a browsing context
if (!parentContainer && ( window.location !== window.parent.location)) {
Expand Down Expand Up @@ -278,7 +384,7 @@
}

// If the spatnav container is document and it can be scrolled, scroll the document
if (!container.parentElement && !isHTMLScrollBoundary(container, dir)) {
if (!getParentElement(container) && !isHTMLScrollBoundary(container, dir)) {
moveScroll(container.ownerDocument.documentElement, dir);
return true;
}
Expand All @@ -299,21 +405,21 @@
function getSpatialNavigationCandidates (container, option = {mode: 'visible'}) {
let candidates = [];

if (container.childElementCount > 0) {
if (!container.parentElement) {
if (getChildElementCount(container) > 0) {
if (!getParentElement(container)) {
container = container.getElementsByTagName('body')[0] || document.body;
}
const children = container.children;
const children = getChildren(container);
for (const elem of children) {
if (isDelegableContainer(elem)) {
candidates.push(elem);
} else if (isFocusable(elem)) {
candidates.push(elem);

if (!isContainer(elem) && elem.childElementCount) {
if (!isContainer(elem) && getChildElementCount(elem)) {
candidates = candidates.concat(getSpatialNavigationCandidates(elem, {mode: 'all'}));
}
} else if (elem.childElementCount) {
} else if (getChildElementCount(elem)) {
candidates = candidates.concat(getSpatialNavigationCandidates(elem, {mode: 'all'}));
}
}
Expand Down Expand Up @@ -367,12 +473,12 @@
const defaultContainer = targetElement.getSpatialNavigationContainer();
let defaultCandidates = getSpatialNavigationCandidates(defaultContainer);
const container = args.container || defaultContainer;
if (args.container && (defaultContainer.contains(args.container))) {
if (args.container && (elementContains(defaultContainer, args.container))) {
defaultCandidates = defaultCandidates.concat(getSpatialNavigationCandidates(container));
}
const candidates = (args.candidates && args.candidates.length > 0) ?
args.candidates.filter((candidate) => container.contains(candidate)) :
defaultCandidates.filter((candidate) => container.contains(candidate) && (container !== candidate));
args.candidates.filter((candidate) => elementContains(container, candidate)) :
defaultCandidates.filter((candidate) => elementContains(container, candidate) && (container !== candidate));

// Find the best candidate
// 5
Expand All @@ -383,7 +489,7 @@
// Divide internal or external candidates
candidates.forEach(candidate => {
if (candidate !== targetElement) {
(targetElement.contains(candidate) && targetElement !== candidate ? internalCandidates : externalCandidates).push(candidate);
(elementContains(targetElement, candidate) && targetElement !== candidate ? internalCandidates : externalCandidates).push(candidate);
}
});

Expand All @@ -392,8 +498,8 @@
let overlappedContainer = candidates.filter(candidate => (isContainer(candidate) && isEntirelyVisible(targetElement, candidate)));
let overlappedByParent = overlappedContainer.map((elm) => elm.focusableAreas()).flat().filter(candidate => candidate !== targetElement);

internalCandidates = internalCandidates.concat(fullyOverlapped).filter((candidate) => container.contains(candidate));
externalCandidates = externalCandidates.concat(overlappedByParent).filter((candidate) => container.contains(candidate));
internalCandidates = internalCandidates.concat(fullyOverlapped).filter((candidate) => elementContains(container, candidate));
externalCandidates = externalCandidates.concat(overlappedByParent).filter((candidate) => elementContains(container, candidate));

// Filter external Candidates
if (externalCandidates.length > 0) {
Expand Down Expand Up @@ -450,7 +556,7 @@
return candidates;

// Offscreen handling when originalContainer is not <HTML>
if (originalContainer.parentElement && container !== originalContainer && !isVisible(currentElm)) {
if (getParentElement(originalContainer) && container !== originalContainer && !isVisible(currentElm)) {
eventTargetRect = getBoundingClientRect(originalContainer);
} else {
eventTargetRect = searchOriginRect || getBoundingClientRect(currentElm);
Expand All @@ -464,15 +570,15 @@
if ((isContainer(currentElm) || currentElm.nodeName === 'BODY') && !(currentElm.nodeName === 'INPUT')) {
return candidates.filter(candidate => {
const candidateRect = getBoundingClientRect(candidate);
return container.contains(candidate) &&
((currentElm.contains(candidate) && isInside(eventTargetRect, candidateRect) && candidate !== currentElm) ||
return elementContains(container, candidate) &&
((elementContains(currentElm, candidate) && isInside(eventTargetRect, candidateRect) && candidate !== currentElm) ||
isOutside(candidateRect, eventTargetRect, dir));
});
} else {
return candidates.filter(candidate => {
const candidateRect = getBoundingClientRect(candidate);
const candidateBody = (candidate.nodeName === 'IFRAME') ? candidate.contentDocument.body : null;
return container.contains(candidate) &&
return elementContains(container, candidate) &&
candidate !== currentElm && candidateBody !== currentElm &&
isOutside(candidateRect, eventTargetRect, dir) &&
!isInside(eventTargetRect, candidateRect);
Expand Down Expand Up @@ -582,15 +688,15 @@
let container = this;

do {
if (!container.parentElement) {
if (!getParentElement(container)) {
if (window.location !== window.parent.location) {
container = window.parent.document.documentElement;
} else {
container = window.document.documentElement;
}
break;
} else {
container = container.parentElement;
container = getParentElement(container);
}
} while (!isContainer(container));
return container;
Expand All @@ -606,15 +712,15 @@
let scrollContainer = element;

do {
if (!scrollContainer.parentElement) {
if (!getParentElement(scrollContainer)) {
if (window.location !== window.parent.location) {
scrollContainer = window.parent.document.documentElement;
} else {
scrollContainer = window.document.documentElement;
}
break;
} else {
scrollContainer = scrollContainer.parentElement;
scrollContainer = getParentElement(scrollContainer);
}
} while (!isScrollContainer(scrollContainer) || !isVisible(scrollContainer));

Expand All @@ -634,7 +740,7 @@
* @returns {sequence<Node>} All focusable elements or only visible focusable elements within the container
*/
function focusableAreas(option = {mode: 'visible'}) {
const container = this.parentElement ? this : document.body;
const container = getParentElement(this) ? this : document.body;
const focusables = Array.prototype.filter.call(container.getElementsByTagName('*'), isFocusable);
return (option.mode === 'all') ? focusables : focusables.filter(isVisible);
}
Expand Down Expand Up @@ -750,7 +856,7 @@
* @returns {Node} The search origin for the spatial navigation
*/
function findSearchOrigin() {
let searchOrigin = document.activeElement;
let searchOrigin = getActiveElement();

if (!searchOrigin || (searchOrigin === document.body && !document.querySelector(':focus'))) {
// When the previous search origin lost its focus by blur: (1) disable attribute (2) visibility: hidden
Expand Down Expand Up @@ -806,7 +912,7 @@
* @returns {boolean}
*/
function isContainer(element) {
return (!element.parentElement) ||
return (!getParentElement(element)) ||
(element.nodeName === 'IFRAME') ||
(isScrollContainer(element)) ||
(isCSSSpatNavContain(element));
Expand Down Expand Up @@ -997,7 +1103,7 @@
function isFocusable(element) {
if ((element.tabIndex < 0) || isAtagWithoutHref(element) || isActuallyDisabled(element) || isExpresslyInert(element) || !isBeingRendered(element))
return false;
else if ((!element.parentElement) || (isScrollable(element) && isOverflow(element)) || (element.tabIndex >= 0))
else if ((!getParentElement(element)) || (isScrollable(element) && isOverflow(element)) || (element.tabIndex >= 0))
return true;
}

Expand Down Expand Up @@ -1052,7 +1158,7 @@
* @returns {boolean}
*/
function isBeingRendered(element) {
if (!isVisibleStyleProperty(element.parentElement))
if (!isVisibleStyleProperty(getParentElement(element)))
return false;
if (!isVisibleStyleProperty(element) || (element.style.opacity === '0') ||
(window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px'))
Expand All @@ -1067,7 +1173,7 @@
* @returns {boolean}
*/
function isVisible(element) {
return (!element.parentElement) || (isVisibleStyleProperty(element) && hitTest(element));
return (!getParentElement(element)) || (isVisibleStyleProperty(element) && hitTest(element));
}

/**
Expand Down Expand Up @@ -1131,8 +1237,8 @@
};

for(const point in hitTestPoint) {
const elemFromPoint = element.ownerDocument.elementFromPoint(...hitTestPoint[point]);
if (element === elemFromPoint || element.contains(elemFromPoint)) {
const elemFromPoint = getElementFromPoint(element.ownerDocument, ...hitTestPoint[point]);
if (element === elemFromPoint || elementContains(element, elemFromPoint)) {
return true;
}
}
Expand Down Expand Up @@ -1510,7 +1616,7 @@
function handlingEditableElement(e) {
const SPINNABLE_INPUT_TYPES = ['email', 'date', 'month', 'number', 'time', 'week'],
TEXT_INPUT_TYPES = ['password', 'text', 'search', 'tel', 'url', null];
const eventTarget = document.activeElement;
const eventTarget = getActiveElement();
const startPosition = eventTarget.selectionStart;
const endPosition = eventTarget.selectionEnd;
const focusNavigableArrowKey = {left: false, up: false, right: false, down: false};
Expand Down Expand Up @@ -1591,7 +1697,7 @@
function getExperimentalAPI() {
function canScroll(container, dir) {
return (isScrollable(container, dir) && !isScrollBoundary(container, dir)) ||
(!container.parentElement && !isHTMLScrollBoundary(container, dir));
(!getParentElement(container) && !isHTMLScrollBoundary(container, dir));
}

function findTarget(findCandidate, element, dir, option) {
Expand Down Expand Up @@ -1625,7 +1731,7 @@
// 6
// Let container be the nearest ancestor of eventTarget
let container = eventTarget.getSpatialNavigationContainer();
let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;
let parentContainer = (getParentElement(container)) ? container.getSpatialNavigationContainer() : null;

// When the container is the viewport of a browsing context
if (!parentContainer && ( window.location !== window.parent.location)) {
Expand Down Expand Up @@ -1657,7 +1763,7 @@
// is unuseful when the focus moves out of the iframe
eventTarget = window.frameElement;
container = window.parent.document.documentElement;
if (container.parentElement)
if (getParentElement(container))
parentContainer = container.getSpatialNavigationContainer();
else {
parentContainer = null;
Expand All @@ -1671,7 +1777,7 @@
}

container = parentContainer;
if (container.parentElement)
if (getParentElement(container))
parentContainer = container.getSpatialNavigationContainer();
else {
parentContainer = null;
Expand Down