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

Ignore non-rendered elements #11

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
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
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
Loading