[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