From cdbf6f3e80f2290b409943b2ec281777066a9a00 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 15 Nov 2024 11:42:45 -0500 Subject: [PATCH] Keyboard a11y improvements (#1274) * 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 --- .../Mesa/Components/MesaTooltip.tsx | 1 + .../buttons/PopoverButton/PopoverButton.tsx | 5 +- .../buttons/SwissArmyButton/index.tsx | 2 +- .../checkboxes/CheckboxTree/CheckboxTree.tsx | 1 + .../CheckboxTree/CheckboxTreeNode.tsx | 43 +++++------ .../components/theming/UIThemeProvider.tsx | 16 ++++- .../ResultTableSummaryView.scss | 1 + .../src/Views/Strategy/StepBoxes.css | 3 +- .../web-common/src/App/UserMenu/UserMenu.jsx | 56 +++------------ .../web-common/src/App/UserMenu/UserMenu.scss | 18 +++-- .../components/homepage/FeaturedTools.scss | 6 ++ .../js/client/components/OrganismFilter.scss | 6 ++ .../js/client/components/OrganismFilter.tsx | 8 ++- .../client/components/layout/OrthoMCLPage.tsx | 71 +++++++++++-------- 14 files changed, 126 insertions(+), 111 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Components/MesaTooltip.tsx b/packages/libs/coreui/src/components/Mesa/Components/MesaTooltip.tsx index ca5656a597..aa1b948790 100644 --- a/packages/libs/coreui/src/components/Mesa/Components/MesaTooltip.tsx +++ b/packages/libs/coreui/src/components/Mesa/Components/MesaTooltip.tsx @@ -63,6 +63,7 @@ const MesaTooltip = ({ enterDelay={showDelay} className={(className ?? '') + (corner ? ` ${corner}` : '')} style={finalStyle} + tabIndex={0} > {children} diff --git a/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx b/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx index 79b5f0f63b..07dcc8db43 100644 --- a/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx +++ b/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx @@ -119,9 +119,12 @@ const PopoverButton = forwardRef( ); 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( diff --git a/packages/libs/coreui/src/components/buttons/SwissArmyButton/index.tsx b/packages/libs/coreui/src/components/buttons/SwissArmyButton/index.tsx index c38b106cc2..7fe308af0c 100644 --- a/packages/libs/coreui/src/components/buttons/SwissArmyButton/index.tsx +++ b/packages/libs/coreui/src/components/buttons/SwissArmyButton/index.tsx @@ -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 diff --git a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTree.tsx b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTree.tsx index da26060416..01c1ca0c05 100644 --- a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTree.tsx +++ b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTree.tsx @@ -968,6 +968,7 @@ function CheckboxTree(props: CheckboxTreeProps) { }, '.arrow-container': { height: '1em', + 'outline-offset': '-1px', }, '.arrow-icon': { fill: '#aaa', diff --git a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx index cdbad9a63f..1f251c584b 100644 --- a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx +++ b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx @@ -161,33 +161,28 @@ export default function CheckboxTreeNode({ {isLeafNode ? null : isActiveSearch ? ( // this retains the space of the expansion toggle icons for easier formatting
- ) : isExpanded ? ( -
- { - e.stopPropagation(); - toggleExpansion(node); - }} - onKeyDown={(e) => - e.key === 'Enter' ? toggleExpansion(node) : null - } - /> -
) : ( -
- { - e.stopPropagation(); +
{ + 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 ? ( + + ) : ( + + )}
)} {!isSelectable || (!isMultiPick && !isLeafNode) ? ( diff --git a/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx b/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx index 16dd37832f..668a387186 100644 --- a/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx +++ b/packages/libs/coreui/src/components/theming/UIThemeProvider.tsx @@ -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'; @@ -14,5 +14,17 @@ export default function UIThemeProvider({ children, }: UIThemeProviderProps) { useCoreUIFonts(); - return {children}; + return ( + + + {children} + + ); } diff --git a/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableSummaryView.scss b/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableSummaryView.scss index bf954c3cc3..c4c5b57208 100644 --- a/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableSummaryView.scss +++ b/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableSummaryView.scss @@ -118,6 +118,7 @@ padding: 5px; background: none; border: none; + outline-offset: -5px; } .RemoveColumnButton { diff --git a/packages/libs/wdk-client/src/Views/Strategy/StepBoxes.css b/packages/libs/wdk-client/src/Views/Strategy/StepBoxes.css index 96271199c6..aaff2e3493 100644 --- a/packages/libs/wdk-client/src/Views/Strategy/StepBoxes.css +++ b/packages/libs/wdk-client/src/Views/Strategy/StepBoxes.css @@ -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; } diff --git a/packages/libs/web-common/src/App/UserMenu/UserMenu.jsx b/packages/libs/web-common/src/App/UserMenu/UserMenu.jsx index bd3073ddcc..d04e25e929 100644 --- a/packages/libs/web-common/src/App/UserMenu/UserMenu.jsx +++ b/packages/libs/web-common/src/App/UserMenu/UserMenu.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import './UserMenu.scss'; @@ -7,45 +8,22 @@ 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', }, ] @@ -53,33 +31,26 @@ class UserMenu extends React.Component { { 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 ( -
+
{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 ( - + {item.text} - + ); })}
@@ -87,7 +58,6 @@ class UserMenu extends React.Component { } render() { - const { onMouseEnter, onMouseLeave } = this; const { user } = this.props; if (!user) return null; @@ -96,11 +66,7 @@ class UserMenu extends React.Component { const Menu = this.renderMenu; return ( -
+
{typeof isGuest === 'undefined' diff --git a/packages/libs/web-common/src/App/UserMenu/UserMenu.scss b/packages/libs/web-common/src/App/UserMenu/UserMenu.scss index 6273f28450..15d5ef1ab8 100644 --- a/packages/libs/web-common/src/App/UserMenu/UserMenu.scss +++ b/packages/libs/web-common/src/App/UserMenu/UserMenu.scss @@ -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; @@ -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; diff --git a/packages/libs/web-common/src/components/homepage/FeaturedTools.scss b/packages/libs/web-common/src/components/homepage/FeaturedTools.scss index abac5a284a..a1d90f70b0 100644 --- a/packages/libs/web-common/src/components/homepage/FeaturedTools.scss +++ b/packages/libs/web-common/src/components/homepage/FeaturedTools.scss @@ -132,6 +132,12 @@ border: 0.2em solid #00304c; } } + + &:focus, + &__selected:focus { + outline: none; + text-decoration: underline; + } } } diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.scss b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.scss index f8136e573a..802ad3c1fa 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.scss +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.scss @@ -89,6 +89,12 @@ white-space: nowrap; width: 18em; + &:hover, + &:focus { + background-color: #396aa4; + background-image: none; + } + &Text { margin: 0 2em; } diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.tsx index 249e1a26b8..4e3478cc74 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/OrganismFilter.tsx @@ -123,11 +123,15 @@ type ExpansionBarProps = { function ExpansionBar(props: ExpansionBarProps) { return ( -
+
+ ); } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/layout/OrthoMCLPage.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/layout/OrthoMCLPage.tsx index eef8cc5544..4cf9377536 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/layout/OrthoMCLPage.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/layout/OrthoMCLPage.tsx @@ -40,6 +40,8 @@ import { } from 'ortho-client/hooks/searchCheckboxTree'; import './OrthoMCLPage.scss'; +import { colors } from '@veupathdb/coreui'; +import { UIThemeProvider } from '@veupathdb/coreui/lib/components/theming'; const cx = makeClassNameHelper('vpdb-'); @@ -86,37 +88,46 @@ export const OrthoMCLPage: FunctionComponent = (props) => { ); return ( - - -
- -
- -
- + + + +
+ +
+ +
+ +
+
{props.children}
+ +
+
+ + +
-
{props.children}
- -
-
- - - -
- - + + + ); };