From 67bd96ea81bdb541015d0faf574bff24b476374a Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Fri, 28 Jan 2022 15:32:35 +0100 Subject: [PATCH] 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. --- gradle/changelog/tooltip-a11y.yaml | 2 + scm-ui/ui-components/src/Tooltip.tsx | 186 +- .../src/__snapshots__/storyshots.test.ts.snap | 4647 +++++++++-------- scm-ui/ui-styles/package.json | 1 - scm-ui/ui-styles/src/components/_tooltip.scss | 39 + scm-ui/ui-styles/src/utils/_post.scss | 2 +- yarn.lock | 5 - 7 files changed, 2794 insertions(+), 2088 deletions(-) create mode 100644 gradle/changelog/tooltip-a11y.yaml create mode 100644 scm-ui/ui-styles/src/components/_tooltip.scss diff --git a/gradle/changelog/tooltip-a11y.yaml b/gradle/changelog/tooltip-a11y.yaml new file mode 100644 index 0000000000..6a18dbbe80 --- /dev/null +++ b/gradle/changelog/tooltip-a11y.yaml @@ -0,0 +1,2 @@ +- type: changed + description: improve tooltip accessibility ([#1938](https://github.com/scm-manager/scm-manager/pull/1938)) diff --git a/scm-ui/ui-components/src/Tooltip.tsx b/scm-ui/ui-components/src/Tooltip.tsx index 4e14405b03..1569353255 100644 --- a/scm-ui/ui-components/src/Tooltip.tsx +++ b/scm-ui/ui-components/src/Tooltip.tsx @@ -21,12 +21,128 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * 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 = { message: string; className?: string; - location: TooltipLocation; + location?: TooltipLocation; multiline?: boolean; children: ReactNode; id?: string; @@ -34,27 +150,53 @@ type Props = { export type TooltipLocation = "bottom" | "right" | "top" | "left"; -class Tooltip extends React.Component { - static defaultProps = { - location: "right" - }; +const Tooltip: FC = ({ className, message, location = "right", multiline, children, id }) => { + const [open, setOpen] = useState(false); - render() { - const { className, message, location, multiline, children, id } = this.props; - let classes = `tooltip has-tooltip-${location}`; - if (multiline) { - classes += " has-tooltip-multiline"; - } - if (className) { - classes += " " + className; - } + useEffect(() => { + const listener = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setOpen(false); + } + }; + window.addEventListener("keydown", listener); + return () => window.removeEventListener("keydown", listener); + }, []); - return ( - - {children} - - ); - } -} + const LocationContainer = Container[location]; + const LocationArrow = Arrow[location]; + + return ( + setOpen(true)} + onMouseLeave={() => setOpen(false)} + onClick={() => setOpen(false)} + > + {open ? ( + <> + + + {message} + + + ) : null} + {children} + + ); +}; export default Tooltip; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 308d3681a2..5344a0034c 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -144,9 +144,10 @@ exports[`Storyshots BreadCrumb Default 1`] = ` tabIndex={0} > Changelog Compl_icated_H€aDer - 2021-03-26 Added Fixed Changed - 2021-03-17 Fixed - 2021-03-12 Added Fixed - 2021-03-03 Fixed - 2021-03-01 Added Fixed Changed - 2021-01-29 Added Changed Fixed - 2020-12-17 Added Changed Fixed - 2020-12-07 Fixed - 2020-12-04 Added Changed Fixed - 2020-11-24 Fixed - 2020-11-20 Added Fixed - 2020-11-11 Fixed - 2020-11-06 Added Fixed - 2020-10-27 Added Changed Fixed - 2020-10-16 Fixed - 2020-10-14 Fixed Changed - 2020-10-12 Added - 2020-10-09 Added Fixed - 2020-09-30 Fixed - 2020-09-25 Added Changed Fixed - 2020-09-10 Added Fixed - 2020-09-01 Added Fixed - 2020-08-14 Added Fixed - 2020-08-04 Added Changed Fixed - 2020-07-23 Added Changed Fixed - 2020-07-03 Added Changed Fixed - 2020-06-23 Fixed - 2020-06-18 Added Fixed - 2020-06-04 Added Fixed - 2020-05-08 Added Changed Fixed - 2020-04-09 Added Changed Fixed - 2020-03-26 Added Changed Removed Fixed - 2020-03-12 Added Changed Fixed Removed - 2020-02-14 Added Changed Fixed - 2020-01-31 Fixed - 2020-01-29 Added Changed Fixed - 2019-12-02 Added