jamesshore / quixote

CSS unit and integration testing
Other
848 stars 45 forks source link

Add CSS clip support #21

Closed woldie closed 6 years ago

woldie commented 8 years ago

I'd like to be able to assert against the CSS clip rectangle edges on an Element the way that assertions can be made against an Element's top/bottom/left/right positions. Here's the MDN link with a description of clip: https://developer.mozilla.org/en-US/docs/Web/CSS/clip

I want to be able to assert against clip because it is a useful style rule for implementing legacy-browser compatible effects like

and all without having to resort to any browser specific hacks.

I've got a pull request open with in-progress code to implement CSS clip that I will continue to refine: https://github.com/jamesshore/quixote/pull/20

jamesshore commented 8 years ago

Can you provide an example or two of how this might be used? I'm having trouble visualizing it. Do you want the clip assertions to be about screen-relative positions or element-relative positions?

woldie commented 8 years ago

EDITED: My clip rule below read "clip: rect(15px 0 0 0)", but it should have read "clip: rect(15px auto auto auto)"


In terms of Quixote I think an applied clip style should be reasoned about like an alternate top/bottom/left/right. A clip rectangle's edges are relative to the top/left of the element, but to make quixote assertions function properly, we should normalize clip's edges the same as element's top/bottom/left/right. I am hoping to do interesting quixote assertions like this:

myElement.assert({
  clip: {
    top: myElement.top.plus(15)
  }
});

Imagine this assertion confirms that the top 15px of myElement will not be painted, which was my intended effect of clip: rect(15px auto auto auto). clip does nothing positionally to an element, it only clips the painting of an element from its edges.

It struck me as I was sitting on the chair where I do all my best thinking that clip works like a partial visibility: hidden, if that helps you visualize it at all. Where they differ is that clip only applies to display: absolute or fixed elements.

jamesshore commented 8 years ago

Okay, so if you had a sprite sheet, you might use it like so? (Granted, you'd probably use background-position instead.)

sprite.assert({
  clip: {
    // only display one sprite
    left: spriteContainer.left,
    right: spriteContainer.right
  },
  // Display the third sprite
  left: spriteContainer.left.minus(spriteContainer.width.times(2))
}

Should we have clip.width and clip.height as well?

Can you provide some more realistic use cases?

woldie commented 8 years ago

It's actually the container that would have the clip applied. Here, I made an illustrated jsfiddle for you to play with: http://jsfiddle.net/uqmxu8x3/

As you can see, just setting the clip simply masks off parts of the container from being painted. Like visibility: hidden, the element still has a bounding box.

jamesshore commented 8 years ago

Thanks. I'm still interested in some real-world use cases of how this is used and what the tests would look like.

woldie commented 8 years ago

Ok, I did some more ducking of the subject. You can also do clipping of an element using overflow: hidden. Overflow: hidden is interesting for clipping purposes because the bounding box of the markup is also clipped, unlike css clip, which only masks the painting. Since overflow: hidden with setting width and height on an element covers most standard clipping use cases, this tells me that css clip is most useful for wipes or partial reveals where you don't want to muck with an element's overall width/height layout in order to mask off part/all of the element from being painted - something I could see would be useful when developing a UI or in videogame sprite use cases.

Interesting links: http://briantree.se/use-css-clip-property/ http://www.kirupa.com/html5/clipping_content_using_css.htm http://tympanus.net/codrops/2013/01/16/understanding-the-css-clip-property/

That last link gives a list of usecases where clip applies:

The last two seem to me to be the killer apps of clip. I gave you a demo of the css sprites. For accessibility, you'll see clip used in the widely used .visuallyhidden css class in the HTML5 boilerplate library. .visuallyhidden completely visually hides an element, while keeping it inline with surrounding content and keyboard focusable, which is super important for developing accessible forms on web pages with styled input tags. .visuallyhidden even has a neat .focused mode whereby you can make the visually hidden element briefly appear when the user has it focused with the keyboard. I could see a lot of keyboard focus testing in conjunction with Quixote asserts in that case to make sure the styles are all working.

radioButton.toDomElement().blur();

radioButton.assert({
  clip: {
    width: 0,
    height: 0
  }
}, "unfocused radio buttons have visuallyhidden mixin applied");

radioButton.toDomElement().focus();

radioButton.assert({
  clip: "auto"
}, "Focused radio buttons are visuallyhidden and not clipped");

I think we'll need a special way to test whether clip is disabled (all of clip is "auto" as shown in my last assert).

jamesshore commented 8 years ago

Thanks, that's helpful. So if I understand you correctly, you're proposing that top, right, etc. correspond to the visible edges of the element's content, relative to the top-left corner of the page.

So the following element:

<div style="
  position: fixed; 
  top: 200px; left: 100px;
  width: 100px; height: 100px;
  border: solid 5px;   // clip starts INSIDE the border
  clip: rect(10px, 40px, 30px, 20px);
">element</div>

would satisfy the following assertion:

element.assert({
  clip: {
    top: 215, bottom: 235, height: 20,
    left: 125, right: 145, width: 20
  }
});

Is that correct?

If that works, then we don't need any special handling for auto.

1) clip:auto is the same as no clipping at all.

<div style="
  position: fixed; 
  top: 200px; left: 100px;
  width: 100px; height: 100px;
  border: solid 5px;   // clip starts INSIDE the border
  clip: auto;
">element</div>
element.assert({
  clip: {
    top: 200, bottom: 300, height: 100,
    left: 100, right: 200, width: 100
  }
});

2) clip: rect(auto, auto, auto, auto) clips to the inside border of the element. EDIT: Or maybe not. See next comment.

