Skip to content

[Bug] - [a11y] MenuItem hardcodes role="menuitem" and role="none" with no way to override #827

@Homa

Description

@Homa

Description

MenuItem hardcodes role="menuitem" on its inner button/link element and role="none" on its <li> wrapper. There is no prop to override these roles.

Consumers of ChatbotConversationHistoryNav can override the <ul> role via menuProps (e.g., menuProps={{ role: 'list' }}), but the child MenuItem roles remain hardcoded regardless of the parent menu role.

Per the WAI-ARIA spec, role="menuitem" is only valid inside role="menu" or role="menubar". When a consumer overrides the parent to role="list", the remaining role="menuitem" on child elements becomes an ARIA violation.

User impact

This was discovered during an accessibility audit. With the default role="menu" on the <ul>, screen readers announce "Press escape to close the menu" when users navigate into the conversation history list. However, pressing Escape does nothing — the list is not a dismissible menu widget, it's a list of conversations inside a drawer panel. This is confusing for screen reader users who expect the announced behavior to work.

On Ask App we can fix the <ul> role via menuProps={{ role: 'list' }}, which eliminates the "Press escape" announcement. But the inner role="menuitem" on buttons remains, causing screen readers to still announce each conversation as a "menu item" rather than a simple button — which is misleading when the container is no longer a menu.

Current behavior

code:

<ChatbotConversationHistoryNav
  menuProps={{ role: 'list', 'aria-label': 'Chat history' }}
  // ...
/>

Rendered accessibility tree:
    ✅ overridden by consumer via menuProps
  • ❌ hardcoded by MenuItem ❌ hardcoded by MenuItem — ARIA violation (menuitem outside of menu) Conversation title
Expected behavior
When the parent menu role is not `"menu"` or `"menubar"`, `MenuItem` should not force menu-specific roles on its elements:

  • implicit role: listitem implicit role: button Conversation title
Proposed solution
In `MenuItem`, the role is currently set unconditionally:

// On the

  • :
    role: !hasCheckbox ? 'none' : 'menuitem'
    // On the inner element:
    role: isSelectMenu ? 'option' : 'menuitem'

    This could check the `role` from `MenuContext` and only apply menu-specific roles when appropriate:
    
    

    const isMenuRole = menuRole === 'menu' || menuRole === 'menubar';
    // On the

  • :
    role: isMenuRole ? (!hasCheckbox ? 'none' : 'menuitem') : undefined
    // On the inner element:
    role: isMenuRole ? (isSelectMenu ? 'option' : 'menuitem') : undefined

    This fix is backward-compatible — it only changes behavior when the parent role is something other than "menu" or `"menubar"`, so existing consumers are unaffected.
    
    ### Steps to Reproduce
    
    1. Use `ChatbotConversationHistoryNav` with `menuProps={{ role: 'list' }}`
    2. Open the conversation history drawer
    3. Inspect the accessibility tree
    4. `<li>` elements still have `role="none"` and `<button>` elements still have `role="menuitem"` despite the parent `<ul>` no longer being `role="menu"`
    5. With a screen reader, each conversation is announced as a "menu item" instead of a "button"
    
    ### Data/JSON Context (if applicable)
    
    _No response_
    
    ### Environment
    
    _No response_
    
    ### Screenshots or Logs
    
    _No response_
    

    Jira Issue: PF-3992

  • Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      No labels
      No labels

      Type

      Projects

      Status

      Needs triage

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions