blob: d5c53357efb6ee6dd3242951d21e78b21f7c5ea2 [file]
// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m from 'mithril';
import {type MountOptions, Portal} from './portal';
import {bindEventListener} from '../base/dom_utils';
import {DisposableStack} from '../base/disposable_stack';
import type {HTMLAttrs} from './common';
import {
createPopper,
type Instance as PopperInstance,
type VirtualElement,
} from '@popperjs/core';
import {classNames} from '../base/classnames';
import {type Point2D, Vector2D} from '../base/geom';
import {PopupPosition} from './popup';
export interface CursorTooltipAttrs extends HTMLAttrs {
// Which side of the cursor to place the tooltip. Defaults to Right.
readonly position?: PopupPosition;
// Distance in px between the tooltip and the cursor. Default = 8.
readonly offset?: number;
}
class VElement implements VirtualElement {
private pos = new Vector2D({x: 0, y: 0});
getBoundingClientRect() {
return new DOMRect(this.pos.x, this.pos.y, 0, 0);
}
setPosition(pos: Point2D) {
this.pos = new Vector2D(pos);
}
}
// Keep track of the mouse position in the document so that the cursor can be
// initially drawn in the correct place, before it's received any mouse events.
let globalMousePos: Point2D;
document.addEventListener('mousemove', (e) => {
globalMousePos = new Vector2D({x: e.clientX, y: e.clientY});
});
/**
* Provides a little tooltip that's permanently attached to the mouse.
*
* Any children are rendered inside - the tooltip is displayed to the bottom
* right if there is room.
*/
export class CursorTooltip implements m.ClassComponent<CursorTooltipAttrs> {
private readonly trash = new DisposableStack();
private readonly virtualElement = new VElement();
private popper?: PopperInstance;
view({children, attrs}: m.Vnode<CursorTooltipAttrs>) {
const {
className,
offset = 8,
position = PopupPosition.Right,
...rest
} = attrs;
return m(
Portal,
{
...rest,
className: classNames('pf-cursor-tooltip', className),
onBeforeContentMount: (dom: Element): MountOptions => {
const closestModal = dom.closest('.pf-overlay-container');
if (closestModal) {
return {container: closestModal};
}
return {container: undefined};
},
onContentMount: (portal) => {
this.virtualElement.setPosition(globalMousePos);
this.popper = createPopper(this.virtualElement, portal, {
placement: position,
modifiers: [
{
name: 'offset',
options: {
offset: [0, offset], // Shift away from cursor
},
},
],
});
},
onContentUnmount: () => {
this.popper?.destroy();
},
},
children,
);
}
oncreate(_: m.VnodeDOM<CursorTooltipAttrs>) {
this.trash.use(
bindEventListener(document, 'mousemove', (e) => {
this.virtualElement.setPosition({x: e.clientX, y: e.clientY});
this.popper?.update();
}),
);
}
onremove(_: m.VnodeDOM<CursorTooltipAttrs, this>) {
this.trash.dispose();
}
}