Bug 1496439 - Implement the flex item sizing outline in the flexbox panel. r=gl

Differential Revision: https://phabricator.services.mozilla.com/D7733

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Patrick Brosset 2018-10-08 09:42:05 +00:00
parent 8eb10516b2
commit 87ac76f795
17 changed files with 587 additions and 6 deletions

View File

@ -181,7 +181,12 @@ BoxModel.prototype = {
this._updateReasons = [];
return null;
}).bind(this))().catch(console.error);
}).bind(this))().catch(error => {
// If we failed because we were being destroyed while waiting for a request, ignore.
if (this.document) {
console.error(error);
}
});
this._lastRequest = lastRequest;
},

View File

@ -0,0 +1,147 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { colorUtils } = require("devtools/shared/css/color");
const Types = require("../types");
class FlexItemSizingOutline extends PureComponent {
static get propTypes() {
return {
color: PropTypes.string.isRequired,
flexDirection: PropTypes.string.isRequired,
flexItem: PropTypes.shape(Types.flexItem).isRequired,
};
}
renderBasisOutline(mainBaseSize) {
return (
dom.div({
className: "flex-outline-basis" + (!mainBaseSize ? " zero-basis" : ""),
style: {
color: colorUtils.setAlpha(this.props.color, 0.4),
},
})
);
}
renderDeltaOutline(mainDeltaSize) {
if (!mainDeltaSize) {
return null;
}
return (
dom.div({
className: "flex-outline-delta",
style: {
backgroundColor: colorUtils.setAlpha(this.props.color, 0.1)
}
})
);
}
renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize) {
const isClamped = mainFinalSize === mainMaxSize ||
mainFinalSize === mainMinSize;
return (
dom.div({
className: "flex-outline-final" + (isClamped ? " clamped" : "")
})
);
}
renderPoint(name) {
return dom.div({ className: `flex-outline-point ${name}`, "data-label": name });
}
render() {
const {
mainBaseSize,
mainDeltaSize,
mainMaxSize,
mainMinSize,
} = this.props.flexItem.flexItemSizing;
const isRow = this.props.flexDirection.startsWith("row");
// Calculate the final size. This is base + delta, then clamped by min or max.
let mainFinalSize = mainBaseSize + mainDeltaSize;
mainFinalSize = Math.max(mainFinalSize, mainMinSize);
mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
// The max size is only interesting to show if it did clamp the item
const showMax = mainMaxSize === mainFinalSize;
// The min size is only really interesting if it actually clamped the item.
const showMin = mainMinSize === mainFinalSize;
// Sort all of the dimensions in order to come up with a grid track template.
// Make mainDeltaSize start from the same point as the other ones so we can compare.
let sizes = [
{ name: "basis-end", size: mainBaseSize },
{ name: "final-end", size: mainFinalSize }
];
if (mainDeltaSize > 0) {
sizes.push({ name: "delta-start", size: mainBaseSize });
sizes.push({ name: "delta-end", size: mainBaseSize + mainDeltaSize });
} else {
sizes.push({ name: "delta-start", size: mainBaseSize + mainDeltaSize });
sizes.push({ name: "delta-end", size: mainBaseSize });
}
if (showMax) {
sizes.push({ name: "max", size: mainMaxSize });
}
if (showMin) {
sizes.push({ name: "min", size: mainMinSize });
}
sizes = sizes.sort((a, b) => a.size - b.size);
let gridTemplateColumns = "[final-start basis-start";
let accumulatedSize = 0;
for (const { name, size } of sizes) {
const breadth = Math.round(size - accumulatedSize);
if (breadth === 0) {
gridTemplateColumns += ` ${name}`;
continue;
}
gridTemplateColumns += `] ${breadth}fr [${name}`;
accumulatedSize = size;
}
gridTemplateColumns += "]";
return (
dom.div({ className: "flex-outline-container" },
dom.div(
{
className: "flex-outline" +
(isRow ? " row" : " column") +
(mainDeltaSize > 0 ? " growing" : " shrinking"),
style: {
color: this.props.color,
gridTemplateColumns
}
},
this.renderPoint("basis"),
this.renderPoint("final"),
showMin ? this.renderPoint("min") : null,
showMax ? this.renderPoint("max") : null,
this.renderBasisOutline(mainBaseSize),
this.renderDeltaOutline(mainDeltaSize),
this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize)
)
)
);
}
}
module.exports = FlexItemSizingOutline;

View File

