mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
improve tooltip accessibility (#1938)
There has been the requirement to improve accessibility for our tooltips by allowing tooltips to be closed via the escape key as well as allowing users to hover over the tooltip text. These combined requirements were not possible with the previous implementation that used a bulma-tooltip extension. That meant we had to implement the full tooltip html and css from scratch. A declared goal was to keep the new implementation as close to the previous look-and-feel as possible. The redundant dependency has been removed in the process.
This commit is contained in:
committed by
GitHub
parent
de36c0d09d
commit
67bd96ea81
2
gradle/changelog/tooltip-a11y.yaml
Normal file
2
gradle/changelog/tooltip-a11y.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: changed
|
||||||
|
description: improve tooltip accessibility ([#1938](https://github.com/scm-manager/scm-manager/pull/1938))
|
||||||
@@ -21,12 +21,128 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { ReactNode } from "react";
|
import React, { FC, ReactNode, useEffect, useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
// See for css reference: https://github.com/Wikiki/bulma-tooltip/blob/master/src/sass/index.sass
|
||||||
|
|
||||||
|
const TooltipWrapper = styled.span`
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ArrowBase = styled.span`
|
||||||
|
z-index: 1020;
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ArrowTop = styled(ArrowBase)`
|
||||||
|
top: 0;
|
||||||
|
right: auto;
|
||||||
|
bottom: auto;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 5px 5px 0 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ArrowRight = styled(ArrowBase)`
|
||||||
|
top: auto;
|
||||||
|
right: 0;
|
||||||
|
bottom: 50%;
|
||||||
|
left: auto;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-right: -5px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
margin-left: auto;
|
||||||
|
border-width: 5px 5px 5px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ArrowLeft = styled(ArrowBase)`
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
bottom: 50%;
|
||||||
|
left: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 5px 0 5px 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ArrowBottom = styled(ArrowBase)`
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 0 5px 5px 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Arrow = {
|
||||||
|
bottom: ArrowBottom,
|
||||||
|
left: ArrowLeft,
|
||||||
|
right: ArrowRight,
|
||||||
|
top: ArrowTop
|
||||||
|
};
|
||||||
|
|
||||||
|
const TooltipContainerBase = styled.div<{ multiline?: boolean }>`
|
||||||
|
z-index: 1020;
|
||||||
|
position: absolute;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
hyphens: auto;
|
||||||
|
text-overflow: ${({ multiline }) => (multiline ? "clip" : "ellipsis")};
|
||||||
|
white-space: ${({ multiline }) => (multiline ? "normal" : "pre")};
|
||||||
|
max-width: ${({ multiline }) => (multiline ? "15rem" : "auto")};
|
||||||
|
width: ${({ multiline }) => (multiline ? "15rem" : "auto")};
|
||||||
|
word-break: ${({ multiline }) => (multiline ? "keep-all" : "unset")};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TooltipContainerTop = styled(TooltipContainerBase)`
|
||||||
|
left: 50%;
|
||||||
|
bottom: calc(100% + 5px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TooltipContainerBottom = styled(TooltipContainerBase)`
|
||||||
|
left: 50%;
|
||||||
|
top: calc(100% + 5px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TooltipContainerLeft = styled(TooltipContainerBase)`
|
||||||
|
right: calc(100% + 5px);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TooltipContainerRight = styled(TooltipContainerBase)`
|
||||||
|
left: calc(100% + 5px);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = {
|
||||||
|
bottom: TooltipContainerBottom,
|
||||||
|
left: TooltipContainerLeft,
|
||||||
|
right: TooltipContainerRight,
|
||||||
|
top: TooltipContainerTop
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message: string;
|
message: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
location: TooltipLocation;
|
location?: TooltipLocation;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -34,27 +150,53 @@ type Props = {
|
|||||||
|
|
||||||
export type TooltipLocation = "bottom" | "right" | "top" | "left";
|
export type TooltipLocation = "bottom" | "right" | "top" | "left";
|
||||||
|
|
||||||
class Tooltip extends React.Component<Props> {
|
const Tooltip: FC<Props> = ({ className, message, location = "right", multiline, children, id }) => {
|
||||||
static defaultProps = {
|
const [open, setOpen] = useState(false);
|
||||||
location: "right"
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
useEffect(() => {
|
||||||
const { className, message, location, multiline, children, id } = this.props;
|
const listener = (event: KeyboardEvent) => {
|
||||||
let classes = `tooltip has-tooltip-${location}`;
|
if (event.key === "Escape") {
|
||||||
if (multiline) {
|
setOpen(false);
|
||||||
classes += " has-tooltip-multiline";
|
}
|
||||||
}
|
};
|
||||||
if (className) {
|
window.addEventListener("keydown", listener);
|
||||||
classes += " " + className;
|
return () => window.removeEventListener("keydown", listener);
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
return (
|
const LocationContainer = Container[location];
|
||||||
<span className={classes} data-tooltip={message} aria-label={message} id={id}>
|
const LocationArrow = Arrow[location];
|
||||||
{children}
|
|
||||||
</span>
|
return (
|
||||||
);
|
<TooltipWrapper
|
||||||
}
|
onMouseEnter={() => setOpen(true)}
|
||||||
}
|
onMouseLeave={() => setOpen(false)}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<>
|
||||||
|
<LocationArrow className={`tooltip-arrow-${location}-border-color`} />
|
||||||
|
<LocationContainer
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
"is-size-7",
|
||||||
|
"is-family-primary",
|
||||||
|
"has-rounded-border",
|
||||||
|
"has-text-white",
|
||||||
|
"has-background-grey-dark",
|
||||||
|
"has-text-weight-semibold"
|
||||||
|
)}
|
||||||
|
multiline={multiline}
|
||||||
|
aria-live="polite"
|
||||||
|
id={id}
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</LocationContainer>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</TooltipWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Tooltip;
|
export default Tooltip;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@
|
|||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||||
"bulma": "^0.9.3",
|
"bulma": "^0.9.3",
|
||||||
"bulma-popover": "^1.0.0",
|
"bulma-popover": "^1.0.0",
|
||||||
"bulma-tooltip": "^3.0.0",
|
|
||||||
"react-diff-view": "^2.4.1"
|
"react-diff-view": "^2.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
39
scm-ui/ui-styles/src/components/_tooltip.scss
Normal file
39
scm-ui/ui-styles/src/components/_tooltip.scss
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.tooltip-arrow-top-border-color {
|
||||||
|
border-color: $grey-dark transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow-right-border-color {
|
||||||
|
border-color: transparent $grey-dark transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow-bottom-border-color {
|
||||||
|
border-color: transparent transparent $grey-dark transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow-left-border-color {
|
||||||
|
border-color: transparent transparent transparent $grey-dark;
|
||||||
|
}
|
||||||
@@ -23,6 +23,6 @@
|
|||||||
*/
|
*/
|
||||||
@import "bulma/bulma";
|
@import "bulma/bulma";
|
||||||
@import "../variables/_derived.scss";
|
@import "../variables/_derived.scss";
|
||||||
@import "bulma-tooltip/dist/css/bulma-tooltip.min";
|
|
||||||
@import "bulma-popover/css/bulma-popover";
|
@import "bulma-popover/css/bulma-popover";
|
||||||
@import "../components/_main.scss";
|
@import "../components/_main.scss";
|
||||||
|
@import "../components/_tooltip.scss";
|
||||||
|
|||||||
@@ -6638,11 +6638,6 @@ bulma-popover@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/bulma-popover/-/bulma-popover-1.0.3.tgz#bc948b7b855bedbe8adf20737fc569ac87623a91"
|
resolved "https://registry.yarnpkg.com/bulma-popover/-/bulma-popover-1.0.3.tgz#bc948b7b855bedbe8adf20737fc569ac87623a91"
|
||||||
integrity sha512-/StxOsgTvxrK1OIVrzUk7w4+Lyg9VvXDeh/wj9HhsYoCkqLqRi+EYm9htX6FP0GvscwlPradkgZG6bb997oBBA==
|
integrity sha512-/StxOsgTvxrK1OIVrzUk7w4+Lyg9VvXDeh/wj9HhsYoCkqLqRi+EYm9htX6FP0GvscwlPradkgZG6bb997oBBA==
|
||||||
|
|
||||||
bulma-tooltip@^3.0.0:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/bulma-tooltip/-/bulma-tooltip-3.0.2.tgz#2cf0abab1de2eba07f9d84eb7f07a8a88819ea92"
|
|
||||||
integrity sha512-CsT3APjhlZScskFg38n8HYL8oYNUHQtcu4sz6ERarxkUpBRbk9v0h/5KAvXeKapVSn2dp9l7bOGit5SECP8EWQ==
|
|
||||||
|
|
||||||
bulma@^0.9.3:
|
bulma@^0.9.3:
|
||||||
version "0.9.3"
|
version "0.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.3.tgz#ddccb7436ebe3e21bf47afe01d3c43a296b70243"
|
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.3.tgz#ddccb7436ebe3e21bf47afe01d3c43a296b70243"
|
||||||
|
|||||||
Reference in New Issue
Block a user