patternfly / patternfly/chatbot
[Bug] - [a11y] MenuItem hardcodes role="menuitem" and role="none" with no way to override
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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