Skip to content

Conversation

caitlinmcdougall
Copy link

Fixes #0000

Checklist

  • Includes tests
  • Update documentation

Changes proposed in this pull request:

ScreenReader accessibility improvements for the ItemRenderer, MenuItem, QueryList and Select components

Added proper ARIA relationships and controls:

  • Added id prop to ItemRenderer interface to enable aria-activedescendant pattern
  • Enhanced MenuItem component to accept and use the id prop
  • Added aria-controls pointing to the listbox in select component
  • Proper aria-activedescendant that references the currently focused option

Improved focus management in QueryList:

  • Added listboxHasVisualFocus state to track when arrow key navigation is active
  • Only set aria-activedescendant when users navigate with arrow keys, not during typing
  • Added handleInputFocus and handleInputBlur methods to manage this behavior
  • Clear visual focus state when input receives focus or user starts typing

Updated films example:

  • Modified getFilmItemProps return type to use Omit<MenuItemProps, "key"> to prevent React key conflicts
  • Added conditional id prop spreading in the props object

Reviewers should focus on:

Ensuring this allows for backwards compatibility with other users of the component.

Screenshot

@changelog-app
Copy link

changelog-app bot commented Aug 13, 2025

Generate changelog in packages/select/changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

MenuItem and QueryList ScreenReader Accessibility Improvements

Check the box to generate changelog(s)

  • Generate changelog entry

@svc-palantir-github
Copy link

Remove redundant optionality

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@caitlinmcdougall caitlinmcdougall marked this pull request as draft August 13, 2025 10:11
@svc-palantir-github
Copy link

Merge branch 'develop' into cmcdougall/accessibility_changes_menu_item

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Copy link
Contributor

@ggdouglas ggdouglas left a comment

Choose a reason for hiding this comment

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

I'm a bit confused as to what the specific accessibility improvements here are. Can you provide some examples of what you're trying to achieve?

Generally supportive of allowing id to be passed on to MenuItem and for setting aria-activedescendant (though I have questions regarding how this is currently implemented).

{ handleClick, handleFocus, modifiers, query, ref }: ItemRendererProps,
): MenuItemProps & React.Attributes {
itemProps: ItemRendererProps,
): Omit<MenuItemProps, "key"> & React.Attributes {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're omitting key from MenuItemProps for the return type, we should also remove React.Attributes since the intersection of that type just ends up adding it back:

interface Attributes {
    key?: Key | null | undefined;
}
Suggested change
): Omit<MenuItemProps, "key"> & React.Attributes {
): Omit<MenuItemProps, "key"> {

I'm supportive of this fix for the docs examples, but could we split that part out into its own PR?

* limitations under the License.
*/

import * as React from "react";
Copy link
Contributor

Choose a reason for hiding this comment

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

This React import can be removed:

Suggested change
import * as React from "react";

super(props);

// Generate unique ID for accessibility
this.listId = props.listId ?? `bp5-query-list-${Utils.uniqueId("ql")}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest removing the hardcoded version prefix and replacing with the following:

Suggested change
this.listId = props.listId ?? `bp5-query-list-${Utils.uniqueId("ql")}`;
this.listId = props.listId ?? Utils.uniqueId("bp-query-list");

// When input gains focus, we don't immediately set visual focus state
// Visual focus state is only set when the user navigates with arrow keys
// This ensures screen readers only announce activedescendant when appropriate
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we defining this empty focus event handler?

menuProps: {
...menuProps,
id: this.listId,
role: "listbox",
Copy link
Contributor

Choose a reason for hiding this comment

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

The default itemListRenderer implementation already includes this role. Do we need to include it again here?

<Menu role="listbox" {...listProps.menuProps} ulRef={listProps.itemsParentRef}>

menuProps,
menuProps: {
...menuProps,
id: this.listId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't setting an id here replace the id referenced by the outermost combobox that already defines aria-controls? I'm a bit confused as to what we're trying to accomplish. Here's a before/after of these changes on a simple example to illustrate:

Before After
before after

https://codesandbox.io/p/sandbox/v4x4r8


const input = (
<InputGroup
aria-activedescendant={listProps.activeItemId}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we setting aria-activedescendant on the filter input group? Shouldn't we be setting this on the outermost combobox element?

Here's the WAI Select-Only Combobox Example for reference:

Screenshot 2025-08-13 at 12 14 27@2x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants