From f5c70f023581be259233aede59dff70311729214 Mon Sep 17 00:00:00 2001 From: cary-rowen Date: Fri, 7 Aug 2026 14:21:18 +0800 Subject: [PATCH 1/2] Add Quick Navigation for clickable elements --- source/UIAHandler/browseMode.py | 13 ++++++++++ source/browseMode.py | 15 +++++++++++ source/virtualBuffers/MSHTML.py | 6 +++++ source/virtualBuffers/gecko_ia2.py | 2 ++ tests/system/robot/chromeTests.py | 38 ++++++++++++++++++++++++++++ tests/system/robot/chromeTests.robot | 4 +++ user_docs/en/changes.md | 1 + user_docs/en/userGuide.md | 1 + 8 files changed, 80 insertions(+) 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 3d569e56220..0d9bf52ce9d 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 2de90a0bf2d..3ad6c313a42 100755 --- a/source/virtualBuffers/gecko_ia2.py +++ b/source/virtualBuffers/gecko_ia2.py @@ -486,6 +486,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 c9c04690bd7..4c3f9dac309 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 46f5b1bcd07..7f88a4c1bcc 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 ## chrome_link tests ### Link destination reporting (NVDA+K) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 3770511e583..70073e87bb8 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -273,6 +273,7 @@ When resetting NVDA to factory defaults, an Undo button is now available to rest * After installing or updating NVDA, a dialog now offers options to restart Windows, start the installed copy, or exit the installer. (#19268, #19718, @kefaslungu) * Added an unassigned command to toggle keyboard layout. (#19211, @CyrilleB79) * Added an unassigned Quick Navigation Command for jumping to next/previous slider in browse mode. (#17005, @tareh7z) +* Added an unassigned Quick Navigation Command for jumping to next/previous clickable element in browse mode. (#14429, @cary-rowen) * When resetting the configuration to factory defaults from the NVDA menu, a dialog is now shown afterwards with an Undo button to restore the previous configuration. The triple-press keyboard shortcut (`NVDA+control+r`) is not affected, as it is intended for recovery scenarios. (#19575, @bramd) * Added an unassigned command to report the current status of the Screen Curtain. (#19759) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 06a008f0dfe..73ccdf1651d 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 From 43955cfc877238977750b72df8cf8f3dde290d05 Mon Sep 17 00:00:00 2001 From: cary-rowen Date: Mon, 10 Aug 2026 13:45:47 +0800 Subject: [PATCH 2/2] Update changeLog item. --- user_docs/en/changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 70073e87bb8..ca28a44719d 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. @@ -273,7 +274,6 @@ When resetting NVDA to factory defaults, an Undo button is now available to rest * After installing or updating NVDA, a dialog now offers options to restart Windows, start the installed copy, or exit the installer. (#19268, #19718, @kefaslungu) * Added an unassigned command to toggle keyboard layout. (#19211, @CyrilleB79) * Added an unassigned Quick Navigation Command for jumping to next/previous slider in browse mode. (#17005, @tareh7z) -* Added an unassigned Quick Navigation Command for jumping to next/previous clickable element in browse mode. (#14429, @cary-rowen) * When resetting the configuration to factory defaults from the NVDA menu, a dialog is now shown afterwards with an Undo button to restore the previous configuration. The triple-press keyboard shortcut (`NVDA+control+r`) is not affected, as it is intended for recovery scenarios. (#19575, @bramd) * Added an unassigned command to report the current status of the Screen Curtain. (#19759)