tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 686747256c18c6779a1e0a41fdebd7d459201d9e
parent 508bd44a39fb44bcad2779cdb9d2c060e083c780
Author: Eitan Isaacson <eitan@monotonous.org>
Date:   Wed, 22 Oct 2025 03:12:53 +0000

Bug 1942799 - P2: Make root editables multiline by default and expose value. r=morgan

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

Diffstat:
Maccessible/base/ARIAStateMap.cpp | 36+++++++++++++++++++++++++++++++-----
Maccessible/base/ARIAStateMap.h | 1+
Maccessible/generic/LocalAccessible.cpp | 14+++++++++-----
Maccessible/ipc/RemoteAccessible.cpp | 3++-
Maccessible/tests/browser/text/browser_editabletext.js | 46++++++++++++++++++++++++++++++++++++++++++++++
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 @@ -301,7 +301,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, + } +);