Improve tooltip positioning

Signed-off-by: Michael Weimann <michaelw@matrix.org>
This commit is contained in:
Michael Weimann 2022-05-09 15:58:16 +02:00 committed by Michael Weimann
parent 1e73184b78
commit 7ed3089434
16 changed files with 112 additions and 68 deletions

View file

@ -25,7 +25,9 @@ limitations under the License.
z-index: unset; z-index: unset;
width: max-content; width: max-content;
left: 72px; left: 72px;
top: 0; // top edge starting at 50 % of parent - 50 % of itself -> centered vertically
top: 50%;
transform: translateY(-50%);
} }
.mx_MiniAvatarUploader_indicator { .mx_MiniAvatarUploader_indicator {

View file

@ -70,8 +70,6 @@ limitations under the License.
font-weight: 500; font-weight: 500;
max-width: 300px; max-width: 300px;
word-break: break-word; word-break: break-word;
margin-left: 6px;
margin-right: 6px;
background-color: #21262C; // Same on both themes background-color: #21262C; // Same on both themes
color: $accent-fg-color; color: $accent-fg-color;

View file

@ -406,10 +406,6 @@ limitations under the License.
} }
} }
.mx_RoomSublist_addRoomTooltip {
margin-top: -3px;
}
.mx_RoomSublist_skeletonUI { .mx_RoomSublist_skeletonUI {
position: relative; position: relative;
margin-left: 4px; margin-left: 4px;

View file

@ -64,6 +64,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload"; import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { Alignment } from "../views/elements/Tooltip";
interface IProps { interface IProps {
space: Room; space: Room;
@ -583,7 +584,7 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu
Button = AccessibleTooltipButton; Button = AccessibleTooltipButton;
props = { props = {
tooltip: _t("Select a room below first"), tooltip: _t("Select a room below first"),
yOffset: -40, alignment: Alignment.Top,
}; };
} }

View file

@ -60,7 +60,6 @@ export const BetaPill = ({
</div> </div>
</div>} </div>}
onClick={onClick} onClick={onClick}
yOffset={-10}
> >
{ _t("Beta") } { _t("Beta") }
</AccessibleTooltipButton>; </AccessibleTooltipButton>;

View file

@ -136,7 +136,6 @@ const Entry: React.FC<IEntryProps> = ({ room, type, content, matrixClient: cli,
className="mx_ForwardList_roomButton" className="mx_ForwardList_roomButton"
onClick={jumpToRoom} onClick={jumpToRoom}
title={_t("Open room")} title={_t("Open room")}
yOffset={-20}
alignment={Alignment.Top} alignment={Alignment.Top}
> >
<DecoratedRoomAvatar room={room} avatarSize={32} /> <DecoratedRoomAvatar room={room} avatarSize={32} />
@ -151,7 +150,6 @@ const Entry: React.FC<IEntryProps> = ({ room, type, content, matrixClient: cli,
onClick={send} onClick={send}
disabled={disabled} disabled={disabled}
title={title} title={title}
yOffset={-20}
alignment={Alignment.Top} alignment={Alignment.Top}
> >
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div> <div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>

View file

@ -26,7 +26,6 @@ interface IProps extends React.ComponentProps<typeof AccessibleButton> {
label?: string; label?: string;
tooltipClassName?: string; tooltipClassName?: string;
forceHide?: boolean; forceHide?: boolean;
yOffset?: number;
alignment?: Alignment; alignment?: Alignment;
onHover?: (hovering: boolean) => void; onHover?: (hovering: boolean) => void;
onHideTooltip?(ev: SyntheticEvent): void; onHideTooltip?(ev: SyntheticEvent): void;
@ -76,13 +75,12 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
render() { render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, onHideTooltip, const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
...props } = this.props; ...props } = this.props;
const tip = this.state.hover && <Tooltip const tip = this.state.hover && <Tooltip
tooltipClassName={tooltipClassName} tooltipClassName={tooltipClassName}
label={tooltip || title} label={tooltip || title}
yOffset={yOffset}
alignment={alignment} alignment={alignment}
/>; />;
return ( return (

View file

@ -51,7 +51,7 @@ const FacePile: FC<IProps> = ({ members, faceSize, overflow, tooltip, children,
return <div {...props} className="mx_FacePile"> return <div {...props} className="mx_FacePile">
{ tooltip ? ( { tooltip ? (
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}> <TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip}>
{ pileContents } { pileContents }
</TextWithTooltip> </TextWithTooltip>
) : ( ) : (

View file

@ -23,8 +23,6 @@ import classNames from 'classnames';
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
const MIN_TOOLTIP_HEIGHT = 25;
export enum Alignment { export enum Alignment {
Natural, // Pick left or right Natural, // Pick left or right
Left, Left,
@ -33,7 +31,6 @@ export enum Alignment {
Bottom, // Centered Bottom, // Centered
InnerBottom, // Inside the target, at the bottom InnerBottom, // Inside the target, at the bottom
TopRight, // On top of the target, right aligned TopRight, // On top of the target, right aligned
TopCenter, // On top of the target, center aligned
} }
export interface ITooltipProps { export interface ITooltipProps {
@ -48,7 +45,6 @@ export interface ITooltipProps {
// the react element to put into the tooltip // the react element to put into the tooltip
label: React.ReactNode; label: React.ReactNode;
alignment?: Alignment; // defaults to Natural alignment?: Alignment; // defaults to Natural
yOffset?: number;
// id describing tooltip // id describing tooltip
// used to associate tooltip with target for a11y // used to associate tooltip with target for a11y
id?: string; id?: string;
@ -66,7 +62,6 @@ export default class Tooltip extends React.Component<ITooltipProps> {
public static readonly defaultProps = { public static readonly defaultProps = {
visible: true, visible: true,
yOffset: 0,
alignment: Alignment.Natural, alignment: Alignment.Natural,
}; };
@ -102,50 +97,49 @@ export default class Tooltip extends React.Component<ITooltipProps> {
// positioned, also taking into account any window zoom // positioned, also taking into account any window zoom
private updatePosition(style: CSSProperties) { private updatePosition(style: CSSProperties) {
const parentBox = this.parent.getBoundingClientRect(); const parentBox = this.parent.getBoundingClientRect();
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
} else {
// The tooltip is larger than the parent height: figure out what offset
// we need so that we're still centered.
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
const width = UIStore.instance.windowWidth; const width = UIStore.instance.windowWidth;
const spacing = 6;
const parentWidth = ( const parentWidth = (
this.props.maxParentWidth this.props.maxParentWidth
? Math.min(parentBox.width, this.props.maxParentWidth) ? Math.min(parentBox.width, this.props.maxParentWidth)
: parentBox.width : parentBox.width
); );
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.scrollY; const baseTop = parentBox.top + window.scrollY;
const top = baseTop + offset; const centerTop = parentBox.top + window.scrollY + (parentBox.height / 2);
const right = width - parentBox.left - window.scrollX; const right = width - parentBox.left - window.scrollX;
const left = parentBox.right + window.scrollX; const left = parentBox.right + window.scrollX;
const horizontalCenter = ( const horizontalCenter = (
parentBox.left - window.scrollX + (parentWidth / 2) parentBox.left - window.scrollX + (parentWidth / 2)
); );
switch (this.props.alignment) { switch (this.props.alignment) {
case Alignment.Natural: case Alignment.Natural:
if (parentBox.right > width / 2) { if (parentBox.right > width / 2) {
style.right = right; style.right = right + spacing;
style.top = top; style.top = centerTop;
style.transform = "translateY(-50%)";
break; break;
} }
// fall through to Right // fall through to Right
case Alignment.Right: case Alignment.Right:
style.left = left; style.left = left + spacing;
style.top = top; style.top = centerTop;
style.transform = "translateY(-50%)";
break; break;
case Alignment.Left: case Alignment.Left:
style.right = right; style.right = right + spacing;
style.top = top; style.top = centerTop;
style.transform = "translateY(-50%)";
break; break;
case Alignment.Top: case Alignment.Top:
style.top = baseTop - 16; style.top = baseTop - spacing;
style.left = horizontalCenter; style.left = horizontalCenter;
style.transform = "translate(-50%, -100%)";
break; break;
case Alignment.Bottom: case Alignment.Bottom:
style.top = baseTop + parentBox.height; style.top = baseTop + parentBox.height + spacing;
style.left = horizontalCenter; style.left = horizontalCenter;
style.transform = "translate(-50%)";
break; break;
case Alignment.InnerBottom: case Alignment.InnerBottom:
style.top = baseTop + parentBox.height - 50; style.top = baseTop + parentBox.height - 50;
@ -153,14 +147,10 @@ export default class Tooltip extends React.Component<ITooltipProps> {
style.transform = "translate(-50%)"; style.transform = "translate(-50%)";
break; break;
case Alignment.TopRight: case Alignment.TopRight:
style.top = baseTop - 5; style.top = baseTop - spacing;
style.right = width - parentBox.right - window.scrollX; style.right = width - parentBox.right - window.scrollX;
style.transform = "translate(5px, -100%)"; style.transform = "translateY(-100%)";
break; break;
case Alignment.TopCenter:
style.top = baseTop - 5;
style.left = horizontalCenter;
style.transform = "translate(-50%, -100%)";
} }
return style; return style;

View file

@ -34,7 +34,6 @@ const TooltipTarget: React.FC<IProps> = ({
id, id,
label, label,
alignment, alignment,
yOffset,
tooltipClassName, tooltipClassName,
maxParentWidth, maxParentWidth,
...rest ...rest
@ -51,7 +50,6 @@ const TooltipTarget: React.FC<IProps> = ({
className={className} className={className}
tooltipClassName={tooltipClassName} tooltipClassName={tooltipClassName}
label={label} label={label}
yOffset={yOffset}
alignment={alignment} alignment={alignment}
visible={isVisible} visible={isVisible}
maxParentWidth={maxParentWidth} maxParentWidth={maxParentWidth}

View file

@ -166,7 +166,6 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
title={openTitle} title={openTitle}
forceHide={!(isPinned || isMaximised)} forceHide={!(isPinned || isMaximised)}
disabled={isPinned || isMaximised} disabled={isPinned || isMaximised}
yOffset={-48}
> >
<WidgetAvatar app={app} /> <WidgetAvatar app={app} />
<span>{ name }</span> <span>{ name }</span>
@ -178,7 +177,6 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
isExpanded={menuDisplayed} isExpanded={menuDisplayed}
onClick={openMenu} onClick={openMenu}
title={_t("Options")} title={_t("Options")}
yOffset={-24}
/> } /> }
<AccessibleTooltipButton <AccessibleTooltipButton
@ -186,13 +184,11 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
onClick={togglePin} onClick={togglePin}
title={pinTitle} title={pinTitle}
disabled={cannotPin} disabled={cannotPin}
yOffset={-24}
/> />
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_RoomSummaryCard_app_maximiseToggle" className="mx_RoomSummaryCard_app_maximiseToggle"
onClick={toggleMaximised} onClick={toggleMaximised}
title={maximiseTitle} title={maximiseTitle}
yOffset={-24}
/> />
{ contextMenu } { contextMenu }

View file

@ -421,7 +421,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
recordingTooltip = <Tooltip recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", { seconds: secondsLeft })} label={_t("%(seconds)ss left", { seconds: secondsLeft })}
alignment={Alignment.Top} alignment={Alignment.Top}
yOffset={-50}
/>; />;
} }

View file

@ -233,7 +233,7 @@ interface ReadReceiptPersonProps extends IReadReceiptProps {
function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick }: ReadReceiptPersonProps) { function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick }: ReadReceiptPersonProps) {
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({ const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
alignment: Alignment.TopCenter, alignment: Alignment.Top,
tooltipClassName: "mx_ReadReceiptGroup_person--tooltip", tooltipClassName: "mx_ReadReceiptGroup_person--tooltip",
label: ( label: (
<> <>

View file

@ -39,8 +39,6 @@ import { MediaDeviceKindEnum } from "../../../../MediaDeviceHandler";
// height to get the max height of the video // height to get the max height of the video
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px) const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
const TOOLTIP_Y_OFFSET = -24;
const CONTROLS_HIDE_DELAY = 2000; const CONTROLS_HIDE_DELAY = 2000;
interface IButtonProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title"> { interface IButtonProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title"> {
@ -69,7 +67,6 @@ const CallViewToggleButton: React.FC<IButtonProps> = ({
className={classes} className={classes}
title={isOn ? onLabel : offLabel} title={isOn ? onLabel : offLabel}
alignment={Alignment.Top} alignment={Alignment.Top}
yOffset={TOOLTIP_Y_OFFSET}
{...props} {...props}
> >
{ children } { children }
@ -267,7 +264,6 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
isExpanded={this.state.showDialpad} isExpanded={this.state.showDialpad}
title={_t("Dialpad")} title={_t("Dialpad")}
alignment={Alignment.Top} alignment={Alignment.Top}
yOffset={TOOLTIP_Y_OFFSET}
/> } /> }
<CallViewDropdownButton <CallViewDropdownButton
state={!this.props.buttonsState.micMuted} state={!this.props.buttonsState.micMuted}
@ -306,14 +302,12 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
isExpanded={this.state.showMoreMenu} isExpanded={this.state.showMoreMenu}
title={_t("More")} title={_t("More")}
alignment={Alignment.Top} alignment={Alignment.Top}
yOffset={TOOLTIP_Y_OFFSET}
/> } /> }
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_CallViewButtons_button mx_CallViewButtons_button_hangup" className="mx_CallViewButtons_button mx_CallViewButtons_button_hangup"
onClick={this.props.handlers.onHangupClick} onClick={this.props.handlers.onHangupClick}
title={_t("Hangup")} title={_t("Hangup")}
alignment={Alignment.Top} alignment={Alignment.Top}
yOffset={TOOLTIP_Y_OFFSET}
/> />
</div> </div>
); );

View file

@ -30,7 +30,6 @@ describe('<TooltipTarget />', () => {
"className": 'test className', "className": 'test className',
"tooltipClassName": 'test tooltipClassName', "tooltipClassName": 'test tooltipClassName',
"label": 'test label', "label": 'test label',
"yOffset": 1,
"alignment": Alignment.Left, "alignment": Alignment.Left,
"id": 'test id', "id": 'test id',
'data-test-id': 'test', 'data-test-id': 'test',
@ -64,13 +63,17 @@ describe('<TooltipTarget />', () => {
expect(getVisibleTooltip()).toBeFalsy(); expect(getVisibleTooltip()).toBeFalsy();
}); });
it('displays tooltip on mouseover', () => { for (const alignment in Alignment) {
const wrapper = getComponent(); if (isNaN(Number(alignment))) {
it(`displays ${alignment} aligned tooltip on mouseover`, () => {
const wrapper = getComponent({ alignment: Alignment[alignment] });
act(() => { act(() => {
Simulate.mouseOver(wrapper); Simulate.mouseOver(wrapper);
}); });
expect(getVisibleTooltip()).toMatchSnapshot(); expect(getVisibleTooltip()).toMatchSnapshot();
}); });
}
}
it('hides tooltip on mouseleave', () => { it('hides tooltip on mouseleave', () => {
const wrapper = getComponent(); const wrapper = getComponent();

View file

@ -1,9 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<TooltipTarget /> displays tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="right: 1024px; top: -26px; display: block;" style="top: 6px; left: 0px; transform: translate(-50%); display: block;"
>
<div
class="mx_Tooltip_chevron"
/>
test label
</div>
`;
exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="top: -50px; left: 0px; transform: translate(-50%); display: block;"
>
<div
class="mx_Tooltip_chevron"
/>
test label
</div>
`;
exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="right: 1030px; top: 0px; transform: translateY(-50%); display: block;"
>
<div
class="mx_Tooltip_chevron"
/>
test label
</div>
`;
exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="left: 6px; top: 0px; transform: translateY(-50%); display: block;"
>
<div
class="mx_Tooltip_chevron"
/>
test label
</div>
`;
exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="left: 6px; top: 0px; transform: translateY(-50%); display: block;"
>
<div
class="mx_Tooltip_chevron"
/>
test label
</div>
`;
exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="top: -6px; left: 0px; transform: translate(-50%, -100%); display: block;"
>
<div
class="mx_Tooltip_chevron"
/>
test label
</div>
`;
exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
style="top: -6px; right: 1024px; transform: translateY(-100%); display: block;"
> >
<div <div
class="mx_Tooltip_chevron" class="mx_Tooltip_chevron"