Skip to content

Commit 0670648

Browse files
committed
fix(ui-menu): screenreaders should read the correct number of menu items
Closes: INSTUI-4285 Tested on VoiceOver, NVDA, JAWS. Also simplified DOM structure quite a bit TEST PLAN: Open the Menu example and go trough it with NVDA, JAWS, VoiceOver. You should be able to navigate and they should read the correct menu item number (e.g. 3 of 7)
1 parent 8842575 commit 0670648

File tree

6 files changed

+74
-132
lines changed

6 files changed

+74
-132
lines changed

packages/ui-menu/src/Menu/MenuItem/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,6 @@ class MenuItem extends Component<MenuItemProps, MenuItemState> {
187187
return 'menuitemcheckbox'
188188
case 'radio':
189189
return 'menuitemradio'
190-
case 'flyout':
191-
return 'button'
192190
default:
193191
return 'menuitem'
194192
}

packages/ui-menu/src/Menu/MenuItemGroup/__new-tests__/MenuItemGroup.test.tsx

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,44 +40,11 @@ describe('<MenuItemGroup />', () => {
4040
<MenuItemSeparator />
4141
</MenuItemGroup>
4242
)
43-
const group = container.querySelector("[id*='MenuItemGroup_']")
44-
const groupMenu = screen.getByRole('menu')
45-
43+
const group = container.querySelector("[class*='menuItemGroup']")
4644
expect(group).toBeInTheDocument()
4745
expect(group).toHaveTextContent('Menu Label')
48-
49-
expect(groupMenu).toBeInTheDocument()
50-
expect(groupMenu).toHaveTextContent('Item Text 1')
51-
expect(groupMenu).toHaveTextContent('Item Text 2')
52-
})
53-
54-
it('should set the role to "menu"', () => {
55-
const { container } = render(
56-
<MenuItemGroup label="Select one">
57-
<MenuItem>Foo</MenuItem>
58-
<MenuItem>Bar</MenuItem>
59-
<MenuItemSeparator />
60-
</MenuItemGroup>
61-
)
62-
const menuItemGroup = container.querySelector(
63-
"[class*='menuItemGroup__items']"
64-
)
65-
66-
expect(menuItemGroup).toHaveAttribute('role', 'menu')
67-
})
68-
69-
it('should set the list item role to "none"', () => {
70-
render(
71-
<MenuItemGroup label="Select one">
72-
<MenuItem>Food</MenuItem>
73-
<MenuItem>Bar</MenuItem>
74-
</MenuItemGroup>
75-
)
76-
const menu = screen.getByRole('menu')
77-
const menuListItem = menu.firstChild as HTMLElement
78-
79-
expect(menuListItem.tagName).toBe('LI')
80-
expect(menuListItem).toHaveAttribute('role', 'none')
46+
expect(group).toHaveTextContent('Item Text 1')
47+
expect(group).toHaveTextContent('Item Text 2')
8148
})
8249