<div style="
  position: fixed; 
  top: 200px; left: 100px;
  width: 100px; height: 100px;
  border: solid 5px;   // clip starts INSIDE the border
  clip: rect(auto, auto, auto, auto);
">element</div>
element.assert({
  clip: {
    top: 205, bottom: 295, height: 90,
    left: 105, right: 195, width: 90
  }
});
jamesshore commented 8 years ago

Actually, regarding the border, MDN says clip is relative to inside border edge. But my JSFiddle tests showed otherwise. So clip: rect(auto, auto, auto, auto) should behave the same as case 1) above, unless I've misunderstood something somewhere.

woldie commented 8 years ago

So if I understand you correctly, you're proposing that top, right, etc. correspond to the visible edges of the element's content, relative to the top-left corner of the page.

Is that correct? If that works, then we don't need any special handling for auto.

Yes, I believe you have this correct. If you have "auto" as any of the edge values in the rect expression, then you should not need any special handling. element.top == element.clip.top when clip: rect(auto 1px 2px 3px) is set, for example.

Actually, regarding the border, MDN says clip is relative to inside border edge. But my JSFiddle tests showed otherwise. So clip: rect(auto, auto, auto, auto) should behave the same as case 1) above, unless I've misunderstood something somewhere.

I found that "inside" language in the MDN article confusing, but maybe I was reading into it too much. Looking at the CSS spec's clipping section: http://www.w3.org/TR/CSS2/visufx.html#clipping they say, "In CSS 2.1, the only valid value is: rect(, , , ) where and specify offsets from the top border edge of the box, and , and specify offsets from the left border edge of the box." That seems to jive more with my fiddling.

clip:auto is the same as no clipping at all.

But I think your assertion is wrong there. My understanding is, when clip is disabled on a box, there are cases where the children of that box can bust out of the bounding box of the parent. With clip: rect, you can crop those children from being painted (even though they still bust out bounding box-wise.) With overflow: hidden you crop their painting to the parent's bounding box and the children's bounding boxes do not bust out. I'll try to make a fiddle to show this effect.

woldie commented 8 years ago

Ok, here we go: http://jsfiddle.net/9y0xnpza/

The first example shows a position: absolute inside a position: absolute container. This shows a case where the bounding box of the inner will not be constrained by the container and rather than busting the bounding box of the container out, the child simply busts through.

The second example is clip: rect enabled, notice the versatility of being able to set the clip rectangle outside the bounding box of the container and partially clip the child.

The third example is overflow: hidden. The versatility here lies in the styling rather than the clipping edges which must be the border box. Border-radius applies to the clipped child.

The fourth example is overflow: hidden and clip. Overflow: hidden trumps clip, so you can't clip outside the parent's border box, however, you can clip into the border box, which I am doing on the right.

