Repository: nvaccess/nvda

Issue:
NVDA repeats the word "clickable" several times in a row before just saying what the item is.
In some pages, NVDA repeats the word clickable several times before saying what the clickable item is. Something like: "clickable clickable clickable clickable clickable clickable clickable clickable click here to begin."
For example: go to `my.snhu.edu`. From the top of the page, press `f` to go to the next form field.
NVDA uses the word "clickable" eight times in the course of saying I landed on an edit box that is titled: "search this site." This is, I am sure, because of bad web design, but NVDA should cope with this and not say the word "clickable" more than once per item, and never for form controls, at least, it seems obvious that a button or text field is clickable, this is how one interacts with them.

**Edit**
Since I am not able to reproduce this via `snhu.edu` site, consider the following example (paste into the address bar in Firefox or Chrome):
`data:text/html,<p>text before the clickable div</p><div onClick=";"><div onClick=";">This is some text before the button <br /><button>foo</button></div> bar</div><p>This is the paragraph</p>`

- After NVDA reads out "text before the clickable div"
- Press `f` to jump to the button.

### Actual behaviour:
Chrome
```
clickable
foo
button
```
Firefox
```
clickable
clickable
foo
button
```


Discussion:
While this certainly isn't a new discovery and I don't believe this is the first time it has been reported either, can @jcsteh, @feerrenrut and @leonardder please take a look?
Please see relevant issue that discusses this issue in more depth: #5481 and #6399
#3976 also seems related, unconfirmed if it's a duplicate.
Also potentially related: #6069
### Problem:

- Clickable gets reported when it shouldn't
- Sometimes, it even gets repeated multiple times in a row: clickable clickable clickable clickable
- This has been reported several times and is very annoying for users

### Explanation:

- We report clickable when entering any element which exposes a click action if that element doesn't have better semantic info (e.g. button, check box, etc.)
    * `<button onClick=";">foo</button>` reports "button foo"
- We also do this for ancestors
    * `<div onClick=";"><button>foo</button></div>` would report "clickable button foo"
- If entering multiple clickable ancestors, we report clickable multiple times
    * `<div onClick=";"><div onClick=";"><button>foo</button></div></div>` reports "clickable clickable button foo"
- Putting click handlers on an ancestor handling clicks for several descendants is becoming a common pattern
    * Often, it's quite far up the tree
    * Sometimes, it's even on the body (but we ignore this case)
- Chrome actually exposes the click action for all descendants
    * This is bad for us because we can't tell whether the node really had its own click handler
    * Need to raise this with Google

### Possible fixes:

1. Disable reporting of clickable by default
1. Report clickable for any text without enclosing semantics
    - `<div onClick=";"><button>b</button>t</div>` would report "button b clickable t"
1. Collapsing clickables
    - If we would report clickable more than once for a line, just report it once
    - `<div onClick=";"><div onClick=";"<button>b</button></div></div>` would report "clickable button b"
1. Report clickable only for nodes which effectively contain only text
    - `<div onClick=";"><button>b</button>t</div>` would not report clickable
    - `<div onClick=";">t</div> would report "clickable t"`
    - It's not enough to just do this for one level; `<div onClick=";"><div>t</div></div>` should report "clickable t"
1.  Report clickable only when entering the outermost element.
    - This has similar results to collapsing clickables for some cases: `<div onClick=";"><div onClick=";"<button>b</button></div></div>` would report "clickable button b" for both solutions.
    - However: `<div onClick=";">t<div onClick=";"<button>b</button></div></div>`  would report "clickable t clickable button b" for collapsing, "clickable t button b" (no clickable before button) for outermost.


Choosing a solution:

### Report clickable only for nodes which effectively contain only text 
The most ideal solution theoretically. The problem is that for technical reasons, we can probably only do this if the node is within the current line; i.e. we wouldn't report clickable if the text spanned multiple lines. That might be a problem for long link-like things or clickable paragraphs.

