Minor accessibility fixes to new slim sidebar menu items (#8015)

* Consistently set `aria-haspopup="menu"` for all sidebar menu items that have sub-menus (LB (Ben Johnston))
 * Make sure `aria-expanded` is always explicitly set as a string in sidebar (LB (Ben Johnston))
 * Use a button element instead of a link for page explorer menu item, for the correct semantics and behavior (LB (Ben Johnston))
pull/8132/head
LB (Ben Johnston) 2022-03-16 00:53:40 +10:00 zatwierdzone przez GitHub
rodzic 14280ad7ea
commit 042d077fc1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
15 zmienionych plików z 465 dodań i 27 usunięć

Wyświetl plik

@ -28,6 +28,9 @@ Changelog
* Fix: Improve the contrast of the “Remember me” checkbox against the login pages background (Steven Steinwand)
* Fix: Group permission rows with custom permissions no longer have extra padding (Steven Steinwand)
* Fix: Make sure the focus outline of checkboxes is fully around the outer border (Steven Steinwand)
* Fix: Consistently set `aria-haspopup="menu"` for all sidebar menu items that have sub-menus (LB (Ben Johnston))
* Fix: Make sure `aria-expanded` is always explicitly set as a string in sidebar (LB (Ben Johnston))
* Fix: Use a button element instead of a link for page explorer menu item, for the correct semantics and behavior (LB (Ben Johnston))
2.16.2 (xx.xx.xxxx) - IN DEVELOPMENT

Wyświetl plik

@ -0,0 +1,68 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Sidebar } from './Sidebar';
describe('Sidebar', () => {
const strings = {};
it('should render with the minimum required props', () => {
const wrapper = shallow(<Sidebar modules={[]} strings={strings} />);
expect(wrapper).toMatchSnapshot();
});
it('should toggle the slim mode in the sidebar when outer button clicked', () => {
const onExpandCollapse = jest.fn();
const wrapper = shallow(
<Sidebar
modules={[]}
onExpandCollapse={onExpandCollapse}
strings={strings}
/>,
);
// default expanded (non-slim)
expect(
wrapper.find('.sidebar__collapse-toggle').prop('aria-expanded'),
).toEqual('true');
expect(wrapper.find('.sidebar--slim')).toHaveLength(0);
expect(onExpandCollapse).not.toHaveBeenCalled();
// toggle slim mode
wrapper.find('.sidebar__collapse-toggle').simulate('click');
expect(
wrapper.find('.sidebar__collapse-toggle').prop('aria-expanded'),
).toEqual('false');
expect(wrapper.find('.sidebar--slim')).toHaveLength(1);
expect(onExpandCollapse).toHaveBeenCalledWith(true);
});
it('should toggle the sidebar visibility on click (used on mobile)', () => {
const onExpandCollapse = jest.fn();
const wrapper = shallow(
<Sidebar
modules={[]}
onExpandCollapse={onExpandCollapse}
strings={strings}
/>,
);
// default not expanded
expect(wrapper.find('.sidebar-nav-toggle').prop('aria-expanded')).toEqual(
'false',
);
expect(wrapper.find('.sidebar-nav-toggle--open')).toHaveLength(0);
// toggle expanded mode
wrapper.find('.sidebar-nav-toggle').simulate('click');
// check it is expanded
expect(wrapper.find('.sidebar-nav-toggle').prop('aria-expanded')).toEqual(
'true',
);
expect(wrapper.find('.sidebar-nav-toggle--open')).toHaveLength(1);
});
});

Wyświetl plik

