зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1518487 - implement simple accessible list component similar to the one used in debugger.html and memory panel. r=nchevobbe
MozReview-Commit-ID: 2KCXrB9zCr0 Differential Revision: https://phabricator.services.mozilla.com/D19052 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
162e11d369
Коммит
2a58419670
|
@ -0,0 +1,41 @@
|
|||
/* 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/. */
|
||||
|
||||
/* List */
|
||||
|
||||
.list {
|
||||
background-color: var(--theme-sidebar-background);
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.list:focus, .list .list-item-content:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.list::-moz-focus-inner, .list .list-item-content::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.list li.current {
|
||||
background-color: var(--theme-toolbar-hover);
|
||||
}
|
||||
|
||||
.list:focus li.current, .list li.active.current {
|
||||
background-color: var(--theme-emphasized-splitter-color);
|
||||
}
|
||||
|
||||
.list:focus li:not(.current):hover,
|
||||
.list:not(:focus) li:not(.active):hover {
|
||||
background-color: var(--theme-selection-background-hover);
|
||||
}
|
||||
|
||||
.list .list-item-content:not(:empty) {
|
||||
font-size: 12px;
|
||||
overflow: auto;
|
||||
}
|
|
@ -0,0 +1,378 @@
|
|||
/* 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 {
|
||||
createFactory,
|
||||
createRef,
|
||||
Component,
|
||||
cloneElement,
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const { ul, li, div } = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
|
||||
const { scrollIntoView } = require("devtools/client/shared/scroll");
|
||||
|
||||
loader.lazyRequireGetter(this, "focusableSelector", "devtools/client/shared/focus", true);
|
||||
|
||||
class ListItemClass extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
active: PropTypes.bool,
|
||||
current: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
item: PropTypes.shape({
|
||||
component: PropTypes.object,
|
||||
componentProps: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.contentRef = createRef();
|
||||
|
||||
this._setTabbableState = this._setTabbableState.bind(this);
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
this._wrapMoveFocus = this._wrapMoveFocus.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._setTabbableState();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._setTabbableState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all elements that are focusable with a keyboard inside the list item.
|
||||
*/
|
||||
getFocusableElements() {
|
||||
return Array.from(this.contentRef.current.querySelectorAll(focusableSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap and move keyboard focus to first/last focusable element inside the list item to
|
||||
* prevent the focus from escaping the list item container.
|
||||
* element).
|
||||
*
|
||||
* @param {DOMNode} current currently focused element
|
||||
* @param {Boolean} back direction
|
||||
* @return {Boolean} true there is a newly focused element.
|
||||
*/
|
||||
_wrapMoveFocus(current, back) {
|
||||
const elms = this.getFocusableElements();
|
||||
let next;
|
||||
|
||||
if (elms.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (back) {
|
||||
if (elms.indexOf(current) === 0) {
|
||||
next = elms[elms.length - 1];
|
||||
next.focus();
|
||||
}
|
||||
} else if (elms.indexOf(current) === elms.length - 1) {
|
||||
next = elms[0];
|
||||
next.focus();
|
||||
}
|
||||
|
||||
return !!next;
|
||||
}
|
||||
|
||||
_onKeyDown(event) {
|
||||
const { target, key, shiftKey } = event;
|
||||
|
||||
if (key !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusMoved = this._wrapMoveFocus(target, shiftKey);
|
||||
if (focusMoved) {
|
||||
// Focus was moved to the begining/end of the list, so we need to prevent the
|
||||
// default focus change that would happen here.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that none of the focusable elements inside the list item container are
|
||||
* tabbable if the list item is not active. If the list item is active and focus is
|
||||
* outside its container, focus on the first focusable element inside.
|
||||
*/
|
||||
_setTabbableState() {
|
||||
const elms = this.getFocusableElements();
|
||||
if (elms.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.props.active) {
|
||||
elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elms.includes(document.activeElement)) {
|
||||
elms[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { active, item, current, onClick } = this.props;
|
||||
const { className, component, componentProps } = item;
|
||||
|
||||
return (
|
||||
li({
|
||||
className: `${className}${current ? " current" : ""}${active ? " active" : ""}`,
|
||||
id: item.key,
|
||||
onClick,
|
||||
onKeyDownCapture: active && this._onKeyDown,
|
||||
},
|
||||
div({
|
||||
className: "list-item-content",
|
||||
role: "presentation",
|
||||
ref: this.contentRef,
|
||||
}, cloneElement(component, componentProps || {}))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ListItem = createFactory(ListItemClass);
|
||||
|
||||
class List extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
// A list of all items to be rendered using a List component.
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
component: PropTypes.object,
|
||||
componentProps: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
key: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
|
||||
// Note: the two properties below are mutually exclusive. Only one of the
|
||||
// label properties is necessary.
|
||||
// ID of an element whose textual content serves as an accessible label for
|
||||
// a list.
|
||||
labelledBy: PropTypes.string,
|
||||
|
||||
// Accessibility label for a list widget.
|
||||
label: PropTypes.string,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.listRef = createRef();
|
||||
|
||||
this.state = {
|
||||
active: null,
|
||||
current: null,
|
||||
mouseDown: false,
|
||||
};
|
||||
|
||||
this._setCurrentItem = this._setCurrentItem.bind(this);
|
||||
this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
|
||||
this._preventDefaultAndStopPropagation =
|
||||
this._preventDefaultAndStopPropagation.bind(this);
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { active, current, mouseDown } = this.state;
|
||||
|
||||
return current !== nextState.current ||
|
||||
active !== nextState.active ||
|
||||
mouseDown === nextState.mouseDown;
|
||||
}
|
||||
|
||||
_preventArrowKeyScrolling(e) {
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
case "ArrowDown":
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight":
|
||||
this._preventDefaultAndStopPropagation(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_preventDefaultAndStopPropagation(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.nativeEvent) {
|
||||
if (e.nativeEvent.preventDefault) {
|
||||
e.nativeEvent.preventDefault();
|
||||
}
|
||||
if (e.nativeEvent.stopPropagation) {
|
||||
e.nativeEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the passed in item to be the current item.
|
||||
*
|
||||
* @param {null|Number} index
|
||||
* The index of the item in to be set as current, or undefined to unset the
|
||||
* current item.
|
||||
*/
|
||||
_setCurrentItem(index = -1, options = {}) {
|
||||
const item = this.props.items[index];
|
||||
if (item !== undefined && !options.preventAutoScroll) {
|
||||
const element = document.getElementById(item.key);
|
||||
scrollIntoView(element, {
|
||||
...options,
|
||||
container: this.listRef.current,
|
||||
});
|
||||
}
|
||||
|
||||
const state = {};
|
||||
if (this.state.active != undefined) {
|
||||
state.active = null;
|
||||
if (this.listRef.current !== document.activeElement) {
|
||||
this.listRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.current !== index) {
|
||||
this.setState({
|
||||
...state,
|
||||
current: index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles key down events in the list's container.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
_onKeyDown(e) {
|
||||
const { active, current } = this.state;
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventArrowKeyScrolling(e);
|
||||
|
||||
const { length } = this.props.items;
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
(current > 0) && this._setCurrentItem(current - 1, { alignTo: "top" });
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
(current < length - 1) && this._setCurrentItem(
|
||||
current + 1, { alignTo: "bottom" });
|
||||
break;
|
||||
|
||||
case "Home":
|
||||
this._setCurrentItem(0, { alignTo: "top" });
|
||||
break;
|
||||
|
||||
case "End":
|
||||
this._setCurrentItem(length - 1, { alignTo: "bottom" });
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
case " ":
|
||||
// On space or enter make current list item active. This means keyboard focus
|
||||
// handling is passed on to the component within the list item.
|
||||
if (document.activeElement === this.listRef.current) {
|
||||
this._preventDefaultAndStopPropagation(e);
|
||||
if (active !== current) {
|
||||
this.setState({ active: current });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "Escape":
|
||||
// If current list item is active, make it inactive and let keyboard focusing be
|
||||
// handled normally.
|
||||
this._preventDefaultAndStopPropagation(e);
|
||||
if (active != null) {
|
||||
this.setState({ active: null });
|
||||
}
|
||||
|
||||
this.listRef.current.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { active, current } = this.state;
|
||||
const { items } = this.props;
|
||||
|
||||
return (
|
||||
ul({
|
||||
ref: this.listRef,
|
||||
className: "list",
|
||||
tabIndex: 0,
|
||||
onKeyDown: this._onKeyDown,
|
||||
onKeyPress: this._preventArrowKeyScrolling,
|
||||
onKeyUp: this._preventArrowKeyScrolling,
|
||||
onMouseDown: () => this.setState({ mouseDown: true }),
|
||||
onMouseUp: () => this.setState({ mouseDown: false }),
|
||||
onFocus: () => {
|
||||
if (current != null || this.state.mouseDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only set default current to the first list item if current item is
|
||||
// not yet set and the focus event is not the result of a mouse
|
||||
// interarction.
|
||||
this._setCurrentItem(0);
|
||||
},
|
||||
onClick: () => {
|
||||
// Focus should always remain on the list container itself.
|
||||
this.listRef.current.focus();
|
||||
},
|
||||
onBlur: e => {
|
||||
if (active != null) {
|
||||
const { relatedTarget } = e;
|
||||
if (!this.listRef.current.contains(relatedTarget)) {
|
||||
this.setState({ active: null });
|
||||
}
|
||||
}
|
||||
},
|
||||
"aria-label": this.props.label,
|
||||
"aria-labelledby": this.props.labelledBy,
|
||||
"aria-activedescendant": (current != null) ? items[current].key : null,
|
||||
},
|
||||
items.map((item, index) => {
|
||||
return ListItem({
|
||||
item,
|
||||
current: index === current,
|
||||
active: index === active,
|
||||
// We make a key unique depending on whether the list item is in active or
|
||||
// inactive state to make sure that it is actually replaced and the tabbable
|
||||
// state is reset.
|
||||
key: `${item.key}-${index === active ? "active" : "inactive"}`,
|
||||
// Since the user just clicked the item, there's no need to check if it should
|
||||
// be scrolled into view.
|
||||
onClick: () => this._setCurrentItem(index, { preventAutoScroll: true }),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ListItem: ListItemClass,
|
||||
List,
|
||||
};
|
|
@ -19,6 +19,8 @@ DevToolsModules(
|
|||
'AutoCompletePopup.js',
|
||||
'Frame.js',
|
||||
'HSplitBox.js',
|
||||
'List.css',
|
||||
'List.js',
|
||||
'MdnLink.css',
|
||||
'MdnLink.js',
|
||||
'NotificationBox.css',
|
||||
|
|
|
@ -7,6 +7,8 @@ support-files =
|
|||
[test_frame_01.html]
|
||||
[test_frame_02.html]
|
||||
[test_HSplitBox_01.html]
|
||||
[test_list.html]
|
||||
[test_list_keyboard.html]
|
||||
[test_notification_box_01.html]
|
||||
[test_notification_box_02.html]
|
||||
[test_notification_box_03.html]
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
<!-- 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/. -->
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that List renders correctly.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>List component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript"></script>
|
||||
<script src="list.snapshots.js" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
|
||||
"use strict";
|
||||
|
||||
window.onload = async function() {
|
||||
try {
|
||||
const { div } = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const React = browserRequire("devtools/client/shared/vendor/react");
|
||||
const {
|
||||
Simulate,
|
||||
renderIntoDocument,
|
||||
findRenderedDOMComponentWithClass,
|
||||
scryRenderedDOMComponentsWithTag,
|
||||
scryRenderedComponentsWithType,
|
||||
} = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
|
||||
const { List, ListItem } =
|
||||
browserRequire("devtools/client/shared/components/List");
|
||||
|
||||
const testItems = [
|
||||
{
|
||||
component: div({ className: "item-1" }, "Test List Item 1"),
|
||||
className: "list-item-1",
|
||||
key: "list-item-1",
|
||||
},
|
||||
{
|
||||
component: div({ className: "item-2" }, "Test List Item 2"),
|
||||
className: "list-item-2",
|
||||
key: "list-item-2",
|
||||
},
|
||||
{
|
||||
component: div({ className: "item-3" }, "Test List Item 3"),
|
||||
className: "list-item-3",
|
||||
key: "list-item-3",
|
||||
},
|
||||
];
|
||||
|
||||
const listReactEl = React.createElement(List, {
|
||||
items: testItems,
|
||||
labelledBy: "test-labelledby",
|
||||
});
|
||||
|
||||
const list = renderIntoDocument(listReactEl);
|
||||
const listEl = findRenderedDOMComponentWithClass(list, "list");
|
||||
const items = scryRenderedComponentsWithType(list, ListItem);
|
||||
let itemEls = scryRenderedDOMComponentsWithTag(list, "li");
|
||||
|
||||
function testCurrent(index) {
|
||||
is(list.state.current, index, "Correct current item.");
|
||||
is(listEl.getAttribute("aria-activedescendant"), testItems[index].key,
|
||||
"Correct active descendant.");
|
||||
}
|
||||
|
||||
is(items.length, 3, "Correct number of list item components in tree.");
|
||||
is(itemEls.length, 3, "Correct number of list items is rendered.");
|
||||
info("Testing initial tree properties.");
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index];
|
||||
const itemEl = itemEls[index];
|
||||
const { active, current, item: itemProp } = item.props;
|
||||
const content = itemEl.querySelector(".list-item-content");
|
||||
|
||||
is(active, false, "Correct active state.");
|
||||
is(current, false, "Correct current state.");
|
||||
is(itemProp, testItems[index], "Correct rendered item.");
|
||||
is(item.contentRef.current, content, "Correct content ref.");
|
||||
|
||||
is(itemEl.className, testItems[index].className, "Correct list item class.");
|
||||
is(itemEl.id, testItems[index].key, "Correct list item it.");
|
||||
is(content.getAttribute("role"), "presentation", "Correct content role.");
|
||||
|
||||
is(content.innerHTML,
|
||||
`<div class="item-${index + 1}">Test List Item ${index + 1}</div>`,
|
||||
"Content rendered correctly.");
|
||||
}
|
||||
|
||||
is(list.state.current, null, "Current item is not set by default.");
|
||||
is(list.state.active, null, "Active item is not set by default.");
|
||||
is(list.listRef.current, listEl, "Correct list ref.");
|
||||
|
||||
is(listEl.className, "list", "Correct list class.");
|
||||
is(listEl.tabIndex, 0, "List is focusable.");
|
||||
ok(!listEl.hasAttribute("aria-label"), "List has no label.");
|
||||
is(listEl.getAttribute("aria-labelledby"), "test-labelledby",
|
||||
"Correct list labelled by attribute.");
|
||||
ok(!listEl.hasAttribute("aria-activedescendant"),
|
||||
"No active descendant set by default.");
|
||||
|
||||
Simulate.focus(listEl);
|
||||
testCurrent(0);
|
||||
|
||||
Simulate.click(itemEls[2]);
|
||||
testCurrent(2);
|
||||
|
||||
Simulate.blur(listEl);
|
||||
testCurrent(2);
|
||||
|
||||
Simulate.focus(listEl);
|
||||
testCurrent(2);
|
||||
} catch (e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,283 @@
|
|||
<!-- 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/. -->
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that List component has working keyboard interactions.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>List component keyboard test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
|
||||
"use strict";
|
||||
|
||||
window.onload = function() {
|
||||
try {
|
||||
const { a, button, div } =
|
||||
require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const React = browserRequire("devtools/client/shared/vendor/react");
|
||||
const {
|
||||
Simulate,
|
||||
findRenderedDOMComponentWithClass,
|
||||
findRenderedDOMComponentWithTag,
|
||||
scryRenderedDOMComponentsWithTag,
|
||||
} = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
|
||||
const { List } =
|
||||
browserRequire("devtools/client/shared/components/List");
|
||||
|
||||
const testItems = [
|
||||
{
|
||||
component: div({}, "Test List Item 1"),
|
||||
className: "list-item-1",
|
||||
key: "list-item-1",
|
||||
},
|
||||
{
|
||||
component: div({},
|
||||
"Test List Item 2",
|
||||
a({ href: "#" }, "Focusable 1"),
|
||||
button({ }, "Focusable 2")),
|
||||
className: "list-item-2",
|
||||
key: "list-item-2",
|
||||
},
|
||||
{
|
||||
component: div({}, "Test List Item 3"),
|
||||
className: "list-item-3",
|
||||
key: "list-item-3",
|
||||
},
|
||||
];
|
||||
|
||||
const list = React.createElement(List, {
|
||||
items: testItems,
|
||||
labelledby: "test-labelledby",
|
||||
});
|
||||
|
||||
const tree = ReactDOM.render(list, document.body);
|
||||
const listEl = findRenderedDOMComponentWithClass(tree, "list");
|
||||
const items = scryRenderedDOMComponentsWithTag(tree, "li");
|
||||
const defaultFocus = listEl.ownerDocument.body;
|
||||
|
||||
function blurEl(el) {
|
||||
// Simulate.blur does not seem to update the activeElement.
|
||||
el.blur();
|
||||
}
|
||||
|
||||
function focusEl(el) {
|
||||
// Simulate.focus does not seem to update the activeElement.
|
||||
el.focus();
|
||||
}
|
||||
|
||||
const tests = [{
|
||||
name: "Test default List state. Keyboard focus is set to document body by default.",
|
||||
state: { current: null, active: null },
|
||||
activeElement: defaultFocus,
|
||||
}, {
|
||||
name: "Current item must be set to the first list item on initial focus. " +
|
||||
"Keyboard focus should be set on list's conatiner (<ul>).",
|
||||
action: () => focusEl(listEl),
|
||||
activeElement: listEl,
|
||||
state: { current: 0 },
|
||||
}, {
|
||||
name: "Current item should remain set even when the list is blured. " +
|
||||
"Keyboard focus should be set back to document body.",
|
||||
action: () => blurEl(listEl),
|
||||
state: { current: 0 },
|
||||
activeElement: defaultFocus,
|
||||
}, {
|
||||
name: "Unset list's current state.",
|
||||
action: () => tree.setState({ current: null }),
|
||||
state: { current: null },
|
||||
}, {
|
||||
name: "Current item must be re-set again to the first list item on initial " +
|
||||
"focus. Keyboard focus should be set on list's conatiner (<ul>).",
|
||||
action: () => focusEl(listEl),
|
||||
activeElement: listEl,
|
||||
state: { current: 0 },
|
||||
}, {
|
||||
name: "Current item should be updated to next on ArrowDown.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
|
||||
state: { current: 1 },
|
||||
}, {
|
||||
name: "Current item should be updated to last on ArrowDown.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
|
||||
state: { current: 2 },
|
||||
}, {
|
||||
name: "Current item should remain on last on ArrowDown.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
|
||||
state: { current: 2 },
|
||||
}, {
|
||||
name: "Current item should be updated to previous on ArrowUp.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
|
||||
state: { current: 1 },
|
||||
}, {
|
||||
name: "Current item should be updated to first on ArrowUp.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
|
||||
state: { current: 0 },
|
||||
}, {
|
||||
name: "Current item should remain on first on ArrowUp.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
|
||||
state: { current: 0 },
|
||||
}, {
|
||||
name: "Current item should be updated to last on End.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "End" }},
|
||||
state: { current: 2 },
|
||||
}, {
|
||||
name: "Current item should be updated to first on Home.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "Home" }},
|
||||
state: { current: 0 },
|
||||
}, {
|
||||
name: "Current item should be set as active on Enter.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
|
||||
state: { current: 0, active: 0 },
|
||||
activeElement: listEl,
|
||||
}, {
|
||||
name: "Active item should be unset on Escape.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "Escape" }},
|
||||
state: { current: 0, active: null },
|
||||
}, {
|
||||
name: "Current item should be set as active on Space.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: " " }},
|
||||
state: { current: 0, active: 0 },
|
||||
activeElement: listEl,
|
||||
}, {
|
||||
name: "Current item should unset when focus leaves the list.",
|
||||
action: () => blurEl(listEl),
|
||||
state: { current: 0, active: null },
|
||||
activeElement: defaultFocus,
|
||||
}, {
|
||||
name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.",
|
||||
action: () => focusEl(listEl),
|
||||
activeElement: listEl,
|
||||
}, {
|
||||
name: "Current item should be updated to next on ArrowDown.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
|
||||
state: { current: 1, active: null },
|
||||
}, {
|
||||
name: "Current item should be set as active on Enter. Keyboard focus should be " +
|
||||
"set on the first focusable element inside the list item, if available.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
|
||||
state: { current: 1, active: 1 },
|
||||
get activeElement() {
|
||||
// When list item becomes active/inactive, it is replaced with a newly rendered
|
||||
// one.
|
||||
return findRenderedDOMComponentWithTag(tree, "a");
|
||||
},
|
||||
}, {
|
||||
name: "Keyboard focus should be set to next tabbable element inside the active " +
|
||||
"list item on Tab.",
|
||||
action() {
|
||||
synthesizeKey("KEY_Tab");
|
||||
},
|
||||
state: { current: 1, active: 1 },
|
||||
get activeElement() {
|
||||
// When list item becomes active/inactive, it is replaced with a newly rendered
|
||||
// one.
|
||||
return findRenderedDOMComponentWithTag(tree, "button");
|
||||
},
|
||||
}, {
|
||||
name: "Keyboard focus should wrap inside the list item when focused on last " +
|
||||
"tabbable element.",
|
||||
action() {
|
||||
synthesizeKey("KEY_Tab");
|
||||
},
|
||||
state: { current: 1, active: 1 },
|
||||
get activeElement() {
|
||||
return findRenderedDOMComponentWithTag(tree, "a");
|
||||
},
|
||||
}, {
|
||||
name: "Keyboard focus should wrap inside the list item when focused on first " +
|
||||
"tabbable element.",
|
||||
action() {
|
||||
synthesizeKey("KEY_Tab", { shiftKey: true });
|
||||
},
|
||||
state: { current: 1, active: 1 },
|
||||
get activeElement() {
|
||||
return findRenderedDOMComponentWithTag(tree, "button");
|
||||
},
|
||||
}, {
|
||||
name: "Active item should be unset on Escape. Focus should move back to the " +
|
||||
"list container.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "Escape" }},
|
||||
state: { current: 1, active: null },
|
||||
activeElement: listEl,
|
||||
}, {
|
||||
name: "Current item should be set as active on Space. Keyboard focus should be " +
|
||||
"set on the first focusable element inside the list item, if available.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: " " }},
|
||||
state: { current: 1, active: 1 },
|
||||
get activeElement() {
|
||||
// When list item becomes active/inactive, it is replaced with a newly rendered
|
||||
// one.
|
||||
return findRenderedDOMComponentWithTag(tree, "a");
|
||||
},
|
||||
}, {
|
||||
name: "Current item should remain set even when the list is blured. " +
|
||||
"Keyboard focus should be set back to document body.",
|
||||
action: () => listEl.ownerDocument.activeElement.blur(),
|
||||
state: { current: 1, active: null, },
|
||||
activeElement: defaultFocus,
|
||||
}, {
|
||||
name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.",
|
||||
action: () => focusEl(listEl),
|
||||
state: { current: 1, active: null },
|
||||
activeElement: listEl,
|
||||
}, {
|
||||
name: "Current item should be updated to previous on ArrowUp.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
|
||||
state: { current: 0, active: null },
|
||||
}, {
|
||||
name: "Current item should be set as active on Enter.",
|
||||
event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
|
||||
state: { current: 0, active: 0 },
|
||||
activeElement: listEl,
|
||||
}, {
|
||||
name: "Keyboard focus should move to another focusable element outside of the " +
|
||||
"list when there's nothing to focus on inside the list item.",
|
||||
action() {
|
||||
synthesizeKey("KEY_Tab", { shiftKey: true });
|
||||
},
|
||||
state: { current: 0, active: null },
|
||||
activeElement: listEl.ownerDocument.documentElement,
|
||||
}];
|
||||
|
||||
for (const test of tests) {
|
||||
const { action, condition, event, state, name } = test;
|
||||
|
||||
is(listEl, findRenderedDOMComponentWithClass(tree, "list"), "Sanity check");
|
||||
|
||||
info(name);
|
||||
if (event) {
|
||||
const { type, options, el } = event;
|
||||
Simulate[type](el, options);
|
||||
} else if (action) {
|
||||
action();
|
||||
}
|
||||
|
||||
if (test.activeElement) {
|
||||
is(listEl.ownerDocument.activeElement, test.activeElement,
|
||||
"Focus is set correctly.");
|
||||
}
|
||||
|
||||
for (let key in state) {
|
||||
is(tree.state[key], state[key], `${key} state is correct.`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче