Skip to content

Commit

Permalink
Keyboard a11y improvements (#1274)
Browse files Browse the repository at this point in the history
* Enable focus ring

* Restore focus to button when popover is closed

* do not remove outline style by default

* prevent focus loss when toggling expansion state of internal node via keyboard

* make user menu fully keyboard accessible

* Make tooltip container keyboard accessible

* Improve focus ring styling for basket button in result table

* Make step edit button fully keyboard accessible

* Make organism filter toggle button fully keyboard accessible

* Add UIThemeProvider to ortho-site

* Improve focus styling for featured tools
  • Loading branch information
dmfalke authored Nov 15, 2024
1 parent bf7a289 commit cdbf6f3
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const MesaTooltip = ({
enterDelay={showDelay}
className={(className ?? '') + (corner ? ` ${corner}` : '')}
style={finalStyle}
tabIndex={0}
>
{children}
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,12 @@ const PopoverButton = forwardRef<PopoverButtonHandle, PopoverButtonProps>(
);

const onCloseHandler = useCallback(() => {
setTimeout(() => {
anchorEl?.focus(); // return focus to button
});
setAnchorEl(null);
onClose && onClose();
}, [onClose]);
}, [anchorEl, onClose]);

// Expose the `close()` method to external components via ref
useImperativeHandle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default function SwissArmyButton({
styleSpec[styleState].color ?? 'transparent'
} !important`,
borderRadius: styleSpec[styleState].border?.radius ?? 5,
outlineStyle: styleSpec[styleState].border?.style ?? 'none',
outlineStyle: styleSpec[styleState].border?.style,
outlineColor: styleSpec[styleState].border?.color,
outlineWidth: styleSpec[styleState].border?.width,
outlineOffset: styleSpec[styleState].border?.width
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ function CheckboxTree<T>(props: CheckboxTreeProps<T>) {
},
'.arrow-container': {
height: '1em',
'outline-offset': '-1px',
},
'.arrow-icon': {
fill: '#aaa',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,33 +161,28 @@ export default function CheckboxTreeNode<T>({
{isLeafNode ? null : isActiveSearch ? (
// this retains the space of the expansion toggle icons for easier formatting
<div className="active-search-buffer"></div>
) : isExpanded ? (
<div className="arrow-container">
<ArrowDown
className="arrow-icon"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
toggleExpansion(node);
}}
onKeyDown={(e) =>
e.key === 'Enter' ? toggleExpansion(node) : null
}
/>
</div>
) : (
<div className="arrow-container">
<ArrowRight
className="arrow-icon"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
<div
className="arrow-container"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
toggleExpansion(node);
}}
onKeyDown={(e) => {
const toggleKeys = isExpanded
? ['Enter', 'ArrowLeft']
: ['Enter', 'ArrowRight'];
if (toggleKeys.includes(e.key)) {
toggleExpansion(node);
}}
onKeyDown={(e) =>
e.key === 'Enter' ? toggleExpansion(node) : null
}
/>
}}
>
{isExpanded ? (
<ArrowDown className="arrow-icon" />
) : (
<ArrowRight className="arrow-icon" />
)}
</div>
)}
{!isSelectable || (!isMultiPick && !isLeafNode) ? (
Expand Down
16 changes: 14 additions & 2 deletions packages/libs/coreui/src/components/theming/UIThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { ThemeProvider } from '@emotion/react';
import { css, Global, ThemeProvider } from '@emotion/react';
import { useCoreUIFonts } from '../../hooks';

import { UITheme } from './types';
Expand All @@ -14,5 +14,17 @@ export default function UIThemeProvider({
children,
}: UIThemeProviderProps) {
useCoreUIFonts();
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
return (
<ThemeProvider theme={theme}>
<Global
styles={css`
*:focus {
outline: 2px solid
${theme.palette.primary.hue[theme.palette.primary.level]};
}
`}
/>
{children}
</ThemeProvider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
padding: 5px;
background: none;
border: none;
outline-offset: -5px;
}

.RemoveColumnButton {
Expand Down
3 changes: 2 additions & 1 deletion packages/libs/wdk-client/src/Views/Strategy/StepBoxes.css
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@ button.StepBoxes--EditButton:hover {
background: yellow;
}

.StrategyPanel--Panel:hover button.StepBoxes--EditButton {
.StrategyPanel--Panel:hover button.StepBoxes--EditButton,
.StrategyPanel--Panel:focus-within button.StepBoxes--EditButton {
display: inline;
}

Expand Down
56 changes: 11 additions & 45 deletions packages/libs/web-common/src/App/UserMenu/UserMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';

import './UserMenu.scss';

Expand All @@ -7,87 +8,56 @@ import { IconAlt as Icon } from '@veupathdb/wdk-client/lib/Components';
class UserMenu extends React.Component {
constructor(props) {
super(props);
this.state = { isEntered: false, isHovered: false };
this.renderMenu = this.renderMenu.bind(this);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
}

onMouseEnter(event) {
this.setState({ isEntered: true, isHovered: true });
}

onMouseLeave(event) {
this.setState({ isEntered: false });

setTimeout(() => {
if (!this.state.isEntered) {
this.setState({ isHovered: false });
}
}, 500);
}

renderMenu() {
const { user, actions, webAppUrl } = this.props;
const { showLoginForm } = actions;
const { isHovered } = this.state;
const { properties } = user.properties ? user : { properties: null };
const { firstName, lastName } = properties
? properties
: { firstName: '', lastName: '' };
const { user } = this.props;
const items = user.isGuest
? [
{
icon: 'sign-in',
text: 'Login',
onClick: () => actions.showLoginForm(window.location.href),
route: '/user/login',
},
{
icon: 'user-plus',
text: 'Register',
href: webAppUrl + '/app/user/registration',
route: '/user/registration',
target: '_blank',
},
]
: [
{
icon: 'vcard',
text: 'My Profile',
href: webAppUrl + '/app/user/profile',
route: '/user/profile',
},
{
icon: 'power-off',
text: 'Log Out',
onClick: () => actions.showLogoutWarning(window.location.href),
route: '/user/logout',
},
];

return (
<div className={'UserMenu-Pane' + (!isHovered ? ' inert' : '')}>
<div className="UserMenu-Pane">
{items.map((item, key) => {
const { onClick, href, target } = item;
const { route, target } = item;
const className = 'UserMenu-Pane-Item';

let props = {
className,
onClick: onClick ? onClick : () => null,
};
if (href) props = Object.assign({}, props, { href, target });
const Element = href ? 'a' : 'div';

return (
<Element key={key} {...props}>
<Link key={key} className={className} to={route} target={target}>
<Icon fa={item.icon + ' UserMenu-Pane-Item-Icon'} />
{item.text}
</Element>
</Link>
);
})}
</div>
);
}

render() {
const { onMouseEnter, onMouseLeave } = this;
const { user } = this.props;
if (!user) return null;

Expand All @@ -96,11 +66,7 @@ class UserMenu extends React.Component {
const Menu = this.renderMenu;

return (
<div
className="box UserMenu"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className="box UserMenu">
<Icon className="UserMenu-Icon" fa={iconClass} />
<span className="UserMenu-Title">
{typeof isGuest === 'undefined'
Expand Down
18 changes: 13 additions & 5 deletions packages/libs/web-common/src/App/UserMenu/UserMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,22 @@ $white: #e0e0e0;
padding: 5px;
font-style: italic;
}

&:focus-within,
&:hover {
.UserMenu-Pane {
opacity: 1;
pointer-events: all;
transition: none;
}
}
}

.UserMenu-Pane {
transition: opacity 500ms 500ms;
opacity: 0;
pointer-events: none;

right: 0;
top: 100%;
color: black;
Expand All @@ -33,17 +46,12 @@ $white: #e0e0e0;
min-width: 150px;
position: absolute;
border-radius: 10px;
transition: all 0.2s;
line-height: 1em;
background-color: $white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
.fa {
color: $red;
}
&.inert {
opacity: 0;
pointer-events: none;
}
&::after {
top: -3px;
width: 10px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@
border: 0.2em solid #00304c;
}
}

&:focus,
&__selected:focus {
outline: none;
text-decoration: underline;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
white-space: nowrap;
width: 18em;

&:hover,
&:focus {
background-color: #396aa4;
background-image: none;
}

&Text {
margin: 0 2em;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,15 @@ type ExpansionBarProps = {

function ExpansionBar(props: ExpansionBarProps) {
return (
<div className={cx('--ExpansionBar')} onClick={props.onClick}>
<button
type="button"
className={cx('--ExpansionBar')}
onClick={props.onClick}
>
{props.arrow}
<span className={cx('--ExpansionBarText')}>{props.message}</span>
{props.arrow}
</div>
</button>
);
}

Expand Down
Loading

0 comments on commit cdbf6f3

Please sign in to comment.