diff --git a/source/UIAHandler/browseMode.py b/source/UIAHandler/browseMode.py index 6be5c19201a..44d33255e0c 100644 --- a/source/UIAHandler/browseMode.py +++ b/source/UIAHandler/browseMode.py @@ -692,6 +692,19 @@ def _iterNodesByType(self, nodeType, direction="next", pos=None): UIAHandler.UIA_SliderControlTypeId, ) return UIAControlQuicknavIterator(nodeType, self, pos, condition, direction) + elif nodeType == "clickable": + # Match the generic control types which UIAWeb exposes with State.CLICKABLE. + condition = createUIAMultiPropertyCondition( + { + UIAHandler.UIA_ControlTypePropertyId: [ + UIAHandler.UIA_TextControlTypeId, + UIAHandler.UIA_GroupControlTypeId, + UIAHandler.UIA_ImageControlTypeId, + ], + UIAHandler.UIA_IsInvokePatternAvailablePropertyId: True, + }, + ) + return UIAControlQuicknavIterator(nodeType, self, pos, condition, direction) elif nodeType == "nonTextContainer": condition = createUIAMultiPropertyCondition( { diff --git a/source/browseMode.py b/source/browseMode.py index 1d7714bbc06..efda0e8767c 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -1234,6 +1234,21 @@ def script_prevSelectedElement(self, gesture: inputCore.InputGesture) -> None: # Translators: Label announced when cycling browse mode touch navigation element types in browse mode. touchLabel=_("sliders"), ) +qn( + "clickable", + key=None, + # Translators: Input help message for a quick navigation command in browse mode. + nextDoc=_("moves to the next clickable element"), + # Translators: Message presented when the browse mode element is not found. + nextError=_("no next clickable element"), + # Translators: Input help message for a quick navigation command in browse mode. + prevDoc=_("moves to the previous clickable element"), + # Translators: Message presented when the browse mode element is not found. + prevError=_("no previous clickable element"), + readUnit=textInfos.UNIT_LINE, + # Translators: Label announced when cycling browse mode touch navigation element types in browse mode. + touchLabel=_("clickable elements"), +) qn( "article", key=None, diff --git a/source/virtualBuffers/MSHTML.py b/source/virtualBuffers/MSHTML.py index b754815b384..eda0e83608f 100644 --- a/source/virtualBuffers/MSHTML.py +++ b/source/virtualBuffers/MSHTML.py @@ -401,6 +401,12 @@ def _searchableAttribsForNodeType(self, nodeType): {"IAccessible::role": [oleacc.ROLE_SYSTEM_SLIDER]}, {"HTMLAttrib::role": ["slider"]}, ] + elif nodeType == "clickable": + attrs = [ + {"HTMLAttrib::onclick": [VBufStorage_findMatch_notEmpty]}, + {"HTMLAttrib::onmousedown": [VBufStorage_findMatch_notEmpty]}, + {"HTMLAttrib::onmouseup": [VBufStorage_findMatch_notEmpty]}, + ] elif nodeType == "table": attrs = {"IHTMLDOMNode::nodeName": ["TABLE"]} if not config.conf["documentFormatting"]["includeLayoutTables"]: diff --git a/source/virtualBuffers/gecko_ia2.py b/source/virtualBuffers/gecko_ia2.py index 4b9953ba6c6..7ee0ba6fa5b 100755 --- a/source/virtualBuffers/gecko_ia2.py +++ b/source/virtualBuffers/gecko_ia2.py @@ -488,6 +488,8 @@ def _searchableAttribsForNodeType(self, nodeType): {"IAccessible::role": [oleacc.ROLE_SYSTEM_SLIDER]}, {"IAccessible2::attribute_xml-roles": [VBufStorage_findMatch_word("slider")]}, ] + elif nodeType == "clickable": + attrs = {"IAccessibleAction_click": [VBufStorage_findMatch_notEmpty]} elif nodeType == "graphic": attrs = {"IAccessible::role": [oleacc.ROLE_SYSTEM_GRAPHIC]} elif nodeType == "blockQuote": diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index 81a67d56a98..527218ed569 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -2760,6 +2760,44 @@ def test_styleNav(): _asserts.strings_match(actualSpeech, "No next same style text") +def test_clickableNavigation() -> None: + """Tests that unassigned quick navigation commands move between clickable elements.""" + spy: "NVDASpyLib" = _NvdaLib.getSpyLib() + spy.assignGesture( + "kb:z", + "browseMode", + "BrowseModeTreeInterceptor", + "nextClickable", + ) + spy.assignGesture( + "kb:shift+z", + "browseMode", + "BrowseModeTreeInterceptor", + "previousClickable", + ) + # The navigation must use the clickable metadata even when its speech reporting is disabled. + spy.set_configValue(["documentFormatting", "reportClickable"], False) + _chrome.prepareChrome(""" +

Before the custom controls

+ +
First custom control
+

Between the custom controls

+
Second custom control
+

After the custom controls

+ """) + + actualSpeech = _chrome.getSpeechAfterKey("z") + _asserts.strings_match(actualSpeech, "First custom control") + actualSpeech = _chrome.getSpeechAfterKey("z") + _asserts.strings_match(actualSpeech, "Second custom control") + actualSpeech = _chrome.getSpeechAfterKey("z") + _asserts.strings_match(actualSpeech, "no next clickable element") + actualSpeech = _chrome.getSpeechAfterKey("shift+z") + _asserts.strings_match(actualSpeech, "First custom control") + actualSpeech = _chrome.getSpeechAfterKey("shift+z") + _asserts.strings_match(actualSpeech, "no previous clickable element") + + def test_ariaErrorMessage(): _chrome.prepareChrome("""

Native valid

diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 8a8592f9aba..87c4dcd0211 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -246,6 +246,10 @@ styleNav [Documentation] Same style navigation [Tags] chrome_misc test_styleNav +Clickable navigation + [Documentation] Navigate between clickable elements using unassigned quick navigation commands + [Tags] chrome_misc + test_clickableNavigation Native selection mode focus mode caret movement [Documentation] The caret can be moved in focus mode while native selection mode is enabled (#19075) [Tags] chrome_misc diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 19e7e9f3b2f..016f21a52ee 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -18,6 +18,7 @@ * All four edges are supported. Note that the Windows taskbar may override gestures on an edge. Gestures from the taskbar edge open the Start menu or Action Center, NVDA will not receive them. +* Added an unassigned Quick Navigation Command for jumping to next/previous clickable element in browse mode. (#14429, @cary-rowen) * On supported braille displays, pressing multiple routing keys simultaneously can now be bound to a new "multi routing" gesture. (#20001, @LeonarddeR) * The "select range" command, which selects the text from the first up to the last pressed routing key, is bound to this gesture by default on supporting drivers. * Drivers with built-in support for multi routing: ALVA, Albatross (only when combined with `home1` or `home2`), Baum (and compatible), Freedom Scientific Focus/PAC Mate, HumanWare Brailliant BI/B series, Handy Tech, NLS eReader Zoomax, Seika Notetaker, and Standard HID Braille displays. diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 674327041a6..dc24f87284f 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -1150,6 +1150,7 @@ Here is a list of available commands: * Toggle button * Progress bar * Slider +* Clickable element * Reference * Math formula * Vertically aligned paragraph