diff --git a/accessible/interfaces/nsIAccessibleText.idl b/accessible/interfaces/nsIAccessibleText.idl index d3c1ce0b16ae5..d2c1ef19f71cf 100644 --- a/accessible/interfaces/nsIAccessibleText.idl +++ b/accessible/interfaces/nsIAccessibleText.idl @@ -37,6 +37,8 @@ interface nsIAccessibleText : nsISupports */ attribute long caretOffset; + void getCaretRect(out long x, out long y, out long width, out long height); + readonly attribute long characterCount; readonly attribute long selectionCount; diff --git a/accessible/tests/browser/bounds/browser.ini b/accessible/tests/browser/bounds/browser.ini index 0327310554b7d..078659a97da69 100644 --- a/accessible/tests/browser/bounds/browser.ini +++ b/accessible/tests/browser/bounds/browser.ini @@ -10,6 +10,7 @@ prefs = javascript.options.asyncstack_capture_debuggee_only=false [browser_accessible_moved.js] +[browser_caret_rect.js] [browser_position.js] [browser_test_resolution.js] skip-if = os == 'win' # bug 1372296 diff --git a/accessible/tests/browser/bounds/browser_caret_rect.js b/accessible/tests/browser/bounds/browser_caret_rect.js new file mode 100644 index 0000000000000..ac0ee3aa5014f --- /dev/null +++ b/accessible/tests/browser/bounds/browser_caret_rect.js @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function getCaretRect(browser, id) { + // The caret rect can only be queried on LocalAccessible. On Windows, we do + // send it across processes with caret events, but this currently can't be + // queried outside of the event, nor with XPCOM. + const [x, y, w, h] = await invokeContentTask(browser, [id], contentId => { + const node = content.document.getElementById(contentId); + const contentAcc = content.CommonUtils.accService.getAccessibleFor(node); + contentAcc.QueryInterface(Ci.nsIAccessibleText); + const caretX = {}; + const caretY = {}; + const caretW = {}; + const caretH = {}; + contentAcc.getCaretRect(caretX, caretY, caretW, caretH); + return [caretX.value, caretY.value, caretW.value, caretH.value]; + }); + info(`Caret bounds: ${x}, ${y}, ${w}, ${h}`); + return [x, y, w, h]; +} + +async function testCaretRect(browser, docAcc, id, offset) { + const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]); + is(acc.caretOffset, offset, `Caret at offset ${offset}`); + const charX = {}; + const charY = {}; + const charW = {}; + const charH = {}; + const atEnd = offset == acc.characterCount; + const empty = offset == 0 && atEnd; + const queryOffset = atEnd && !empty ? offset - 1 : offset; + acc.getCharacterExtents( + queryOffset, + charX, + charY, + charW, + charH, + COORDTYPE_SCREEN_RELATIVE + ); + info( + `Character ${queryOffset} bounds: ${charX.value}, ${charY.value}, ${charW.value}, ${charH.value}` + ); + const [caretX, caretY, caretW, caretH] = await getCaretRect(browser, id); + if (atEnd) { + ok(caretX > charX.value, "Caret x after last character x"); + } else { + is(caretX, charX.value, "Caret x same as character x"); + } + is(caretY, charY.value, "Caret y same as character y"); + is(caretW, 1, "Caret width is 1"); + if (!empty) { + is(caretH, charH.value, "Caret height same as character height"); + } +} + +function getAccBounds(acc) { + const x = {}; + const y = {}; + const w = {}; + const h = {}; + acc.getBounds(x, y, w, h); + return [x.value, y.value, w.value, h.value]; +} + +/** + * Test the caret rect in content documents. + */ +addAccessibleTask( + ` + + + `, + async function (browser, docAcc) { + async function runTests() { + const input = findAccessibleChildByID(docAcc, "input", [ + nsIAccessibleText, + ]); + info("Focusing input"); + let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + input.takeFocus(); + await caretMoved; + await testCaretRect(browser, docAcc, "input", 0); + info("Setting caretOffset to 1"); + caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + input.caretOffset = 1; + await caretMoved; + await testCaretRect(browser, docAcc, "input", 1); + info("Setting caretOffset to 2"); + caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + input.caretOffset = 2; + await caretMoved; + await testCaretRect(browser, docAcc, "input", 2); + info("Resetting caretOffset to 0"); + input.caretOffset = 0; + + const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [ + nsIAccessibleText, + ]); + info("Focusing emptyInput"); + caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput); + emptyInput.takeFocus(); + await caretMoved; + await testCaretRect(browser, docAcc, "emptyInput", 0); + } + + await runTests(); + + // Check that the caret rect is correct when the title bar is shown. + if (LINUX || Services.env.get("MOZ_HEADLESS")) { + // Disabling tabs in title bar doesn't change the bounds on Linux or in + // headless mode. + info("Skipping title bar tests"); + return; + } + const [, origDocY] = getAccBounds(docAcc); + info("Showing title bar"); + let titleBarChanged = BrowserTestUtils.waitForMutationCondition( + document.documentElement, + { attributes: true, attributeFilter: ["tabsintitlebar"] }, + () => !document.documentElement.hasAttribute("tabsintitlebar") + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.inTitlebar", false]], + }); + await titleBarChanged; + const [, newDocY] = getAccBounds(docAcc); + Assert.greater( + newDocY, + origDocY, + "Doc has larger y after title bar change" + ); + await runTests(); + await SpecialPowers.popPrefEnv(); + }, + { chrome: true, topLevel: true } +); diff --git a/accessible/xpcom/xpcAccessibleHyperText.cpp b/accessible/xpcom/xpcAccessibleHyperText.cpp index 0eb51dbeab6f1..2708d902f57d5 100644 --- a/accessible/xpcom/xpcAccessibleHyperText.cpp +++ b/accessible/xpcom/xpcAccessibleHyperText.cpp @@ -246,6 +246,28 @@ xpcAccessibleHyperText::SetCaretOffset(int32_t aCaretOffset) { return NS_OK; } +NS_IMETHODIMP +xpcAccessibleHyperText::GetCaretRect(int32_t* aX, int32_t* aY, int32_t* aWidth, + int32_t* aHeight) { + NS_ENSURE_ARG_POINTER(aX); + NS_ENSURE_ARG_POINTER(aY); + NS_ENSURE_ARG_POINTER(aWidth); + NS_ENSURE_ARG_POINTER(aHeight); + *aX = *aY = *aWidth = *aHeight; + + if (!mIntl) { + return NS_ERROR_FAILURE; + } + if (mIntl->IsRemote()) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsIWidget* widget; + LayoutDeviceIntRect rect = IntlLocal()->GetCaretRect(&widget); + rect.GetRect(aX, aY, aWidth, aHeight); + return NS_OK; +} + NS_IMETHODIMP xpcAccessibleHyperText::GetSelectionCount(int32_t* aSelectionCount) { NS_ENSURE_ARG_POINTER(aSelectionCount);