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
This commit is contained in:
Eitan Isaacson 2020-07-27 22:20:06 +00:00
Родитель f14676f0ce
Коммит 80f99b2bcb
7 изменённых файлов: 177 добавлений и 125 удалений

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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];
}

Просмотреть файл

@ -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.

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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;

Просмотреть файл

@ -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;

Просмотреть файл

@ -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(
`<a href="#">link</a> <input id="input">`,
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(
`<div id="input" contentEditable="true" tabindex="0" role="textbox" aria-multiline="true"><div id="inner"><br /></div></div>`,
async (browser, accDoc) => {
await focusIntoInputAndType(accDoc, "input", "inner");
}
);