commit c1c636ea74632040337fff8114dc2bd0b9624f20
parent 35f6ad218775c11df3ed619a58ca5d92368b460f
Author: Eitan Isaacson <eitan@monotonous.org>
Date: Tue, 21 Oct 2025 16:58:07 +0000
Bug 1942799 - P2: Make root editables multiline by default and expose value. r=morgan
Differential Revision: https://phabricator.services.mozilla.com/D268628
Diffstat:
5 files changed, 89 insertions(+), 11 deletions(-)
diff --git a/accessible/base/ARIAStateMap.cpp b/accessible/base/ARIAStateMap.cpp
@@ -30,6 +30,9 @@ struct EnumTypeData {
// States to clear in case of match.
const uint64_t mClearState;
+
+ // State if attribute is missing or value doesn't match any enum values.
+ const uint64_t mDefaultState;
};
enum ETokenType {
@@ -88,6 +91,7 @@ bool aria::MapToState(EStateRule aRule, dom::Element* aElement,
{states::SUPPORTS_AUTOCOMPLETION,
states::HASPOPUP | states::SUPPORTS_AUTOCOMPLETION,
states::HASPOPUP | states::SUPPORTS_AUTOCOMPLETION},
+ 0,
0};
MapEnumType(aElement, aState, data);
@@ -99,6 +103,7 @@ bool aria::MapToState(EStateRule aRule, dom::Element* aElement,
nsGkAtoms::aria_busy,
{nsGkAtoms::_true, nsGkAtoms::error, nullptr},
{states::BUSY, states::INVALID},
+ 0,
0};
MapEnumType(aElement, aState, data);
@@ -180,11 +185,26 @@ bool aria::MapToState(EStateRule aRule, dom::Element* aElement,
}
case eARIAMultiline: {
- static const TokenTypeData data(nsGkAtoms::aria_multiline,
- eBoolType | eDefinedIfAbsent, 0,
- states::MULTI_LINE, states::SINGLE_LINE);
+ static const EnumTypeData data = {
+ nsGkAtoms::aria_multiline,
+ {nsGkAtoms::_true, nsGkAtoms::_false, nullptr},
+ {states::MULTI_LINE, states::SINGLE_LINE},
+ states::MULTI_LINE | states::SINGLE_LINE,
+ states::SINGLE_LINE};
- MapTokenType(aElement, aState, data);
+ MapEnumType(aElement, aState, data);
+ return true;
+ }
+
+ case eARIAMultilineByDefault: {
+ static const EnumTypeData data = {
+ nsGkAtoms::aria_multiline,
+ {nsGkAtoms::_true, nsGkAtoms::_false, nullptr},
+ {states::MULTI_LINE, states::SINGLE_LINE},
+ states::MULTI_LINE | states::SINGLE_LINE,
+ states::MULTI_LINE};
+
+ MapEnumType(aElement, aState, data);
return true;
}
@@ -202,7 +222,8 @@ bool aria::MapToState(EStateRule aRule, dom::Element* aElement,
nsGkAtoms::aria_orientation,
{nsGkAtoms::horizontal, nsGkAtoms::vertical, nullptr},
{states::HORIZONTAL, states::VERTICAL},
- states::HORIZONTAL | states::VERTICAL};
+ states::HORIZONTAL | states::VERTICAL,
+ 0};
MapEnumType(aElement, aState, data);
return true;
@@ -302,6 +323,11 @@ static void MapEnumType(dom::Element* aElement, uint64_t* aState,
case 2:
*aState = (*aState & ~aData.mClearState) | aData.mStates[2];
return;
+ default:
+ if (aData.mDefaultState) {
+ *aState = (*aState & ~aData.mClearState) | aData.mDefaultState;
+ }
+ return;
}
}
diff --git a/accessible/base/ARIAStateMap.h b/accessible/base/ARIAStateMap.h
@@ -35,6 +35,7 @@ enum EStateRule {
eARIAInvalid,
eARIAModal,
eARIAMultiline,
+ eARIAMultilineByDefault,
eARIAMultiSelectable,
eARIAOrientation,
eARIAPressed,
diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp
@@ -1706,6 +1706,12 @@ void LocalAccessible::ApplyARIAState(uint64_t* aState) const {
aria::MapToState(aria::eARIAPressed, element, aState);
}
+ if (!IsTextField() && IsEditableRoot()) {
+ // HTML text fields will have their own multi/single line calcuation in
+ // NativeState.
+ aria::MapToState(aria::eARIAMultilineByDefault, element, aState);
+ }
+
if (!roleMapEntry) return;
*aState |= roleMapEntry->state;
@@ -1755,18 +1761,16 @@ void LocalAccessible::Value(nsString& aValue) const {
}
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
- if (!roleMapEntry) {
- return;
- }
// Value of textbox is a textified subtree.
- if (roleMapEntry->Is(nsGkAtoms::textbox)) {
+ if ((roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox)) ||
+ (IsGeneric() && IsEditableRoot())) {
nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
return;
}
// Value of combobox is a text of current or selected item.
- if (roleMapEntry->Is(nsGkAtoms::combobox)) {
+ if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) {
LocalAccessible* option = CurrentItem();
if (!option) {
uint32_t childCount = ChildCount();
diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp
@@ -302,7 +302,8 @@ void RemoteAccessible::Value(nsString& aValue) const {
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
// Value of textbox is a textified subtree.
- if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox)) {
+ if ((roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox)) ||
+ (IsGeneric() && IsEditableRoot())) {
nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
return;
}
diff --git a/accessible/tests/browser/text/browser_editabletext.js b/accessible/tests/browser/text/browser_editabletext.js
@@ -4,6 +4,9 @@
"use strict";
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
async function testEditable(browser, acc, aBefore = "", aAfter = "") {
async function resetInput() {
if (acc.childCount <= 1) {
@@ -259,3 +262,46 @@ addAccessibleTask(
is(input.value, "aefdef", "input value correct after pasting");
}
);
+
+addAccessibleTask(
+ `<div id="editable" contenteditable="true"><p id="p">one</p></div>`,
+ async function testNoRoleEditable(browser, docAcc) {
+ const editable = findAccessibleChildByID(docAcc, "editable");
+ is(editable.value, "one", "initial value correct");
+ ok(true, "Set initial text");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("p").firstChild.data = "two";
+ });
+ await untilCacheIs(() => editable.value, "two", "value changed correctly");
+
+ function isMultiline() {
+ let extState = {};
+ editable.getState({}, extState);
+ return (
+ !!(extState.value & EXT_STATE_MULTI_LINE) &&
+ !(extState.value & EXT_STATE_SINGLE_LINE)
+ );
+ }
+
+ ok(isMultiline(), "Editable is in multiline state");
+ await invokeSetAttribute(browser, "editable", "aria-multiline", "false");
+ await untilCacheOk(() => !isMultiline(), "editable is in singleline state");
+
+ await invokeSetAttribute(browser, "editable", "aria-multiline");
+ await untilCacheOk(() => isMultiline(), "editable is in multi-line again");
+
+ await invokeSetAttribute(browser, "editable", "contenteditable");
+ await untilCacheOk(() => {
+ let extState = {};
+ editable.getState({}, extState);
+ return (
+ !(extState.value & EXT_STATE_MULTI_LINE) &&
+ !(extState.value & EXT_STATE_SINGLE_LINE)
+ );
+ }, "editable should have neither multi-line nor single-line state");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ }
+);