Bug 606082 - update accessible tree when root element is changed, r=marcoz, davidb, bz, a=blocking

This commit is contained in:
Alexander Surkov 2010-11-06 12:11:08 +08:00
Родитель c22068623c
Коммит a015a8fcc2
9 изменённых файлов: 472 добавлений и 42 удалений

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

@ -156,7 +156,7 @@ public:
*/
virtual nsINode* GetNode() const { return mContent; }
nsIContent* GetContent() const { return mContent; }
nsIDocument* GetDocumentNode() const
virtual nsIDocument* GetDocumentNode() const
{ return mContent ? mContent->GetOwnerDoc() : nsnull; }
/**

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

@ -477,55 +477,69 @@ nsAccessibilityService::ContentRangeInserted(nsIPresShell* aPresShell,
nsIContent* aStartChild,
nsIContent* aEndChild)
{
#ifdef DEBUG_A11Y
#ifdef DEBUG_CONTENTMUTATION
nsAutoString tag;
aStartChild->Tag()->ToString(tag);
nsIAtom* id = aStartChild->GetID();
nsCAutoString strid;
if (id)
id->ToUTF8String(strid);
nsIAtom* atomid = aStartChild->GetID();
nsCAutoString id;
if (atomid)
atomid->ToUTF8String(id);
nsAutoString ctag;
nsCAutoString cid;
nsIAtom* catomid = nsnull;
if (aContainer) {
aContainer->Tag()->ToString(ctag);
nsIAtom* cid = aContainer->GetID();
nsCAutoString strcid;
if (cid)
cid->ToUTF8String(strcid);
catomid = aContainer->GetID();
if (catomid)
catomid->ToUTF8String(cid);
}
printf("\ncontent inserted: %s@id='%s', container: %s@id='%s', end node: %p\n\n",
NS_ConvertUTF16toUTF8(tag).get(), strid.get(),
NS_ConvertUTF16toUTF8(ctag).get(), strcid.get(), aEndChild);
NS_ConvertUTF16toUTF8(tag).get(), id.get(),
NS_ConvertUTF16toUTF8(ctag).get(), cid.get(), aEndChild);
#endif
// XXX: bug 606082. aContainer is null when root element is inserted into
// document, we need to handle this and update the tree, also we need to
// update a content node of the document accessible.
if (aContainer) {
nsDocAccessible* docAccessible = GetDocAccessible(aPresShell->GetDocument());
if (docAccessible)
docAccessible->UpdateTree(aContainer, aStartChild, aEndChild, PR_TRUE);
}
}
void
nsAccessibilityService::ContentRemoved(nsIPresShell* aPresShell,
nsIContent* aContainer,
nsIContent* aChild)
{
#ifdef DEBUG_A11Y
nsAutoString id;
aChild->Tag()->ToString(id);
printf("\ncontent removed: %s\n", NS_ConvertUTF16toUTF8(id).get());
#ifdef DEBUG_CONTENTMUTATION
nsAutoString tag;
aChild->Tag()->ToString(tag);
nsIAtom* atomid = aChild->GetID();
nsCAutoString id;
if (atomid)
atomid->ToUTF8String(id);
nsAutoString ctag;
nsCAutoString cid;
nsIAtom* catomid = nsnull;
if (aContainer) {
aContainer->Tag()->ToString(ctag);
catomid = aContainer->GetID();
if (catomid)
catomid->ToUTF8String(cid);
}
printf("\ncontent removed: %s@id='%s', container: %s@id='%s'\n\n",
NS_ConvertUTF16toUTF8(tag).get(), id.get(),
NS_ConvertUTF16toUTF8(ctag).get(), cid.get());
#endif
// XXX: bug 606082. aContainer is null when root element is inserted into
// document, we need to handle this and update the tree, perhaps destroy
// the document accessible.
if (aContainer) {
nsDocAccessible* docAccessible = GetDocAccessible(aPresShell->GetDocument());
if (docAccessible)
docAccessible->UpdateTree(aContainer, aChild, aChild->GetNextSibling(),
PR_FALSE);
}
}
void
nsAccessibilityService::PresShellDestroyed(nsIPresShell *aPresShell)

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

@ -285,8 +285,12 @@ nsDocAccessible::GetStateInternal(PRUint32 *aState, PRUint32 *aExtraState)
return NS_OK_DEFUNCT_OBJECT;
}
if (aExtraState)
*aExtraState = 0;
if (aExtraState) {
// The root content of the document might be removed so that mContent is
// out of date.
*aExtraState = (mContent->GetCurrentDoc() == mDocument) ?
0 : nsIAccessibleStates::EXT_STATE_STALE;
}
#ifdef MOZ_XUL
nsCOMPtr<nsIXULDocument> xulDoc(do_QueryInterface(mDocument));
@ -1346,22 +1350,49 @@ nsDocAccessible::UpdateTree(nsIContent* aContainerNode,
// Since this information may be not correct then we need to fire some events
// regardless the document loading state.
// Update the whole tree of this document accessible when the container is
// null (document element is inserted or removed).
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
nsIEventStateManager* esm = presShell->GetPresContext()->EventStateManager();
PRBool fireAllEvents = PR_TRUE;//IsContentLoaded() || esm->IsHandlingUserInputExternal();
// We don't create new accessibles on content removal.
nsAccessible* container = aIsInsert ?
GetAccService()->GetAccessibleOrContainer(aContainerNode, mWeakShell) :
GetAccService()->GetCachedAccessibleOrContainer(aContainerNode);
// XXX: bug 608887 reconsider accessible tree update logic because
// 1) elements appended outside the HTML body don't get accessibles;
// 2) the document having elements that should be accessible may function
// without body.
nsAccessible* container = nsnull;
if (aIsInsert) {
container = aContainerNode ?
GetAccService()->GetAccessibleOrContainer(aContainerNode, mWeakShell) :
this;
// The document children were changed; the root content might be affected.
if (container == this) {
nsIContent* rootContent = nsCoreUtils::GetRoleContent(mDocument);
// No root content (for example HTML document element was inserted but no
// body). Nothing to update.
if (!rootContent)
return;
// New root content has been inserted, update it and update the tree.
if (rootContent != mContent)
mContent = rootContent;
}
// XXX: Invalidate parent-child relations for container accessible and its
// children because there's no good way to find insertion point of new child
// accessibles into accessible tree. We need to invalidate children even
// there's no inserted accessibles in the end because accessible children
// are created while parent recaches child accessibles.
container->InvalidateChildren();
} else {
// Don't create new accessibles on content removal.
container = aContainerNode ?
GetAccService()->GetCachedAccessibleOrContainer(aContainerNode) :
this;
}
EIsFromUserInput fromUserInput = esm->IsHandlingUserInputExternal() ?

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

@ -108,6 +108,7 @@ public:
virtual nsIFrame* GetFrame();
virtual PRBool IsDefunct();
virtual nsINode* GetNode() const { return mDocument; }
virtual nsIDocument* GetDocumentNode() const { return mDocument; }
// nsAccessible
virtual PRUint32 NativeRole();

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

@ -38,6 +38,7 @@ const EXT_STATE_EXPANDABLE = nsIAccessibleStates.EXT_STATE_EXPANDABLE;
const EXT_STATE_HORIZONTAL = nsIAccessibleStates.EXT_STATE_HORIZONTAL;
const EXT_STATE_MULTI_LINE = nsIAccessibleStates.EXT_STATE_MULTI_LINE;
const EXT_STATE_SINGLE_LINE = nsIAccessibleStates.EXT_STATE_SINGLE_LINE;
const EXT_STATE_STALE = nsIAccessibleStates.EXT_STATE_STALE;
const EXT_STATE_SUPPORTS_AUTOCOMPLETION =
nsIAccessibleStates.EXT_STATE_SUPPORTS_AUTOCOMPLETION;
const EXT_STATE_VERTICAL = nsIAccessibleStates.EXT_STATE_VERTICAL;

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

@ -28,6 +28,7 @@
testStates(docAcc, 0, EXT_STATE_EDITABLE);
testStates("article", 0, EXT_STATE_EDITABLE);
testStates("article", 0, EXT_STATE_EDITABLE);
testStates("editable_article", 0, EXT_STATE_EDITABLE);
document.designMode = "off";

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

@ -46,6 +46,7 @@ include $(DEPTH)/config/autoconf.mk
include $(topsrcdir)/config/rules.mk
_TEST_FILES =\
test_doc.html \
test_list_editabledoc.html \
test_list.html \
test_recreation.html \

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

@ -0,0 +1,370 @@
<!DOCTYPE html>
<html>
<head>
<title>Test document root content mutations</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script type="application/javascript"
src="chrome://mochikit/content/MochiKit/packed.js"></script>
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="../role.js"></script>
<script type="application/javascript"
src="../states.js"></script>
<script type="application/javascript"
src="../events.js"></script>
<script type="application/javascript">
////////////////////////////////////////////////////////////////////////////
// Helpers
function getDocNode(aID)
{
return getNode(aID).contentDocument;
}
function getDocChildNode(aID)
{
return getDocNode(aID).body.firstChild;
}
function rootContentReplaced(aID, aTextName)
{
this.eventSeq = [
new invokerChecker(EVENT_SHOW, getDocChildNode, aID),
new invokerChecker(EVENT_REORDER, getDocNode, aID)
];
this.finalCheck = function rootContentReplaced_finalCheck()
{
var tree = {
role: ROLE_DOCUMENT,
children: [
{
role: ROLE_TEXT_LEAF,
name: aTextName
}
]
};
testAccessibleTree(getDocNode(aID), tree);
}
}
function rootContentRemoved(aID)
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, null),
new invokerChecker(EVENT_REORDER, getDocNode, aID)
];
this.preinvoke = function rootContentRemoved_preinvoke()
{
// Set up target for hide event before we invoke.
var text = getAccessible(getAccessible(getDocNode(aID)).firstChild,
[nsIAccessNode]).DOMNode;
this.eventSeq[0].target = text;
}
this.finalCheck = function rootContentRemoved_finalCheck()
{
var tree = {
role: ROLE_DOCUMENT,
states: {
// Out of date root content involves stale state presence.
states: 0,
extraStates: EXT_STATE_STALE
},
children: [ ]
};
testAccessibleTree(getDocNode(aID), tree);
}
}
function rootContentInserted(aID, aTextName)
{
this.eventSeq = [
new invokerChecker(EVENT_SHOW, getDocChildNode, aID),
new invokerChecker(EVENT_REORDER, getDocNode, aID)
];
this.finalCheck = function rootContentInserted_finalCheck()
{
var tree = {
role: ROLE_DOCUMENT,
states: {
states: 0,
extraStates: 0,
absentStates: 0,
absentExtraStates: EXT_STATE_STALE
},
children: [
{
role: ROLE_TEXT_LEAF,
name: aTextName
}
]
};
testAccessibleTree(getDocNode(aID), tree);
}
}
////////////////////////////////////////////////////////////////////////////
// Invokers
function writeIFrameDoc(aID)
{
this.__proto__ = new rootContentReplaced(aID, "hello");
this.invoke = function writeIFrameDoc_invoke()
{
var docNode = getDocNode(aID);
// We can't use open/write/close outside of iframe document because of
// security error.
var script = docNode.createElement("script");
script.textContent = "document.open(); document.write('hello'); document.close();";
docNode.body.appendChild(script);
}
this.getID = function writeIFrameDoc_getID()
{
return "write document";
}
}
/**
* Replace HTML element.
*/
function replaceIFrameHTMLElm(aID)
{
this.__proto__ = new rootContentReplaced(aID, "New Wave");
this.invoke = function replaceIFrameHTMLElm_invoke()
{
var docNode = getDocNode(aID);
var newHTMLNode = docNode.createElement("html");
var newBodyNode = docNode.createElement("body");
var newTextNode = docNode.createTextNode("New Wave");
newBodyNode.appendChild(newTextNode);
newHTMLNode.appendChild(newBodyNode);
docNode.replaceChild(newHTMLNode, docNode.documentElement);
}
this.getID = function replaceIFrameBody_getID()
{
return "replace HTML element";
}
}
/**
* Replace HTML body.
*/
function replaceIFrameBody(aID)
{
this.__proto__ = new rootContentReplaced(aID, "New Hello");
this.invoke = function replaceIFrameBody_invoke()
{
var docNode = getDocNode(aID);
var newBodyNode = docNode.createElement("body");
var newTextNode = docNode.createTextNode("New Hello");
newBodyNode.appendChild(newTextNode);
docNode.documentElement.replaceChild(newBodyNode, docNode.body);
}
this.finalCheck = function replaceIFrameBody_finalCheck()
{
var tree = {
role: ROLE_DOCUMENT,
children: [
{
role: ROLE_TEXT_LEAF,
name: "New Hello"
}
]
};
testAccessibleTree(getDocNode(aID), tree);
}
this.getID = function replaceIFrameBody_getID()
{
return "replace body";
}
}
/**
* Open/close document pair.
*/
function openIFrameDoc(aID)
{
this.__proto__ = new rootContentRemoved(aID);
this.invoke = function openIFrameDoc_invoke()
{
this.preinvoke();
// Open document.
var docNode = getDocNode(aID);
var script = docNode.createElement("script");
script.textContent = "function closeMe() { document.write('Works?'); document.close(); } window.closeMe = closeMe; document.open();";
docNode.body.appendChild(script);
}
this.getID = function openIFrameDoc_getID()
{
return "open document";
}
}
function closeIFrameDoc(aID)
{
this.__proto__ = new rootContentInserted(aID, "Works?");
this.invoke = function closeIFrameDoc_invoke()
{
// Write and close document.
getDocNode(aID).write('Works?'); getDocNode(aID).close();
}
this.getID = function closeIFrameDoc_getID()
{
return "close document";
}
}
/**
* Remove/insert HTML element pair.
*/
function removeHTMLFromIFrameDoc(aID)
{
this.__proto__ = new rootContentRemoved(aID);
this.invoke = function removeHTMLFromIFrameDoc_invoke()
{
this.preinvoke();
// Remove HTML element.
var docNode = getDocNode(aID);
docNode.removeChild(docNode.firstChild);
}
this.getID = function removeHTMLFromIFrameDoc_getID()
{
return "remove HTML element";
}
}
function insertHTMLToIFrameDoc(aID)
{
this.__proto__ = new rootContentInserted(aID, "Haha");
this.invoke = function insertHTMLToIFrameDoc_invoke()
{
// Insert HTML element.
var docNode = getDocNode(aID);
var html = docNode.createElement("html");
var body = docNode.createElement("body");
var text = docNode.createTextNode("Haha");
body.appendChild(text);
html.appendChild(body);
docNode.appendChild(html);
}
this.getID = function insertHTMLToIFrameDoc_getID()
{
return "insert HTML element document";
}
}
/**
* Remove/insert HTML body pair.
*/
function removeBodyFromIFrameDoc(aID)
{
this.__proto__ = new rootContentRemoved(aID);
this.invoke = function removeBodyFromIFrameDoc_invoke()
{
this.preinvoke();
// Remove body element.
var docNode = getDocNode(aID);
docNode.documentElement.removeChild(docNode.body);
}
this.getID = function removeBodyFromIFrameDoc_getID()
{
return "remove body element";
}
}
function insertBodyToIFrameDoc(aID)
{
this.__proto__ = new rootContentInserted(aID, "Yo ho ho i butylka roma!");
this.invoke = function insertBodyToIFrameDoc_invoke()
{
// Insert body element.
var docNode = getDocNode(aID);
var body = docNode.createElement("body");
var text = docNode.createTextNode("Yo ho ho i butylka roma!");
body.appendChild(text);
docNode.documentElement.appendChild(body);
}
this.getID = function insertBodyToIFrameDoc_getID()
{
return "insert body element";
}
}
////////////////////////////////////////////////////////////////////////////
// Test
//gA11yEventDumpID = "eventdump"; // debug stuff
var gQueue = null;
function doTest()
{
gQueue = new eventQueue();
gQueue.push(new writeIFrameDoc("iframe"));
gQueue.push(new replaceIFrameHTMLElm("iframe"));
gQueue.push(new replaceIFrameBody("iframe"));
gQueue.push(new openIFrameDoc("iframe"));
gQueue.push(new closeIFrameDoc("iframe"));
gQueue.push(new removeHTMLFromIFrameDoc("iframe"));
gQueue.push(new insertHTMLToIFrameDoc("iframe"));
gQueue.push(new removeBodyFromIFrameDoc("iframe"));
gQueue.push(new insertBodyToIFrameDoc("iframe"));
gQueue.invoke(); // SimpleTest.finish() will be called in the end
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTest);
</script>
</head>
<body>
<a target="_blank"
title="Update accessible tree when root element is changed"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=606082">Mozilla Bug 606082</a>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
<iframe id="iframe"></iframe>
<div id="eventdump"></div>
</body>
</html>

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

@ -6898,6 +6898,17 @@ nsCSSFrameConstructor::ContentRangeInserted(nsIContent* aContainer,
#endif
}
#ifdef ACCESSIBILITY
if (mPresShell->IsAccessibilityActive()) {
nsCOMPtr<nsIAccessibilityService> accService =
do_GetService("@mozilla.org/accessibilityService;1");
if (accService) {
accService->ContentRangeInserted(mPresShell, aContainer,
aStartChild, aEndChild);
}
}
#endif
return NS_OK;
}