Bug 1343167 - Add navigation for the box model's position, padding, border, margin and content layout. r=jdescottes

This commit is contained in:
Gabriel Luong 2017-04-06 11:24:38 -04:00
Родитель 0c31e96434
Коммит 7afb653508
7 изменённых файлов: 370 добавлений и 34 удалений

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

@ -30,6 +30,14 @@ module.exports = createClass({
mixins: [ addons.PureRenderMixin ],
onKeyDown(event) {
let { target } = event;
if (target == this.boxModelContainer) {
this.boxModelMain.onKeyDown(event);
}
},
render() {
let {
boxModel,
@ -45,10 +53,19 @@ module.exports = createClass({
return dom.div(
{
className: "boxmodel-container",
tabIndex: 0,
ref: div => {
this.boxModelContainer = div;
},
onKeyDown: this.onKeyDown,
},
BoxModelMain({
boxModel,
boxModelContainer: this.boxModelContainer,
setSelectedNode,
ref: boxModelMain => {
this.boxModelMain = boxModelMain;
},
onHideBoxModelHighlighter,
onShowBoxModelEditor,
onShowBoxModelHighlighter,

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

@ -17,6 +17,8 @@ module.exports = createClass({
propTypes: {
box: PropTypes.string.isRequired,
direction: PropTypes.string,
focusable: PropTypes.bool.isRequired,
level: PropTypes.string.isRequired,
property: PropTypes.string.isRequired,
textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
@ -28,7 +30,7 @@ module.exports = createClass({
let { property, onShowBoxModelEditor } = this.props;
editableItem({
element: this.refs.span,
element: this.boxModelEditable,
}, (element, event) => {
onShowBoxModelEditor(element, event, property);
});
@ -38,6 +40,8 @@ module.exports = createClass({
let {
box,
direction,
focusable,
level,
property,
textContent,
} = this.props;
@ -57,8 +61,11 @@ module.exports = createClass({
{
className: "boxmodel-editable",
"data-box": box,
tabIndex: box === level && focusable ? 0 : -1,
title: property,
ref: "span",
ref: span => {
this.boxModelEditable = span;
},
},
textContent
)

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

@ -6,10 +6,13 @@
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
const { KeyCodes } = require("devtools/client/shared/keycodes");
const { LocalizationHelper } = require("devtools/shared/l10n");
const BoxModelEditable = createFactory(require("./BoxModelEditable"));
// Reps
const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
const Rep = createFactory(REPS.Rep);
@ -28,6 +31,7 @@ module.exports = createClass({
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
boxModelContainer: PropTypes.object.isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
@ -37,11 +41,92 @@ module.exports = createClass({
mixins: [ addons.PureRenderMixin ],
getInitialState() {
return {
"activedescendant": null,
focusable: false,
};
},
componentDidUpdate() {
let { layout } = this.props.boxModel;
let displayPosition = this.getDisplayPosition();
let isContentBox = this.getContextBox();
this.layouts = {
"position": new Map([
[KeyCodes.DOM_VK_ESCAPE, this.positionLayout],
[KeyCodes.DOM_VK_DOWN, this.marginLayout],
[KeyCodes.DOM_VK_RETURN, this.positionEditable],
[KeyCodes.DOM_VK_UP, null],
["click", this.positionLayout]
]),
"margin": new Map([
[KeyCodes.DOM_VK_ESCAPE, this.marginLayout],
[KeyCodes.DOM_VK_DOWN, this.borderLayout],
[KeyCodes.DOM_VK_RETURN, this.marginEditable],
[KeyCodes.DOM_VK_UP, displayPosition ? this.positionLayout : null],
["click", this.marginLayout]
]),
"border": new Map([
[KeyCodes.DOM_VK_ESCAPE, this.borderLayout],
[KeyCodes.DOM_VK_DOWN, this.paddingLayout],
[KeyCodes.DOM_VK_RETURN, this.borderEditable],
[KeyCodes.DOM_VK_UP, this.marginLayout],
["click", this.borderLayout]
]),
"padding": new Map([
[KeyCodes.DOM_VK_ESCAPE, this.paddingLayout],
[KeyCodes.DOM_VK_DOWN, isContentBox ? this.contentLayout : null],
[KeyCodes.DOM_VK_RETURN, this.paddingEditable],
[KeyCodes.DOM_VK_UP, this.borderLayout],
["click", this.paddingLayout]
]),
"content": new Map([
[KeyCodes.DOM_VK_ESCAPE, this.contentLayout],
[KeyCodes.DOM_VK_DOWN, null],
[KeyCodes.DOM_VK_RETURN, this.contentEditable],
[KeyCodes.DOM_VK_UP, this.paddingLayout],
["click", this.contentLayout]
])
};
},
getAriaActiveDescendant() {
let activeDescendant = this.state["activedescendant"];
if (!activeDescendant) {
let { layout } = this.props.boxModel;
let displayPosition = this.getDisplayPosition();
let nextLayout = displayPosition ? this.positionLayout : this.marginLayout;
activeDescendant = nextLayout.getAttribute("data-box");
this.setAriaActive(nextLayout);
}
return activeDescendant;
},
getBorderOrPaddingValue(property) {
let { layout } = this.props.boxModel;
return layout[property] ? parseFloat(layout[property]) : "-";
},
/**
* Returns true if the layout box sizing is context box and false otherwise.
*/
getContextBox() {
let { layout } = this.props.boxModel;
return layout["box-sizing"] == "content-box";
},
/**
* Returns true if the position is displayed and false otherwise.
*/
getDisplayPosition() {
let { layout } = this.props.boxModel;
return layout.position && layout.position != "static";
},
getHeightValue(property) {
let { layout } = this.props.boxModel;
@ -117,13 +202,64 @@ module.exports = createClass({
return value;
},
/**
* Move the focus to the next/previous editable element of the current layout.
*
* @param {Element} target
* Node to be observed
* @param {Boolean} shiftKey
* Determines if shiftKey was pressed
* @param {String} level
* Current active layout
*/
moveFocus: function ({ target, shiftKey }, level) {
let editBoxes = [
...findDOMNode(this).querySelectorAll(`[data-box="${level}"].boxmodel-editable`)
];
let editingMode = target.tagName === "input";
// target.nextSibling is input field
let position = editingMode ? editBoxes.indexOf(target.nextSibling)
: editBoxes.indexOf(target);
if (position === editBoxes.length - 1 && !shiftKey) {
position = 0;
} else if (position === 0 && shiftKey) {
position = editBoxes.length - 1;
} else {
shiftKey ? position-- : position++;
}
let editBox = editBoxes[position];
editBox.focus();
if (editingMode) {
editBox.click();
}
},
/**
* Active aria-level set to current layout.
*
* @param {Element} nextLayout
* Element of next layout that user has navigated to
*/
setAriaActive(nextLayout) {
let { boxModelContainer } = this.props;
// We set this attribute for testing purposes.
boxModelContainer.setAttribute("activedescendant", nextLayout.className);
this.setState({
["activedescendant"]: nextLayout.getAttribute("data-box"),
});
},
/**
* While waiting for a reps fix in https://github.com/devtools-html/reps/issues/92,
* translate nodeFront to a grip-like object that can be used with an ElementNode rep.
*
* @params {NodeFront} nodeFront
* The NodeFront for which we want to create a grip-like object.
* @returns {Object} a grip-like object that can be used with Reps.
* @param {NodeFront} nodeFront
* The NodeFront for which we want to create a grip-like object.
* @return {Object} a grip-like object that can be used with Reps.
*/
translateNodeFrontToGrip(nodeFront) {
let {
@ -180,6 +316,96 @@ module.exports = createClass({
});
},
/**
* Handle keyboard navigation and focus for box model layouts.
*
* Updates active layout on arrow key navigation
* Focuses next layout's editboxes on enter key
* Unfocuses current layout's editboxes when active layout changes
* Controls tabbing between editBoxes
*
* @param {Event} event
* The event triggered by a keypress on the box model
*/
onKeyDown(event) {
let { target, keyCode } = event;
let isEditable = target._editable || target.editor;
let level = this.getAriaActiveDescendant();
let editingMode = target.tagName === "input";
switch (keyCode) {
case KeyCodes.DOM_VK_RETURN:
if (!isEditable) {
this.setState({ focusable: true });
let editableBox = this.layouts[level].get(keyCode);
if (editableBox) {
editableBox.boxModelEditable.focus();
}
}
break;
case KeyCodes.DOM_VK_DOWN:
case KeyCodes.DOM_VK_UP:
if (!editingMode) {
event.preventDefault();
this.setState({ focusable: false });
let nextLayout = this.layouts[level].get(keyCode);
this.setAriaActive(nextLayout);
if (target && target._editable) {
target.blur();
}
this.props.boxModelContainer.focus();
}
break;
case KeyCodes.DOM_VK_TAB:
if (isEditable) {
event.preventDefault();
this.moveFocus(event, level);
}
break;
case KeyCodes.DOM_VK_ESCAPE:
if (target._editable) {
event.preventDefault();
event.stopPropagation();
this.setState({ focusable: false });
this.props.boxModelContainer.focus();
}
break;
default:
break;
}
},
/**
* Update aria-active on mouse click.
*
* @param {Event} event
* The event triggered by a mouse click on the box model
*/
onLevelClick(event) {
let { target } = event;
let { layout } = this.props.boxModel;
let displayPosition = this.getDisplayPosition();
let isContentBox = this.getContextBox();
// Avoid switching the aria active descendant to the position or content layout
// if those are not editable.
if ((!displayPosition && target == this.positionLayout) ||
(!isContentBox && target == this.contentLayout)) {
return;
}
let nextLayout = this.layouts[target.getAttribute("data-box")].get("click");
this.setAriaActive(nextLayout);
if (target && target._editable) {
target.blur();
}
},
render() {
let {
boxModel,
@ -201,7 +427,7 @@ module.exports = createClass({
let paddingBottom = this.getBorderOrPaddingValue("padding-bottom");
let paddingLeft = this.getBorderOrPaddingValue("padding-left");
let displayPosition = layout.position && layout.position != "static";
let displayPosition = this.getDisplayPosition();
let positionTop = this.getPositionValue("top");
let positionRight = this.getPositionValue("right");
let positionBottom = this.getPositionValue("bottom");
@ -215,6 +441,9 @@ module.exports = createClass({
height = this.getHeightValue(height);
width = this.getWidthValue(width);
let { focusable } = this.state;
let level = this.state["activedescendant"];
let contentBox = layout["box-sizing"] == "content-box" ?
dom.p(
{
@ -222,7 +451,12 @@ module.exports = createClass({
},
BoxModelEditable({
box: "content",
focusable,
level,
property: "width",
ref: editable => {
this.contentEditable = editable;
},
textContent: width,
onShowBoxModelEditor
}),
@ -232,6 +466,8 @@ module.exports = createClass({
),
BoxModelEditable({
box: "content",
focusable,
level,
property: "height",
textContent: height,
onShowBoxModelEditor
@ -253,6 +489,12 @@ module.exports = createClass({
return dom.div(
{
className: "boxmodel-main",
"data-box": "position",
ref: div => {
this.positionLayout = div;
},
onClick: this.onLevelClick,
onKeyDown: this.onKeyDown,
onMouseOver: this.onHighlightMouseOver,
onMouseOut: this.props.onHideBoxModelHighlighter,
},
@ -301,6 +543,9 @@ module.exports = createClass({
className: "boxmodel-margins",
"data-box": "margin",
title: BOXMODEL_L10N.getStr("boxmodel.margin"),
ref: div => {
this.marginLayout = div;
},
},
dom.span(
{
@ -315,6 +560,9 @@ module.exports = createClass({
className: "boxmodel-borders",
"data-box": "border",
title: BOXMODEL_L10N.getStr("boxmodel.border"),
ref: div => {
this.borderLayout = div;
},
},
dom.span(
{
@ -329,11 +577,17 @@ module.exports = createClass({
className: "boxmodel-paddings",
"data-box": "padding",
title: BOXMODEL_L10N.getStr("boxmodel.padding"),
ref: div => {
this.paddingLayout = div;
},
},
dom.div({
className: "boxmodel-contents",
"data-box": "content",
title: BOXMODEL_L10N.getStr("boxmodel.content"),
ref: div => {
this.contentLayout = div;
},
})
)
)
@ -343,7 +597,12 @@ module.exports = createClass({
BoxModelEditable({
box: "position",
direction: "top",
focusable,
level,
property: "position-top",
ref: editable => {
this.positionEditable = editable;
},
textContent: positionTop,
onShowBoxModelEditor,
})
@ -353,6 +612,8 @@ module.exports = createClass({
BoxModelEditable({
box: "position",
direction: "right",
focusable,
level,
property: "position-right",
textContent: positionRight,
onShowBoxModelEditor,
@ -363,6 +624,8 @@ module.exports = createClass({
BoxModelEditable({
box: "position",
direction: "bottom",
focusable,
level,
property: "position-bottom",
textContent: positionBottom,
onShowBoxModelEditor,
@ -373,6 +636,8 @@ module.exports = createClass({
BoxModelEditable({
box: "position",
direction: "left",
focusable,
level,
property: "position-left",
textContent: positionLeft,
onShowBoxModelEditor,
@ -382,13 +647,20 @@ module.exports = createClass({
BoxModelEditable({
box: "margin",
direction: "top",
focusable,
level,
property: "margin-top",
ref: editable => {
this.marginEditable = editable;
},
textContent: marginTop,
onShowBoxModelEditor,
}),
BoxModelEditable({
box: "margin",
direction: "right",
focusable,
level,
property: "margin-right",
textContent: marginRight,
onShowBoxModelEditor,
@ -396,6 +668,8 @@ module.exports = createClass({
BoxModelEditable({
box: "margin",
direction: "bottom",
focusable,
level,
property: "margin-bottom",
textContent: marginBottom,
onShowBoxModelEditor,
@ -403,6 +677,8 @@ module.exports = createClass({
BoxModelEditable({
box: "margin",
direction: "left",
focusable,
level,
property: "margin-left",
textContent: marginLeft,
onShowBoxModelEditor,
@ -410,13 +686,20 @@ module.exports = createClass({
BoxModelEditable({
box: "border",
direction: "top",
focusable,
level,
property: "border-top-width",
ref: editable => {
this.borderEditable = editable;
},
textContent: borderTop,
onShowBoxModelEditor,
}),
BoxModelEditable({
box: "border",
direction: "right",
focusable,
level,
property: "border-right-width",
textContent: borderRight,
onShowBoxModelEditor,
@ -424,6 +707,8 @@ module.exports = createClass({
BoxModelEditable({
box: "border",
direction: "bottom",
focusable,
level,
property: "border-bottom-width",
textContent: borderBottom,
onShowBoxModelEditor,
@ -431,6 +716,8 @@ module.exports = createClass({
BoxModelEditable({
box: "border",
direction: "left",
focusable,
level,
property: "border-left-width",
textContent: borderLeft,
onShowBoxModelEditor,
@ -438,13 +725,20 @@ module.exports = createClass({
BoxModelEditable({
box: "padding",
direction: "top",
focusable,
level,
property: "padding-top",
ref: editable => {
this.paddingEditable = editable;
},
textContent: paddingTop,
onShowBoxModelEditor,
}),
BoxModelEditable({
box: "padding",
direction: "right",
focusable,
level,
property: "padding-right",
textContent: paddingRight,
onShowBoxModelEditor,
@ -452,6 +746,8 @@ module.exports = createClass({
BoxModelEditable({
box: "padding",
direction: "bottom",
focusable,
level,
property: "padding-bottom",
textContent: paddingBottom,
onShowBoxModelEditor,
@ -459,6 +755,8 @@ module.exports = createClass({
BoxModelEditable({
box: "padding",
direction: "left",
focusable,
level,
property: "padding-left",
textContent: paddingLeft,
onShowBoxModelEditor,

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

@ -22,7 +22,6 @@ support-files =
[browser_boxmodel_editablemodel_stylerules.js]
[browser_boxmodel_guides.js]
[browser_boxmodel_navigation.js]
skip-if = true # Bug 1336198
[browser_boxmodel_offsetparent.js]
[browser_boxmodel_positions.js]
[browser_boxmodel_properties.js]

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

@ -28,49 +28,65 @@ add_task(function* () {
function* testInitialFocus(inspector, view) {
info("Test that the focus is on margin layout.");
let viewdoc = view.doc;
let boxmodel = viewdoc.getElementById("boxmodel-wrapper");
let viewdoc = view.document;
let boxmodel = viewdoc.querySelector(".boxmodel-container");
boxmodel.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
is(boxmodel.getAttribute("aria-activedescendant"), "boxmodel-margins",
"Should be set to the margin layout.");
is(boxmodel.getAttribute("activedescendant"), "boxmodel-main",
"Should be set to the position layout.");
}
function* testChangingLevels(inspector, view) {
info("Test that using arrow keys updates level.");
let viewdoc = view.doc;
let boxmodel = viewdoc.getElementById("boxmodel-wrapper");
let viewdoc = view.document;
let boxmodel = viewdoc.querySelector(".boxmodel-container");
boxmodel.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
EventUtils.synthesizeKey("VK_ESCAPE", {});
EventUtils.synthesizeKey("VK_DOWN", {});
is(boxmodel.getAttribute("aria-activedescendant"), "boxmodel-borders",
is(boxmodel.getAttribute("activedescendant"), "boxmodel-margins",
"Should be set to the margin layout.");
EventUtils.synthesizeKey("VK_DOWN", {});
is(boxmodel.getAttribute("activedescendant"), "boxmodel-borders",
"Should be set to the border layout.");
EventUtils.synthesizeKey("VK_DOWN", {});
is(boxmodel.getAttribute("aria-activedescendant"), "boxmodel-padding",
is(boxmodel.getAttribute("activedescendant"), "boxmodel-paddings",
"Should be set to the padding layout.");
EventUtils.synthesizeKey("VK_DOWN", {});
is(boxmodel.getAttribute("activedescendant"), "boxmodel-contents",
"Should be set to the content layout.");
EventUtils.synthesizeKey("VK_UP", {});
is(boxmodel.getAttribute("activedescendant"), "boxmodel-paddings",
"Should be set to the padding layout.");
EventUtils.synthesizeKey("VK_UP", {});
is(boxmodel.getAttribute("aria-activedescendant"), "boxmodel-borders",
is(boxmodel.getAttribute("activedescendant"), "boxmodel-borders",
"Should be set to the border layout.");
EventUtils.synthesizeKey("VK_UP", {});
is(boxmodel.getAttribute("aria-activedescendant"), "boxmodel-margins",
is(boxmodel.getAttribute("activedescendant"), "boxmodel-margins",
"Should be set to the margin layout.");
EventUtils.synthesizeKey("VK_UP", {});
is(boxmodel.getAttribute("activedescendant"), "boxmodel-main",
"Should be set to the position layout.");
}
function* testTabbingWrapAround(inspector, view) {
info("Test that using arrow keys updates level.");
let viewdoc = view.doc;
let boxmodel = viewdoc.getElementById("boxmodel-wrapper");
let viewdoc = view.document;
let boxmodel = viewdoc.querySelector(".boxmodel-container");
boxmodel.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
let editLevel = boxmodel.getAttribute("aria-activedescendant");
let dataLevel = viewdoc.getElementById(editLevel).getAttribute("data-box");
let editLevel = boxmodel.getAttribute("activedescendant");
let dataLevel = viewdoc.querySelector(`.${editLevel}`).getAttribute("data-box");
let editBoxes = [...viewdoc.querySelectorAll(
`[data-box="${dataLevel}"].boxmodel-editable`)];
@ -86,18 +102,19 @@ function* testTabbingWrapAround(inspector, view) {
function* testChangingLevelsByClicking(inspector, view) {
info("Test that clicking on levels updates level.");
let viewdoc = view.doc;
let boxmodel = viewdoc.getElementById("boxmodel-wrapper");
let viewdoc = view.document;
let boxmodel = viewdoc.querySelector(".boxmodel-container");
boxmodel.focus();
let marginLayout = viewdoc.getElementById("boxmodel-margins");
let borderLayout = viewdoc.getElementById("boxmodel-borders");
let paddingLayout = viewdoc.getElementById("boxmodel-padding");
let layouts = [paddingLayout, borderLayout, marginLayout];
let marginLayout = viewdoc.querySelector(".boxmodel-margins");
let borderLayout = viewdoc.querySelector(".boxmodel-borders");
let paddingLayout = viewdoc.querySelector(".boxmodel-paddings");
let contentLayout = viewdoc.querySelector(".boxmodel-contents");
let layouts = [contentLayout, paddingLayout, borderLayout, marginLayout];
layouts.forEach(layout => {
layout.click();
is(boxmodel.getAttribute("aria-activedescendant"), layout.id,
is(boxmodel.getAttribute("activedescendant"), layout.className,
"Should be set to" + layout.getAttribute("data-box") + "layout.");
});
}

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

@ -135,7 +135,7 @@
<div id="computedview-container">
<div id="computedview-container-focusable" tabindex="-1">
<div id="boxmodel-wrapper" tabindex="0">
<div id="boxmodel-wrapper">
</div>
<div id="propertyContainer" class="theme-separator" tabindex="0" dir="ltr">

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

@ -7,10 +7,6 @@
*/
.boxmodel-container {
/* The view will grow bigger as the window gets resized, until 400px */
max-width: 400px;
margin: 0px auto;
padding: 0;
overflow: auto;
}
@ -33,9 +29,11 @@
position: relative;
color: var(--theme-selection-color);
/* Make sure there is some space between the window's edges and the regions */
margin: 14px 14px 4px 14px;
margin: 14px auto;
width: calc(100% - 2 * 14px);
min-width: 240px;
/* The view will grow bigger as the window gets resized, until 400px */
max-width: 400px;
}
.boxmodel-box {