Bug 1506787: Support tabindex attribute (including value -1) on non-control XUL elements. r=smaug

Previously, the tabindex attribute wasn't supported on non-control XUL elements at all.
The only way to make those focusable was to use -moz-user-focus: normal.
However, that caused the element to be included in the tab order; there was no way to make it focusable but not tabbable.
This can now be achieved using tabindex="-1".
This will primarily be useful for buttons on toolbars, which will be grouped under a single tab stop for efficiency.

For consistency, this also changes the behaviour of tabindex="-1" with -moz-user-focus: ignore on XUL controls.
Previously, -moz-user-focus: ignore would override tabindex="-1", making the element unfocusable.
Now, the tabindex attribute always overrides if explicitly specified.

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
James Teh 2018-11-17 02:38:27 +00:00
parent 29161b173a
commit 9e968bb929
5 changed files with 106 additions and 76 deletions

View File

@ -7,10 +7,9 @@
interface nsIControllers; interface nsIControllers;
[scriptable, uuid(ea7f92d0-b379-4107-91b4-1e69bdd771e3)] [scriptable, uuid(bdc1d047-6d22-4813-bc50-638ccb349c7d)]
interface nsIDOMXULControlElement : nsISupports { interface nsIDOMXULControlElement : nsISupports {
attribute boolean disabled; attribute boolean disabled;
attribute long tabIndex;
// XXX defined in XULElement, but should be defined here // XXX defined in XULElement, but should be defined here
// readonly attribute nsIControllers controllers; // readonly attribute nsIControllers controllers;

View File

@ -1637,7 +1637,7 @@ SimpleTest.waitForFocus(startTest);
</hbox> </hbox>
<hbox> <hbox>
<button id="o2" accesskey="o" style="-moz-user-focus: ignore;" label="no tabindex"/> <button id="o2" accesskey="o" style="-moz-user-focus: ignore;" label="no tabindex"/>
<button id="o4" style="-moz-user-focus: ignore;" label="tabindex = -1" tabindex="-1"/> <button id="o4" style="-moz-user-focus: ignore;" label="no tabindex"/>
<button id="t6" style="-moz-user-focus: ignore;" label="tabindex = 0" tabindex="0"/> <button id="t6" style="-moz-user-focus: ignore;" label="tabindex = 0" tabindex="0"/>
<button id="t2" style="-moz-user-focus: ignore;" label="tabindex = 2" tabindex="2"/> <button id="t2" style="-moz-user-focus: ignore;" label="tabindex = 2" tabindex="2"/>
</hbox> </hbox>
@ -1663,17 +1663,17 @@ SimpleTest.waitForFocus(startTest);
<vbox> <vbox>
<hbox> <hbox>
<dropmarker id="o6" value="no tabindex"/> <dropmarker id="o6" value="no tabindex"/>
<dropmarker id="o8" value="tabindex = -1" tabindex="-1"/> <dropmarker id="o8" value="no tabindex"/>
<dropmarker id="o10" value="tabindex = 0" tabindex="0"/> <dropmarker id="o10" value="no tabindx"/>
<dropmarker id="o12" value="tabindex = 2" tabindex="2"/> <dropmarker id="o12" value="no tabindex"/>
<dropmarker id="t9" accesskey="r" style="-moz-user-focus: normal;" value="no tabindex" /> <dropmarker id="t9" accesskey="r" style="-moz-user-focus: normal;" value="no tabindex" />
<dropmarker id="t10" style="-moz-user-focus: normal;" value="tabindex = -1" tabindex="-1" /> <dropmarker id="t10" style="-moz-user-focus: normal;" value="no tabindex"/>
<dropmarker id="t11" style="-moz-user-focus: normal;" value="tabindex = 0" tabindex="0" /> <dropmarker id="t11" style="-moz-user-focus: normal;" value="tabindex = 0" tabindex="0" />
<dropmarker id="t12" style="-moz-user-focus: normal;" value="tabindex = 2" tabindex="2" /> <dropmarker id="t12" style="-moz-user-focus: normal;" value="no tabindex"/>
<dropmarker id="o14" style="-moz-user-focus: ignore;" value="no tabindex"/> <dropmarker id="o14" style="-moz-user-focus: ignore;" value="no tabindex"/>
<dropmarker id="o16" style="-moz-user-focus: ignore;" value="tabindex = -1" tabindex="-1"/> <dropmarker id="o16" style="-moz-user-focus: ignore;" value="no tabindex"/>
<dropmarker id="n1" style="-moz-user-focus: none;" value="tabindex = 0" tabindex="0"/> <dropmarker id="n1" style="-moz-user-focus: none;" value="no tabindex"/>
<dropmarker id="n3" style="-moz-user-focus: none;" value="tabindex = 2" tabindex="2"/> <dropmarker id="n3" style="-moz-user-focus: none;" value="no tabindex"/>
</hbox> </hbox>
</vbox> </vbox>
<browser id="childframe" type="content" src="child_focus_frame.html" width="300" height="195"/> <browser id="childframe" type="content" src="child_focus_frame.html" width="300" height="195"/>

View File

@ -75,6 +75,8 @@ interface XULElement : Element {
[Throws] [Throws]
readonly attribute BoxObject? boxObject; readonly attribute BoxObject? boxObject;
[SetterThrows]
attribute long tabIndex;
[Throws] [Throws]
void focus(); void focus();
[Throws] [Throws]

View File

@ -439,16 +439,15 @@ nsXULElement::IsFocusableInternal(int32_t *aTabIndex, bool aWithMouse)
* For controls, the element cannot be focused and is not part of the tab * For controls, the element cannot be focused and is not part of the tab
* order if it is disabled. * order if it is disabled.
* *
* Controls (those that implement nsIDOMXULControlElement): * -moz-user-focus is overridden if a tabindex (even -1) is specified.
*
* Specifically, the behaviour for all XUL elements is as follows:
* *aTabIndex = -1 no tabindex Not focusable or tabbable * *aTabIndex = -1 no tabindex Not focusable or tabbable
* *aTabIndex = -1 tabindex="-1" Not focusable or tabbable * *aTabIndex = -1 tabindex="-1" Focusable but not tabbable
* *aTabIndex = -1 tabindex=">=0" Focusable and tabbable * *aTabIndex = -1 tabindex=">=0" Focusable and tabbable
* *aTabIndex >= 0 no tabindex Focusable and tabbable * *aTabIndex >= 0 no tabindex Focusable and tabbable
* *aTabIndex >= 0 tabindex="-1" Focusable but not tabbable * *aTabIndex >= 0 tabindex="-1" Focusable but not tabbable
* *aTabIndex >= 0 tabindex=">=0" Focusable and tabbable * *aTabIndex >= 0 tabindex=">=0" Focusable and tabbable
* Non-controls:
* *aTabIndex = -1 Not focusable or tabbable
* *aTabIndex >= 0 Focusable and tabbable
* *
* If aTabIndex is null, then the tabindex is not computed, and * If aTabIndex is null, then the tabindex is not computed, and
* true is returned for non-disabled controls and false otherwise. * true is returned for non-disabled controls and false otherwise.
@ -483,36 +482,32 @@ nsXULElement::IsFocusableInternal(int32_t *aTabIndex, bool aWithMouse)
} }
if (aTabIndex) { if (aTabIndex) {
if (xulControl) { if (HasAttr(kNameSpaceID_None, nsGkAtoms::tabindex)) {
if (HasAttr(kNameSpaceID_None, nsGkAtoms::tabindex)) { // The tabindex attribute was specified, so the element becomes
// if either the aTabIndex argument or a specified tabindex is non-negative, // focusable.
// the element becomes focusable. shouldFocus = true;
int32_t tabIndex = 0; *aTabIndex = TabIndex();
xulControl->GetTabIndex(&tabIndex);
shouldFocus = *aTabIndex >= 0 || tabIndex >= 0;
*aTabIndex = tabIndex;
} else {
// otherwise, if there is no tabindex attribute, just use the value of
// *aTabIndex to indicate focusability. Reset any supplied tabindex to 0.
shouldFocus = *aTabIndex >= 0;
if (shouldFocus)
*aTabIndex = 0;
}
if (shouldFocus && sTabFocusModelAppliesToXUL &&
!(sTabFocusModel & eTabFocus_formElementsMask)) {
// By default, the tab focus model doesn't apply to xul element on any system but OS X.
// on OS X we're following it for UI elements (XUL) as sTabFocusModel is based on
// "Full Keyboard Access" system setting (see mac/nsILookAndFeel).
// both textboxes and list elements (i.e. trees and list) should always be focusable
// (textboxes are handled as html:input)
// For compatibility, we only do this for controls, otherwise elements like <browser>
// cannot take this focus.
if (IsNonList(mNodeInfo))
*aTabIndex = -1;
}
} else { } else {
// otherwise, if there is no tabindex attribute, just use the value of
// *aTabIndex to indicate focusability. Reset any supplied tabindex to 0.
shouldFocus = *aTabIndex >= 0; shouldFocus = *aTabIndex >= 0;
if (shouldFocus) {
*aTabIndex = 0;
}
}
if (xulControl && shouldFocus && sTabFocusModelAppliesToXUL &&
!(sTabFocusModel & eTabFocus_formElementsMask)) {
// By default, the tab focus model doesn't apply to xul element on any system but OS X.
// on OS X we're following it for UI elements (XUL) as sTabFocusModel is based on
// "Full Keyboard Access" system setting (see mac/nsILookAndFeel).
// both textboxes and list elements (i.e. trees and list) should always be focusable
// (textboxes are handled as html:input)
// For compatibility, we only do this for controls, otherwise elements like <browser>
// cannot take this focus.
if (IsNonList(mNodeInfo)) {
*aTabIndex = -1;
}
} }
} }
@ -1035,6 +1030,11 @@ nsXULElement::ParseAttribute(int32_t aNamespaceID,
nsIPrincipal* aMaybeScriptedPrincipal, nsIPrincipal* aMaybeScriptedPrincipal,
nsAttrValue& aResult) nsAttrValue& aResult)
{ {
if (aNamespaceID == kNameSpaceID_None &&
aAttribute == nsGkAtoms::tabindex) {
return aResult.ParseIntValue(aValue);
}
// Parse into a nsAttrValue // Parse into a nsAttrValue
if (!nsStyledElement::ParseAttribute(aNamespaceID, aAttribute, aValue, if (!nsStyledElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
aMaybeScriptedPrincipal, aResult)) { aMaybeScriptedPrincipal, aResult)) {
@ -1960,6 +1960,10 @@ nsXULPrototypeElement::SetAttrAt(uint32_t aPos, const nsAString& aValue,
return NS_OK; return NS_OK;
} }
// Don't abort if parsing failed, it could just be malformed css. // Don't abort if parsing failed, it could just be malformed css.
} else if (mAttributes[aPos].mName.Equals(nsGkAtoms::tabindex)) {
mAttributes[aPos].mValue.ParseIntValue(aValue);
return NS_OK;
} }
mAttributes[aPos].mValue.ParseStringOrAtom(aValue); mAttributes[aPos].mValue.ParseStringOrAtom(aValue);

