Skip to content

Add touch based web element navigation in browse mode#19414

Open
kefaslungu wants to merge 14 commits intonvaccess:masterfrom
kefaslungu:webmode
Open

Add touch based web element navigation in browse mode#19414
kefaslungu wants to merge 14 commits intonvaccess:masterfrom
kefaslungu:webmode

Conversation

@kefaslungu
Copy link

@kefaslungu kefaslungu commented Jan 3, 2026

Link to issue number:

Closes #3424

Summary of the issue:

As noted in the above issue, touch users do not have a dedicated way to perform browse mode style navigation of web content using touch gestures.

Description of user facing changes:

A new Web touch navigation mode has been introduced. When active, touch gestures allow users to navigate common web elements such as links, buttons, headings, form fields, landmarks, and other structural elements in browse mode documents.
This enables touch based navigation that mirrors existing browse mode navigation commands.
When browse mode is exited, the Web touch mode is automatically removed and touch navigation returns to its previous behavior.

Description of developer facing changes:

New touch gesture scripts have been added to invoke existing browse mode navigation commands. These scripts reuse the existing BrowseModeTreeInterceptor logic rather than introducing new navigation implementations.
Supporting logic has been added to track the active browse mode context and route touch gestures to the appropriate browse mode commands when available.

Description of development approach:

This change was implemented by subscribing to browse mode state change notifications and using them as the authoritative signal for enabling and disabling web specific touch behavior. The active browse mode tree interceptor is cached on activation and cleared on deactivation.

Testing strategy:

Manual testing was performed using touch navigation in multiple browse mode documents across supported web browsers. Testing verified:

  • Automatic activation of Web touch mode when entering browse mode
  • Correct removal of Web touch mode when leaving browse mode
  • Correct navigation between web elements using touch gestures
  • No appearance of Web touch mode in non browse mode contexts

Known issues with pull request:

None

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

kefaslungu and others added 5 commits January 3, 2026 15:35
Signed-off-by: kefaslungu <jameskefaslungu@gmail.com>
Signed-off-by: kefaslungu <jameskefaslungu@gmail.com>
Signed-off-by: kefaslungu <jameskefaslungu@gmail.com>
@seanbudd seanbudd added the conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. label Jan 6, 2026
@seanbudd
Copy link
Member

seanbudd commented Feb 3, 2026

@kefaslungu - do you intend to finish this?

@kefaslungu kefaslungu marked this pull request as ready for review February 3, 2026 08:41
@kefaslungu kefaslungu requested review from a team as code owners February 3, 2026 08:41
@kefaslungu
Copy link
Author

Hi @seanbudd ,

It is now ready for review. I've decided to send another PR for the remaining features discussed at #3424

Copy link
Member

@SaschaCowley SaschaCowley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welcome @kefaslungu! Thanks for opening your first PR for NVDA!

There are a few things in here that have me concerned, but overall I really like the direction you're going.

Comment on lines 4666 to 4675
"default",
"link",
"button",
"form field",
"heading",
"frame",
"table",
"list",
"graphic",
"landmark",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all need to be made translateable, since they're used in the UI. For example

		# Translators: An element type in browse mode.
		"link",