### Report clickable only when entering the outermost element
Very clear, simple, unambiguous. However, with the rising prevalence of clickable on high ancestors, I'm not sure how useful this would be; you might just get "clickable" once at the start of a big region and then never again.

### Disabling reporting of clickable by default 
Clear and simple. Clickable is going to be inaccurate in at least some use cases no matter what we do. However, users might not know when they need to enable it. It will also probably be unpopular, especially since other screen readers report clickable.

### Report clickable for any text without enclosing semantics 
If click handler is on a high ancestor, clickable will get reported for almost all "text" on the page. I don't think this is a reasonable option given that this pattern is becoming more common.

### Collapsing clickables
Suffers from asymmetry/ambiguity; doesn't eliminate multiple clickables interspersed with other semantics.
@feerrenrut What's the difference between Chrome/Firefox here? Is Firefox filtering out clickables through the tree if they are only set on the parent? I think the solution that would match the Firefox behavior, either as fix in Chrome or NVDA, would be the prefered one. This is what users might expect already from Firefox usage and in my experience in Firefox it works fine.
@bramd, in short this also effects Firefox. One of the first steps for resolving this issue is to provide a full set of examples to exercise all the known use cases. I have updated the description of this issue with a sample that demonstrates the problem.
Chrome here. We currently have an issue open to address this issue, and can expose things like Firefox does, or however the community agrees we should do it.

One thought was that we continue to expose the click action on the root of a subtree with a click handler, and expose a different action, like "click-descendant" for the subtree descendants.

In any case, we want to fix this on our side as well.
The issue of repeating the word "clickable" several times is, in particular, something which can be duplicated and observed with Google Chrome. I'm using Chrome V63.0.
For instance, If you go to https://groups.google.com/forum/?nomobile=true#!forum/eyes-free and, most notably, try to navigate the list of the latest messages via I and Shift+I, you'll hear the word "clickable" 3 or 4 times with each key press. This is also duplicable if you open a message and try pressing B and Shift+B to sort of move past each message by reaching the "Sign in to reply" button.
@amirsol81 please try in Chrome Canary. This should be fixed now. Sorry for the annoying bug :) 
I think it is wrong to assume that everything with a click listener on it is going to be meaningful to the user, and that it should be announced. I have code that looks for clicks on elements that are not a fly-out menu, and when it receives them it closes the fly-out menus. 99% of the clicks do nothing, because most of the time those menus aren't open. I have several other utility click handlers on the same complex page that are not really there for overt interaction in the way clicking a button is. I even have something that records all clicks or keypresses on all elements, in order to determine when a focus outline should be drawn for certain element types (only when a keypress happens right before the focus, similar to how OS X handles focus rings natively). IMO, the only things that should be announced as clickable are things that are focused, and not all their ancestors. 
This same thing is happening to me. I'm using a screen reader for accessibility testing, and half of the items say "clickable" 6-8 times before announcing what the item is. Some of these items are NOT clickable. That's the weird thing.
I have seen at least 3 people (in a limited setting as well) use competitors screen readers instead of NVDA because of this. This needs fixed badly, or removed. It quite literally is losing us users by the dozens.
I agree with @derekriemer’s assessment. If people have to turn the feature off because it’s incredibly disruptive, it might as well not be there. At the very least, I recommend disabling the announcement by default until it’s fixed. Let users enable it if they so wish. There are several sites where I can find a good use for this feature but don’t use it because it’s too disruptive in other places.

I am one of those who have to use other screenreaders summtimes to get 
around this


On 12/04/2018 06:24 a. m., Derek Riemer wrote:
>
> I have seen at least 3 people (in a limited setting as well) use 
> competitors screen readers instead of NVDA because of this. This needs 
> fixed badly, or removed. It quite literally is losing us users by the 
> dozens.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub 
> <https://github.com/nvaccess/nvda/issues/7430#issuecomment-380786522>, 
> or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/API6eX6L01kw6VcfQr5vq7RbZ462tv5Sks5tn0dwgaJpZM4OgEi6>.
>


Interesting. I am not visually impaired, but I downloaded a screenreader because I am a UX designer who is trying to design with accessibility in mind! This has been frustrating for ME, and I have the visuals to go along with it so I can "cheat" if I have to. I can't imagine how obnoxious and confusing this would be for my users who are actually seeing impaired. I have turned off the feature, but I can't guarantee that my users do. JAWS is so expensive; I hope NVDA continues to improve for the sake of the many people out there using screen readers. Note -- I also like Mac's screen reader, which is much more intuitive and includes helpers. There is still a huge learning curve for navigating using a keyboard + screen reader, regardless of the software. 

Resolving patch:
diff --git a/source/braille.py b/source/braille.py
--- a/source/braille.py
+++ b/source/braille.py
@@ -663,9 +663,6 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig):
 
 	if presCat == field.PRESCAT_LAYOUT:
 		text = []
-		# The only item we report for these fields is clickable, if present.
-		if controlTypes.STATE_CLICKABLE in states:
-			text.append(getBrailleTextForProperties(states={controlTypes.STATE_CLICKABLE}))
 		if current:
 			text.append(getBrailleTextForProperties(current=current))
 		return TEXT_SEPARATOR.join(text) if len(text) != 0 else None
@@ -829,8 +826,12 @@ def _addTextWithFields(self, info, formatConfig, isSelection=False):
 		ctrlFields = []
 		typeform = louis.plain_text
 		formatFieldAttributesCache = getattr(info.obj, "_brailleFormatFieldAttributesCache", {})
+		# When true, we are inside a clickable field, and should therefore not report any more new clickable fields
+		inClickable=False
 		for command in info.getTextWithFields(formatConfig=formatConfig):
 			if isinstance(command, basestring):
+				# Text should break a run of clickables
+				inClickable=False
 				self._isFormatFieldAtStart = False
 				if not command:
 					continue
@@ -872,7 +873,20 @@ def _addTextWithFields(self, info, formatConfig, isSelection=False):
 					if self._skipFieldsNotAtStartOfNode and not field.get("_startOfNode"):
 						text = None
 					else:
+						textList=[]
+						if not inClickable and formatConfig['reportClickable']:
+							states=field.get('states')
+							if states and controlTypes.STATE_CLICKABLE in states:
+								# We have entered an outer most clickable or entered a new clickable after exiting a previous one 
+								# Report it if there is nothing else interesting about the field
+								field._presCat=presCat=field.getPresentationCategory(ctrlFields,formatConfig)
+								if not presCat or presCat is field.PRESCAT_LAYOUT:
+									textList.append(positiveStateLabels[controlTypes.STATE_CLICKABLE])
+								inClickable=True
 						text = info.getControlFieldBraille(field, ctrlFields, True, formatConfig)
+						if text:
+							textList.append(text)
+						text=" ".join(textList)
 					# Place this field on a stack so we can access it for controlEnd.
 					ctrlFields.append(field)
 					if not text:
@@ -892,6 +906,8 @@ def _addTextWithFields(self, info, formatConfig, isSelection=False):
 					# Map this field text to the start of the field's content.
 					self._addFieldText(text, self._currentContentPos)
 				elif cmd == "controlEnd":
+					# Exiting a controlField should break a run of clickables
+					inClickable=False
 					field = ctrlFields.pop()
 					text = info.getControlFieldBraille(field, ctrlFields, False, formatConfig)
 					if not text:
diff --git a/source/speech.py b/source/speech.py
--- a/source/speech.py
+++ b/source/speech.py
@@ -853,9 +853,20 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
 				isTextBlank=False
 				_speakTextInfo_addMath(speechSequence,info,field)
 