jamesshore commented 8 years ago

@woldie Are you sure you shared the right fiddle? I only see the first example.

woldie commented 8 years ago

@jamesshore whoops, pilot error: http://jsfiddle.net/9y0xnpza/1/

jamesshore commented 8 years ago

Okay, that clears things up nicely, and I think it's a good example of the tests the code will need to pass.

I think we'll need the following descriptors:

I like your use of clip: { top: etc } in the assert() call. We'll also need code to support that.

I think I'd like to take each of these as a separate pull request. (Well, except center and middle, which can come along with the ElementClipEdge if you want.)

woldie commented 8 years ago

Okay, let me get asserts all working right with ElementClipEdge top, bottom, left, and right. And then we can look at making separate PR's for ElementClipSize and Center.

Agree we'll need to make some changes to QElement.assert() to make the nested descriptors work properly. I'll want a descriptor for the disablement of clip, something like ElementClipAuto. That would cover this assertion:

myElement.assert({
    clip: "auto"
});

That's basically asserting that clip is not enabled on myElement. The clip enabled alternative is to assert on the child descriptors of clip, like this:

myElement.assert({
    clip: {
        left: 20px,
        right: 20px
    }
}); 

Clip's value in the assert can either be an object or a string, and QElement.assert() will need to be able to discern when to engage the ElementClipAuto or the nested ElementClipEdge descriptors by the clip value's type.

jamesshore commented 8 years ago

I'd rather not support the clip: "auto" case, but instead use the second alternative you described (without the "px", as positions are always pixel values).

The reason: descriptors provide a user-focused abstraction over CSS. They aren't meant to replicate the CSS, but instead to provide a view into the results created by the CSS. In this case, "auto" isn't a result, it's an implementation detail. The result we're asserting on is "what part of the element is visible on the page, and where."

jamesshore commented 8 years ago

Other than that, I agree that ElementClipEdge is the right place to start. Everything's easy once that's working. (In fact, I think there may be an option to have just one size descriptor class, similar to the way we have just one Center class.)

woldie commented 8 years ago

I see what you're saying about using numbers for literals, so the following would be the right way to assert:

myElement.assert({
    clip: {
        left: 20,
        right: 20
    }
}); 

Regarding testing for not being clipped, I think you need a way to assert for "not clipped" since the clip rect can be at, inside, or outside the border box edges - there's no integer literal that could express "not clipped". That said, your point about "auto" being an implementation detail is right on. It's confusing and auto says nothing about what the effect will be.

If I have convinced you that "not clipped" is a thing, then I think we should have something that lets us assert something is not clipped. Either boolean false, or some quixote.NOT_CLIPPED symbolic constant. For example,

myElement.assert({
    clip: false
}, "My Element is not clipped"); 
jamesshore commented 8 years ago

Regarding testing for not being clipped, I think you need a way to assert for "not clipped" since the clip rect can be at, inside, or outside the border box edges - there's no integer literal that could express "not clipped".

The whole point of descriptors is to abstract these sorts of hard problems--to express what's actually visible to the user on the page. "Not clipped" has to have some sort of visual effect, and that's what the descriptor should represent.

I'm thinking that the descriptor should represent "the element's visible content area." And the numbers should be Position objects, which are relative to the top-left corner of the page. So, to build on your fiddle: http://jsfiddle.net/bsLL2j9c/2/ we'd get these assertions:

