Repository: nvaccess/nvda

Issue:
aria-sort changes not announced when the change is not on the focused element

### Steps to reproduce:
1. Visit https://w3c.github.io/aria-practices/examples/grid/dataGrids.html
2. Navigate to the grid in the second example
3. Navigate to the first column header (the sort state is announced)
4. Change the sort state

### Actual behavior:
Changes to the aria-sort property are not announced

### Expected behavior:
Changes to aria-sort are announced

### System configuration
#### NVDA installed

#### NVDA version: 2019.3.1

#### Windows version: Windows 10, version 1909

#### Name and version of other software in use when reproducing the issue:
- Firefox 74.0
- Chrome 80.0.3987.149
- Edge 82.0.458.0



Discussion:
at least for Firefox cc: @jcsteh is this correctly exposed by Firefox?

Maybe @aleventhal could also comment on Cromium and how is it exposed there.
Firefox exposes this correctly and fires the correct events. NVDA even handles these events.

The issue here is that aria-sort is set on the cell, but the element the user actually activates is a button inside the cell. NVDA only reports changes that occur on the focused element, and since the button is the focused element and aria-sort isn't set on the button, no change is reported. Contrast this with aria-sort being set on the focused element activated by the user:

`data:text/html,<button aria-sort="ascending" onclick="this.setAttribute('aria-sort', 'descending');">Test</button>`

I can understand why it's been done this way. The question is whether it's reasonable to expect NVDA to handle this case, and if so, how NVDA should reasonably handle this. Reporting changes to any element would be bad. Reporting changes to any element in the focus ancestry might do it. Better still, NVDA might want to report changes to any element in the ancestry of the object at the cursor.
Question then: does putting the aria-sort attribute on the button address the use case of helping the AT understand that the column that button is present in is sorted in a particular way? Or does the aria-sort attribute NEED to be on the TH in order for it to have any useful meaning?

Put another way, can we simply start putting aria-sort on our inner buttons (inside the TH) as in your example above, or will that not work in actual practice?
As an experiment, I removed the inner <button> element altogether, and instead, put the "onclick" handler on the TH element itself, and then tested using the keyboard. While I was able to change the aria-sort attribute programmatically on the very element that had focus, the change was nonetheless not announced, so I wonder whether the above statement is, in fact, true.

Looks like the current workaround of using a separate aria-live element to announce changes to sorting is still the way to go here for now?

Tested in Google Chrome (latest), Windows 10.
> As an experiment, I removed the inner element altogether, and instead, put the "onclick" handler on the TH element itself

That's what I would have suggested. However, you should also make the th focusable (tabindex) if you haven't already. I don't really see the need for the inner button.

> While I was able to change the aria-sort attribute programmatically on the very element that had focus, the change was nonetheless not announced, so I wonder whether the above statement is, in fact, true.

The above statement was specific to Firefox. I tested in Chrome and it is indeed broken there. The problem is that Chrome isn't firing the correct events.
> Question then: does putting the aria-sort attribute on the button address the use case of helping the AT understand that the column that button is present in is sorted in a particular way? Or does the aria-sort attribute NEED to be on the TH in order for it to have any useful meaning?

Putting aria-sort on a button in actual practice is a violation of the ARIA spec. My apologies for using that simplified example. It needs to be on the th.
The Chrome specific issue is covered in #10774.
@jcsteh a more realistic example of why a grid might control sorting with a button within the cell rather than with the cell itself is when a header cell might have multiple actions that can be taken, e.g. sorting, filtering, selection, etc. Sometimes there are just multiple buttons within the cell, and sometimes there's a single actions menu, where the sort functionality is controlled by a menuitem.

Both of those implementations are allowed by the ARIA spec, but as you mentioned the actual `aria-sort` value would need to be on the gridcell, even if focus is within it. Do you think supporting that pattern is something you'd consider for NVDA?
I understand the use case. I'd agree it probably makes sense to support
this, but that's not my decision; I'm not a core developer.


Resolving patch:
diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py
--- a/source/NVDAObjects/__init__.py
+++ b/source/NVDAObjects/__init__.py
@@ -1205,7 +1205,18 @@ def event_selection(self):
 		self.event_stateChange()
 
 	def event_stateChange(self):
-		if self is api.getFocusObject():
+		# Automatically announce state changes for certain objects.
+		inFocus = (
+			# this is the current focus:
+			# E.g. announcing the checked state of a checkbox
+			self is api.getFocusObject()
+			# this is a focus ancestor:
+			# Including the ancestors supports scenarios such as
+			# when pressing a focused button changes the state of an ancestor container,
+			# E.g. a button inside a column header that changes the sorting state of the column (#10890)
+			or any(self is obj for obj in api.getFocusAncestors())
+		)
+		if inFocus:
 			speech.speakObjectProperties(self, states=True, reason=controlTypes.OutputReason.CHANGE)
 		braille.handler.handleUpdate(self)
 		vision.handler.handleUpdate(self, property="states")


Test patch:
diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py
--- a/tests/system/robot/chromeTests.py
+++ b/tests/system/robot/chromeTests.py
@@ -2106,6 +2106,55 @@ def test_focus_mode_on_focusable_read_only_lists():
 	)
 
 
+def test_i10890():
+	"""
+	Ensure that sort state is announced on a column header when changed with inner button
+	"""
+	spy = _NvdaLib.getSpyLib()
+	# Chrome sometimes exposes tables as clickable, sometimes not.
+	# This test does not need to know, so disable reporting of clickables.
+	spy.set_configValue(["documentFormatting", "reportClickable"], False)
+	testFile = os.path.join(ARIAExamplesDir, "grid", "datagrids.html")
+	_chrome.prepareChrome(
+		f"""
+			<iframe src="{testFile}"></iframe>
+		"""
+	)
+	# Jump to the Example 2 heading
+	_chrome.getSpeechAfterKey("3")
+	actualSpeech = _chrome.getSpeechAfterKey("3")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Example 2: Sortable Data Grid With Editable Cells",
+			"heading",
+			"level 3",
+		])
+	)
+	# Jump to the table
+	actualSpeech = _chrome.getSpeechAfterKey("t")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Transactions January 1 through January 7",
+			"table",
+			"with 8 rows and 6 columns",
+			"row 1",
+			"column 1",
+			"sorted ascending",
+			"Date",
+			"button",
+		])
+	)
+	# Press the button
+	actualSpeech = _chrome.getSpeechAfterKey("space")
+	# and ensure that the new sort state is spoken.
+	_asserts.strings_match(
+		actualSpeech,
+		"sorted descending",
+	)
+
+
 def test_ARIASwitchRole():
 	"""
 	Ensure that ARIA switch controls have an appropriate role and states in browse mode.
diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot
--- a/tests/system/robot/chromeTests.robot
+++ b/tests/system/robot/chromeTests.robot
@@ -138,6 +138,9 @@ focus mode is turned on on focused read-only list item
 ARIA details role
 	[Documentation]	Test aria details roles being announced on discovery
 	test_mark_aria_details_role
+i10890
+	[Documentation]	Test sort state is announced on column header when changed with inner button
+	test_i10890
 ARIA switch role
 	[Documentation]	Test aria switch control has appropriate role and states in browse mode and when focused
 	test_ARIASwitchRole