8350
it('should default to children with type "radio"', () => {
@@ -114,10 +81,8 @@ describe('<MenuItemGroup />', () => {
11481
<MenuItemSeparator />
11582
</MenuItemGroup>
11683
)
117-
const menu = screen.getByRole('menu')
11884
const menuItems = screen.getAllByRole('menuitemradio')
11985

120-
expect(menu).toHaveAttribute('aria-disabled', 'true')
12186
expect(menuItems).toHaveLength(2)
12287
expect(menuItems[0]).toHaveAttribute('aria-disabled', 'true')
12388
expect(menuItems[1]).toHaveAttribute('aria-disabled', 'true')

packages/ui-menu/src/Menu/MenuItemGroup/index.tsx

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,8 @@ class MenuItemGroup extends Component<MenuGroupProps, MenuGroupState> {
8282
selected: this.selectedFromChildren(props) || props.defaultSelected!
8383
}
8484
}
85-
86-
this._labelId = props.deterministicId!('MenuItemGroup')
8785
}
8886

89-
private _labelId: string
9087
ref: Element | null = null
9188

9289
handleRef = (el: Element | null) => {
@@ -210,23 +207,18 @@ class MenuItemGroup extends Component<MenuGroupProps, MenuGroupState> {
210207
++index
211208
const value = child.props.value || index
212209

213-
return (
214-
<li role="none">
215-
{' '}
216-
{safeCloneElement(child, {
217-
tabIndex: isTabbable && index === 0 ? 0 : -1,
218-
controls,
219-
value,
220-
children: child.props.children,
221-
type: allowMultiple ? 'checkbox' : 'radio',
222-
ref: this.props.itemRef,
223-
disabled: disabled || child.props.disabled,
224-
selected: this.selected.indexOf(value) > -1,
225-
onSelect: this.handleSelect,
226-
onMouseOver
227-
})}{' '}
228-
</li>
229-
)
210+
return safeCloneElement(child, {
211+
tabIndex: isTabbable && index === 0 ? 0 : -1,
212+
controls,
213+
value,
214+
children: child.props.children,
215+
type: allowMultiple ? 'checkbox' : 'radio',
216+
ref: this.props.itemRef,
217+
disabled: disabled || child.props.disabled,
218+
selected: this.selected.indexOf(value) > -1,
219+
onSelect: this.handleSelect,
220+
onMouseOver
221+
})
230222
} else {
231223
return child
232224
}
@@ -239,18 +231,15 @@ class MenuItemGroup extends Component<MenuGroupProps, MenuGroupState> {
239231
<span
240232
{...props}
241233
css={this.props.styles?.menuItemGroup}
242-
role="presentation"
243234
ref={this.handleRef}
244235
>
245-
<span id={this._labelId}>{this.renderLabel()}</span>
246-
<ul
247-
role="menu"
236+
{this.renderLabel()}
237+
<div
248238
css={this.props.styles?.items}
249239
aria-disabled={this.props.disabled ? 'true' : undefined}
250-
aria-labelledby={this._labelId}
251240
>
252241
{this.renderChildren()}
253-
</ul>
242+
</div>
254243
</span>
255244
)
256245
}

packages/ui-menu/src/Menu/MenuItemSeparator/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ class MenuItemSeparator extends Component<MenuSeparatorProps> {
6666

6767
render() {
6868
const props = omitProps(this.props, MenuItemSeparator.allowedProps)
69+
// role="separator" would fit better here, but it causes NVDA to stop the
70+
// MenuItem count after it
6971
return (
7072
<div
7173
{...props}

packages/ui-menu/src/Menu/index.tsx

Lines changed: 51 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,15 @@ class Menu extends Component<MenuProps> {
9595
_popover: Popover | null = null
9696
_trigger: MenuItem | (React.ReactInstance & { focus?: () => void }) | null =
9797
null
98-
_menu: HTMLUListElement | null = null
98+
_menu: HTMLElement | null = null
9999
_labelId = this.props.deterministicId!('Menu__label')
100100

101101
_activeSubMenu?: Menu | null
102102
_id: string
103103

104104
ref: Element | null = null
105105

106-
handleRef = (el: HTMLUListElement | null) => {
106+
handleRef = (el: HTMLElement | null) => {
107107
const { menuRef } = this.props
108108
this._menu = el
109109
if (typeof menuRef === 'function') {
@@ -169,7 +169,7 @@ class Menu extends Component<MenuProps> {
169169
}
170170
}
171171

172-
handleMenuKeyDown = (event: React.KeyboardEvent<HTMLUListElement>) => {
172+
handleMenuKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
173173
const key = event && event.keyCode
174174
const { down, up, tab, left } = keycode.codes
175175
const pgdn = keycode.codes['page down']
@@ -338,7 +338,7 @@ class Menu extends Component<MenuProps> {
338338
if (
339339
matchComponentTypes<MenuSeparatorChild>(child, ['MenuItemSeparator'])
340340
) {
341-
return <li role="none">{child}</li>
341+
return child
342342
}
343343

344344
const menuItemChild = child
@@ -350,71 +350,59 @@ class Menu extends Component<MenuProps> {
350350
this.props.controls
351351

352352
if (matchComponentTypes<MenuItemChild>(child, ['MenuItem'])) {
353-
return (
354-
<li role="none">
355-
{safeCloneElement(child, {
356-
controls,
357-
children: child.props.children,
358-
disabled: disabled || child.props.disabled,
359-
onFocus: this.handleMenuItemFocus,
360-
onBlur: this.handleMenuItemBlur,
361-
onSelect: this.handleMenuItemSelect,
362-
onMouseOver: this.handleMenuItemMouseOver,
363-
tabIndex: isTabbable ? 0 : -1
364-
})}
365-
</li>
366-
)
353+
return safeCloneElement(child, {
354+
controls,
355+
children: child.props.children,
356+
disabled: disabled || child.props.disabled,
357+
onFocus: this.handleMenuItemFocus,
358+
onBlur: this.handleMenuItemBlur,
359+
onSelect: this.handleMenuItemSelect,
360+
onMouseOver: this.handleMenuItemMouseOver,
361+
tabIndex: isTabbable ? 0 : -1
362+
})
367363
}
368364

369365
if (matchComponentTypes<MenuGroupChild>(child, ['MenuItemGroup'])) {
370-
return (
371-
<li role="none">
372-
{safeCloneElement(child, {
373-
label: child.props.label,
374-
controls,
375-
disabled: disabled || child.props.disabled,
376-
onFocus: this.handleMenuItemFocus,
377-
onBlur: this.handleMenuItemBlur,
378-
onSelect: this.handleMenuItemSelect,
379-
onMouseOver: this.handleMenuItemMouseOver,
380-
isTabbable
381-
})}
382-
</li>
383-
)
366+
return safeCloneElement(child, {
367+
label: child.props.label,
368+
controls,
369+
disabled: disabled || child.props.disabled,
370+
onFocus: this.handleMenuItemFocus,
371+
onBlur: this.handleMenuItemBlur,
372+
onSelect: this.handleMenuItemSelect,
373+
onMouseOver: this.handleMenuItemMouseOver,
374+
isTabbable
375+
})
384376
}
385377

386378
if (matchComponentTypes(child, ['Menu'])) {
387379
const submenuDisabled = disabled || child.props.disabled
388380

389-
return (
390-
<li role="none">
391-
{safeCloneElement(child, {
392-
type: 'flyout',
393-
controls,
394-
disabled: submenuDisabled,
395-
onSelect: this.handleMenuItemSelect,
396-
placement: 'end top',
397-
offsetX: -5,
398-
offsetY: 5,
399-
withArrow: false,
400-
onToggle: this.handleSubMenuToggle,
401-
onDismiss: this.handleSubMenuDismiss,
402-
trigger: (
403-
<MenuItem
404-
onMouseOver={this.handleMenuItemMouseOver}
405-
onFocus={this.handleMenuItemFocus}
406-
onBlur={this.handleMenuItemBlur}
407-
tabIndex={isTabbable ? 0 : -1}
408-
type="flyout"
409-
disabled={submenuDisabled}
410-
renderLabelInfo={child.props.renderLabelInfo}
411-
>
412-
{child.props.title || child.props.label}
413-
</MenuItem>
414-
)
415-
})}
416-
</li>
417-
)
381+
return safeCloneElement(child, {
382+
type: 'flyout',
383+
controls,
384+
disabled: submenuDisabled,
385+
onSelect: this.handleMenuItemSelect,
386+
placement: 'end top',
387+
offsetX: -5,
388+
offsetY: 5,
389+
withArrow: false,
390+
onToggle: this.handleSubMenuToggle,
391+
onDismiss: this.handleSubMenuDismiss,
392+
trigger: (
393+
<MenuItem
394+
onMouseOver={this.handleMenuItemMouseOver}
395+
onFocus={this.handleMenuItemFocus}
396+
onBlur={this.handleMenuItemBlur}
397+
tabIndex={isTabbable ? 0 : -1}
398+
type="flyout"
399+
disabled={submenuDisabled}
400+
renderLabelInfo={child.props.renderLabelInfo}
401+
>
402+
{child.props.title || child.props.label}
403+
</MenuItem>
404+
)
405+
})
418406
}
419407
return
420408
}
@@ -434,7 +422,7 @@ class Menu extends Component<MenuProps> {
434422
registerMenuItem: this.registerMenuItem
435423
}}
436424
>
437-
<ul
425+
<div
438426
role="menu"
439427
aria-label={label}
440428
tabIndex={0}
@@ -447,7 +435,7 @@ class Menu extends Component<MenuProps> {
447435
ref={this.handleRef}
448436
>
449437
{this.renderChildren()}
450-
</ul>
438+
</div>
451439
</MenuContext.Provider>
452440
)
453441
}

packages/ui-menu/src/Menu/props.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,15 @@ type MenuOwnProps = {
111111
/**
112112
* Callback fired on the onKeyDown of the `<Menu />`
113113
*/
114-
onKeyDown?: (event: React.KeyboardEvent<HTMLUListElement>) => void
114+
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void
115115
/**
116116
* Callback fired on the onKeyUp of the `<Menu />`
117117
*/
118-
onKeyUp?: (event: React.KeyboardEvent<HTMLUListElement>) => void
118+
onKeyUp?: (event: React.KeyboardEvent<HTMLElement>) => void
119119
/**
120120
* A function that returns a reference to the `<Menu />`
121121
*/
122-
menuRef?: (el: HTMLUListElement | null) => void
122+
menuRef?: (el: HTMLElement | null) => void
123123
/**
124124
* A function that returns a reference to the `<Popover />`
125125
*/

0 commit comments

Comments
 (0)