Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ When this option is enabled, the following arguments are become irrelevant: `@po

Optional. By default, tooltips will be positioned next to the element that caused them to display. When this argument is specified, the tooltip will still show when mousing over the `@element`, but will be positioned next to a _different_ element.

#### `@followMouse`

Note: You must be opted in to `@usePopover` to use `@followMouse`

Optional. Instead of a statically positioned tooltip, make the tooltip follow the position of the mouse when it is inside of the tooltipper, anchored to the axis the tooltip is positioned on. (ie. if the tooltip on the right hand side of the tooltipper, then it will only move on the Y axis)

### API

#### `isLoading`
Expand Down
1 change: 1 addition & 0 deletions demo-app/app.gts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Router.map(function () {
this.route('tether');
this.route('use-click');
this.route('use-focus');
this.route('follow-mouse');
});

export class App extends EmberApp {
Expand Down
1 change: 1 addition & 0 deletions demo-app/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
@import 'tables.css';
@import 'util.css';
@import 'tether.css';
@import 'follow-mouse.css';
38 changes: 38 additions & 0 deletions demo-app/styles/follow-mouse.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.follow-mouse-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 100px;
}

.follow-mouse-tooltipper {
width: 200px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
}

.follow-mouse-tooltip {
position: fixed;
position-try:
flip-inline,
flip-block,
flip-inline flip-block;
}

.follow-mouse-tooltip-top {
position-area: top;
}

.follow-mouse-tooltip-bottom {
position-area: bottom;
}

.follow-mouse-tooltip-left {
position-area: left;
}

.follow-mouse-tooltip-right {
position-area: right;
}
6 changes: 6 additions & 0 deletions demo-app/templates/application.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ import '../styles/app.css';
<LinkTo @route="use-focus">
Use Focus
</LinkTo>

|

<LinkTo @route="follow-mouse">
Follow mouse
</LinkTo>
</p>

{{outlet}}
Expand Down
54 changes: 54 additions & 0 deletions demo-app/templates/follow-mouse.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Tooltip from '#src/components/tooltip';

<template>
<p>
Move the tooltip with the mouse, anchored to the axis matching the edge it
is on
</p>

<div class="follow-mouse-container">
<div class="follow-mouse-tooltipper">
Top
<Tooltip
@usePopover={{true}}
@followMouse={{true}}
class="follow-mouse-tooltip follow-mouse-tooltip-top"
>
Hello World
</Tooltip>
</div>

<div class="follow-mouse-tooltipper">
Bottom
<Tooltip
@usePopover={{true}}
@followMouse={{true}}
class="follow-mouse-tooltip follow-mouse-tooltip-bottom"
>
Hello World
</Tooltip>
</div>

<div class="follow-mouse-tooltipper">
Left
<Tooltip
@usePopover={{true}}
@followMouse={{true}}
class="follow-mouse-tooltip follow-mouse-tooltip-left"
>
Hello World
</Tooltip>
</div>

<div class="follow-mouse-tooltipper">
Right
<Tooltip
@usePopover={{true}}
@followMouse={{true}}
class="follow-mouse-tooltip follow-mouse-tooltip-right"
>
Hello World
</Tooltip>
</div>
</div>
</template>
70 changes: 67 additions & 3 deletions src/components/tooltip.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { waitFor } from '@ember/test-waiters';
import { waitForAnimation } from '@zestia/animation-utils';
import autoPosition from '../utils/auto-position.js';
import getPositionArea from '../utils/position-area.js';
import getSide from '../utils/get-side.js';
import Component from '@glimmer/component';
const { max } = Math;

Expand All @@ -27,6 +28,7 @@ export default class TooltipComponent extends Component {
@tracked tooltipCoords = [0, 0];
@tracked tooltipElement;
@tracked tooltipPosition;
@tracked tooltipSide;

hideTimer;
isOverTooltipElement;
Expand All @@ -51,9 +53,17 @@ export default class TooltipComponent extends Component {
}

get tooltipStyle() {
const [x, y] = this.tooltipCoords;
if (!this.args.usePopover) {
const [x, y] = this.tooltipCoords;

return htmlSafe(`top: ${y}px; left: ${x}px`);
return htmlSafe(`top: ${y}px; left: ${x}px`);
}

if (this.shouldFollowMouse) {
return htmlSafe(`translate: var(--offset-x, 0px) var(--offset-y, 0px)`);
}

return '';
Comment on lines 55 to +66

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not a fan of this, but also not a fan of what it would take to not apply this.tooltipStyle in the template.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

as in I just want to return null from the getter 😂

}

get hideDelay() {
Expand Down Expand Up @@ -173,6 +183,14 @@ export default class TooltipComponent extends Component {
return getPosition(this.positionElement, window, this.columns, this.rows);
}

get shouldFollowMouse() {
if (this.args.useClick) {
return false;
}

return this.args.followMouse && this.args.usePopover;
}

handleMouseEnterTooltipperElement = () => {
this.#hideFocusOnlyTooltip();
this.isOverTooltipperElement = true;
Expand All @@ -185,6 +203,18 @@ export default class TooltipComponent extends Component {
this.#scheduleHideTooltip();
};

handleMouseMoveTooltipperElement = (event) => {
if (!this.tooltipElement) {
return;
}

if (this.tooltipElement.contains(event.target)) {
return;
}

this.#followMouse(event);
};

handleClickTooltipperElement = (event) => {
event.stopPropagation();
this.tooltipService.hideAllTooltips();
Expand Down Expand Up @@ -234,6 +264,26 @@ export default class TooltipComponent extends Component {
this.#showTooltip();
};

#followMouse() {
const tooltipperRect = this.tooltipperElement.getBoundingClientRect();

let offsetX = 0;
let offsetY = 0;

if (this.tooltipSide === 'top' || this.tooltipSide === 'bottom') {
offsetX =
event.clientX - (tooltipperRect.left + tooltipperRect.width / 2);
}

if (this.tooltipSide === 'left' || this.tooltipSide === 'right') {
offsetY =
event.clientY - (tooltipperRect.top + tooltipperRect.height / 2);
}

this.tooltipElement.style.setProperty(`--offset-x`, `${offsetX}px`);
this.tooltipElement.style.setProperty(`--offset-y`, `${offsetY}px`);
}

async #load() {
const start = Date.now();

Expand Down Expand Up @@ -433,13 +483,18 @@ export default class TooltipComponent extends Component {
return autoPosition(this.referencePosition);
}

#getTooltipSide() {
return getSide(this.tooltipperElement, this.tooltipElement);
}

#tether() {
if (!this.positionElement) {
return;
}

this.tooltipCoords = this.#getTooltipCoords();
this.tooltipPosition = this.#getTooltipPosition();
this.tooltipSide = this.#getTooltipSide();

this.tetherID = requestAnimationFrame(this.#tether.bind(this));
}
Expand Down Expand Up @@ -511,6 +566,10 @@ export default class TooltipComponent extends Component {
this.#add(el, 'focus', this.handleFocusTooltipperElement);
this.#add(el, 'blur', this.handleBlurTooltipperElement);
}

if (this.shouldFollowMouse) {
this.#add(el, 'mousemove', this.handleMouseMoveTooltipperElement);
}
}

return () => {
Expand All @@ -527,6 +586,10 @@ export default class TooltipComponent extends Component {
this.#rem(el, 'focus', this.handleFocusTooltipperElement);
this.#rem(el, 'blur', this.handleBlurTooltipperElement);
}

if (this.shouldFollowMouse) {
this.#rem(el, 'mousemove', this.handleMouseMoveTooltipperElement);
}
}
};
});
Expand Down Expand Up @@ -600,9 +663,10 @@ export default class TooltipComponent extends Component {
class="tooltip"
data-showing="{{this.shouldShowTooltip}}"
data-position={{this.tooltipPosition}}
data-side={{this.tooltipSide}}
data-sticky="{{this.isSticky}}"
id={{this.id}}
style={{unless @usePopover this.tooltipStyle}}
style={{this.tooltipStyle}}
role="tooltip"
aria-live="polite"
popover={{if @usePopover "manual"}}
Expand Down
34 changes: 34 additions & 0 deletions src/utils/get-side.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default function getSide(anchor, popover) {
const anchorRect = anchor.getBoundingClientRect();
const popoverRect = popover.getBoundingClientRect();

const popoverEdge = {
top: Math.ceil(popoverRect.top),
bottom: Math.ceil(popoverRect.bottom),
left: Math.ceil(popoverRect.left),
right: Math.ceil(popoverRect.right)
};

const anchorEdge = {
top: Math.ceil(anchorRect.top),
bottom: Math.ceil(anchorRect.bottom),
left: Math.ceil(anchorRect.left),
right: Math.ceil(anchorRect.right)
};

if (popoverEdge.bottom <= anchorEdge.top) {
return 'top';
}

if (popoverEdge.top >= anchorEdge.bottom) {
return 'bottom';
}

if (popoverEdge.right <= anchorEdge.left) {
return 'left';
}

if (popoverEdge.left >= anchorEdge.right) {
return 'right';
}
}
Loading
Loading