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