Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/usa-tooltip/src/index.js
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

To prevent the tooltip from closing when you move the mouse out of the tooltip trigger, we needed to:

  1. Target the usa-tooltip wrapper instead of the tooltip trigger, and
  2. Replace mouseout with mouseleave. mouseleave only triggers when the mouse has exited the element and all its descendents, whereas mouseout triggers with each element in the group. See reference below for more details:

"...mouseleave is fired when the pointer has exited the element and all of its descendants, whereas mouseout is fired when the pointer leaves the element or leaves one of the element's descendants (even if the pointer is still within the element)." 1

Footnotes

  1. https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseleave_event

Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ const tooltip = behavior(
showToolTip(body, trigger, trigger.dataset.position);
},
},
"mouseout focusout": {
focusout: {
[TOOLTIP_TRIGGER](e) {
const { body } = getTooltipElements(e.target);

Expand All @@ -407,6 +407,14 @@ const tooltip = behavior(
init(root) {
selectOrMatches(TOOLTIP, root).forEach((tooltipTrigger) => {
setUpAttributes(tooltipTrigger);

const { body, wrapper } = getTooltipElements(tooltipTrigger);
wrapper.addEventListener("mouseleave", () => hideToolTip(body));
});
},
teardown(root) {
selectOrMatches(TOOLTIP, root).forEach((tooltipWrapper) => {
tooltipWrapper.removeEventListener("mouseleave", hideToolTip);
});
},
setup: setUpAttributes,
Expand Down
59 changes: 53 additions & 6 deletions packages/usa-tooltip/src/styles/_usa-tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@
// Variables
$triangle-size: 5px;

/// Create a spacer to increase target area for tooltip triangle.
///
/// @param {String} $direction - The direction of the tooltip; can be top, bottom, left, right.
///
/// @example
/// @include tooltip-spacer("top");
///
/// @output
/// .usa-tooltip__body--top::before {
/// top: 100%;
/// height: 5px;
/// left: 0;
/// right: 0;
/// }
@mixin tooltip-spacer($direction) {
&::before {
#{$direction}: 100%;

@if ($direction == "left") or ($direction == "right") {
bottom: 0;
top: 0;
width: $triangle-size;
} @else {
height: $triangle-size;
left: 0;
right: 0;
}
}
}

/* Tooltips */
.usa-tooltip {
display: inline-block;
Expand All @@ -28,19 +58,17 @@ $triangle-size: 5px;
font-size: size("ui", $theme-tooltip-font-size);
opacity: 0; // Required for recalculating position.
padding: units(1);
pointer-events: none;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

We needed to remove the pointer-event styles here because they were causing the tooltip to be removed from the usa-tooltip hit boundary. This caused mouseleave to be triggered when hovering over the tooltip.

width: auto;
white-space: pre;
z-index: 100000;
position: absolute;
/* positioning is completed with JS */

&:after {
&::after {
content: "";
display: block;
width: 0;
height: 0;
pointer-events: none;
border-left: $triangle-size solid transparent;
border-right: $triangle-size solid transparent;
border-top: $triangle-size solid color($theme-tooltip-background-color);
Expand All @@ -49,6 +77,15 @@ $triangle-size: 5px;
left: 50%;
margin-left: -$triangle-size;
}

// This pseudo element fills the gap between the tooltip trigger and body.
// Filling this gap allows the tooltip to stay open when the pointer moves
// from the tooltip trigger to the body.
&::before {
content: "";
display: block;
position: absolute;
}
}
Copy link
Contributor Author

@amyleadem amyleadem May 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

I added these before pseudo elements to fill the gap between the tooltip trigger and tooltip. Filling this gap prevents mouseleave from triggering when the mouse enters that gap when trying to hover over the tooltip.

image


.usa-tooltip__body--wrap {
Expand All @@ -66,8 +103,14 @@ $triangle-size: 5px;
opacity: 1;
}

.usa-tooltip__body--top {
@include tooltip-spacer("top");
}

.usa-tooltip__body--bottom {
&:after {
@include tooltip-spacer("bottom");

&::after {
border-left: $triangle-size solid transparent;
border-right: $triangle-size solid transparent;
border-bottom: $triangle-size solid color($theme-tooltip-background-color);
Expand All @@ -78,7 +121,9 @@ $triangle-size: 5px;
}

.usa-tooltip__body--right {
&:after {
@include tooltip-spacer("right");

&::after {
border-top: $triangle-size solid transparent;
border-bottom: $triangle-size solid transparent;
border-right: $triangle-size solid color($theme-tooltip-background-color);
Expand All @@ -92,7 +137,9 @@ $triangle-size: 5px;
}

.usa-tooltip__body--left {
&:after {
@include tooltip-spacer("left");

&::after {
border-top: $triangle-size solid transparent;
border-bottom: $triangle-size solid transparent;
border-left: $triangle-size solid color($theme-tooltip-background-color);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="usa-prose measure-5 padding-2 border-1px border-gray-20 margin-bottom-4">
<p class="font-body-lg text-bold margin-top-0">Test that mouse event listeners target the correct tooltip when no parent wrapper is present</p>
<p>To test this, confirm that mouseover/mouseleave events work as expected when there is no parent wrapper on `usa-tooltip` elements.</p>
</div>

<h2>Tooltip with no wrapper</h2>

<button type="button" class="usa-button usa-tooltip" data-position="left" title="Show on left">Show on left</button>

<h2>Another tooltip with no wrapper</h2>

<a href="#" class="usa-tooltip" data-position="bottom" title="Show on bottom">Show on bottom</button>
34 changes: 34 additions & 0 deletions packages/usa-tooltip/src/test/tooltips.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ const tests = [
];

const EVENTS = {
mouseover(el) {
const mouseoverEvent = new Event("mouseover", {
bubbles: true,
cancelable: true,
});

el.dispatchEvent(mouseoverEvent);
},
mouseleave(el) {
const mouseleaveEvent = new Event("mouseleave", {
cancelable: true,
});

el.dispatchEvent(mouseleaveEvent);
},
escape(el) {
const escapeKeyEvent = new KeyboardEvent("keydown", {
key: "Escape",
Expand All @@ -26,12 +41,14 @@ tests.forEach(({ name, selector: containerSelector }) => {
const { body } = document;
let tooltipBody;
let tooltipTrigger;
let tooltipWrapper;

beforeEach(() => {
body.innerHTML = TEMPLATE;
tooltip.on(containerSelector());
tooltipBody = body.querySelector(".usa-tooltip__body");
tooltipTrigger = body.querySelector(".usa-tooltip__trigger");
tooltipWrapper = body.querySelector(".usa-tooltip");
});

afterEach(() => {
Expand Down Expand Up @@ -64,6 +81,23 @@ tests.forEach(({ name, selector: containerSelector }) => {
assert.strictEqual(tooltipBody.classList.contains("is-set"), false);
});

it("tooltip is visible on mouseover", () => {
EVENTS.mouseover(tooltipTrigger);
assert.strictEqual(tooltipBody.classList.contains("is-set"), true);
});

it("tooltip is hidden on mouseleave", () => {
EVENTS.mouseover(tooltipTrigger);
EVENTS.mouseleave(tooltipWrapper);
assert.strictEqual(tooltipBody.classList.contains("is-set"), false);
});

it("tooltip content is hoverable", () => {
EVENTS.mouseover(tooltipTrigger);
EVENTS.mouseover(tooltipBody);
assert.strictEqual(tooltipBody.classList.contains("is-set"), true);
});

it("tooltip is hidden on escape keydown", () => {
tooltipTrigger.focus();
EVENTS.escape(tooltipTrigger);
Expand Down
3 changes: 3 additions & 0 deletions packages/usa-tooltip/src/usa-tooltip.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Component from "./usa-tooltip.twig";
import TestComponent from "./test/test-patterns/test-usa-tooltip-utilities.twig";
import TestNoWrapperComponent from "./test/test-patterns/test-usa-tooltip-no-wrapper.twig";
import UtilityComponent from "./usa-tooltip--utilities.twig";

export default {
Expand All @@ -8,8 +9,10 @@ export default {

const Template = (args) => Component(args);
const TestTemplate = (args) => TestComponent(args);
const TestNoWrapperTemplate = (args) => TestNoWrapperComponent(args);
const UtilityTemplate = (args) => UtilityComponent(args);

export const Tooltip = Template.bind({});
export const TooltipUtility = UtilityTemplate.bind({});
export const Test = TestTemplate.bind({});
export const TestNoWrapper = TestNoWrapperTemplate.bind({});