Skip to content

Commit

Permalink
Ignore non-rendered elements
Browse files Browse the repository at this point in the history
  • Loading branch information
krksgbr committed Sep 17, 2024
1 parent 2956641 commit 75d8e41
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 2 deletions.
8 changes: 8 additions & 0 deletions cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,12 @@ describe("focus-shift spec", () => {
expect(before).to.not.equal(after)
})
})

it(
"ignores non-rendered elements",
testFor("./cypress/fixtures/non-rendered-elements.html", { className: "columns" }, [
{ eventType: "keydown", selector: "#first-button", options: keyevent({ key: "ArrowRight" }) },
{ eventType: "keydown", selector: "#last-button", options: keyevent({ key: "ArrowRight" }) }
])
)
})
42 changes: 42 additions & 0 deletions cypress/fixtures/non-rendered-elements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<style>
.opacityZero {
opacity: 0;
}
.displayNone {
display: none;
}
.visibilityHidden {
visibility: hidden;
}
.visibilityCollapse {
visibility: collapse;
}
.zeroWidth {
display: inline-block;
width: 0;
overflow: hidden;
}
.zeroHeight {
display: inline-block;
height: 0;
overflow: hidden;
}
</style>
</head>
<body>
<button id="first-button">First Button</button>
<button class="opacityZero">Transparent Button</button>
<button class="displayNone">Hidden Button</button>
<button class="visibilityHidden">Hidden Button</button>
<button class="visibilityCollapse">Hidden Button</button>
<span tabindex="1" class="zeroWidth">Hidden Element</span>
<span tabindex="1" class="zeroHeight">Hidden Element</span>
<button id="last-button">Last Button</button>
<script src="../../index.js"></script>
</body>
</html>
49 changes: 47 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function focusInitial(direction, container) {
.filter(hasTabIndex)
.filter((elem) => elem.tabIndex > 0)
const markedElement = getMinimumBy(tabindexed, (elem) => elem.tabIndex)
if (markedElement != null) {
if (markedElement != null && isBeingRendered(markedElement)) {
applyFocus(direction, makeVirtualOrigin(direction), markedElement)
return
}
Expand Down Expand Up @@ -137,7 +137,8 @@ function getFocusableElements(container) {
* - it is a descendant of an element marked with `data-focus-skip`,
* - it is a descendant of a closed `details` element,
* - it is `disabled`,
* - it is `inert`.
* - it is `inert`
* - it is not being rendered.
*
* Otherwise it counts as focusable.
*
Expand All @@ -162,9 +163,53 @@ function isFocusable(element) {
// Descends from closed details element
if (hasClosedDetailsAncestor(element)) return false

return isBeingRendered(element)
}

/**
* Decide whether an element is being rendered or not.
*
* An element is not being rendered if:
* 1. An element has the style "visibility: hidden | collapse" or "display: none". (Note: these are inherited.)
* 2. An element has the style "opacity: 0". (Somewhat of a white lie, as it will still affect layout.)
* 3. The width or height of an element is explicitly set to 0.
* 4. An element's parent is hidden.
*
* @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
* @function isBeingRendered
* @param element {Element}
* @returns {boolean}
*/

function isBeingRendered(element) {
if (element.parentElement) {
const parentStyle = window.getComputedStyle(element.parentElement, null)
if (hasHidingStyleProperty(parentStyle)) return false
}
const elementStyle = window.getComputedStyle(element, null)
if (
hasHidingStyleProperty(elementStyle) ||
elementStyle.getPropertyValue("width") === "0px" ||
elementStyle.getPropertyValue("height") === "0px"
)
return false
return true
}

/**
* Determine if a style declaration has any properties that make an element hidden.
* @function hasHidingStyleProperty
* @param style {CSSStyleDeclaration}
* @returns {boolean}
*/
function hasHidingStyleProperty(style) {
return (
style.getPropertyValue("display") === "none" ||
["hidden", "collapse"].includes(style.getPropertyValue("visibility")) ||
style.getPropertyValue("opacity") === "0"
)
}

/**
* Tests whether the element is contained within a closed `details` element.
*
Expand Down

0 comments on commit 75d8e41

Please sign in to comment.