+	# When true, we are inside a clickable field, and should therefore not announce any more new clickable fields
+	inClickable=False
 	#Get speech text for any fields in the new controlFieldStack that are not in the old controlFieldStack
 	for count in xrange(commonFieldCount,len(newControlFieldStack)):
 		field=newControlFieldStack[count]
+		if not inClickable and formatConfig['reportClickable']:
+			states=field.get('states')
+			if states and controlTypes.STATE_CLICKABLE in states:
+				# We entered the most outer clickable, so announce it, if we won't be announcing anything else interesting for this field
+				presCat=field.getPresentationCategory(newControlFieldStack[0:count],formatConfig,reason)
+				if not presCat or presCat is field.PRESCAT_LAYOUT:
+					speechSequence.append(controlTypes.stateLabels[controlTypes.STATE_CLICKABLE])
+					isTextBlank=False
+				inClickable=True
 		text=info.getControlFieldSpeech(field,newControlFieldStack[0:count],"start_addedToControlFieldStack",formatConfig,extraDetail,reason=reason)
 		if text:
 			speechSequence.append(text)
@@ -887,6 +898,8 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
 				speakTextInfoState.updateObj()
 		return
 
+	# Similar to before, but If the most inner clickable is exited, then we allow announcing clickable for the next lot of clickable fields entered.
+	inClickable=False
 	#Move through the field commands, getting speech text for all controlStarts, controlEnds and formatChange commands
 	#But also keep newControlFieldStack up to date as we will need it for the ends
 	# Add any text to a separate list, as it must be handled differently.
@@ -897,6 +910,8 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
 	indentationDone=False
 	for command in textWithFields:
 		if isinstance(command,basestring):
+			# Text should break a run of clickables
+			inClickable=False
 			if reportIndentation and not indentationDone:
 				indentation,command=splitTextIndentation(command)
 				# Combine all indentation into one string for later processing.
@@ -915,9 +930,24 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
 			if  command.command=="controlStart":
 				# Control fields always start a new chunk, even if they have no field text.
 				inTextChunk=False
-				fieldText=info.getControlFieldSpeech(command.field,newControlFieldStack,"start_relative",formatConfig,extraDetail,reason=reason)
+				tempTextList=[]
+				if not inClickable and formatConfig['reportClickable']:
+					states=command.field.get('states')
+					if states and controlTypes.STATE_CLICKABLE in states:
+						# We have entered an outer most clickable or entered a new clickable after exiting a previous one 
+						# Announce it if there is nothing else interesting about the field, but not if the user turned it off. 
+						presCat=command.field.getPresentationCategory(newControlFieldStack[0:],formatConfig,reason)
+						if not presCat or presCat is command.field.PRESCAT_LAYOUT:
+							tempTextList.append(controlTypes.stateLabels[controlTypes.STATE_CLICKABLE])
+						inClickable=True
+				text=info.getControlFieldSpeech(command.field,newControlFieldStack,"start_relative",formatConfig,extraDetail,reason=reason)
+				if text:
+					tempTextList.append(text)
+				fieldText=" ".join(tempTextList)
 				newControlFieldStack.append(command.field)
 			elif command.command=="controlEnd":
+				# Exiting a controlField should break a run of clickables
+				inClickable=False
 				# Control fields always start a new chunk, even if they have no field text.
 				inTextChunk=False
 				fieldText=info.getControlFieldSpeech(newControlFieldStack[-1],newControlFieldStack[0:-1],"end_relative",formatConfig,extraDetail,reason=reason)
@@ -1239,9 +1269,6 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
 	# Special cases
 	elif not speakEntry and fieldType in ("start_addedToControlFieldStack","start_relative"):
 		out = []
-		if not extraDetail and controlTypes.STATE_CLICKABLE in states: 
-			# Clickable.
-			out.append(getSpeechTextForProperties(states=set([controlTypes.STATE_CLICKABLE])))
 		if ariaCurrent:
 			out.append(ariaCurrentText)
 		return CHUNK_SEPARATOR.join(out)


Test patch: