Skip to content

Commit

Permalink
[WNMGDS-811] Tooltip updates (#947)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bernardwang authored Mar 3, 2021
1 parent 842f892 commit b576cc1
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,27 @@ ReactDOM.render(
triggerClassName="ds-c-tooltip__trigger-icon"
triggerActiveClassName="ds-c-tooltip-icon--active"
>
<p className="ds-u-margin--0">
{'Tooltip trigger uses <TooltipIcon> for the trigger content'}
</p>
{'Tooltip trigger uses <TooltipIcon> for the trigger content'}
</Tooltip>
</div>
<div className="ds-u-display--flex ds-u-align-items--center ds-u-margin-y--2">
<p className="ds-u-margin--0 ds-u-margin-right--1">Tooltip using a</p>
<Tooltip triggerContent="text trigger" triggerClassName="ds-c-tooltip__trigger-link">
<p className="ds-u-margin--0">Tooltip trigger is styled with dashed underline</p>
Tooltip trigger is styled with dashed underline
</Tooltip>
</div>
<div className="ds-u-display--flex ds-u-align-items--center ds-u-margin-y--2">
<p className="ds-u-margin--0 ds-u-margin-right--1">Tooltip with </p>
<Tooltip
interactive
triggerContent="interactive content"
triggerClassName="ds-c-tooltip__trigger-link"
>
<p className="ds-u-margin--0">
<Tooltip triggerContent="interactive content" triggerClassName="ds-c-tooltip__trigger-link">
<>
{
'Tooltip remains active when the mouse hovers over the tooltip body. Tooltip can contain '
}
<a className="ds-c-link--inverse" href="#noop">
links
</a>
{' and other interactive content'}
</p>
</>
</Tooltip>
</div>
<div className="ds-u-display--flex ds-u-align-items--center ds-u-margin-y--2">
Expand All @@ -48,7 +42,7 @@ ReactDOM.render(
triggerContent="placement"
triggerClassName="ds-c-tooltip__trigger-link"
>
<p className="ds-u-margin--0">Tooltip positioned on the right</p>
Tooltip positioned on the right
</Tooltip>
</div>
<div className="ds-u-display--flex ds-u-align-items--center ds-u-margin-y--2">
Expand All @@ -58,17 +52,12 @@ ReactDOM.render(
triggerContent="offset"
triggerClassName="ds-c-tooltip__trigger-link"
>
<p className="ds-u-margin--0">Tooltip positioned with custom offset</p>
Tooltip positioned with custom offset
</Tooltip>
</div>
<div className="ds-u-display--flex ds-u-align-items--center ds-u-margin-y--2">
<p className="ds-u-margin--0 ds-u-margin-right--1">Tooltip dialog activated </p>
<Tooltip
interactive
dialog
triggerContent="on click"
triggerClassName="ds-c-tooltip__trigger-link"
>
<Tooltip dialog triggerContent="on click" triggerClassName="ds-c-tooltip__trigger-link">
<>
<p className="ds-u-margin--0">
{
Expand All @@ -93,13 +82,12 @@ ReactDOM.render(
<Tooltip
ariaLabel="Label describing the subject of the inverse tooltip"
inversed
interactive
placement="right"
triggerContent={<TooltipIcon inversed />}
triggerClassName="ds-c-tooltip__trigger-icon"
triggerActiveClassName="ds-c-tooltip-icon--active"
>
<p className="ds-u-margin--0">Inverse tooltip styles applied</p>
Inverse tooltip styles applied
</Tooltip>
</div>
</>,
Expand Down
105 changes: 79 additions & 26 deletions packages/design-system/src/components/Tooltip/Tooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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);
}
}
Expand All @@ -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) {
Expand All @@ -97,8 +118,8 @@ export class Tooltip extends React.Component {

renderTrigger() {
const {
dialog,
ariaLabel,
dialog,
triggerActiveClassName,
triggerClassName,
triggerContent,
Expand All @@ -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 (
<TriggerComponent
{...eventHandlers}
type={TriggerComponent === 'button' ? 'button' : undefined}
aria-label={ariaLabel || ''}
aria-describedby={this.id}
className={triggerClasses}
ref={this.setTriggerElement}
href={triggerHref}
{...eventHandlers}
>
{triggerContent}
</TriggerComponent>
Expand All @@ -143,7 +171,6 @@ export class Tooltip extends React.Component {
dialog,
children,
inversed,
interactive,
interactiveBorder,
placement,
className,
Expand All @@ -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 = () => (
<div
id={this.id}
Expand All @@ -173,16 +206,14 @@ export class Tooltip extends React.Component {
className
)}
style={tooltipStyle}
onMouseEnter={() => (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}
>
<div className="ds-c-tooltip__arrow" data-popper-arrow />
<div className="ds-c-tooltip__content ds-base">{children}</div>
{interactive && (
{!dialog && (
<div className="ds-c-tooltip__interactive-border" style={interactiveBorderStyle} />
)}
</div>
Expand All @@ -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()}
Expand All @@ -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 (
<div>
<span {...eventHandlers}>
{this.renderTrigger()}
{this.renderContent()}
</div>
</span>
);
}
}
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down
Loading

0 comments on commit b576cc1

Please sign in to comment.