@ -4,7 +4,12 @@
"use strict";
const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
const {
createElement,
createFactory,
Fragment,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { getStr } = require("devtools/client/inspector/layout/utils/l10n");
@ -15,6 +20,9 @@ loader.lazyGetter(this, "FlexContainerProperties", function() {
loader.lazyGetter(this, "FlexItemList", function() {
return createFactory(require("./FlexItemList"));
});
loader.lazyGetter(this, "FlexItemSizingOutline", function() {
return createFactory(require("./FlexItemSizingOutline"));
});
loader.lazyGetter(this, "FlexItemSizingProperties", function() {
return createFactory(require("./FlexItemSizingProperties"));
});
@ -27,6 +35,7 @@ const Types = require("../types");
class Flexbox extends PureComponent {
static get propTypes() {
return {
flexbox: PropTypes.shape(Types.flexbox).isRequired,
flexContainer: PropTypes.shape(Types.flexContainer).isRequired,
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
@ -60,6 +69,9 @@ class Flexbox extends PureComponent {
}
renderFlexItemSizing() {
const {
color,
} = this.props.flexbox;
const {
flexItems,
flexItemShown,
@ -71,10 +83,17 @@ class Flexbox extends PureComponent {
return null;
}
return FlexItemSizingProperties({
flexDirection: properties["flex-direction"],
flexItem,
});
return createElement(Fragment, null,
FlexItemSizingOutline({
color,
flexDirection: properties["flex-direction"],
flexItem,
}),
FlexItemSizingProperties({
flexDirection: properties["flex-direction"],
flexItem,
})
);
}
render() {

View File

@ -11,6 +11,7 @@ DevToolsModules(
'FlexItem.js',
'FlexItemList.js',
'FlexItemSelector.js',
'FlexItemSizingOutline.js',
'FlexItemSizingProperties.js',
'Header.js',
)

View File

@ -14,3 +14,5 @@ DevToolsModules(
'flexbox.js',
'types.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']

View File

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../../.eslintrc.mochitests.js"
};

View File

@ -0,0 +1,13 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
doc_flexbox_simple.html
head.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js
[browser_flexbox_item_outline_exists.js]
[browser_flexbox_item_outline_rotates_for_column.js]
[browser_flexbox_item_outline_has_correct_layout.js]

View File

@ -0,0 +1,34 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the flex item outline exists when a flex item is selected.
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
add_task(async function() {
await addTab(TEST_URI);
const { inspector, flexboxInspector } = await openLayoutView();
const { document: doc } = flexboxInspector;
// Select a flex item in the test document and wait for the outline to be rendered.
const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container");
await selectNode(".item", inspector);
const [flexOutlineContainer] = await onFlexItemOutlineRendered;
ok(flexOutlineContainer, "The flex outline exists in the DOM");
const [basis, final] = [...flexOutlineContainer.querySelectorAll(
".flex-outline-basis, .flex-outline-final")];
ok(basis, "The basis outline exists");
ok(final, "The final outline exists");
const [basisPoint, finalPoint] = [...flexOutlineContainer.querySelectorAll(
".flex-outline-point.basis, .flex-outline-point.final")];
ok(basisPoint, "The basis point exists");
ok(finalPoint, "The final point exists");
});

View File

@ -0,0 +1,52 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the flex item outline has a correct layout. This outline is built using css
// grid under the hood to position everything. So we want to check that the template for
// this grid has been correctly generated depending on the item that is currently
// selected.
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
const TEST_DATA = [{
selector: ".shrinking .item",
expectedGridTemplate: "[final-start basis-start] 300fr [final-end delta-start] " +
"200fr [basis-end delta-end]"
}, {
selector: ".shrinking.is-clamped .item",
expectedGridTemplate: "[final-start basis-start] 300fr [delta-start] " +
"50fr [final-end min] 150fr [basis-end delta-end]"
}, {
selector: ".growing .item",
expectedGridTemplate: "[final-start basis-start] 200fr [basis-end delta-start] " +
"100fr [final-end delta-end]"
}, {
selector: ".growing.is-clamped .item",
expectedGridTemplate: "[final-start basis-start] 200fr [basis-end delta-start] " +
"50fr [final-end max] 50fr [delta-end]"
}];
add_task(async function() {
await addTab(TEST_URI);
const { inspector, flexboxInspector } = await openLayoutView();
const { document: doc } = flexboxInspector;
for (const {selector, expectedGridTemplate} of TEST_DATA) {
info(`Checking the grid template for the flex item outline for ${selector}`);
const flexOutline = await selectNodeAndGetFlexOutline(selector, inspector, doc);
is(flexOutline.style.gridTemplateColumns, expectedGridTemplate,
"Grid template is correct");
}
});
async function selectNodeAndGetFlexOutline(selector, inspector, doc) {
const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline");
await selectNode(selector, inspector);
const [flexOutlineContainer] = await onFlexItemOutlineRendered;
return flexOutlineContainer;
}

View File

@ -0,0 +1,39 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the flex item outline is rotated for flex items in a column flexbox layout.
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
add_task(async function() {
await addTab(TEST_URI);
const { inspector, flexboxInspector } = await openLayoutView();
const { document: doc } = flexboxInspector;
// Select a flex item in the row flexbox layout.
let onFlexItemOutlineRendered = waitForDOM(doc,
".flex-outline-container .flex-outline");
await selectNode(".container .item", inspector);
let [flexOutline] = await onFlexItemOutlineRendered;
ok(flexOutline.classList.contains("row"), "The flex outline has the row class");
// Check that the outline is wider than it is tall in the configuration.
let bounds = flexOutline.getBoxQuads()[0].getBounds();
ok(bounds.width > bounds.height, "The outline looks like a row");
// Select a flex item in the column flexbox layout.
onFlexItemOutlineRendered = waitForDOM(doc,
".flex-outline-container .flex-outline");
await selectNode(".container.column .item", inspector);
([flexOutline] = await onFlexItemOutlineRendered);
ok(flexOutline.classList.contains("column"), "The flex outline has the column class");
// Check that the outline is taller than it is wide in the configuration.
bounds = flexOutline.getBoxQuads()[0].getBounds();
ok(bounds.height > bounds.width, "The outline looks like a column");
});

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.container {
width: 300px;
height: 150px;
margin: 10px;
display: flex;
}
.container.column {
height: 300px;
width: 150px;
flex-direction: column;
}
.item {
background: #0004;
}
.shrinking .item {
flex-basis: 500px;
flex-shrink: 1;
}
.shrinking.is-clamped .item {
min-width: 350px;
}
.growing .item {
flex-basis: 200px;
flex-grow: 1;
}
.growing.is-clamped .item {
max-width: 250px;
}
</style>
<div class="container">
<div class="item">flex item in a row flex container</div>
</div>
<div class="container column">
<div class="item">flex item in a column flex container</div>
</div>
<div class="container shrinking">
<div class="item">Shrinking flex item</div>
</div>
<div class="container shrinking is-clamped">
<div class="item">Shrinking and clamped flex item</div>
</div>
<div class="container growing">
<div class="item">Growing flex item</div>
</div>
<div class="container growing is-clamped">
<div class="item">Growing and clamped flex item</div>
</div>

View File

@ -0,0 +1,21 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../test/head.js */
"use strict";
// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
this);
// Make sure only the flexbox layout accordion is opened, and the others are closed.
Services.prefs.setBoolPref("devtools.layout.flexbox.opened", true);
Services.prefs.setBoolPref("devtools.layout.boxmodel.opened", false);
Services.prefs.setBoolPref("devtools.layout.grid.opened", false);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.layout.flexbox.opened");
Services.prefs.clearUserPref("devtools.layout.boxmodel.opened");
Services.prefs.clearUserPref("devtools.layout.grid.opened");
});

View File

@ -146,6 +146,7 @@ function openLayoutView() {
inspector: data.inspector,
boxmodel: data.inspector.getPanel("boxmodel"),
gridInspector: data.inspector.layoutview.gridInspector,
flexboxInspector: data.inspector.layoutview.flexboxInspector,
layoutView: data.inspector.layoutview,
testActor: data.testActor
};

View File

@ -107,6 +107,7 @@ devtools.jar:
skin/images/accessibility.svg (themes/images/accessibility.svg)
skin/images/add.svg (themes/images/add.svg)
skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg)
skin/images/arrowhead-right.svg (themes/images/arrowhead-right.svg)
skin/images/arrowhead-down.svg (themes/images/arrowhead-down.svg)
skin/images/arrowhead-up.svg (themes/images/arrowhead-up.svg)
skin/images/breadcrumbs-divider.svg (themes/images/breadcrumbs-divider.svg)
@ -278,6 +279,7 @@ devtools.jar:
skin/images/reveal.svg (themes/images/reveal.svg)
skin/images/select-arrow.svg (themes/images/select-arrow.svg)
skin/images/settings.svg (themes/images/settings.svg)
skin/images/lock.svg (themes/images/lock.svg)
# Debugger
skin/images/debugger/angular.svg (themes/images/debugger/angular.svg)

View File

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="context-fill #0C0C0D" fill-rule="evenodd" clip-rule="evenodd" d="M8.293 4.293a1 1 0 0 1 1.414 0l7 7a1 1 0 0 1 0 1.414l-7 7a1 1 0 0 1-1.414-1.414L14.586 12 8.293 5.707a1 1 0 0 1 0-1.414z"></path>
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="context-fill">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 7a3 3 0 1 1 6 0v3H9V7zm-2 3V7a5 5 0 1 1 10 0v3h1a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@ -158,10 +158,187 @@
width: 85%;
}
/**
* Flex Item Sizing Outline
*/
.flex-outline-container {
display: flex;
justify-content: center;
}
.flex-outline {
display: grid;
margin: 2em 0;
grid-auto-rows: 35px;
flex-basis: 80%;
max-width: 450px;
}
.flex-outline.column {
transform: translate(50%, -2em) rotate(.25turn);
transform-origin: center left;
flex-basis: 150px;
margin-bottom: 120px;
}
.flex-outline-final,
.flex-outline-basis,
.flex-outline-delta {
grid-row: 1;
}
.flex-outline-final {
border: 1px solid currentColor;
position: relative;
grid-column: final-start / final-end;
}
.flex-outline-final.clamped::after {
content: "";
background-color: var(--theme-body-background);
background-image: url(chrome://devtools/skin/images/lock.svg);
background-size: 16px;
background-repeat: no-repeat;
background-position: center 1px;
fill: currentColor;
-moz-context-properties: fill;
width: 20px;
height: 20px;
position: absolute;
right: -10px;
top: 6px;
border-radius: 50%;
}
.flex-outline.column .flex-outline-final.clamped::after {
transform: rotate(-.25turn);
}
.flex-outline-basis {
border-style: dotted;
border-width: 3px;
margin: 1px 0;
grid-column: basis-start / basis-end;
}
.flex-outline-basis.zero-basis {
border-width: 0 0 0 3px;
}
.flex-outline-delta {
background-repeat: round;
fill: currentColor;
-moz-context-properties: fill;
grid-column: delta-start / delta-end;
margin: 4px;
}
.flex-outline.growing .flex-outline-delta {
background-image: url(chrome://devtools/skin/images/arrowhead-right.svg);
}
.flex-outline.shrinking .flex-outline-delta {
background-image: url(chrome://devtools/skin/images/arrowhead-left.svg);
}
.flex-outline-point {
position: relative;
-moz-user-select: none;
}
.flex-outline-point {
grid-row: 1;
}
.flex-outline-point.basis {
grid-column-end: basis-end;
justify-self: end;
}
.flex-outline.shrinking .flex-outline-point.basis {
grid-column-start: basis-end;
justify-self: start;
}
.flex-outline-point.final {
grid-column-start: final-end;
left: -1px;
}
.flex-outline.shrinking .flex-outline-point.final {
grid-column-end: final-end;
grid-column-start: unset;
justify-self: end;
}
.flex-outline-point.min {
grid-column: min;
place-self: end;
}
.flex-outline.shrinking .flex-outline-point.min {
place-self: end start;
}
.flex-outline-point.max {
grid-column: max;
align-self: end;
left: -1px;
}
.flex-outline-point::before {
content: attr(data-label);
display: block;
position: relative;
padding: 0 3px;
height: 10px;
border-color: currentColor;
border-style: solid;
border-width: 0;
}
.flex-outline.column .flex-outline-point::before {
padding: 0;
writing-mode: sideways-lr;
}
.flex-outline-point.basis::before,
.flex-outline-point.final::before {
top: -12px;
}
.flex-outline-point.min::before,
.flex-outline-point.max::before {
bottom: -12px;
}
.flex-outline.column .flex-outline-point.min::before,
.flex-outline.column .flex-outline-point.min::before {
text-indent: -12px;
}
.flex-outline-point.final::before,
.flex-outline.shrinking .flex-outline-point.min::before,
.flex-outline-point.max::before,
.flex-outline.shrinking .flex-outline-point.basis::before {
border-width: 0 0 0 1px;
}
.flex-outline-point.basis::before,
.flex-outline-point.min::before,
.flex-outline.shrinking .flex-outline-point.final::before {
border-width: 0 1px 0 0;
}
/**
* Flex Item Sizing Properties
*/
#flex-item-sizing-properties {
padding-top: 0;
}
#flex-item-sizing-properties span {
font-weight: 600;
}