Bug 248717

Summary: AX: Don't include password input value in aria-labelledby description
Product: WebKit Reporter: Tommy McHugh <thomas_mchugh>
Component: AccessibilityAssignee: Nobody <webkit-unassigned>
Status: RESOLVED FIXED    
Severity: Normal CC: aboxhall, andresg_22, apinheiro, cfleizach, dmazzoni, ews-watchlist, jcraig, jdiggs, samuel_white, tyler_w, webkit-bug-importer
Priority: P2 Keywords: InRadar
Version: Safari 16   
Hardware: All   
OS: All   
Attachments:
Description Flags
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch none

Description Tommy McHugh 2022-12-03 12:10:07 PST
If an element has an aria-labelledby attribute to a password input we should not make the description of that element the value of the password.
Comment 1 Radar WebKit Bug Importer 2022-12-03 12:10:17 PST
<rdar://problem/102938762>
Comment 2 Tommy McHugh 2022-12-03 12:15:57 PST
Created attachment 463871 [details]
Patch
Comment 3 Tommy McHugh 2022-12-03 12:26:19 PST
Created attachment 463872 [details]
Patch
Comment 4 Tyler Wilcock 2022-12-03 12:32:29 PST
Comment on attachment 463872 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=463872&action=review

> LayoutTests/accessibility/aria-labelledby-on-password-input.html:8
> +<div id="content">

I think you can either remove this `id`, or use it to hide the test text before `finishJSTest()` like so:

document.getElementById("content").style.visibility = "hidden";

> LayoutTests/accessibility/aria-labelledby-on-password-input.html:29
> +            await waitFor(() => password1.stringValue.length === "AXValue: hello".length);

Does this work instead of checking the length?

await waitFor(() => password1.stringValue === "AXValue: hello");

> LayoutTests/accessibility/aria-labelledby-on-password-input.html:34
> +            document.getElementById("button-2").setAttribute("aria-labelledBy", "text-1");

I think aria-labelledBy is typically all lowercased:

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby

> LayoutTests/accessibility/aria-labelledby-on-password-input.html:42
> +            output += expect("button2.description", "'AXDescription: This is before my password field This is after my password field'");

Could maybe make the test smaller while still providing the same diagnostics with:

"This is before my password field" -> "Before password"
"This is after my password field" -> "After password"
Comment 5 Tommy McHugh 2022-12-03 12:45:11 PST
Created attachment 463873 [details]
Patch
Comment 6 Tommy McHugh 2022-12-03 12:47:18 PST
(In reply to Tyler Wilcock from comment #4)
> Does this work instead of checking the length?
> 
> await waitFor(() => password1.stringValue === "AXValue: hello");
Unfortunately we can't do this here because we don't return the actual password value here only the text representation being rendered (sometimes ****). Length check seems to be what the other password tests use.
Comment 7 Tommy McHugh 2022-12-03 13:02:15 PST
Created attachment 463874 [details]
Patch
Comment 8 Andres Gonzalez 2022-12-06 05:03:59 PST
(In reply to Tommy McHugh from comment #7)
> Created attachment 463874 [details]
> Patch

--- a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp

+    if (auto* input = dynamicDowncast<HTMLInputElement>(element)) {
+        if (input->isPasswordField())
+            return String();

This should return a string with the same length as input->value() consisting of a repeated mask char, e.g., if input->value() is "blah", the return value here should "****".

--- /dev/null
+++ b/LayoutTests/accessibility/aria-labelledby-on-password-input.html

+        setTimeout(async function() {
+            await waitFor(() => password1.stringValue.length === "AXValue: hello".length);
+            output += expect("button1.description", "'AXDescription: '");

+        setTimeout(async function() {
+            await waitFor(() => password1.stringValue.length === "AXValue: hello".length);
+            output += expect("button1.description", "'AXDescription: '");

Should be:

+            output += expect("button1.description", "'AXDescription: *****'");

The same applies to the other instances. In general the value of the password field should not be exposed as an empty string but instead as a mask string, so that the VO user can arrow through it, know how many chars are in the filed, etc., i.e., to have the same experience as the sighted user.
Comment 9 Tommy McHugh 2022-12-06 17:53:53 PST
Created attachment 463911 [details]
Patch
Comment 10 Tommy McHugh 2022-12-06 17:57:48 PST
(In reply to Andres Gonzalez from comment #8)
> The same applies to the other instances. In general the value of the
> password field should not be exposed as an empty string but instead as a
> mask string, so that the VO user can arrow through it, know how many chars
> are in the filed, etc., i.e., to have the same experience as the sighted
> user.
Sounds good! Made this the bullet character mask to match what iOS wrapper uses. AXValue already returns masked characters, it's a VoiceOver specific issue why we don't announce those.
Comment 11 Andres Gonzalez 2022-12-07 06:23:24 PST
(In reply to Tommy McHugh from comment #9)
> Created attachment 463911 [details]
> Patch

--- a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp

Can we do a char replacement in place, something like:

+    if (auto* input = dynamicDowncast<HTMLInputElement>(element)) {
+        String inputValue = input->value();
+        if (input->isPasswordField()) {
+            for (size_t i = 0; i < inputValue.length(); i++)
+                inputValue[i] = '•';
+        }
+        return inputValue;
+    }
Comment 12 Tommy McHugh 2022-12-08 11:12:34 PST
(In reply to Andres Gonzalez from comment #11)
> (In reply to Tommy McHugh from comment #9)
> > Created attachment 463911 [details]
> > Patch
> 
> --- a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
> +++ b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
> 
> Can we do a char replacement in place, something like:
This doesn't work, WTFString can't assign here for characters. Took a look at header for WTFString and didn't find another method either that would do the equivalent.
Comment 13 Andres Gonzalez 2022-12-13 08:00:56 PST
(In reply to Tommy McHugh from comment #12)
> (In reply to Andres Gonzalez from comment #11)
> > (In reply to Tommy McHugh from comment #9)
> > > Created attachment 463911 [details]
> > > Patch
> > 
> > --- a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
> > +++ b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp
> > 
> > Can we do a char replacement in place, something like:
> This doesn't work, WTFString can't assign here for characters. Took a look
> at header for WTFString and didn't find another method either that would do
> the equivalent.

this may be more efficient than multiple reallocations and concatenations:

+    if (auto* input = dynamicDowncast<HTMLInputElement>(element)) {
+        String inputValue = input->value();
+        if (input->isPasswordField()) {
+            StringBuilder passwordValue;
+            passwordValue.reserveCapacity(inputValue.length());
+            for (size_t i = 0; i < inputValue.length(); i++)
+                passwordValue.append(''•');
+            return passwordValue.toString();
+        }
+        return inputValue;
+    }
Comment 14 Tommy McHugh 2023-03-30 12:11:54 PDT
Created attachment 465685 [details]
Patch
Comment 15 Tyler Wilcock 2023-03-31 11:47:38 PDT
rdar://102815043
Comment 16 EWS 2023-03-31 12:16:20 PDT
Committed 262433@main (3b6d017ba868): <https://commits.webkit.org/262433@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 465685 [details].