Skip to content

8361283: [Accessibility,macOS,VoiceOver] VoiceOver announced Tab items of JTabbedPane as RadioButton on macOS #26096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2025, 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
Expand Down Expand Up @@ -860,7 +860,13 @@ - (NSAccessibilityRole)accessibilityRole
- (NSString *)accessibilityRoleDescription
{
// first ask AppKit for its accessible role description for a given AXRole
NSString *value = NSAccessibilityRoleDescription([self accessibilityRole], nil);
NSString *value = nil;

if ([[self javaRole] isEqualToString:@"pagetab"]) {
Copy link
Contributor

@savoptik savoptik Jul 4, 2025

Choose a reason for hiding this comment

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

It seems it would be better to simply override:

- (NSString *)accessibilityRoleDescription

in src/java.desktop/macosx/native/libawt_lwawt/awt/a11y/TabButtonAccessibility.m

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought of overriding this method but the changes are very minimal.
Even if this method is overridden in TabButtonAccessibility.m and the value returned is nil then we need to either fallback to parent's class method or need to copy the entire implementation in TabButtonAccessibility.

So, I think this should be ok to cater the changes here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do it where it makes sense as part of the tab button implementation, rather than adding workarounds to the general implementation.

Check the [super accessibilityRoleDescription] and if it returns nil, return your implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check the [super accessibilityRoleDescription] and if it returns nil, return your implementation.

As I mentioned earlier in the description, accessibility role description is fetched in accessibilityRoleDescription API in CommonComponentAccessiblity.m file, the sub-role passed as a parameter is nil, returned value is RadioButton, [super accessibilityRoleDescription] will not return nil.

Anyways, I don't see this method getting invoked in TabButtonAccessibility by a11y client.

Copy link
Contributor

Choose a reason for hiding this comment

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

Try to debug and investigate why [TabbuttonAccessibility accessibilityRoleDescription] is not being called. If you have already attempted to override it with the changes you need.

The logic of creating tabs in TabGroupAccessibility indicates that such an override should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The accessible class created for a java role pagetab i.e. a tab component is CommonComponentAccessibility.
The parent for pagetab is TabGroupAccessibility and the object returned from rowRolesMapForParent is null for TabGroupAccessibility in getComponentAccessibilityClass method. Since there is no entry in the rolesMap for the pagetab role, the returned class is CommonComponentAccessibility.
So, the accessibilityRoleDescription API is invoked from CommonComponentAccessibility and not from TabButtonAccessibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for the reference! However, I would like to advise you to thoroughly examine the code of TabGroupAccessibility. This is because it has its own implementations for creating child components.

Therefore, I am once again asking you to try to investigate the situation in more detail. There is a possibility that the mechanism is broken, and TabGroupAccessibility is not being created or used at all, which is incorrect.

Additionally, all objects in the hierarchy of native objects inherit from CommonComponentAccessibility in one way or another. If the accessibilityRoleDescription selector is not defined in TabGroupAccessibility, it may not be called.

I don’t have a Mac at hand right now; otherwise, I would debug this case myself and provide more detailed assistance.

value = NSAccessibilityRoleDescription([self accessibilityRole], NSAccessibilityTabButtonSubrole);
} else {
value = NSAccessibilityRoleDescription([self accessibilityRole], nil);
}

if (value == nil) {
// query java if necessary
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025, 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.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;

/*
* @test
* @bug 8361283
* @library /java/awt/regtesthelpers
* @build PassFailJFrame
* @requires (os.family == "mac")
* @summary VO shouldn't announce the tab items as RadioButton
* @run main/manual AccessibleTabbedPaneRoleTest
*/

public class AccessibleTabbedPaneRoleTest {

public static void main(String[] args) throws Exception {
String INSTRUCTIONS = """
This test is applicable only on macOS.

Test UI contains a JFrame containing JTabbedPane with multiple tabs.

Follow these steps to test the behaviour:

1. Start the VoiceOver (Press Command + F5) application.
2. Test Frame should have focus. If not, then bring focus to test frame.
3. Press Left / Right arrow key to move to next and prevoius tab.
4. VO should announce "Tab" in stead of "RadioButton" for tab items.
(For e.g. When Tab 1 is selected, VO should announce "Tab 1, selected,
tab, group).
5. Press Pass if you are able to hear correct announcements
else Fail.""";

PassFailJFrame.builder()
.instructions(INSTRUCTIONS)
.columns(45)
.testUI(AccessibleTabbedPaneRoleTest::createUI)
.build()
.awaitAndCheck();
}

private static JFrame createUI() {
int NUM_TABS = 6;
JFrame frame = new JFrame("Test Frame");
JTabbedPane tabPane = new JTabbedPane();
tabPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
tabPane.setTabPlacement(JTabbedPane.TOP);
for (int i = 0; i < NUM_TABS; ++i) {
tabPane.addTab("Tab " + i , new JLabel("Content Area"));
}
JPanel panel = new JPanel(new BorderLayout());
panel.add(tabPane, BorderLayout.CENTER);
frame.add(panel);
frame.setSize(400, 100);
return frame;
}
}