patternfly / patternfly/chatbot

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

Open
#827 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
6
Forks
37
Avg merge
3d 19h
Merged PRs (30d)
5

Description

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

  • Contributor guide

    Open the contributing guide

    First steps

    1. Read the whole issue, then the project's contributing guide.
    2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
    3. Fork the repository and make your change on a branch.
    4. Open a pull request that references the issue number.

    Research direction

    Start with the MenuItem component and MenuContext, then trace how ChatbotConversationHistoryNav passes menuProps and the parent role. Verify the role behavior for menu, menubar, and list parents, and confirm that non-menu parents render implicit list item and button semantics without menu-specific roles.

    Written by the indexing model from the issue text.

    Assessment

    Tech stack
    typescript
    Domain
    accessibility, frontend
    Issue type
    Bug
    Difficulty
    3/5
    Estimated time
    1-2 days
    Activity status
    Active
    Clarity
    Clearly specified
    Newbie friendliness
    68/100

    Get new issues in your inbox

    A short digest of beginner-friendly GitHub issues.