mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-12 14:20:14 +02:00
Differential Revision: https://phabricator.services.mozilla.com/D17526 --HG-- extra : moz-landing-system : lando
396 lines
11 KiB
JavaScript
396 lines
11 KiB
JavaScript
/* 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";
|
|
|
|
// This is loaded into all XUL windows. Wrap in a block to prevent
|
|
// leaking to window scope.
|
|
{
|
|
class MozRadiogroup extends MozElements.BaseControl {
|
|
constructor() {
|
|
super();
|
|
|
|
this.addEventListener("mousedown", (event) => {
|
|
if (this.disabled)
|
|
event.preventDefault();
|
|
});
|
|
|
|
/**
|
|
* keyboard navigation Here's how keyboard navigation works in radio groups on Windows:
|
|
* The group takes 'focus'
|
|
* The user is then free to navigate around inside the group
|
|
* using the arrow keys. Accessing previous or following radio buttons
|
|
* is done solely through the arrow keys and not the tab button. Tab
|
|
* takes you to the next widget in the tab order
|
|
*/
|
|
this.addEventListener("keypress", (event) => {
|
|
if (event.key != " " || event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.selectedItem = this.focusedItem;
|
|
this.selectedItem.doCommand();
|
|
// Prevent page from scrolling on the space key.
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", (event) => {
|
|
if (event.keyCode != KeyEvent.DOM_VK_UP || event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.checkAdjacentElement(false);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", (event) => {
|
|
if (event.keyCode != KeyEvent.DOM_VK_LEFT || event.originalTarget != this) {
|
|
return;
|
|
}
|
|
// left arrow goes back when we are ltr, forward when we are rtl
|
|
this.checkAdjacentElement(document.defaultView.getComputedStyle(
|
|
this).direction == "rtl");
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", (event) => {
|
|
if (event.keyCode != KeyEvent.DOM_VK_DOWN || event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.checkAdjacentElement(true);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", (event) => {
|
|
if (event.keyCode != KeyEvent.DOM_VK_RIGHT || event.originalTarget != this) {
|
|
return;
|
|
}
|
|
// right arrow goes forward when we are ltr, back when we are rtl
|
|
this.checkAdjacentElement(document.defaultView.getComputedStyle(
|
|
this).direction == "ltr");
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
/**
|
|
* set a focused attribute on the selected item when the group
|
|
* receives focus so that we can style it as if it were focused even though
|
|
* it is not (Windows platform behaviour is for the group to receive focus,
|
|
* not the item
|
|
*/
|
|
this.addEventListener("focus", (event) => {
|
|
if (event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.setAttribute("focused", "true");
|
|
if (this.focusedItem)
|
|
return;
|
|
|
|
var val = this.selectedItem;
|
|
if (!val || val.disabled || val.hidden || val.collapsed) {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) {
|
|
val = children[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.focusedItem = val;
|
|
});
|
|
|
|
this.addEventListener("blur", (event) => {
|
|
if (event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.removeAttribute("focused");
|
|
this.focusedItem = null;
|
|
});
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
this.init();
|
|
if (!this.value) {
|
|
this.selectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
init() {
|
|
this._radioChildren = null;
|
|
|
|
if (this.getAttribute("disabled") == "true")
|
|
this.disabled = true;
|
|
|
|
var children = this._getRadioChildren();
|
|
var length = children.length;
|
|
for (var i = 0; i < length; i++) {
|
|
if (children[i].getAttribute("selected") == "true") {
|
|
this.selectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
|
|
var value = this.value;
|
|
if (value)
|
|
this.value = value;
|
|
}
|
|
|
|
/**
|
|
* Called when a new <radio> gets added and XBL construction happens on
|
|
* it. Sometimes the XBL construction happens after the <radiogroup> has
|
|
* already been added to the DOM. This can happen due to asynchronous XBL
|
|
* construction (see Bug 1496137), or just due to normal DOM appending after
|
|
* the <radiogroup> is created. When this happens, reinitialize the UI if
|
|
* necessary to make sure the state is consistent.
|
|
*
|
|
* @param {DOMNode} child
|
|
* The <radio> element that got added
|
|
*/
|
|
radioChildConstructed(child) {
|
|
if (!this._radioChildren || !this._radioChildren.includes(child)) {
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
set value(val) {
|
|
this.setAttribute("value", val);
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; i++) {
|
|
if (String(children[i].value) == String(val)) {
|
|
this.selectedItem = children[i];
|
|
break;
|
|
}
|
|
}
|
|
return val;
|
|
}
|
|
|
|
get value() {
|
|
return this.getAttribute("value");
|
|
}
|
|
|
|
set disabled(val) {
|
|
if (val)
|
|
this.setAttribute("disabled", "true");
|
|
else
|
|
this.removeAttribute("disabled");
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
children[i].disabled = val;
|
|
}
|
|
return val;
|
|
}
|
|
|
|
get disabled() {
|
|
if (this.getAttribute("disabled") == "true")
|
|
return true;
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (!children[i].hidden && !children[i].collapsed && !children[i].disabled)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
get itemCount() {
|
|
return this._getRadioChildren().length;
|
|
}
|
|
|
|
set selectedIndex(val) {
|
|
this.selectedItem = this._getRadioChildren()[val];
|
|
return val;
|
|
}
|
|
|
|
get selectedIndex() {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i].selected)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
set selectedItem(val) {
|
|
var focused = this.getAttribute("focused") == "true";
|
|
var alreadySelected = false;
|
|
|
|
if (val) {
|
|
alreadySelected = val.getAttribute("selected") == "true";
|
|
val.setAttribute("focused", focused);
|
|
val.setAttribute("selected", "true");
|
|
this.setAttribute("value", val.value);
|
|
} else {
|
|
this.removeAttribute("value");
|
|
}
|
|
|
|
// uncheck all other group nodes
|
|
var children = this._getRadioChildren();
|
|
var previousItem = null;
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i] != val) {
|
|
if (children[i].getAttribute("selected") == "true")
|
|
previousItem = children[i];
|
|
|
|
children[i].removeAttribute("selected");
|
|
children[i].removeAttribute("focused");
|
|
}
|
|
}
|
|
|
|
var event = document.createEvent("Events");
|
|
event.initEvent("select", false, true);
|
|
this.dispatchEvent(event);
|
|
|
|
if (focused) {
|
|
if (alreadySelected) {
|
|
// Notify accessibility that this item got focus.
|
|
event = document.createEvent("Events");
|
|
event.initEvent("DOMMenuItemActive", true, true);
|
|
val.dispatchEvent(event);
|
|
} else {
|
|
// Only report if actual change
|
|
if (val) {
|
|
// Accessibility will fire focus for this.
|
|
event = document.createEvent("Events");
|
|
event.initEvent("RadioStateChange", true, true);
|
|
val.dispatchEvent(event);
|
|
}
|
|
|
|
if (previousItem) {
|
|
event = document.createEvent("Events");
|
|
event.initEvent("RadioStateChange", true, true);
|
|
previousItem.dispatchEvent(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
get selectedItem() {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i].selected)
|
|
return children[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
set focusedItem(val) {
|
|
if (val) {
|
|
val.setAttribute("focused", "true");
|
|
// Notify accessibility that this item got focus.
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("DOMMenuItemActive", true, true);
|
|
val.dispatchEvent(event);
|
|
}
|
|
|
|
// unfocus all other group nodes
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i] != val)
|
|
children[i].removeAttribute("focused");
|
|
}
|
|
return val;
|
|
}
|
|
|
|
get focusedItem() {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i].getAttribute("focused") == "true")
|
|
return children[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
checkAdjacentElement(aNextFlag) {
|
|
var currentElement = this.focusedItem || this.selectedItem;
|
|
var i;
|
|
var children = this._getRadioChildren();
|
|
for (i = 0; i < children.length; ++i) {
|
|
if (children[i] == currentElement)
|
|
break;
|
|
}
|
|
var index = i;
|
|
|
|
if (aNextFlag) {
|
|
do {
|
|
if (++i == children.length)
|
|
i = 0;
|
|
if (i == index)
|
|
break;
|
|
}
|
|
while (children[i].hidden || children[i].collapsed || children[i].disabled);
|
|
// XXX check for display/visibility props too
|
|
|
|
this.selectedItem = children[i];
|
|
children[i].doCommand();
|
|
} else {
|
|
do {
|
|
if (i == 0)
|
|
i = children.length;
|
|
if (--i == index)
|
|
break;
|
|
}
|
|
while (children[i].hidden || children[i].collapsed || children[i].disabled);
|
|
// XXX check for display/visibility props too
|
|
|
|
this.selectedItem = children[i];
|
|
children[i].doCommand();
|
|
}
|
|
}
|
|
|
|
_getRadioChildren() {
|
|
if (this._radioChildren)
|
|
return this._radioChildren;
|
|
|
|
var radioChildren = [];
|
|
|
|
if (this.hasChildNodes()) {
|
|
return this._radioChildren = [...this.querySelectorAll("radio")]
|
|
.filter(r => r.control == this);
|
|
}
|
|
|
|
// We don't have child nodes.
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/" +
|
|
"gatekeeper/there.is.only.xul";
|
|
|
|
var elems = this.ownerDocument.getElementsByAttribute("group", this.id);
|
|
for (var i = 0; i < elems.length; i++) {
|
|
if ((elems[i].namespaceURI == XUL_NS) &&
|
|
(elems[i].localName == "radio")) {
|
|
radioChildren.push(elems[i]);
|
|
}
|
|
}
|
|
return this._radioChildren = radioChildren;
|
|
}
|
|
|
|
getIndexOfItem(item) {
|
|
return this._getRadioChildren().indexOf(item);
|
|
}
|
|
|
|
getItemAtIndex(index) {
|
|
var children = this._getRadioChildren();
|
|
return (index >= 0 && index < children.length) ? children[index] : null;
|
|
}
|
|
|
|
appendItem(label, value) {
|
|
var radio = document.createXULElement("radio");
|
|
radio.setAttribute("label", label);
|
|
radio.setAttribute("value", value);
|
|
this.appendChild(radio);
|
|
return radio;
|
|
}
|
|
}
|
|
|
|
MozXULElement.implementCustomInterface(MozRadiogroup, [
|
|
Ci.nsIDOMXULSelectControlElement,
|
|
Ci.nsIDOMXULRadioGroupElement,
|
|
]);
|
|
|
|
customElements.define("radiogroup", MozRadiogroup);
|
|
}
|