8286258: [Accessibility,macOS,VoiceOver] VoiceOver reads the spinner value wrong and sometime partially

Reviewed-by: psadhukhan, asemenov
This commit is contained in:
Alexander Zuev 2026-01-22 16:36:24 +00:00
parent 07f6617e0b
commit 8c82b58db9
4 changed files with 139 additions and 4 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, JetBrains s.r.o.. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
@ -29,5 +29,6 @@
@interface NavigableTextAccessibility : CommonComponentAccessibility <NSAccessibilityNavigableStaticText>
@property(readonly) BOOL accessibleIsPasswordText;
@property BOOL announceEditUpdates;
@end

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, JetBrains s.r.o.. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
@ -60,6 +60,22 @@ static jmethodID sjm_getAccessibleEditableText = NULL;
return [fJavaRole isEqualToString:@"passwordtext"];
}
- (id)init {
self = [super init];
if (self) {
_announceEditUpdates = YES;
}
return self;
}
- (void)suppressEditUpdates {
_announceEditUpdates = NO;
}
- (void)resumeEditUpdates {
_announceEditUpdates = YES;
}
// NSAccessibilityElement protocol methods
- (NSRect)accessibilityFrameForRange:(NSRange)range
@ -117,6 +133,9 @@ static jmethodID sjm_getAccessibleEditableText = NULL;
- (NSString *)accessibilityStringForRange:(NSRange)range
{
if (!_announceEditUpdates) {
return @"";
}
JNIEnv *env = [ThreadUtilities getJNIEnv];
GET_CACCESSIBLETEXT_CLASS_RETURN(nil);
DECLARE_STATIC_METHOD_RETURN(jm_getStringForRange, sjc_CAccessibleText, "getStringForRange",
@ -306,6 +325,12 @@ static jmethodID sjm_getAccessibleEditableText = NULL;
return [super accessibilityParent];
}
- (void)postSelectedTextChanged
{
[super postSelectedTextChanged];
[self resumeEditUpdates];
}
/*
* Other text methods
- (NSRange)accessibilitySharedCharacterRange;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -24,6 +24,7 @@
*/
#import "SpinboxAccessibility.h"
#import "ThreadUtilities.h"
#define INCREMENT 0
#define DECREMENT 1
@ -44,7 +45,15 @@
- (id _Nullable)accessibilityValue
{
return [super accessibilityValue];
id val = [super accessibilityValue];
NSArray *clist = [super accessibilityChildren];
for (NSUInteger i = 0; i < [clist count]; i++) {
id child = [clist objectAtIndex:i];
if ([child conformsToProtocol:@protocol(NSAccessibilityNavigableStaticText)]) {
val = [child accessibilityValue];
}
}
return val;
}
- (BOOL)accessibilityPerformIncrement
@ -68,4 +77,18 @@
return [super accessibilityParent];
}
- (void)postValueChanged
{
AWT_ASSERT_APPKIT_THREAD;
NSAccessibilityPostNotification(self, NSAccessibilityValueChangedNotification);
NSArray *clist = [super accessibilityChildren];
for (NSUInteger i = 0; i < [clist count]; i++) {
id child = [clist objectAtIndex:i];
if ([child conformsToProtocol:@protocol(NSAccessibilityNavigableStaticText)]) {
NSAccessibilityPostNotification(child, NSAccessibilityLayoutChangedNotification);
[child suppressEditUpdates];
}
}
}
@end

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.awt.GridLayout;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerListModel;
/*
* @test
* @bug 8286258
* @library /java/awt/regtesthelpers
* @build PassFailJFrame
* @requires (os.family == "mac")
* @summary Checks that JSpinner with custom model announces
* the value every time it is changed
* @run main/manual CustomSpinnerAccessibilityTest
*/
public class CustomSpinnerAccessibilityTest extends JPanel {
private static final String INSTRUCTIONS = """
1. Turn on VoiceOver
2. In the window named "Test UI" click on the text editor inside the
spinner component
3. Using up and down arrows change current month
4. Wait for the VoiceOver to finish speaking
5. Repeat steps 3 and 4 couple more times
If every time value of the spinner is changed VoiceOver
announces the new value click "Pass".
If instead the value is narrated only partially
and the new value is never fully narrated press "Fail".
""";
public CustomSpinnerAccessibilityTest() {
super(new GridLayout(0, 2));
String[] monthStrings = new java.text.DateFormatSymbols().getMonths();
int lastIndex = monthStrings.length - 1;
if (monthStrings[lastIndex] == null
|| monthStrings[lastIndex].length() <= 0) {
String[] tmp = new String[lastIndex];
System.arraycopy(monthStrings, 0,
tmp, 0, lastIndex);
monthStrings = tmp;
}
SpinnerListModel model = new SpinnerListModel(monthStrings);
JLabel label = new JLabel("Month: ");
add(label);
JSpinner spinner = new JSpinner(model);
label.setLabelFor(spinner);
add(spinner);
}
public static void main(String[] args) throws InterruptedException,
InvocationTargetException {
PassFailJFrame.builder()
.title("Custom Spinner Accessibility Test")
.instructions(INSTRUCTIONS)
.testUI(CustomSpinnerAccessibilityTest::new)
.build()
.awaitAndCheck();
}
}