@ -38,7 +38,7 @@ export interface SidebarProps {
export const Sidebar: React.FunctionComponent<SidebarProps> = ({
modules,
currentPath,
collapsedOnLoad,
collapsedOnLoad = false,
strings,
navigate,
onExpandCollapse,
@ -104,8 +104,7 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
};
}, [slim]);
const onClickCollapseToggle = (e: React.MouseEvent) => {
e.preventDefault();
const onClickCollapseToggle = () => {
setCollapsed(!collapsed);
if (onExpandCollapse) {
@ -113,8 +112,7 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
}
};
const onClickOpenCloseToggle = (e: React.MouseEvent) => {
e.preventDefault();
const onClickOpenCloseToggle = () => {
setVisibleOnMobile(!visibleOnMobile);
setExpandingOrCollapsing(true);
@ -185,10 +183,11 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
>
<div className="sidebar__inner">
<button
className="button sidebar__collapse-toggle"
onClick={onClickCollapseToggle}
aria-label={strings.TOGGLE_SIDEBAR}
aria-expanded={!slim}
className="button sidebar__collapse-toggle"
aria-expanded={slim ? 'false' : 'true'}
type="button"
>
{collapsed ? (
<Icon name="angle-double-right" />
@ -211,12 +210,13 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
<button
onClick={onClickOpenCloseToggle}
aria-label={strings.TOGGLE_SIDEBAR}
aria-expanded={visibleOnMobile}
aria-expanded={visibleOnMobile ? 'true' : 'false'}
className={
'button sidebar-nav-toggle' +
(isMobile ? ' sidebar-nav-toggle--mobile' : '') +
(visibleOnMobile ? ' sidebar-nav-toggle--open' : '')
}
type="button"
>
{visibleOnMobile ? <Icon name="cross" /> : <Icon name="bars" />}
</button>

Wyświetl plik

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sidebar should render with the minimum required props 1`] = `
<Fragment>
<div
className="sidebar"
>
<div
className="sidebar__inner"
>
<button
aria-expanded="true"
className="button sidebar__collapse-toggle"
onClick={[Function]}
type="button"
>
<Icon
name="angle-double-left"
/>
</button>
<div
className="sidebar__peek-hover-area"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
/>
</div>
</div>
<button
aria-expanded="false"
className="button sidebar-nav-toggle"
onClick={[Function]}
type="button"
>
<Icon
name="bars"
/>
</button>
</Fragment>
`;

Wyświetl plik

@ -1,6 +1,5 @@
import * as React from 'react';
import Button from '../../Button/Button';
import Icon from '../../Icon/Icon';
import { MenuItemProps } from './MenuItem';
import { LinkMenuItemDefinition } from './LinkMenuItem';
@ -47,9 +46,7 @@ export const PageExplorerMenuItem: React.FunctionComponent<
}
}, [isOpen]);
const onClick = (e: React.MouseEvent) => {
e.preventDefault();
const onClick = () => {
// Open/close explorer
if (isOpen) {
dispatch({
@ -65,7 +62,7 @@ export const PageExplorerMenuItem: React.FunctionComponent<
};
const className =
'sidebar-menu-item' +
'sidebar-menu-item sidebar-page-explorer-item' +
(isActive ? ' sidebar-menu-item--active' : '') +
(isInSubMenu ? ' sidebar-menu-item--in-sub-menu' : '');
@ -75,15 +72,17 @@ export const PageExplorerMenuItem: React.FunctionComponent<
return (
<li className={className}>
<Button
dialogTrigger={true}
<button
onClick={onClick}
className="sidebar-menu-item__link"
aria-haspopup="menu"
aria-expanded={isOpen ? 'true' : 'false'}
type="button"
>
<Icon name="folder-open-inverse" className="icon--menuitem" />
<span className="menuitem-label">{item.label}</span>
<Icon className={sidebarTriggerIconClassName} name="arrow-right" />
</Button>
</button>
<div>
<SidebarPanel
isVisible={isVisible}

Wyświetl plik

@ -0,0 +1,70 @@
import React from 'react';
import { shallow } from 'enzyme';
import { PageExplorerMenuItem } from './PageExplorerMenuItem';
describe('PageExplorerMenuItem', () => {
const state = { activePath: '.reports.workflows', navigationPath: '' };
it('should render with the minimum required props', () => {
const wrapper = shallow(
<PageExplorerMenuItem item={{}} path=".explorer" state={state} />,
);
expect(wrapper).toMatchSnapshot();
});
it('should expand the explorer menu when clicked', () => {
const dispatch = jest.fn();
const preventDefault = jest.fn();
const wrapper = shallow(
<PageExplorerMenuItem
dispatch={dispatch}
item={{}}
path=".explorer"
state={state}
/>,
);
expect(
wrapper.find('.sidebar-menu-item__link').prop('aria-expanded'),
).toEqual('false');
expect(wrapper.find('SidebarPanel').prop('isOpen')).toBe(false);
expect(dispatch).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled();
// click the button
wrapper
.find('.sidebar-menu-item__link')
.simulate('click', { preventDefault });
expect(dispatch).toHaveBeenCalledWith({
path: '.explorer',
type: 'set-navigation-path',
});
expect(preventDefault).not.toHaveBeenCalled();
// manually update the state as if the redux action was dispatched
wrapper.setProps({
state: { activePath: '.reports.workflows', navigationPath: '.explorer' },
});
// check that the expanded state is working
expect(
wrapper.find('.sidebar-menu-item__link').prop('aria-expanded'),
).toEqual('true');
expect(wrapper.find('SidebarPanel').prop('isOpen')).toBe(true);
// click the button to close
wrapper
.find('.sidebar-menu-item__link')
.simulate('click', { preventDefault });
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenLastCalledWith({
path: '',
type: 'set-navigation-path',
});
expect(preventDefault).not.toHaveBeenCalled();
});
});

Wyświetl plik

@ -0,0 +1,71 @@
import React from 'react';
import { shallow } from 'enzyme';
import { SubMenuItem } from './SubMenuItem';
describe('SubMenuItem', () => {
const strings = {};
const state = { activePath: '.reports.workflows', navigationPath: '' };
it('should render with the minimum required props', () => {
const wrapper = shallow(
<SubMenuItem
item={{ classNames: '', menuItems: [] }}
items={[]}
state={state}
path=".reports"
strings={strings}
/>,
);
expect(wrapper).toMatchSnapshot();
});
it('should provide a button to expand the sub-menu', () => {
const dispatch = jest.fn();
const wrapper = shallow(
<SubMenuItem
dispatch={dispatch}
item={{ classNames: '', menuItems: [] }}
items={[]}
state={state}
path=".reports"
strings={strings}
/>,
);
expect(wrapper.find('.sidebar-menu-item__link').prop('aria-expanded')).toBe(
'false',
);
expect(dispatch).not.toHaveBeenCalled();
expect(wrapper.find('.sidebar-sub-menu-item--open')).toHaveLength(0);
// click the sub menu item
wrapper.find('.sidebar-menu-item__link').simulate('click');
// check the dispatch function (redux state) was called
expect(dispatch).toHaveBeenCalledWith({
path: '.reports',
type: 'set-navigation-path',
});
// manually update the state as if the redux action was dispatched
wrapper.setProps({
state: { navigationPath: '.reports', activePath: '.reports.workflows' },
});
expect(wrapper.find('.sidebar-menu-item__link').prop('aria-expanded')).toBe(
'true',
);
expect(wrapper.find('.sidebar-sub-menu-item--open')).toHaveLength(1);
// click a second time to 'close'
wrapper.find('.sidebar-menu-item__link').simulate('click');
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenLastCalledWith({
path: '',
type: 'set-navigation-path',
});
});
});

Wyświetl plik

@ -37,7 +37,7 @@ export const SubMenuItem: React.FunctionComponent<SubMenuItemProps> = ({
}
}, [isOpen]);
const onClick = (e: React.MouseEvent) => {
const onClick = () => {
if (isOpen) {
const pathComponents = path.split('.');
pathComponents.pop();
@ -52,8 +52,6 @@ export const SubMenuItem: React.FunctionComponent<SubMenuItemProps> = ({
path,
});
}
e.preventDefault();
};
const className =
@ -70,8 +68,9 @@ export const SubMenuItem: React.FunctionComponent<SubMenuItemProps> = ({
<button
onClick={onClick}
className={`sidebar-menu-item__link ${item.classNames}`}
aria-haspopup="true"
aria-haspopup="menu"
aria-expanded={isOpen ? 'true' : 'false'}
type="button"
>
{item.iconName && (
<Icon name={item.iconName} className="icon--menuitem" />

Wyświetl plik

@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageExplorerMenuItem should render with the minimum required props 1`] = `
<li
className="sidebar-menu-item sidebar-page-explorer-item"
>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-menu-item__link"
onClick={[Function]}
type="button"
>
<Icon
className="icon--menuitem"
name="folder-open-inverse"
/>
<span
className="menuitem-label"
/>
<Icon
className="sidebar-sub-menu-trigger-icon"
name="arrow-right"
/>
</button>
<div>
<SidebarPanel
depth={2}
isOpen={false}
isVisible={false}
widthPx={485}
>
<Provider
store={
Object {
"@@observable": [Function],
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<Connect(PageExplorer)
isVisible={false}
/>
</Provider>
</SidebarPanel>
</div>
</li>
`;

Wyświetl plik

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SubMenuItem should render with the minimum required props 1`] = `
<li
className="sidebar-menu-item sidebar-sub-menu-item sidebar-menu-item--active"
>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-menu-item__link "
onClick={[Function]}
type="button"
>
<span
className="menuitem-label"
/>
<Icon
className="sidebar-sub-menu-trigger-icon"
name="arrow-right"
/>
</button>
<SidebarPanel
depth={2}
isOpen={false}
isVisible={false}
>
<div
className="sidebar-sub-menu-panel"
>
<h2
className=""
id="wagtail-sidebar-submenu-reports-title"
/>
<ul
aria-labelledby="wagtail-sidebar-submenu-reports-title"
/>
</div>
</SidebarPanel>
</li>
`;

Wyświetl plik

@ -0,0 +1,45 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Menu } from './MainMenu';
describe('Menu', () => {
const strings = {};
const user = { avatarUrl: 'https://gravatar/profile' };
it('should render with the minimum required props', () => {
const wrapper = shallow(
<Menu
accountMenuItems={[]}
menuItems={[]}
strings={strings}
user={user}
/>,
);
expect(wrapper).toMatchSnapshot();
});
it('should toggle the sidebar footer (account) when clicked', () => {
const wrapper = shallow(
<Menu
accountMenuItems={[]}
menuItems={[]}
strings={strings}
user={user}
/>,
);
// default is closed
expect(wrapper.find('.sidebar-footer__account').prop('aria-expanded')).toBe(
'false',
);
expect(wrapper.find('.sidebar-footer--open')).toHaveLength(0);
wrapper.find('.sidebar-footer__account').simulate('click');
expect(wrapper.find('.sidebar-footer__account').prop('aria-expanded')).toBe(
'true',
);
expect(wrapper.find('.sidebar-footer--open')).toHaveLength(1);
});
});

Wyświetl plik

@ -170,9 +170,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
}
}, [expandingOrCollapsing]);
const onClickAccountSettings = (e: React.MouseEvent) => {
e.preventDefault();
const onClickAccountSettings = () => {
if (accountSettingsOpen) {
dispatch({
type: 'set-navigation-path',
@ -206,10 +204,11 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
<button
className="sidebar-footer__account"
title={strings.EDIT_YOUR_ACCOUNT}
aria-label={strings.EDIT_YOUR_ACCOUNT}
onClick={onClickAccountSettings}
aria-haspopup="true"
aria-label={strings.EDIT_YOUR_ACCOUNT}
aria-haspopup="menu"
aria-expanded={accountSettingsOpen ? 'true' : 'false'}
type="button"
>
<div className="avatar square avatar-on-dark">
<img src={user.avatarUrl} alt="" />

Wyświetl plik

@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Menu should render with the minimum required props 1`] = `
<Fragment>
<nav
className="sidebar-main-menu"
>
<ul
className="sidebar-main-menu__list"
/>
</nav>
<div
className="sidebar-footer"
>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-footer__account"
onClick={[Function]}
type="button"
>
<div
className="avatar square avatar-on-dark"
>
<img
alt=""
src="https://gravatar/profile"
/>
</div>
<div
className="sidebar-footer__account-toggle"
>
<div
className="sidebar-footer__account-label"
/>
<Icon
className="sidebar-footer__account-icon"
name="arrow-up"
/>
</div>
</button>
<ul />
</div>
</Fragment>
`;

Wyświetl plik

@ -18,7 +18,9 @@ describe('Homepage', () => {
});
it('axe page explorer', async () => {
const trigger = await page.$('[aria-haspopup="dialog"]');
const trigger = await page.$(
'.sidebar-page-explorer-item [aria-haspopup="menu"]',
);
await trigger.click();
await expect(page).toPassAxeTests({
include: '.sidebar-main-menu',
@ -26,7 +28,9 @@ describe('Homepage', () => {
});
it('axe sidebar sub-menu', async () => {
const trigger = await page.$('[aria-haspopup="true"]');
const trigger = await page.$(
'.sidebar-sub-menu-item [aria-haspopup="menu"]',
);
await trigger.click();
await expect(page).toPassAxeTests({
include: '.sidebar-main-menu',

Wyświetl plik

@ -53,6 +53,9 @@ The panel types `StreamFieldPanel`, `RichTextFieldPanel`, `ImageChooserPanel`, `
* Improve the contrast of the “Remember me” checkbox against the login pages background (Steven Steinwand)
* Group permission rows with custom permissions no longer have extra padding (Steven Steinwand)
* Make sure the focus outline of checkboxes is fully around the outer border (Steven Steinwand)
* Consistently set `aria-haspopup="menu"` for all sidebar menu items that have sub-menus (LB (Ben Johnston))
* Make sure `aria-expanded` is always explicitly set as a string in sidebar (LB (Ben Johnston))
* Use a button element instead of a link for page explorer menu item, for the correct semantics and behavior (LB (Ben Johnston))
## Upgrade considerations