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

      No fields configured for Bug.

      Projects

      Status

      Needs triage

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions