[Webkit-unassigned] [Bug 265683] AX: add Mac API to get selected text range overlapping static text element
bugzilla-daemon at webkit.org
bugzilla-daemon at webkit.org
Tue Dec 5 07:17:40 PST 2023
https://bugs.webkit.org/show_bug.cgi?id=265683
--- Comment #14 from Andres Gonzalez <andresg_22 at apple.com> ---
(In reply to Dominic Mazzoni from comment #12)
> Created attachment 468884 [details]
> Patch
Thanks for doing this, looks good overall, just a few more comments.
diff --git a/Source/WebCore/accessibility/AXTextMarker.cpp b/Source/WebCore/accessibility/AXTextMarker.cpp
index d75920ae9a14..69022fc47757 100644
--- a/Source/WebCore/accessibility/AXTextMarker.cpp
+++ b/Source/WebCore/accessibility/AXTextMarker.cpp
@@ -299,6 +299,38 @@ std::optional<CharacterRange> AXTextMarkerRange::characterRange() const
return { { m_start.m_data.characterOffset, m_end.m_data.characterOffset - m_start.m_data.characterOffset } };
}
+std::optional<AXTextMarkerRange> AXTextMarkerRange::intersectionWith(const AXTextMarkerRange& other) const
+{
+ if (m_start.m_data.treeID != m_end.m_data.treeID
+ || other.m_start.m_data.treeID != other.m_end.m_data.treeID
+ || m_start.m_data.treeID != other.m_start.m_data.treeID) {
AG: you may want to enclose this bool expression in UNLIKELY(...).
+ ASSERT_NOT_REACHED();
+ return std::nullopt;
+ }
+
+ // Fast path: both ranges span one object
+ if (m_start.m_data.objectID == m_end.m_data.objectID
+ && other.m_start.m_data.objectID == other.m_end.m_data.objectID) {
+ if (m_start.m_data.objectID != other.m_start.m_data.objectID)
+ return std::nullopt;
+
+ unsigned startOffset = std::max(m_start.m_data.characterOffset, other.m_start.m_data.characterOffset);
+ unsigned endOffset = std::min(m_end.m_data.characterOffset, other.m_end.m_data.characterOffset);
+
+ auto startMarker = AXTextMarker({ (AXID)m_start.m_data.treeID, (AXID)m_start.m_data.objectID, nullptr, startOffset, Position::PositionIsOffsetInAnchor, Affinity::Downstream, 0, startOffset });
AG: instead of casting (AXID), you can do m_start.m_data.treeID() and .objectID().
+ auto endMarker = AXTextMarker({ (AXID)m_start.m_data.treeID, (AXID)m_start.m_data.objectID, nullptr, endOffset, Position::PositionIsOffsetInAnchor, Affinity::Downstream, 0, endOffset });
AG: same here.
+ return { { startMarker, endMarker } };
AG: do we need to check whether startOffset <= endOffset? Otherwise I think we can get a range when there is no intersection.
+ }
+
+ return Accessibility::retrieveValueFromMainThread<std::optional<AXTextMarkerRange>>([this, &other] () -> std::optional<AXTextMarkerRange> {
+ auto intersection = WebCore::intersection(*this, other);
+ if (intersection.isNull())
+ return std::nullopt;
+
+ return { AXTextMarkerRange(intersection) };
+ });
+}
+
String AXTextMarkerRange::debugDescription() const
{
return makeString("start: {", m_start.debugDescription(), "}\nend: {", m_end.debugDescription(), "}");
diff --git a/Source/WebCore/accessibility/AXTextMarker.h b/Source/WebCore/accessibility/AXTextMarker.h
index 2ffaeec026f0..2136c4fae872 100644
--- a/Source/WebCore/accessibility/AXTextMarker.h
+++ b/Source/WebCore/accessibility/AXTextMarker.h
@@ -156,6 +156,8 @@ public:
std::optional<SimpleRange> simpleRange() const;
std::optional<CharacterRange> characterRange() const;
+ std::optional<AXTextMarkerRange> intersectionWith(const AXTextMarkerRange& other) const;
+
#if PLATFORM(MAC)
RetainPtr<AXTextMarkerRangeRef> platformData() const;
operator AXTextMarkerRangeRef() const { return platformData().autorelease(); }
diff --git a/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm b/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm
index f603c3f035b7..426333dc1f08 100644
--- a/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm
+++ b/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm
@@ -292,6 +292,10 @@ using namespace WebCore;
#define NSAccessibilitySelectTextWithCriteriaParameterizedAttribute @"AXSelectTextWithCriteria"
#endif
+#ifndef NSAccessibilityIntersectionWithSelectionRangeAttribute
+#define NSAccessibilityIntersectionWithSelectionRangeAttribute @"AXIntersectionWithSelectionRange"
+#endif
+
// Text search
#ifndef NSAccessibilitySearchTextWithCriteriaParameterizedAttribute
@@ -1159,6 +1163,11 @@ ALLOW_DEPRECATED_IMPLEMENTATIONS_END
[tempArray addObject:NSAccessibilityURLAttribute];
return tempArray;
}();
+ static NeverDestroyed staticTextAttrs = [] {
+ auto tempArray = adoptNS([[NSMutableArray alloc] initWithArray:attributes.get().get()]);
+ [tempArray addObject:NSAccessibilityIntersectionWithSelectionRangeAttribute];
+ return tempArray;
+ }();
NSArray *objectAttributes = attributes.get().get();
@@ -1166,6 +1175,8 @@ ALLOW_DEPRECATED_IMPLEMENTATIONS_END
objectAttributes = secureFieldAttributes.get().get();
else if (backingObject->isWebArea())
objectAttributes = webAreaAttrs.get().get();
+ else if (backingObject->isStaticText())
+ objectAttributes = staticTextAttrs.get().get();
else if (backingObject->isTextControl())
objectAttributes = textAttrs.get().get();
else if (backingObject->isLink())
@@ -1626,6 +1637,11 @@ ALLOW_DEPRECATED_IMPLEMENTATIONS_END
}
}
+ if (backingObject->isStaticText()) {
+ if ([attributeName isEqualToString:NSAccessibilityIntersectionWithSelectionRangeAttribute])
+ return [self intersectionWithSelectionRange];
+ }
+
if ([attributeName isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) {
if (backingObject->isSecureField())
return nil;
@@ -2235,6 +2251,25 @@ ALLOW_DEPRECATED_IMPLEMENTATIONS_END
return nil;
}
+- (NSValue *)intersectionWithSelectionRange
+{
+ RefPtr<AXCoreObject> backingObject = self.updateObjectBackingStore;
+ if (!backingObject)
+ return nil;
+
+ auto objectRange = backingObject->textMarkerRange();
+ auto selectionRange = backingObject->selectedTextMarkerRange();
+
+ auto intersection = selectionRange.intersectionWith(objectRange);
+ if (intersection.has_value()) {
+ auto intersectionCharacterRange = intersection->characterRange();
+ if (intersectionCharacterRange.has_value())
+ return [NSValue valueWithRange:intersectionCharacterRange.value()];
+ }
+
+ return nil;
+}
+
- (NSString *)accessibilityPlatformMathSubscriptKey
{
return NSAccessibilityMathSubscriptAttribute;
diff --git a/Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.h b/Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.h
index c682f1d19750..0f38df2383c3 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.h
+++ b/Tools/WebKitTestRunner/InjectedBundle/AccessibilityUIElement.h
@@ -178,6 +178,7 @@ public:
unsigned numberOfCharacters() const;
int insertionPointLineNumber();
JSRetainPtr<JSStringRef> selectedTextRange();
+ JSRetainPtr<JSStringRef> intersectionWithSelectionRange();
JSRetainPtr<JSStringRef> textInputMarkedRange() const;
bool isAtomicLiveRegion() const;
bool isBusy() const;
diff --git a/Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityUIElement.idl b/Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityUIElement.idl
index dc7bfc9d1c12..a5102e5f7a69 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityUIElement.idl
+++ b/Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityUIElement.idl
@@ -60,6 +60,7 @@
readonly attribute unsigned long numberOfCharacters;
readonly attribute long insertionPointLineNumber;
readonly attribute DOMString selectedTextRange;
+ readonly attribute DOMString intersectionWithSelectionRange;
readonly attribute DOMString textInputMarkedRange;
DOMString stringDescriptionOfAttributeValue(DOMString attr);
diff --git a/Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.cpp b/Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.cpp
index 4efe8d005d39..089243e47f62 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.cpp
+++ b/Tools/WebKitTestRunner/InjectedBundle/atspi/AccessibilityUIElementAtspi.cpp
@@ -1426,6 +1426,11 @@ JSRetainPtr<JSStringRef> AccessibilityUIElement::selectedTextRange()
return OpaqueJSString::tryCreate(range).leakRef();
}
+JSRetainPtr<JSStringRef> AccessibilityUIElement::intersectionWithSelectionRange()
+{
+ return nullptr;
+}
+
bool AccessibilityUIElement::setSelectedTextRange(unsigned location, unsigned length)
{
if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
diff --git a/Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.mm b/Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.mm
index 2d566783d910..b708005c52cc 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.mm
+++ b/Tools/WebKitTestRunner/InjectedBundle/ios/AccessibilityUIElementIOS.mm
@@ -986,6 +986,11 @@ JSRetainPtr<JSStringRef> AccessibilityUIElement::selectedTextRange()
return [rangeDescription createJSStringRef];
}
+JSRetainPtr<JSStringRef> AccessibilityUIElement::intersectionWithSelectionRange()
+{
+ return nullptr;
+}
+
bool AccessibilityUIElement::setSelectedTextMarkerRange(AccessibilityTextMarkerRange*)
{
return false;
diff --git a/Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm b/Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm
index b5532f4af0ba..f44996db29ef 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm
+++ b/Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm
@@ -89,6 +89,10 @@
#define NSAccessibilityTextInputMarkedTextMarkerRangeAttribute @"AXTextInputMarkedTextMarkerRange"
#endif
+#ifndef NSAccessibilityIntersectionWithSelectionRangeAttribute
+#define NSAccessibilityIntersectionWithSelectionRangeAttribute @"AXIntersectionWithSelectionRange"
+#endif
+
typedef void (*AXPostedNotificationCallback)(id element, NSString* notification, void* context);
@interface NSObject (WebKitAccessibilityAdditions)
@@ -134,7 +138,15 @@ bool AccessibilityUIElement::isEqual(AccessibilityUIElement* otherElement)
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
bool AccessibilityUIElement::isIsolatedObject() const
{
- return [m_element isIsolatedObject];
+ BOOL value;
+
+ BEGIN_AX_OBJC_EXCEPTIONS
+ s_controller->executeOnAXThreadAndWait([this, &value] {
+ value = [m_element isIsolatedObject];
+ });
+ END_AX_OBJC_EXCEPTIONS
+
+ return value;
}
#endif
@@ -1542,13 +1554,27 @@ JSRetainPtr<JSStringRef> AccessibilityUIElement::selectedTextRange()
auto indexRange = attributeValue(NSAccessibilitySelectedTextRangeAttribute);
if (indexRange)
range = [indexRange rangeValue];
- NSMutableString* rangeDescription = [NSMutableString stringWithFormat:@"{%lu, %lu}", static_cast<unsigned long>(range.location), static_cast<unsigned long>(range.length)];
+ NSString *rangeDescription = [NSString stringWithFormat:@"{%lu, %lu}", static_cast<unsigned long>(range.location), static_cast<unsigned long>(range.length)];
return [rangeDescription createJSStringRef];
END_AX_OBJC_EXCEPTIONS
return nullptr;
}
+JSRetainPtr<JSStringRef> AccessibilityUIElement::intersectionWithSelectionRange()
+{
+ BEGIN_AX_OBJC_EXCEPTIONS
+ auto rangeAttribute = attributeValue(NSAccessibilityIntersectionWithSelectionRangeAttribute);
+ if (rangeAttribute) {
+ NSRange range = [rangeAttribute rangeValue];
+ NSMutableString* rangeDescription = [NSMutableString stringWithFormat:@"{%lu, %lu}", static_cast<unsigned long>(range.location), static_cast<unsigned long>(range.length)];
AG: NSMutableString* rangeDescription -> NSString *rangeDescription
+ return [rangeDescription createJSStringRef];
+ }
+ END_AX_OBJC_EXCEPTIONS
+
+ return nullptr;
+}
+
bool AccessibilityUIElement::setSelectedTextRange(unsigned location, unsigned length)
{
NSRange textRange = NSMakeRange(location, length);
diff --git a/Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.cpp b/Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.cpp
index ff11fb7be199..b22e62ee8d02 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.cpp
+++ b/Tools/WebKitTestRunner/InjectedBundle/win/AccessibilityUIElementWin.cpp
@@ -658,6 +658,12 @@ JSRetainPtr<JSStringRef> AccessibilityUIElement::selectedTextRange()
return nullptr;
}
+JSRetainPtr<JSStringRef> AccessibilityUIElement::intersectionWithSelectionRange()
+{
+ notImplemented();
+ return nullptr;
+}
+
bool AccessibilityUIElement::setSelectedTextRange(unsigned, unsigned)
{
notImplemented();
diff --git a/LayoutTests/accessibility/mac/intersection-with-selection-range-expected.txt b/LayoutTests/accessibility/mac/intersection-with-selection-range-expected.txt
new file mode 100644
index 000000000000..214287c6c542
--- /dev/null
+++ b/LayoutTests/accessibility/mac/intersection-with-selection-range-expected.txt
@@ -0,0 +1,46 @@
+This tests the intersectionWithSelectionRange api, which returns the range of characters in a static text node that are part of the document selection, if any.
+
+Trying range: text1:0 - text1:5 : Alpha
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+Trying range: text1:6 - text1:11 : Bravo
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+Trying range: text2:2 - text2:9 : Charlie
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+Trying range: text2:11 - text2:16 : Delta
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+Trying range: text1:6 - text2:9 : Bravo
+Charlie
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+Trying range: text1:0 - text1:5 : Alpha
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+Trying range: text1:6 - text2:9 : Bravo
+Charlie
+PASS: window.getSelection().toString() === expectedText
+PASS: axStaticText.role === "AXRole: AXStaticText"
+PASS: axStaticText.intersectionWithSelectionRange === expectedAccessibleRange
+
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+Alpha Bravo
+Charlie Delta
+
diff --git a/LayoutTests/accessibility/mac/intersection-with-selection-range.html b/LayoutTests/accessibility/mac/intersection-with-selection-range.html
new file mode 100644
index 000000000000..db826a965400
--- /dev/null
+++ b/LayoutTests/accessibility/mac/intersection-with-selection-range.html
@@ -0,0 +1,64 @@
+<html>
+<head>
+<script src="../../resources/accessibility-helper.js"></script>
+<script src="../../resources/js-test.js"></script>
+</head>
+<body>
+
+<div id="text1">Alpha Bravo</div>
+<div id="text2"> Charlie Delta </div>
+
+<pre id="tree"></pre>
+
+<script>
+ var output = "This tests the intersectionWithSelectionRange api, which returns the range of characters in a static text node that are part of the document selection, if any.\n\n";
+
+ async function selectNodeIdRange(nodeId0, offset0, nodeId1, offset1) {
+ let root = accessibilityController.rootElement;
+ let previousAXSelection = root.stringForTextMarkerRange(root.selectedTextMarkerRange())
+ let sel = window.getSelection();
+ let range = document.createRange();
+ range.setStart(document.getElementById(nodeId0).firstChild, offset0);
+ range.setEnd(document.getElementById(nodeId1).firstChild, offset1);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ if (root.isIsolatedObject) {
AG: we shouldn't need this, isIsolatedObject is a leftover that should be removed along with the test that is using it. Client shouldn't need to know whether it is isolated or not.
+ await waitFor(() => previousAXSelection != root.stringForTextMarkerRange(root.selectedTextMarkerRange()));
+ }
+ }
+
+ async function runTest(nodeId0, offset0, nodeId1, offset1,
+ expectedText,
+ accessibleElementId, expectedAccessibleRange) {
+ output += `Trying range: ${nodeId0}:${offset0} - ${nodeId1}:${offset1} : ${expectedText}\n`;
+ await selectNodeIdRange(nodeId0, offset0, nodeId1, offset1);
+ window.expectedText = expectedText;
+ output += expect('window.getSelection().toString()', 'expectedText');
+
+ axElement = accessibilityController.accessibleElementById(accessibleElementId);
+ axStaticText = axElement.childAtIndex(0);
+ output += expect('axStaticText.role', '"AXRole: AXStaticText"');
+ window.expectedAccessibleRange = expectedAccessibleRange
+ output += expect('axStaticText.intersectionWithSelectionRange', 'expectedAccessibleRange');
+
+ output += '\n';
+ }
+
+ if (window.accessibilityController) {
+ window.jsTestIsAsync = true;
+ setTimeout(async () => {
+ //dumpAccessibilityTree(accessibilityController.rootElement, null, 0, true, false, true);
AG: remove commented line.
+ await runTest('text1', 0, 'text1', 5, 'Alpha', 'text1', '{0, 5}');
+ await runTest('text1', 6, 'text1', 11, 'Bravo', 'text1', '{6, 5}');
+ await runTest('text2', 2, 'text2', 9, 'Charlie', 'text2', '{0, 7}');
+ await runTest('text2', 11, 'text2', 16, 'Delta', 'text2', '{8, 5}');
+ await runTest('text1', 6, 'text2', 9, 'Bravo\nCharlie', 'text1', '{6, 5}');
+ await runTest('text1', 0, 'text1', 5, 'Alpha', 'text2', null);
+ await runTest('text1', 6, 'text2', 9, 'Bravo\nCharlie', 'text2', '{0, 7}');
+ debug(output);
+ finishJSTest();
+ }, 0);
+ }
+</script>
+</body>
+</html>
--
You are receiving this mail because:
You are the assignee for the bug.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.webkit.org/pipermail/webkit-unassigned/attachments/20231205/c718085d/attachment-0001.htm>
More information about the webkit-unassigned
mailing list