View File

@ -13,51 +13,48 @@
Elements are navigated in the following order: Elements are navigated in the following order:
1. tabindex > 0 in tree order 1. tabindex > 0 in tree order
2. tabindex = 0 in tree order 2. tabindex = 0 in tree order
Elements with tabindex = -1 are not in the tab order Elements with tabindex = -1 are focusable, but not in the tab order
--> -->
<hbox> <hbox>
<button id="t5" label="One"/> <button id="t7" label="One"/>
<checkbox id="no1" label="Two" tabindex="-1"/> <checkbox id="f1" label="Two" tabindex="-1"/>
<button id="t6" label="Three" tabindex="0"/> <button id="t8" label="Three" tabindex="0"/>
<checkbox id="t1" label="Four" tabindex="1"/> <checkbox id="t1" label="Four" tabindex="1"/>
</hbox> </hbox>
<hbox> <hbox>
<textbox id="t7" idmod="t3" size="3"/> <textbox id="t9" idmod="t5" size="3"/>
<textbox id="no2" size="3" tabindex="-1"/> <textbox id="f2" size="3" tabindex="-1"/>
<textbox id="t8" idmod="t4" size="3" tabindex="0"/> <textbox id="t10" idmod="t6" size="3" tabindex="0"/>
<textbox id="t2" idmod="t1" size="3" tabindex="1"/> <textbox id="t2" idmod="t1" size="3" tabindex="1"/>
</hbox> </hbox>
<hbox> <hbox>
<button id="no3" style="-moz-user-focus: ignore;" label="One"/> <button id="n1" style="-moz-user-focus: ignore;" label="One"/>
<checkbox id="no4" style="-moz-user-focus: ignore;" label="Two" tabindex="-1"/> <checkbox id="f3" style="-moz-user-focus: ignore;" label="Two" tabindex="-1"/>
<button id="t9" style="-moz-user-focus: ignore;" label="Three" tabindex="0"/> <button id="t11" style="-moz-user-focus: ignore;" label="Three" tabindex="0"/>
<checkbox id="t3" style="-moz-user-focus: ignore;" label="Four" tabindex="1"/> <checkbox id="t3" style="-moz-user-focus: ignore;" label="Four" tabindex="1"/>
</hbox> </hbox>
<hbox> <hbox>
<textbox id="t10" idmod="t5" style="-moz-user-focus: ignore;" size="3"/> <textbox id="t12" idmod="t7" style="-moz-user-focus: ignore;" size="3"/>
<textbox id="no5" style="-moz-user-focus: ignore;" size="3" tabindex="-1"/> <textbox id="f4" style="-moz-user-focus: ignore;" size="3" tabindex="-1"/>
<textbox id="t11" idmod="t6" style="-moz-user-focus: ignore;" size="3" tabindex="0"/> <textbox id="t13" idmod="t8" style="-moz-user-focus: ignore;" size="3" tabindex="0"/>
<textbox id="t4" idmod="t2" style="-moz-user-focus: ignore;" size="3" tabindex="1"/> <textbox id="t4" idmod="t2" style="-moz-user-focus: ignore;" size="3" tabindex="1"/>
</hbox> </hbox>
<richlistbox id="t12" idmod="t7"> <richlistbox id="t14" idmod="t9">
<richlistitem><label value="Item One"/></richlistitem> <richlistitem><label value="Item One"/></richlistitem>
</richlistbox> </richlistbox>
<hbox> <hbox>
<!-- the tabindex attribute does not apply to non-controls, so it <!-- the tabindex attribute applies to non-controls as well. They are not
should be treated as -1 for non-focusable dropmarkers, and 0 focusable unless tabindex is explicitly specified.
for focusable dropmarkers. Thus, the first four dropmarkers
are not in the tab order, and the last four dropmarkers should
be in the tab order just after the listbox above.
--> -->
<dropmarker id="no6"/> <dropmarker id="n2"/>
<dropmarker id="no7" tabindex="-1"/> <dropmarker id="f5" tabindex="-1"/>
<dropmarker id="no8" tabindex="0"/> <dropmarker id="t15" tabindex="0"/>
<dropmarker id="no9" tabindex="1"/> <dropmarker id="t5" idmod="t3" tabindex="1"/>
<dropmarker id="t13" style="-moz-user-focus: normal;"/> <dropmarker id="t16" style="-moz-user-focus: normal;"/>
<dropmarker id="t14" style="-moz-user-focus: normal;" tabindex="-1"/> <dropmarker id="f6" style="-moz-user-focus: normal;" tabindex="-1"/>
<dropmarker id="t15" style="-moz-user-focus: normal;" tabindex="0"/> <dropmarker id="t17" style="-moz-user-focus: normal;" tabindex="0"/>
<dropmarker id="t16" style="-moz-user-focus: normal;" tabindex="1"/> <dropmarker id="t6" idmod="t4" style="-moz-user-focus: normal;" tabindex="1"/>
</hbox> </hbox>
<body xmlns="http://www.w3.org/1999/xhtml"> <body xmlns="http://www.w3.org/1999/xhtml">
@ -73,19 +70,36 @@
SimpleTest.waitForExplicitFinish(); SimpleTest.waitForExplicitFinish();
function checkFocusability(aId, aFocusable)
{
document.activeElement.blur();
let testNode = document.getElementById(aId);
testNode.focus();
let newFocus = document.activeElement;
if (newFocus.localName == "input") {
newFocus = document.getBindingParent(newFocus);
}
let check = aFocusable ? is : isnot;
let focusableText = aFocusable ? "focusable " : "unfocusable ";
check(newFocus, testNode,
".focus() call on " + focusableText + aId);
}
var gAdjustedTabFocusModel = false; var gAdjustedTabFocusModel = false;
var gTestCount = 16; var gTestCount = 17;
var gTestsOccurred = 0; var gTestsOccurred = 0;
let gFocusableNotTabbableCount = 6;
let gNotFocusableCount = 2;
function runTests() function runTests()
{ {
var t; var t;
window.addEventListener("focus", function (event) { function onFocus(event) {
if (t == 1 && event.target.id == "t2") { if (t == 1 && event.target.id == "t2") {
// looks to be using the MacOSX Full Keyboard Access set to Textboxes // looks to be using the MacOSX Full Keyboard Access set to Textboxes
// and lists only so use the idmod attribute instead // and lists only so use the idmod attribute instead
gAdjustedTabFocusModel = true; gAdjustedTabFocusModel = true;
gTestCount = 7; gTestCount = 9;
} }
var attrcompare = gAdjustedTabFocusModel ? "idmod" : "id"; var attrcompare = gAdjustedTabFocusModel ? "idmod" : "id";
@ -102,12 +116,23 @@ function runTests()
if (event.target.localName != "textbox") if (event.target.localName != "textbox")
gTestsOccurred++; gTestsOccurred++;
} }
}, true); }
window.addEventListener("focus", onFocus, true);
for (t = 1; t <= gTestCount; t++) for (t = 1; t <= gTestCount; t++)
synthesizeKey("KEY_Tab"); synthesizeKey("KEY_Tab");
is(gTestsOccurred, gTestCount, "test count"); is(gTestsOccurred, gTestCount, "test count");
window.removeEventListener("focus", onFocus, true);
for (let i = 1; i <= gFocusableNotTabbableCount; ++i) {
checkFocusability("f" + i, true);
}
for (let i = 1; i <= gNotFocusableCount; ++i) {
checkFocusability("n" + i, false);
}
SimpleTest.finish(); SimpleTest.finish();
} }