Bug 1745310 part 1: Account for the impact of negative margins when determining how much space a float-avoiding box will need in order to fit alongside a float. r=emilio

Note: The WPT test included in this test is intended to excercise cases that
are (newly) interoperable between WebKit, Blink, and Gecko (with this patch).

There are other related cases where browsers still disagree; I'll add
additional WPT tests for those cases in a later patch in this series.

Differential Revision: https://phabricator.services.mozilla.com/D145159
This commit is contained in:
Daniel Holbert 2022-05-06 06:05:38 +00:00
Родитель c2bb02d580
Коммит 738efcb1cc
3 изменённых файлов: 275 добавлений и 0 удалений

Просмотреть файл

@ -7714,6 +7714,46 @@ nsBlockFrame::FloatAvoidingISizeToClear nsBlockFrame::ISizeToClearPastFloats(
aState.mReflowInput.mRenderingContext, wm,
aState.mContentArea.ISize(wm));
const LogicalMargin computedMargin = sizingInput.ComputedLogicalMargin(wm);
nscoord marginISize = computedMargin.IStartEnd(wm);
const auto& iSize = reflowInput.mStylePosition->ISize(wm);
if (marginISize < 0 && (iSize.IsAuto() || iSize.IsMozAvailable())) {
// If we get here, floatAvoidingBlock has a negative amount of inline-axis
// margin and an 'auto' (or ~equivalently, -moz-available) inline
// size. Under these circumstances, we use the margin to establish a
// (positive) minimum size for the border-box, in order to satisfy the
// equation in CSS2 10.3.3. That equation essentially simplifies to the
// following:
//
// iSize of margins + iSize of borderBox = iSize of containingBlock
//
// ...where "iSize of borderBox" is the sum of floatAvoidingBlock's
// inline-axis components of border, padding, and {width,height}.
//
// Right now, in the above equation, "iSize of margins" is the only term
// that we know for sure. (And we also know that it's negative, since we
// got here.) The other terms are as-yet unresolved, since the frame has an
// 'auto' iSize, and since we aren't yet sure if we'll clear this frame
// beyond floats or place it alongside them.
//
// However: we *do* know that the equation's "iSize of containingBlock"
// term *must* be non-negative, since boxes' widths and heights generally
// can't be negative in CSS. To satisfy that requirement, we can then
// infer that the equation's "iSize of borderBox" term *must* be large
// enough to cancel out the (known-to-be-negative) "iSize of margins"
// term. Therefore, marginISize value (negated to make it positive)
// establishes a lower-bound for how much inline-axis space our border-box
// will really require in order to fit alongside any floats.
//
// XXXdholbert This explanation is admittedly a bit hand-wavy and may not
// precisely match what any particular spec requires. It's the best
// reasoning I could come up with to explain engines' behavior. Also, our
// behavior with -moz-available doesn't seem particularly correct here, per
// bug 1767217, though that's probably due to a bug elsewhere in our float
// handling code...
result.borderBoxISize = std::max(result.borderBoxISize, -marginISize);
}
result.marginIStart = computedMargin.IStart(wm);
return result;
}

Просмотреть файл

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf-8">
<title>CSS Reference Case</title>
<link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
<script>
const MARGIN_VALS = [-30, -20, -17,
// Values -16 through -1 are non-interoperable and are
// split off to a separate test.
0, 5, 10, 14
// Values over 15 are non-interoperable and are
// split off to a separate test.
];
const HORIZ_SIDES = ["left", "right"]; // Used for 'float:*' and 'margin-*'.
const DIRECTION_VALS = ["ltr", "rtl"];
function newDivWithClassAndParent(className, parent) {
let elem = document.createElement("div");
if (className) {
elem.classList.add(className);
}
parent.appendChild(elem);
return elem;
}
function generateGroup(directionVal, floatVal, marginPropSuffix) {
let group = newDivWithClassAndParent("group", document.body);
group.style.direction = directionVal;
const marginPropName = "margin-" + marginPropSuffix;
for (let v of MARGIN_VALS) {
// In this test, the negative values are specifically the ones that
// are expected to cause wrapping.
const isExpectingToWrap = (v < 0);
let container = newDivWithClassAndParent("container", group);
if (isExpectingToWrap) {
container.style.flexWrap = "wrap";
}
if ((floatVal == "right") != (directionVal == "rtl")) {
// In the corresponding piece of the testcase, the float is floated to
// the inline-end side (for the given writing-mode). We use a
// "row-reverse" flex container as our mockup for that here.
container.style.flexDirection = "row-reverse";
}
let float = newDivWithClassAndParent("float", container);
float.style.cssFloat = floatVal;
let bfc = newDivWithClassAndParent("bfc", container);
if (isExpectingToWrap) {
// If we wrap, then we expect the testcase to resolve the BFC's
// content-box width to be: 30px (container's available space)
// minus 2px (for bfc's border), plus the absolute value of whatever
// (negative) margin value we're testing here.
bfc.style.width = (30 - 2 - v) + "px";
}
// Set the actual margin value that we're testing here, EXCEPT if we're
// not-expecting-to-wrap and the bfc's margin is going to "overlap" the
// float in the testcase. (In this latter case, the margin doesn't
// impact the testcase's rendering, so we take care not to set it here.)
if (isExpectingToWrap || marginPropSuffix != floatVal) {
bfc.style[marginPropName] = v + "px";
}
}
}
function go() {
for (let directionVal of DIRECTION_VALS) {
for (let floatVal of HORIZ_SIDES) {
for (let marginPropSuffix of HORIZ_SIDES) {
generateGroup(directionVal, floatVal, marginPropSuffix);
}
}
}
// Note: the "reftest-wait" usage here isn't strictly necessary; it just
// helps ensure that we actually make it through all of the above JS and
// populate this document with the content that we want to render.
// (Specifically: if we e.g. throw a JS exception somewhere early in both
// the testcase and reference case, then the "reftest-wait" class will
// never be removed; and that will cause the test run to be classified
// as a failure, rather than a trivial "pass" with a visual comparison of
// two blank documents.)
document.documentElement.removeAttribute("class");
}
</script>
<style>
.group {
width: 500px;
border: 1px solid black;
}
.container {
display: inline-flex;
align-content: start;
vertical-align: top;
width: 30px;
height: 40px;
/* This border and margin are just cosmetic, to avoid overlap between
* adjacent containers within a row. */
border: 1px solid gray;
margin-left: 30px;
}
.float {
width: 7px;
height: 8px;
background: fuchsia;
border: 1px solid purple;
margin: 1px 3px 1px 2px;
}
.bfc {
display: flow-root;
background: aqua;
height: 15px;
border: 1px solid blue;
/* We use "flex: 1" (on a flex item) to mock up the fill-available-space
* block-layout behavior in the testcase. */
flex: 1 auto;
}
</style>
<body onload="go()">
</body>
</html>