Comment on lines 4680 to 4717
browseModeCommands = (
(
browseMode.BrowseModeTreeInterceptor.script_nextLink,
browseMode.BrowseModeTreeInterceptor.script_previousLink,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextButton,
browseMode.BrowseModeTreeInterceptor.script_previousButton,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextFormField,
browseMode.BrowseModeTreeInterceptor.script_previousFormField,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextHeading,
browseMode.BrowseModeTreeInterceptor.script_previousHeading,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextFrame,
browseMode.BrowseModeTreeInterceptor.script_previousFrame,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextTable,
browseMode.BrowseModeTreeInterceptor.script_previousTable,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextList,
browseMode.BrowseModeTreeInterceptor.script_previousList,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextGraphic,
browseMode.BrowseModeTreeInterceptor.script_previousGraphic,
),
(
browseMode.BrowseModeTreeInterceptor.script_nextLandmark,
browseMode.BrowseModeTreeInterceptor.script_previousLandmark,
),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be constructed dynamically.

nvda/source/browseMode.py

Lines 605 to 607 in 8dbe0fd

scriptSuffix = itemType[0].upper() + itemType[1:]
scriptName = "next%s" % scriptSuffix
funcName = "script_%s" % scriptName

Comment on lines 4719 to 4765
@scriptHandler.script(
description="Select next web navigation element",
gesture="ts(Web):flickDown",
)
def script_nextWebElement(self, gesture):
ti = api.getNavigatorObject().treeInterceptor
if not isinstance(ti, browseMode.BrowseModeTreeInterceptor):
return

self.webBrowseMode = (self.webBrowseMode + 1) % len(self.webElements)
ui.message(self.webElements[self.webBrowseMode])

@scriptHandler.script(
description="Select previous web navigation element",
gesture="ts(Web):flickUp",
)
def script_prevWebElement(self, gesture):
ti = api.getNavigatorObject().treeInterceptor
if not isinstance(ti, browseMode.BrowseModeTreeInterceptor):
return

self.webBrowseMode = (self.webBrowseMode - 1) % len(self.webElements)
ui.message(self.webElements[self.webBrowseMode])

@scriptHandler.script(gesture="ts(Web):flickRight")
def script_nextSelectedElement(self, gesture):
ti = api.getNavigatorObject().treeInterceptor
if not isinstance(ti, browseMode.BrowseModeTreeInterceptor):
return

if self.webBrowseMode == 0:
self.script_navigatorObject_nextInFlow(gesture)
else:
nextScript, _ = self.browseModeCommands[self.webBrowseMode - 1]
nextScript(ti, gesture)

@scriptHandler.script(gesture="ts(Web):flickLeft")
def script_prevSelectedElement(self, gesture):
ti = api.getNavigatorObject().treeInterceptor
if not isinstance(ti, browseMode.BrowseModeTreeInterceptor):
return

if self.webBrowseMode == 0:
self.script_navigatorObject_previousInFlow(gesture)
else:
_, prevScript = self.browseModeCommands[self.webBrowseMode - 1]
prevScript(ti, gesture)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to make the descriptions for all of these scripts translatable. Also add the browse mode category to the scripts.

if browseMode:
# Entering browse mode
if webModeName not in availableTouchModes:
availableTouchModes.append(webModeName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to add this, and a corresponding translatable name, to touchModeLabels . This doesn't need to be done dynamically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has this comment from sascha been addressed? https://github.com/nvaccess/nvda/pull/19414/files#r2772194150

@josephsl
Copy link
Contributor

josephsl commented Feb 7, 2026

Hi,

I am the original creator of the parts of the code reviewed in this pull request discussion. Kefas has adopted it to NVDA Core expectations and structure.

The comment on implementation in the global commands module can be explained by the fact that the code and the idea behind it comes from an add-on (Enhanced Touch Gestures). The add-on used a global plugin class to house list of navigable elements, browse mode script calls, and the gesture to cycle between web elements. Our (Kefas and I) plan is to adopt Enhanced Touch Gestures add-on based on the pull request code structure and make changes based on review comments, with the end goal of removing web mode from the add-on in favor of NVDA Core version.

Thanks for reviewing Kefas' pull request.

… element registration and settings

Signed-off-by: kefaslungu <jameskefaslungu@gmail.com>
@kefaslungu
Copy link
Author

Hi all,

I've made some changes as follows:

  • Moved all web touch navigation scripts from GlobalCommands into BrowseModeTreeInterceptor, with category=inputCore.SCRCAT_BROWSEMODE and translatable descriptions.
  • Dynamic element registration: addQuickNav now accepts includeInWebTouch=True and auto-registers each element type into _webTouchNavRegistry. _webBrowseElements is built from this registry after all qn() calls, so future NVDA element types appear automatically with no manual updates. Heading sub-types (heading1–9) are excluded via includeInWebTouch=False.
  • Settings: Added a sub-section to BrowseModePanel (one per element type) to control which types appear when cycling.

Copy link
Member

@SaschaCowley SaschaCowley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heading in the right direction, but still needs some work. Good progress though :)

@SaschaCowley SaschaCowley marked this pull request as draft February 20, 2026 08:13
kefaslungu and others added 3 commits February 23, 2026 02:02
Signed-off-by: kefaslungu <jameskefaslungu@gmail.com>
…mode

# Conflicts:
#	source/gui/settingsDialogs.py
@kefaslungu
Copy link
Author

Hi @SaschaCowley,

Thanks for the detailed review! I've addressed all the feedback as follows:

  • Replaced all the 37 boolean config keys with a single string_list.
  • Replaced individual checkboxes with CustomCheckListBox.
  • Added explicit translatable touchLabel to every addQuickNav call, removing the camelCase derivation.
  • Heading levels (1–9) are now registered with their own touch labels rather than excluded.
  • _webBrowseCurrentType is now an instance attribute.
  • Renamed includeInWebTouch → availableInWebTouch.

All that is left now is documentation, but I'll do that as soon as the code is approved.

Cheers!

@kefaslungu kefaslungu marked this pull request as ready for review February 23, 2026 01:20
@seanbudd seanbudd marked this pull request as draft February 26, 2026 00:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements touch-based navigation for web elements in browse mode, addressing issue #3424 from 2013. The feature introduces a new "web" touch mode that allows touchscreen users to navigate structural elements (links, headings, form fields, etc.) using touch gestures, providing an experience similar to keyboard-based browse mode navigation.

Changes:

  • Adds a "web" touch mode that activates automatically when entering browse mode
  • Implements four touch gestures for cycling through and navigating between element types (flick up/down to select element type, left/right to navigate)
  • Provides GUI settings to configure which element types are available for touch navigation
  • Includes comprehensive user documentation explaining the new feature

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
user_docs/en/userGuide.md Adds documentation for the new web touch mode, including a table of gestures and usage explanations
user_docs/en/changes.md Adds changelog entry describing the new feature
source/touchHandler.py Implements browse mode state change handler to add/remove web mode and switch to it automatically
source/gui/settingsDialogs.py Adds UI controls for selecting which web elements are available in touch navigation
source/config/configSpec.py Adds configuration option for web touch navigation elements with sensible defaults
source/browseMode.py Implements core touch navigation scripts, element cycling logic, and registry system for available elements

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kefaslungu
Copy link
Author

Hi @seanbudd,

All requested changes has been made, please take a look.

Comment on lines 606 to 609
@param availableInWebTouch: If True, register this element type for web touch navigation cycling.
Set to False to exclude an element type entirely from web touch navigation.
@param touchLabel: A short, translated, plural label for this element type used in web touch navigation
cycling (e.g. C{_("links")}). Required when C{availableInWebTouch} is True.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see the coding standards for more information.

Suggested change
@param availableInWebTouch: If True, register this element type for web touch navigation cycling.
Set to False to exclude an element type entirely from web touch navigation.
@param touchLabel: A short, translated, plural label for this element type used in web touch navigation
cycling (e.g. C{_("links")}). Required when C{availableInWebTouch} is True.
:param availableInWebTouch: If True, register this element type for web touch navigation cycling.
Set to False to exclude an element type entirely from web touch navigation.
:param touchLabel: A short, translated, plural label for this element type used in web touch navigation
cycling (e.g. ``_("links")``). Required when ``availableInWebTouch`` is True.


#: The itemType currently selected for web touch navigation. None means "default" (all content).
#: Stored as an instance attribute so each document remembers its own preference.
_webBrowseCurrentType: Optional[str] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use | None syntax

if browseMode:
# Entering browse mode
if webModeName not in availableTouchModes:
availableTouchModes.append(webModeName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has this comment from sascha been addressed? https://github.com/nvaccess/nvda/pull/19414/files#r2772194150

"Selects the previous element type for web touch navigation",
),
category=inputCore.SCRCAT_BROWSEMODE,
gesture="ts(Web):flickUp",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
gesture="ts(Web):flickUp",
gesture="ts(web):flickUp",

Signed-off-by: kefaslungu <jameskefaslungu@gmail.com>
@kefaslungu
Copy link
Author

Hi @seanbudd,

All suggestions have been addressed.

@kefaslungu kefaslungu requested a review from seanbudd February 27, 2026 16:59
@kefaslungu kefaslungu marked this pull request as ready for review February 27, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Touch gestures: a way to browse the web via elements using touch gestures

5 participants