зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f14676f0ce
Коммит
80f99b2bcb
|
@ -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");
|
||||
}
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче