From 80f99b2bcbde32d7f44624561e5133366f23a821 Mon Sep 17 00:00:00 2001 From: Eitan Isaacson Date: Mon, 27 Jul 2020 22:20:06 +0000 Subject: [PATCH] Bug 1654603 - Support text entry in contenteditable nested nodes. r=morgan VoiceOver expects text value change events to originate from the editable ancestor. Differential Revision: https://phabricator.services.mozilla.com/D85050 --- accessible/mac/AccessibleWrap.mm | 11 + accessible/mac/Platform.mm | 8 +- accessible/mac/mozAccessible.h | 1 + accessible/mac/mozAccessible.mm | 35 +--- accessible/mac/mozTextAccessible.h | 6 + accessible/mac/mozTextAccessible.mm | 45 +++- .../tests/browser/mac/browser_text_input.js | 196 ++++++++++-------- 7 files changed, 177 insertions(+), 125 deletions(-) diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm index 9ea6ac2094a7..076a05ce3ab8 100644 --- a/accessible/mac/AccessibleWrap.mm +++ b/accessible/mac/AccessibleWrap.mm @@ -123,6 +123,16 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { eventTarget = selEvent->Widget(); break; } + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + Accessible* acc = aEvent->GetAccessible(); + // If there is a text input ancestor, use it as the event source. + while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) { + acc = acc->Parent(); + } + eventTarget = acc ? acc : aEvent->GetAccessible(); + break; + } default: eventTarget = aEvent->GetAccessible(); break; @@ -179,6 +189,7 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { AccTextChangeEvent* tcEvent = downcast_accEvent(aEvent); [nativeAcc handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(tcEvent->ModifiedText()) inserted:tcEvent->IsTextInserted() + inContainer:aEvent->GetAccessible() at:tcEvent->GetStartOffset()]; break; } diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm index f8b634885185..f3ff1afb1d4b 100644 --- a/accessible/mac/Platform.mm +++ b/accessible/mac/Platform.mm @@ -110,9 +110,15 @@ void ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset, bool aIsSele void ProxyTextChangeEvent(ProxyAccessible* aTarget, const nsString& aStr, int32_t aStart, uint32_t aLen, bool aIsInsert, bool aFromUser) { - mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + ProxyAccessible* acc = aTarget; + // If there is a text input ancestor, use it as the event source. + while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) { + acc = acc->Parent(); + } + mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget); [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr) inserted:aIsInsert + inContainer:aTarget at:aStart]; } diff --git a/accessible/mac/mozAccessible.h b/accessible/mac/mozAccessible.h index 145940a5b77b..0ceccf8e482c 100644 --- a/accessible/mac/mozAccessible.h +++ b/accessible/mac/mozAccessible.h @@ -78,6 +78,7 @@ inline mozAccessible* GetNativeFromGeckoAccessible(mozilla::a11y::AccessibleOrPr - (void)handleAccessibleTextChangeEvent:(NSString*)change inserted:(BOOL)isInserted + inContainer:(const mozilla::a11y::AccessibleOrProxy&)container at:(int32_t)start; // internal method to retrieve a child at a given index. diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm index 74d060b089b4..8208e254ef98 100644 --- a/accessible/mac/mozAccessible.mm +++ b/accessible/mac/mozAccessible.mm @@ -7,7 +7,6 @@ #import "MacUtils.h" #import "mozView.h" -#import "GeckoTextMarker.h" #include "Accessible-inl.h" #include "nsAccUtils.h" @@ -786,41 +785,11 @@ struct RoleDescrComparator { return NO; } -enum AXTextEditType { - AXTextEditTypeUnknown, - AXTextEditTypeDelete, - AXTextEditTypeInsert, - AXTextEditTypeTyping, - AXTextEditTypeDictation, - AXTextEditTypeCut, - AXTextEditTypePaste, - AXTextEditTypeAttributesChange -}; - -enum AXTextStateChangeType { - AXTextStateChangeTypeUnknown, - AXTextStateChangeTypeEdit, - AXTextStateChangeTypeSelectionMove, - AXTextStateChangeTypeSelectionExtend -}; - - (void)handleAccessibleTextChangeEvent:(NSString*)change inserted:(BOOL)isInserted + inContainer:(const AccessibleOrProxy&)container at:(int32_t)start { - GeckoTextMarker startMarker(mGeckoAccessible, start); - NSDictionary* userInfo = @{ - @"AXTextChangeElement" : self, - @"AXTextStateChangeType" : @(AXTextStateChangeTypeEdit), - @"AXTextChangeValues" : @[ @{ - @"AXTextChangeValue" : (change ? change : @""), - @"AXTextChangeValueStartMarker" : startMarker.CreateAXTextMarker(), - @"AXTextEditType" : isInserted ? @(AXTextEditTypeTyping) : @(AXTextEditTypeDelete) - } ] - }; - - mozAccessible* webArea = GetNativeFromGeckoAccessible([self geckoDocument]); - [webArea moxPostNotification:NSAccessibilityValueChangedNotification withUserInfo:userInfo]; - [self moxPostNotification:NSAccessibilityValueChangedNotification withUserInfo:userInfo]; + // XXX: Eventually live region handling will go here. } - (void)handleAccessibleEvent:(uint32_t)eventType { diff --git a/accessible/mac/mozTextAccessible.h b/accessible/mac/mozTextAccessible.h index 70d66983f759..e06a12d2653b 100644 --- a/accessible/mac/mozTextAccessible.h +++ b/accessible/mac/mozTextAccessible.h @@ -68,6 +68,12 @@ #pragma mark - mozAccessible +// override +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer:(const mozilla::a11y::AccessibleOrProxy&)container + at:(int32_t)start; + // override - (void)handleAccessibleEvent:(uint32_t)eventType; diff --git a/accessible/mac/mozTextAccessible.mm b/accessible/mac/mozTextAccessible.mm index 5e9877f00cfb..e3ece13aff39 100644 --- a/accessible/mac/mozTextAccessible.mm +++ b/accessible/mac/mozTextAccessible.mm @@ -12,6 +12,7 @@ #include "TextLeafAccessible.h" #import "mozTextAccessible.h" +#import "GeckoTextMarker.h" using namespace mozilla; using namespace mozilla::a11y; @@ -354,12 +355,48 @@ inline NSString* ToNSString(id aValue) { #pragma mark - mozAccessible +enum AXTextEditType { + AXTextEditTypeUnknown, + AXTextEditTypeDelete, + AXTextEditTypeInsert, + AXTextEditTypeTyping, + AXTextEditTypeDictation, + AXTextEditTypeCut, + AXTextEditTypePaste, + AXTextEditTypeAttributesChange +}; + +enum AXTextStateChangeType { + AXTextStateChangeTypeUnknown, + AXTextStateChangeTypeEdit, + AXTextStateChangeTypeSelectionMove, + AXTextStateChangeTypeSelectionExtend +}; + +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer:(const AccessibleOrProxy&)container + at:(int32_t)start { + GeckoTextMarker startMarker(container, start); + NSDictionary* userInfo = @{ + @"AXTextChangeElement" : self, + @"AXTextStateChangeType" : @(AXTextStateChangeTypeEdit), + @"AXTextChangeValues" : @[ @{ + @"AXTextChangeValue" : (change ? change : @""), + @"AXTextChangeValueStartMarker" : startMarker.CreateAXTextMarker(), + @"AXTextEditType" : isInserted ? @(AXTextEditTypeTyping) : @(AXTextEditTypeDelete) + } ] + }; + + mozAccessible* webArea = GetNativeFromGeckoAccessible([self geckoDocument]); + [webArea moxPostNotification:NSAccessibilityValueChangedNotification withUserInfo:userInfo]; + [self moxPostNotification:NSAccessibilityValueChangedNotification withUserInfo:userInfo]; + + [self moxPostNotification:NSAccessibilityValueChangedNotification]; +} + - (void)handleAccessibleEvent:(uint32_t)eventType { switch (eventType) { - case nsIAccessibleEvent::EVENT_VALUE_CHANGE: - case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: - [self moxPostNotification:NSAccessibilityValueChangedNotification]; - break; default: [super handleAccessibleEvent:eventType]; break; diff --git a/accessible/tests/browser/mac/browser_text_input.js b/accessible/tests/browser/mac/browser_text_input.js index e8d048d290f6..960140edaa7c 100644 --- a/accessible/tests/browser/mac/browser_text_input.js +++ b/accessible/tests/browser/mac/browser_text_input.js @@ -114,13 +114,20 @@ async function synthKeyAndTestValueChanged( synthKey, synthEvent, expectedId, + expectedTextSelectionId, expectedChangeValue, expectedEditType, expectedWordAtLeft ) { let valueChangedEvents = Promise.all([ - waitForMacEventWithInfo("AXSelectedTextChanged", matchWebArea(expectedId)), - waitForMacEventWithInfo("AXSelectedTextChanged", matchInput(expectedId)), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchWebArea(expectedTextSelectionId) + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchInput(expectedTextSelectionId) + ), waitForMacEventWithInfo("AXValueChanged", matchWebArea(expectedId)), waitForMacEventWithInfo("AXValueChanged", matchInput(expectedId)), ]); @@ -146,94 +153,109 @@ async function synthKeyAndTestValueChanged( ); } +async function focusIntoInputAndType(accDoc, inputId, innerContainerId) { + let selectionId = innerContainerId ? innerContainerId : inputId; + let input = getNativeInterface(accDoc, inputId); + ok(!input.getAttributeValue("AXFocused"), "input is not focused"); + ok(input.isAttributeSettable("AXFocused"), "input is focusable"); + let events = Promise.all([ + waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXDOMIdentifier") == inputId + ), + waitForMacEventWithInfo("AXSelectedTextChanged", matchWebArea(selectionId)), + waitForMacEventWithInfo("AXSelectedTextChanged", matchInput(selectionId)), + ]); + input.setAttributeValue("AXFocused", true); + await events; + + async function testTextInput( + synthKey, + expectedChangeValue, + expectedWordAtLeft + ) { + await synthKeyAndTestValueChanged( + synthKey, + null, + inputId, + selectionId, + expectedChangeValue, + AXTextEditTypeTyping, + expectedWordAtLeft + ); + } + + await testTextInput("h", "h", "h"); + await testTextInput("e", "e", "he"); + await testTextInput("l", "l", "hel"); + await testTextInput("l", "l", "hell"); + await testTextInput("o", "o", "hello"); + await testTextInput(" ", " ", "hello"); + // You would expect this to be useless but this is what VO + // consumes. I guess it concats the inserted text data to the + // word to the left of the marker. + await testTextInput("w", "w", " "); + await testTextInput("o", "o", "wo"); + await testTextInput("r", "r", "wor"); + await testTextInput("l", "l", "worl"); + await testTextInput("d", "d", "world"); + + async function testTextDelete(expectedChangeValue, expectedWordAtLeft) { + await synthKeyAndTestValueChanged( + "KEY_Backspace", + null, + inputId, + selectionId, + expectedChangeValue, + AXTextEditTypeDelete, + expectedWordAtLeft + ); + } + + await testTextDelete("d", "worl"); + await testTextDelete("l", "wor"); + + await synthKeyAndTestSelectionChanged("KEY_ArrowLeft", null, selectionId, ""); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true }, + selectionId, + "o" + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true }, + selectionId, + "wo" + ); + await synthKeyAndTestSelectionChanged("KEY_ArrowLeft", null, selectionId, ""); + await synthKeyAndTestSelectionChanged( + "KEY_Home", + { shiftKey: true }, + selectionId, + "hello " + ); + await synthKeyAndTestSelectionChanged("KEY_ArrowLeft", null, selectionId, ""); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + { shiftKey: true, altKey: true }, + selectionId, + "hello" + ); +} + // Test text input addAccessibleTask( `link `, async (browser, accDoc) => { - let input = getNativeInterface(accDoc, "input"); - ok(!input.getAttributeValue("AXFocused"), "input is not focused"); - ok(input.isAttributeSettable("AXFocused"), "input is focusable"); - let events = Promise.all([ - waitForMacEvent( - "AXFocusedUIElementChanged", - iface => iface.getAttributeValue("AXDOMIdentifier") == "input" - ), - waitForMacEventWithInfo("AXSelectedTextChanged", matchWebArea("input")), - waitForMacEventWithInfo("AXSelectedTextChanged", matchInput("input")), - ]); - input.setAttributeValue("AXFocused", true); - await events; - - async function testTextInput( - synthKey, - expectedChangeValue, - expectedWordAtLeft - ) { - await synthKeyAndTestValueChanged( - synthKey, - null, - "input", - expectedChangeValue, - AXTextEditTypeTyping, - expectedWordAtLeft - ); - } - - await testTextInput("h", "h", "h"); - await testTextInput("e", "e", "he"); - await testTextInput("l", "l", "hel"); - await testTextInput("l", "l", "hell"); - await testTextInput("o", "o", "hello"); - await testTextInput(" ", " ", "hello"); - // You would expect this to be useless but this is what VO - // consumes. I guess it concats the inserted text data to the - // word to the left of the marker. - await testTextInput("w", "w", " "); - await testTextInput("o", "o", "wo"); - await testTextInput("r", "r", "wor"); - await testTextInput("l", "l", "worl"); - await testTextInput("d", "d", "world"); - - async function testTextDelete(expectedChangeValue, expectedWordAtLeft) { - await synthKeyAndTestValueChanged( - "KEY_Backspace", - null, - "input", - expectedChangeValue, - AXTextEditTypeDelete, - expectedWordAtLeft - ); - } - - await testTextDelete("d", "worl"); - await testTextDelete("l", "wor"); - - await synthKeyAndTestSelectionChanged("KEY_ArrowLeft", null, "input", ""); - await synthKeyAndTestSelectionChanged( - "KEY_ArrowLeft", - { shiftKey: true }, - "input", - "o" - ); - await synthKeyAndTestSelectionChanged( - "KEY_ArrowLeft", - { shiftKey: true }, - "input", - "wo" - ); - await synthKeyAndTestSelectionChanged("KEY_ArrowLeft", null, "input", ""); - await synthKeyAndTestSelectionChanged( - "KEY_Home", - { shiftKey: true }, - "input", - "hello " - ); - await synthKeyAndTestSelectionChanged("KEY_ArrowLeft", null, "input", ""); - await synthKeyAndTestSelectionChanged( - "KEY_ArrowRight", - { shiftKey: true, altKey: true }, - "input", - "hello" - ); + await focusIntoInputAndType(accDoc, "input"); + } +); + +// Test content editable +addAccessibleTask( + `

`, + async (browser, accDoc) => { + await focusIntoInputAndType(accDoc, "input", "inner"); } );