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

Fix toBeVisible visibility #428

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Fix toBeVisible visibility #428

wants to merge 2 commits into from

Conversation

gnapse
Copy link
Member

@gnapse gnapse commented Jan 13, 2022

What:

Fixes #209 (comment)

Why:

Because .toBeVisible(element) is not consistent with how CSS visibility works.

How:

By fixing the logic that calculates how visibility is determined. Enough details are present in the new code in the form of comments explaining the logic.

Checklist:

  • Documentation
  • Tests
  • Ready to be merged

@gnapse gnapse requested a review from eps1lon January 13, 2022 22:38
@gnapse gnapse self-assigned this Jan 13, 2022
@gnapse gnapse changed the title Fix is visible visibility Fix toBeVisible visibility Jan 13, 2022
* 1. One set of properties allow parent elements to fully controls its sub-tree visibility. This
* means that if higher up in the tree some element is not visible by this criteria, it makes the
* entire sub-tree not visible too, and there's nothing that child elements can do to revert it.
* This includes `display: none`, `opacity: 0`, the presence of the `hidden` attribute`, and the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that you can have <div hidden style="display: block" /> which will visible but may be excluded from the a11y tree. It should be documented if display or hidden has priority in your implementation.

* 1. One set of properties allow parent elements to fully controls its sub-tree visibility. This
* means that if higher up in the tree some element is not visible by this criteria, it makes the
* entire sub-tree not visible too, and there's nothing that child elements can do to revert it.
* This includes `display: none`, `opacity: 0`, the presence of the `hidden` attribute`, and the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't include opacity to be honest. 0.00001 will suddenly be considered visible? Opacity also has no relevance to a11y tree inclusion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but that's a separate discussion. Right now this is not new functionality, and I'm merely maintaining backward compatibility.

I agree that 0.0001 could be considered as such, but this was a compromise, because there's no arguing that opacity: 0 makes things not visible. toBeVisible was never about the presence of things in the a11y tree. It was more about actual visibility with your eyes.

I'll open an issue to discuss this. But I will not remove in this PR, as it will be a breaking change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I missed, that you already included opacity.

* This includes `display: none`, `opacity: 0`, the presence of the `hidden` attribute`, and the
* open state of a details/summary elements pair.
*
* 2. The other aspect influencing if an element is visible is the CSS `visibility` style. This one
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
* 2. The other aspect influencing if an element is visible is the CSS `visibility` style. This one
* 2. The other aspect influencing if an element is visible is the CSS `visibility` property. This one

That's how these are named officially i.e. in the spec.

* open state of a details/summary elements pair.
*
* 2. The other aspect influencing if an element is visible is the CSS `visibility` style. This one
* is also inherited. But unlike the previous case, this one can be reverted by child elements.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "also inherited" in this context? Neither display nor opacity are inherited CSS properties. The use of "inherited" is ambiguous in this context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "inherited" is not the best word. What I mean is that setting display: none, opacity: 0, or visibility: hidden in a parent element always hides the child elements too. But, unlike with the former two, the last one is reversible: child elements can make themselves visible over its parent's wish.

I'll reword to not use the word inherit to avoid confusion.

* Hence, the apprach taken by this function is two-fold: it first gets the first set of criteria
* out of the way, analyzing the target element and up its tree. If this branch yields that the
* element is not visible, there's nothing the element could be doing to revert that, so it returns
* false. Only if the first check is true, if proceeds to analyze the `visibility` CSS.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting trade-off. You're basically assuming that checking visibility is more expensive than walking up the tree and checking display etc on every parent.

How was this trade-off motivated?

In the other Testing Library implementations we're checking visibility on the element first since that allows us to potentially bail out of walking up the tree i.e.

 function isElementVisible(element) {
-  return isElementTreeVisible(element) && isElementVisibilityVisible(element)
+  return isElementVisibilityVisible(element) && isElementTreeVisible(element)
 }

Copy link
Member Author

@gnapse gnapse Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was honestly not thinking of performance, and the comment talks more about the logic we follow. Both branches walk up the tree, and either of these branches could bail out more quickly than the other. There's no way to tell.

Though the check for visibility is simpler, so maybe you're right that on average it could be faster. I'm ok with making this change.

Clarified below: #428 (comment)

function isVisibleSummaryDetails(element, previousElement) {
return element.nodeName === 'DETAILS' &&
previousElement.nodeName !== 'SUMMARY'
? element.hasAttribute('open')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question not directly related to this PR: Is the attribute or property relevant i.e. when I set someDetailsElement.open = false will element.hasAttribute('open') return true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly not sure. I did not even implement the support for summary/detail. It may be worth checking.

const {getComputedStyle} = element.ownerDocument.defaultView
const {visibility} = getComputedStyle(element)
return visibility || getElementVisibilityStyle(element.parentElement)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you walk up the tree here? visibility should never be falsy unless the user agent has an incorrect implementation of the CSS spec. visibility has an initial value of visible

@@ -1,33 +1,63 @@
import {checkHtmlElement} from './utils'

function isStyleVisible(element) {
function getElementVisibilityStyle(element) {
if (!element) return 'visible'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit odd. If the element is falsy it should be considered "visible"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, good point. This clarifies your point about bailing out quickly. I'll make the change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, for visibility you don't need to walk up since it the computed visbility of parents doesn't matter. only the computed visibility of the element asserted on matters.

This was relevant in earlier versions of JSOM where visibility wasn't inherited.

expect(subject).not.toBeVisible()
expect(() => expect(subject).toBeVisible()).toThrowError()
describe('with the "hidden" attribute', () => {
it('considers an element to not be visible', () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('considers an element to not be visible', () => {
it('considers an element as not visible', () => {

davikiyo added a commit to davikiyo/simple-ui-components that referenced this pull request Dec 4, 2022
Skipped the modal test as the matcher will be fixed in the future.
testing-library/jest-dom#428
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

toBeVisible not working as expected with visibility property
3 participants