Просмотреть файл

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html class="reftest-wait">
<meta charset="utf-8">
<title>CSS Test: If a BFC's inline-axis margin is sufficiently negative such
that it inflates its border-box to be too large to fit alongside a float,
then it should be pushed below the float</title>
<link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
<link rel="help" href="https://www.w3.org/TR/CSS21/visuren.html#floats">
<meta name="assert" content="The border box of ... an element in the normal flow that establishes a new block formatting context ... must not overlap the margin box of any floats in the same block formatting context as the element itself. If necessary, implementations should clear the said element by placing it below any preceding floats">
<link rel="help" href="https://www.w3.org/TR/CSS21/visudet.html#blockwidth">
<!-- For a BFC with 'width:auto', negative total inline-axis margins will
effectively set a lower-bound for the used border-box width, to satisfy
the equation in CSS2.1 10.3.3. This test exercises scenarios where this
mechanism "props up" the BFC's border-box enough to make its border-box
collide width the float's margin-box, resulting in it needing to be moved
down below the float. -->
<link rel="match" href="floats-wrap-bfc-with-margin-001-ref.html">
<script>
const MARGIN_VALS = [-30, -20, -17,
// Values -16 through -1 are non-interoperable and are
// split off to a separate test.
0, 5, 10, 14
// Values over 15 are non-interoperable and are
// split off to a separate test.
];
const HORIZ_SIDES = ["left", "right"]; // Used for 'float:*' and 'margin-*'.
const DIRECTION_VALS = ["ltr", "rtl"];
function newDivWithClassAndParent(className, parent) {
let elem = document.createElement("div");
if (className) {
elem.classList.add(className);
}
parent.appendChild(elem);
return elem;
}
function generateGroup(directionVal, floatVal, marginPropSuffix) {
let group = newDivWithClassAndParent("group", document.body);
group.style.direction = directionVal;
const marginPropName = "margin-" + marginPropSuffix;
for (let v of MARGIN_VALS) {
let container = newDivWithClassAndParent("container", group);
let float = newDivWithClassAndParent("float", container);
float.style.cssFloat = floatVal;
let bfc = newDivWithClassAndParent("bfc", container);
bfc.style[marginPropName] = v + "px";
}
}
function go() {
for (let directionVal of DIRECTION_VALS) {
for (let floatVal of HORIZ_SIDES) {
for (let marginPropSuffix of HORIZ_SIDES) {
generateGroup(directionVal, floatVal, marginPropSuffix);
}
}
}
// Note: the "reftest-wait" usage here isn't strictly necessary; it just
// helps ensure that we actually make it through all of the above JS and
// populate this document with the content that we want to render.
// (Specifically: if we e.g. throw a JS exception somewhere early in both
// the testcase and reference case, then the "reftest-wait" class will
// never be removed; and that will cause the test run to be classified
// as a failure, rather than a trivial "pass" with a visual comparison of
// two blank documents.)
document.documentElement.removeAttribute("class");
}
</script>
<style>
.group {
width: 500px;
border: 1px solid black;
}
.container {
/* This is the container that holds our float+bfc. We make it an
inline-block so that we can test a bunch of these in a row. */
display: inline-block;
vertical-align: top;
width: 30px;
height: 40px;
/* This border and margin are just cosmetic, to avoid overlap between
* adjacent containers within a row. */
border: 1px solid gray;
margin-left: 30px;
}
.float {
/* We'll set the float property elsewhere (to 'right' or 'left'). */
width: 7px;
height: 8px;
background: fuchsia;
border: 1px solid purple;
/* Each .float's margin-box (which the corresponding .bfc's border-box cannot
* overlap) is 14px wide:
* 7px content + 2px horizontal border + 5px horizontal margin
* Note that we're intentionally using a nonzero 'margin' here, to be sure
* the UA is using the float's margin-box (and not one of its other
* boxes) for this non-overlapping calculation. */
margin: 1px 3px 1px 2px;
}
.bfc {
/* Each .bfc's border-box width is 2px (from the border) plus whatever we
* resolve 'width:auto' to, which is influenced by the particular choice of
* 'margin' values (and the available space). */
display: flow-root;
background: aqua;
height: 15px;
border: 1px solid blue;
}
</style>
<body onload="go()">
</body>
</html>