From b576cc1359226226cf90bab81a108d2a617f6412 Mon Sep 17 00:00:00 2001 From: Bernard Date: Wed, 3 Mar 2021 15:27:54 -0600 Subject: [PATCH] [WNMGDS-811] Tooltip updates (#947) * Update tooltip styles * Add vertical align, remove flex styles * Remove interactive prop, use it by default for WCAG2.2 requirements * Add onOpen onClose handlers, and update event handlers * Simplify event handling for the tooltip dialog * Update snaps * Update packages/design-system/src/components/Tooltip/Tooltip.jsx * Update snaps * Add remove some margins by default in tooltip content --- .../components/Tooltip/Tooltip.example.jsx | 30 ++--- .../src/components/Tooltip/Tooltip.jsx | 105 +++++++++++++----- .../__snapshots__/Tooltip.test.jsx.snap | 90 ++++++++++----- .../src/styles/components/_Tooltip.scss | 19 ++-- .../src/styles/components/_TooltipIcon.scss | 1 + 5 files changed, 161 insertions(+), 84 deletions(-) diff --git a/packages/design-system-docs/src/pages/components/Tooltip/Tooltip.example.jsx b/packages/design-system-docs/src/pages/components/Tooltip/Tooltip.example.jsx index 5f1a03f662..523f2ce437 100644 --- a/packages/design-system-docs/src/pages/components/Tooltip/Tooltip.example.jsx +++ b/packages/design-system-docs/src/pages/components/Tooltip/Tooltip.example.jsx @@ -12,25 +12,19 @@ ReactDOM.render( triggerClassName="ds-c-tooltip__trigger-icon" triggerActiveClassName="ds-c-tooltip-icon--active" > -

- {'Tooltip trigger uses for the trigger content'} -

+ {'Tooltip trigger uses for the trigger content'}

Tooltip using a

-

Tooltip trigger is styled with dashed underline

+ Tooltip trigger is styled with dashed underline

Tooltip with

- -

+ + <> { 'Tooltip remains active when the mouse hovers over the tooltip body. Tooltip can contain ' } @@ -38,7 +32,7 @@ ReactDOM.render( links {' and other interactive content'} -

+
@@ -48,7 +42,7 @@ ReactDOM.render( triggerContent="placement" triggerClassName="ds-c-tooltip__trigger-link" > -

Tooltip positioned on the right

+ Tooltip positioned on the right
@@ -58,17 +52,12 @@ ReactDOM.render( triggerContent="offset" triggerClassName="ds-c-tooltip__trigger-link" > -

Tooltip positioned with custom offset

+ Tooltip positioned with custom offset

Tooltip dialog activated

- + <>

{ @@ -93,13 +82,12 @@ ReactDOM.render( } triggerClassName="ds-c-tooltip__trigger-icon" triggerActiveClassName="ds-c-tooltip-icon--active" > -

Inverse tooltip styles applied

+ Inverse tooltip styles applied
, diff --git a/packages/design-system/src/components/Tooltip/Tooltip.jsx b/packages/design-system/src/components/Tooltip/Tooltip.jsx index 2354497428..64c1e33aae 100644 --- a/packages/design-system/src/components/Tooltip/Tooltip.jsx +++ b/packages/design-system/src/components/Tooltip/Tooltip.jsx @@ -26,7 +26,11 @@ export class Tooltip extends React.Component { this.tooltipElement = elem; }; - this.state = { active: false }; + this.state = { + active: false, + isHover: false, + isMobile: false, + }; } componentDidMount() { @@ -50,10 +54,11 @@ export class Tooltip extends React.Component { } handleClickOutside(event) { - // Closes click only tooltips when mouse clicks outside of tooltip container element - if (this.state.active && this.props.dialog) { + // Closes dialog and mobile tooltips when mouse clicks outside of tooltip element + if (this.state.active && (this.props.dialog || this.state.isMobile)) { + const clickedTrigger = this.triggerElement && this.triggerElement.contains(event.target); const clickedTooltip = this.tooltipElement && this.tooltipElement.contains(event.target); - if (!clickedTooltip) { + if (!clickedTooltip && !clickedTrigger) { this.setTooltipActive(false); } } @@ -68,24 +73,40 @@ export class Tooltip extends React.Component { } setTooltipActive(active) { - this.setState({ active }, () => { - this.popper.forceUpdate(); - }); + if (active !== this.state.active) { + this.setState({ active }, () => { + this.popper.forceUpdate(); + if (active) { + this.props.onOpen && this.props.onOpen(); + } else { + this.props.onClose && this.props.onClose(); + } + }); + } } handleBlur() { // Hide tooltips when blurring away from the trigger or tooltip body + // and when the mouse is not hovering over the tooltip setTimeout(() => { const focusedInsideTrigger = this.triggerElement && this.triggerElement.contains(document.activeElement); const focusedInsideTooltip = this.tooltipElement && this.tooltipElement.contains(document.activeElement); - if (!focusedInsideTrigger && !focusedInsideTooltip) { + if (!focusedInsideTrigger && !focusedInsideTooltip && !this.state.isHover) { this.setTooltipActive(false); } }, 10); } + handleTouch() { + // On mobile, touch -> mouseenter -> click events can all be fired simultaneously + // `isMobile` flag is used inside onClick and onMouseEnter handlers, so touch events can be used in isolation on mobile + // https://stackoverflow.com/a/65055198 + this.setState({ isMobile: true }); + this.setTooltipActive(!this.state.active); + } + triggerComponentType() { let component = this.props.triggerComponent; if (component === 'button' && this.props.triggerHref) { @@ -97,8 +118,8 @@ export class Tooltip extends React.Component { renderTrigger() { const { - dialog, ariaLabel, + dialog, triggerActiveClassName, triggerClassName, triggerContent, @@ -108,30 +129,37 @@ export class Tooltip extends React.Component { const TriggerComponent = this.triggerComponentType(); const triggerClasses = classNames('ds-base', 'ds-c-tooltip__trigger', triggerClassName, { [triggerActiveClassName]: this.state.active, - 'ds-c-tooltip__trigger--click-only-active': this.props.dialog && this.state.active, }); const eventHandlers = dialog ? { - onClick: () => this.setTooltipActive(!this.state.active), + onTouchStart: () => this.handleTouch(), + onClick: () => { + if (!this.state.isMobile) { + this.setTooltipActive(!this.state.active); + } + }, } : { - onTouchStart: () => this.setTooltipActive(!this.state.active), + onTouchStart: () => this.handleTouch(), + onClick: () => { + if (!this.state.isMobile) { + this.setTooltipActive(!this.state.active); + } + }, onFocus: () => this.setTooltipActive(true), onBlur: () => this.handleBlur(), - onMouseEnter: () => this.setTooltipActive(true), - onMouseLeave: () => this.setTooltipActive(false), }; return ( {triggerContent} @@ -143,7 +171,6 @@ export class Tooltip extends React.Component { dialog, children, inversed, - interactive, interactiveBorder, placement, className, @@ -160,6 +187,12 @@ export class Tooltip extends React.Component { zIndex: '-999', // ensures interactive border doesnt cover tooltip content }; + const eventHandlers = dialog + ? {} + : { + onBlur: () => this.handleBlur(), + }; + const tooltipContent = () => (
(interactive ? this.setTooltipActive(true) : null)} - onMouseLeave={() => (dialog ? null : this.setTooltipActive(false))} - onBlur={() => this.handleBlur()} data-placement={placement} aria-hidden={!this.state.active} role={dialog ? 'dialog' : 'tooltip'} + {...eventHandlers} >
{children}
- {interactive && ( + {!dialog && (
)}
@@ -195,6 +226,7 @@ export class Tooltip extends React.Component { focusTrapOptions={{ // Set initialFocus to the tooltip container element in case it contains no focusable elements initialFocus: `#${this.id}`, + clickOutsideDeactivates: true, }} > {tooltipContent()} @@ -207,11 +239,28 @@ export class Tooltip extends React.Component { } render() { + const eventHandlers = this.props.dialog + ? {} + : { + onMouseEnter: () => { + if (!this.state.isMobile) { + this.setState({ isHover: true }); + this.setTooltipActive(true); + } + }, + onMouseLeave: () => { + if (!this.state.isMobile) { + this.setState({ isHover: false }); + this.setTooltipActive(false); + } + }, + }; + return ( -
+ {this.renderTrigger()} {this.renderContent()} -
+ ); } } @@ -246,10 +295,6 @@ Tooltip.propTypes = { * `id` applied to tooltip body container element. If not provided, a unique id will be automatically generated and used. */ id: PropTypes.string, - /** - * Set to `true` if the tooltip content contains tabbable, interactive elements like links or buttons. This prop expands the activation area to include the tooltip itself, allowing the content to interact with mouse events. - */ - interactive: PropTypes.bool, /** * Sets the size of the invisible border around interactive tooltips that prevents it from immediately hiding when the cursor leaves the tooltip. */ @@ -259,6 +304,14 @@ Tooltip.propTypes = { * Applies `skidding` and `distance` offsets to the tooltip relative to the trigger. See the [`popperjs` docs](https://popper.js.org/docs/v2/modifiers/popper-offsets/) for more info. */ offset: PropTypes.arrayOf(PropTypes.number), + /** + * Called when the tooltip is hidden + */ + onClose: PropTypes.func, + /** + * Called when the tooltip is shown + */ + onOpen: PropTypes.func, /** * Placement of the tooltip body relative to the trigger. See the [`popperjs` docs](https://popper.js.org/docs/v2/constructors/#options) for more info. */ diff --git a/packages/design-system/src/components/Tooltip/__snapshots__/Tooltip.test.jsx.snap b/packages/design-system/src/components/Tooltip/__snapshots__/Tooltip.test.jsx.snap index fd698c7969..536b096578 100644 --- a/packages/design-system/src/components/Tooltip/__snapshots__/Tooltip.test.jsx.snap +++ b/packages/design-system/src/components/Tooltip/__snapshots__/Tooltip.test.jsx.snap @@ -1,16 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Tooltip renders custom trigger component 1`] = ` -
+ @@ -26,8 +28,6 @@ exports[`Tooltip renders custom trigger component 1`] = ` data-placement="top" id="trigger_4" onBlur={[Function]} - onMouseEnter={[Function]} - onMouseLeave={[Function]} role="tooltip" style={ Object { @@ -50,21 +50,34 @@ exports[`Tooltip renders custom trigger component 1`] = ` Tooltip body content

+
-
+ `; exports[`Tooltip renders default trigger icon 1`] = ` -
+
+
-
+ `; exports[`Tooltip renders dialog tooltip 1`] = ` -
+
- + `; exports[`Tooltip renders interactive tooltip 1`] = ` -
+
- + `; exports[`Tooltip renders inverse tooltip 1`] = ` -
+
+
- + `; diff --git a/packages/design-system/src/styles/components/_Tooltip.scss b/packages/design-system/src/styles/components/_Tooltip.scss index 007436fb36..47cc43216a 100644 --- a/packages/design-system/src/styles/components/_Tooltip.scss +++ b/packages/design-system/src/styles/components/_Tooltip.scss @@ -22,18 +22,13 @@ $tooltip-background-color-inverse: $color-background !default; // Tooltip trigger style .ds-c-tooltip__trigger { cursor: pointer; -} - -// Prevent the trigger from reactivating the tooltip when clicking outside of tooltipDialog tooltips -.ds-c-tooltip__trigger--click-only-active { - pointer-events: none; + display: inline; + font-size: inherit; + font-weight: inherit; } .ds-c-tooltip__trigger-icon { @extend %trigger-reset-styles; - align-items: center; - display: flex; - justify-content: center; padding: 4px; } @@ -68,6 +63,14 @@ $tooltip-background-color-inverse: $color-background !default; font-size: $small-font-size; font-weight: 400; padding: $spacer-1; + + // Remove tooltip content padding by default + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } } // The invisible area around the tooltip container that keeps the tooltip visible on hover diff --git a/packages/design-system/src/styles/components/_TooltipIcon.scss b/packages/design-system/src/styles/components/_TooltipIcon.scss index b2303e8a03..09d3ec891f 100644 --- a/packages/design-system/src/styles/components/_TooltipIcon.scss +++ b/packages/design-system/src/styles/components/_TooltipIcon.scss @@ -6,6 +6,7 @@ display: inline-block; height: $tooltip-plus-border-size; position: relative; + vertical-align: middle; width: $tooltip-plus-border-size; &:hover {