containerWithAutoClip.assert({ clip: { top: 25, bottom: 150, left: 0, right: 125 });

containerWithClip.assert({ clip: { top: 50, bottom: 150, left: 150, right: 265 });

containerWithOverflowHidden.assert({ clip: { top: 50, bottom: 150, left: 300, right: 400 });

containerWithBoth.assert({ clip: { top: 50, bottom: 150, left: 450, right: 525 });
woldie commented 8 years ago

The whole point of descriptors is to abstract these sorts of hard problems--to express what's actually visible to the user on the page. "Not clipped" has to have some sort of visual effect, and that's what the descriptor should represent.

I think I understand what you're getting at here: your descriptors are meant to detect and normalize applied style states to an element in-context. Your abstraction would be broken if we try to represent the absence of a state.

I think our ace in the hole here in dealing with "not clipped" could be the exception I'm throwing in ElementClipEdge.value(). If you try to get the clip edge value on an element that does not have a clip style set, (or clip: auto is set), then ElementClipEdge.value() throws a ClipNotAppliedException because there is no clip value to get - there is no clip style applied and no clipping performed.

A generalized form for ClipNotAppliedException for all of CSS might be called "StyleImpossible". We could formalize an expected value in assertions.js that would expect those exceptions.

Some styles could have StyleImpossible states and some not. For example, styles like 'background-image' or 'clip' can have states that do not have computable (nor visible) effects. In the case of 'background-image', a value of 'none' basically does nothing on the page, and 'none' is the default value for that style property. Similarly with 'clip', 'auto' is a do-nothing value. Any descriptor written for a style that has a 'none' setting that does not have a visible effect could be eligible for throwing a StyleImpossible if it meets our do-nothing criteria.

I suspect most CSS style properties don't have a 'none' setting. 'background-color's default value is 'transparent', which is a computable style (transparent computes to "rgba(0,0,0,0)") so 'background-color' should always be computable whether the user sets something or not.

Okay ... So, what if we had a way of expressing in the expected value object passed to Assertable.assert() that "we expect the descriptor would fail here if the value() was requested". Sortof like Chai's expect(ftn).to.throw() clause, but perhaps simpler:

myElement.assert({
    clip: quixote.UNSET_STYLE
});

or maybe even:

myElement.assert({
    clip: undefined
});

where Assertable.assert() would try to access all descriptor .value()'s at and below that property in the element, and if any descriptor throws a StyleImpossible, then that expected value passes the assertion. How does this approach sound?

woldie commented 8 years ago

I'm thinking that the descriptor should represent "the element's visible content area." And the numbers should be Position objects, which are relative to the top-left corner of the page. So, to build on your fiddle: http://jsfiddle.net/bsLL2j9c/2/ we'd get these assertions:

Agreed, my latest pull request implementing assert for clip should pass your assertion examples.

jamesshore commented 8 years ago

No special assertion for auto is needed. This fiddle should clear things up:

http://jsfiddle.net/bsLL2j9c/4/

jamesshore commented 8 years ago

Oh, we should also account for the overflow property, as that also clips. (Are there any other CSS properties we need to worry about?)

woldie commented 8 years ago

I think this line from your fiddle is making an assumption that is not correct:

var autoBox = auto.getBoundingClientRect();

The bounding client rect is not the clip rect for #auto because clip is not set at all when clip == auto.

If you want the clip rect to equal the bounding client rect, you would use clip: rect(auto, auto, auto, auto)

To recap - clip: auto is the absence of a clipping shape, and clip: rect(auto, auto, auto, auto) puts a clipping rect on the bounding client rect.

Here's your fiddle with clip applied: http://jsfiddle.net/n6snz6e4/1/

jamesshore commented 8 years ago

Okay, @woldie and I talked on the phone. We've decided to rename the descriptor to visible (visible.top, visible.right, etc.), and it will be the intersection of the CSS properties display, clip, and visible. The descriptor represents the visual boundaries of the element and its sub-elements... that is, if there are no clear pixels, everything that is painted.

This means that, in the case of clip: auto, the descriptor will be the union of the element's visible area and all sub-elements' visible areas.

Open questions: What do we do when the element is not visible at all? For example, visibility: hidden, display: none, or clip: rect(0, 0, 0, 0). Should those cases all be treated the same, or should we distinguish between "invisible" and "not displayed"? Do we need the Position value object to have a "none" state? (Size would be zero when the element is hidden, I think.)

woldie commented 8 years ago

We've decided to rename the descriptor to visible

Heheh, "rename" is a gentle way of putting it. Jim explained that we are not bound to any 1:1 relationship between the descriptors and CSS properties - the descriptors should compute useful facts that could be a rollup of many CSS properties like "the rectangular area of the page over which an element and its descendants draw." And, the clip CSS property is one of many that controls an element's visibility. So we're going to try to roll-up all the techniques used to control visibility into a single visible quixote descriptor.

Scanning the CSS reference on MDN, I see the following properties all may place certain constraints on element visibility:

Other considerations:

jamesshore commented 8 years ago

Nice summary. I've bolded the ones that seem deserving of extra discussion.

woldie commented 8 years ago

overflow: auto, visible, and hidden creates something a bit like an iframe ... there's inner and outer styles we can compute. Looking at your fiddle with Firebug, the scrollTop/scrollLeft, scrollWidth/scrollHeight, and width/height of the element can tell us if a child's content is clipped at all. In the case of your inner tag, Firebug is computing correct looking numbers for scrollWidth (162) and scrollHeight (152). It's a hairy problem for sure, and I wonder if we'd get consistent results cross-browser related to overflow calcs in complex cases.

Some of the esoteric paint concerns created by transitions/transforms and clip-path require different testing methodologies in my opinion. See this video about react-motion and see what that genius is thinking about time and animations in the browser: https://www.youtube.com/watch?v=1tavDv5hXpo I personally think it's safe to put a lot of this stuff out of scope, I just wanted to callout the crazy number of influences that can weigh on an element when it does layout and rendering.

I had some other thoughts that I was putting down when I saw your comment. They're probably less/not relevant given your feedback on my list, but rather than throw em away, I'll leave them here and maybe they'll spark more thoughts:


After reading what I wrote post-coffee and lunch, I think perhaps the platonic vision of quixote visible might be a tough nut to crack. To me, asserting on how an element "flows" is a distinctly different animal than asserting on how it "paints". And given the tools we have at our disposal, it seems to me "flow" can be computed a lot more precisely than "paint" - at least in terms of regions affected.

Also, the min/max of all rects might be interesting from an assertions POV, but it might be good to let the user have access to all the individual, non-overlapping rects of an element and its descendants that we can compute.

jamesshore commented 8 years ago

Right, scrollWidth and scrollHeight. I forgot about those. I used them in the PageSize descriptor to good effect. I think we can make those work cross-browser.

With that, I think we can make a good-enough version of the visible descriptor. We'll have to throw "not implemented" for clip-path and transform (and maybe transition), but the rest seems doable.

Here's a straw-man for what we'll put in the descriptor docs:


Element Visibility

Stability: 1 - Experimental

Descriptors for the visible boundaries of an element are available on QElement instances. These descriptors allow you to make assertions about where the element, including its sub-elements, is actually rendered on the page. Transparent pixels are considered to be "visible," so opacity, presence or absence of content, and so forth don't affect these descriptors.

The CSS properties that influence visibility are display, visibility, overflow, clip, and clip-path. However, clip-path is not supported at this time and will cause an exception to be thrown. (Future versions of Quixote could silently ignore clip-path instead. If you have an opinion about whether Quixote should thrown an exception vs. failing silently, please comment on issue #xx.)

These descriptors are distinct from the above Element Position descriptors in that they reflect how an element is rendered, not just where its bounding box is located. For example, if text overflows the bottom of an element's bounding box, element.visible.bottom will reflect that fact, but element.bottom will not.

Example: TBD (Need good example use case)

jamesshore commented 8 years ago

In writing up that straw-man documentation, I realized that including sub-elements is inconsistent with our other descriptors. What do you think about visible accounting just for the parent element, not its sub-elements?

Handling sub-elements seems like something we could do as a general case at some later date. This goes along with what you said in your "post-lunch" comment. It seems like including subelements would be applicable to all descriptors, not just visible. For example, we might want to be able to say element.all().top to find the top edge of an element's bounding box (including sub-elements).

woldie commented 8 years ago

In writing up that straw-man documentation, I realized that including sub-elements is inconsistent with our other descriptors. What do you think about visible accounting just for the parent element, not its sub-elements?

I'm concerned about sub-elements blowing up in our faces as well. Particularly because of bizarre CSS features like position: absolute which takes an element out of document flow. It might as well not have its parent node except for the fact that some styles still cascade. So, yeah, I think visible should just be for an element, not min/max'ed to its children.

I do like your future feature idea of adding an explicit .all() recursion to visible in order to specify to quixote that you want it to try to min/max an element with all its children. If you're going to do that though, consider nesting QElement's top/bottom/left/right under a visible-like group, named something like bounds or flow ... so that you can give it your .all(). :smirk_cat: (terrible puns, huzzah!)

I believe opacity: 0 is used interchangeably with visibility: hidden, so I don't agree with your excluding it from visible. opacity: 0 has the interesting effect of being not rendered, but still completely interactive. I had to use opacity: 0 in kidcompy to hide a giant textarea tag that blanketed the entire compy display, but was still able to catch all keyboard and mouse clicks and copy-pastes. I think it would be safe to treat filter: opacity(0%) as a synonym for opacity: 0, but perhaps we could throw if both opacity and filter: opacity are set (because wtf?)

How about this for your introductory paragraphs including opacity: 0?

Descriptors for the visible boundaries of an element are available on QElement instances. These descriptors allow you to make assertions about the region where the element is actually rendered on the page. An element's visible boundaries defaults to its bounding box. An element's rendered pixel contents, background colors, colors, etc are considered to be "visible" even if those pixels are drawn fully transparent. Secondary rendering effects introduced by other CSS styles that would disable the element or turn pixels fully transparent will cause the visible boundaries that these descriptors compute to shrink. For example, clip: rect(...) may shrink the visible boundaries of an element from its bounding box and display: none or opacity: 0 will drive the visible boundaries of an element to zero.

The CSS properties that influence visible are display, visibility, overflow, clip, opacity, filter, and clip-path. clip-path is not supported at this time and will cause an exception to be thrown. (Future versions of Quixote could silently ignore clip-path instead. If you have an opinion about whether Quixote should thrown an exception vs. failing silently, please comment on issue #xx.) For filter: opacity and opacity, only values of 0 will influence visible, but Quixote will throw an exception if both styles are detected on an element.

I agree with you throwing when clip-path is set, it's too hard to work into an accurate visible check as you described it.

jamesshore commented 8 years ago

@woldie Sorry not to respond on this. I agree with your changes--please carry on :-)

woldie commented 8 years ago

Ok cool. I have been, slowly but surely.

I'm thinking we need something like an exceptional value as described in the CHECKS pattern in order to express a non visible edge or size. Then I can get rid of my exception throwing, which is nasty. Have you got any thoughts on how we might implement that?

jamesshore commented 8 years ago

Yep, that's my thinking too. Position is the right place to do that... you could either create a "no value" state, perhaps with a "NoPixels" class (like the Pixels class), or you could subclass Position a la the Null Object pattern.

I think Size should be zero when an element is off-screen or invisible... no exceptional value needed.

woldie commented 8 years ago

I'm underway with the ElementVisibleEdge and ElementVisibleSize code. The NoPixels stuff is mostly in, and it's working the way we discussed.

Here's a question: why are you adding the scrollX and scrollY to the ElementEdges? I don't understand what the scroll position of the frame has to do with the ElementEdge. To me, the document floats in the visible viewport of the frame and the document's scroll positions won't weigh on the positions of elements unless you've got position: fixed.

jamesshore commented 8 years ago

It's because the getRawPosition() provides a position relative to top-left of the viewport, not the top-left of the page. If the frame has been scrolled, the results will be wrong unless we add in the scroll position. See the last test in _element_edge_test.js.

jamesshore commented 8 years ago

With the v0.12 release, we now have a QElement.rendered descriptor, which should make this easier. I think we can pick this up again.

jamesshore commented 7 years ago

I'm starting to implement this feature. This will be our tracking comment.

Scenarios to handle:

Descriptors to implement:

Edit: Opacity, filter, and visibility aren't included in this descriptor. We've repurposed this descriptor to just be about whether the element is rendered, even if it's rendered as completely transparent pixels.

In other words, this descriptor tells us what the paintable edges of the element are, not whether anything's painted in the element or not.

jamesshore commented 7 years ago

@woldie, Did I forget anything above?

woldie commented 7 years ago

So clip: rect affects only painting, like a partial visibility: hidden. And overflow: !visible clips the bounding box and content to width and height. scrollTop and scrollLeft affect what part of the clipped content appears.

So, so far, I like the idea of the visible descriptor. That is a nice testable abstraction over the CSS rules. I think we need a similar descriptor that characterizes the interior visible rect of an element of an element, maybe "interior" which is the width/height rect, offset by scrollLeft/scrollTop whenever overflow: !visible?

jamesshore commented 7 years ago

Hmm, yes, I think see what you mean. If 'visible' defines the box on the page that the element shows through, 'interior' is the portion of the element that's showing through? I could see that being useful for something like a sprite sheet.

Interior doesn't seem like the right term for it, though. Also, that should probably be a new issue.

jamesshore commented 7 years ago

I'm currently working on overflow and I think it's pretty tricky. Here's all the cases I can think of. By 'clipped overflow' I mean overflow other than 'visible', like overflow:hidden. @woldie, am I missing any?

woldie commented 7 years ago

interior, interiorVisible, or whatever you choose to call it completes the visibility concept because it answers half of the "what" that is visible.

I suppose "bounding box" is a distinct part of that too, and there is some complexity in computing that too. However, I've come to your way of thinking that visibility is far more interesting to test for than bounding box and it'd be low on my priorities list if you did want to take that on.

I agree 'interior' is a separate ticket.

woldie commented 7 years ago

Yes, very tricky. I think you have the gist of the testing there. Ancestry will form a lot of the complexity for your visibility check. You need to factor in overflow, but also scrollLeft/scrollTop and position.

On position: I believe the way it works is that a position: absolute|fixed takes an element out of the clipping influence of its ancestors (but not positioning.) So, recursively intersecting of an element's visible rect with its ancestor's visible rects should stop at the closest position: absolute|fixed ancestor; that's the only odd case to test for that I can think of right now for position.

On scrollLeft/scrollTop, you have to look at whether an element's ancestor has got overflow: !visible and then translate the element's visible rect by the ancestor's scrollLeft/scrollTop so that you can account for a change to the clippage.

woldie commented 7 years ago

Oh, and to make things even more interesting, the presence of a vertical scrollbar in an overflow: !hidden element is even more complicated ... on some browsers, children of such elements lose width to the scrollbar (when relative sizing) and some do not (Mac, mobile browsers).

Having fixed width children in a overflow: !hidden parent can lead to some sad float: right'ing of those children that you might not notice when doing your primary development on a Mac.

jamesshore commented 7 years ago

As I get into the implementation of this, it looks like visible may not be the right descriptor name after all. I'm thinking we actually have the following:

This is going back towards @woldie's original proposal for a clip descriptor a bit (although it includes overflow and his didn't). The reason I'm thinking it might be a good idea to separate visible and clipped is that visibility and clipping both affect layout differently.

jamesshore commented 7 years ago

Re the scrollbar, I don't think it affects the implementation of this descriptor?

woldie commented 7 years ago

Regarding the scrollbar, for the visible descriptor it does because the scrollbars act like a clip right/bottom when the child is not relatively sized (meaning, the scrollbars visually obscure some of the child, depending on the platform).

With some DOM trickery, you can measure the height/width of the scrollbars that may appear for overflow: !hidden to detect how the current platform behaves. On Mac, it's 0px and on Windows 2000 it's ~30px.

I think just getting visible descriptor to work, as we defined, it has the most applicable value. The way I would define the visible descriptor would be something like: The rectangular area of an element that is visible and painted in the context of all ancestor elements, and then translated to absolute coordinates from the top/left of the page.

Something like 'interior' would describe how the overflowed element is scrolled. So you could somehow assert parent.interior.top == child.visible.top

jamesshore commented 7 years ago

@woldie said:

I believe the way it works is that a position: absolute|fixed takes an element out of the clipping influence of its ancestors (but not positioning.) So, recursively intersecting of an element's visible rect with its ancestor's visible rects should stop at the closest position: absolute|fixed ancestor; that's the only odd case to test for that I can think of right now for position.

I looked into this (see fiddle) and it looks like you're mistaken on this point. Or I've misunderstood you. As the fiddle shows, the child element is clipped by the grandparent, even though the child and parent are both position:absolute.

You're correct on position:fixed, though, it seems like that removes the element from the influence of ancestors' overflow setting entirely.

jamesshore commented 7 years ago

Or maybe you were talking about the effect of position:absolute on clip? I haven't gotten that far yet.

jamesshore commented 7 years ago

Okay, I think I've covered all the cases for overflow. I've pushed the code to Github, @woldie can you take a look? It's in src/descriptors/element_visible_edge.js.