Skip to content

Commit d813de5

Browse files
wolfibDevtools-frontend LUCI CQ
authored andcommitted
Tooltip: improve positioning in fallback case
Simple tooltips try to position themselves to the bottom center or top center of their anchor. If neither of those options lie fully within the viewport, pick a fallback position: - Check whether top or bottom position is better, by picking the option which is vertically out-of-bounds by a smaller amount. - The tooltip position is then adjusted by moving it into the viewport. This means the tooltip is allowed to cover its anchor in the fallback case. Rich Tooltips try to position themselves to the bottom/top left/right of their anchor. If none of these 4 positions lie fully within the viewport, a fallback position is calculated: - Check whether top or bottom position is better, by picking the option which is vertically out-of-bounds by a smaller amount. - Choose left/right according to the preferred order. - The tooltip position is then adjusted such that the tooltip stays in the same corner of the viewport, but is moved into the viewport. This means the tooltip is allowed to cover its anchor in the fallback case. Fixed: 454879285 Change-Id: Ic3338c14ef8091ca4500f6fd290b3b0af1d78d5a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7105438 Commit-Queue: Wolfgang Beyer <[email protected]> Reviewed-by: Ergün Erdoğmuş <[email protected]> Auto-Submit: Wolfgang Beyer <[email protected]>
1 parent c2d3683 commit d813de5

File tree

2 files changed

+204
-45
lines changed

2 files changed

+204
-45
lines changed

front_end/ui/components/tooltips/Tooltip.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,4 +418,139 @@ describe('Tooltip', () => {
418418
assert.exists(tooltip3);
419419
assert.isTrue(tooltip3.open);
420420
});
421+
422+
describe('assigns the correct position', () => {
423+
const inspectorViewRect = {
424+
top: 0,
425+
bottom: 290,
426+
height: 290,
427+
left: 0,
428+
right: 500,
429+
width: 500,
430+
} as DOMRect;
431+
const anchorRect = {
432+
top: 100,
433+
bottom: 200,
434+
height: 100,
435+
left: 200,
436+
right: 400,
437+
width: 200,
438+
} as DOMRect;
439+
440+
it('for default postion bottom span right', () => {
441+
const currentPopoverRect = {
442+
height: 80,
443+
width: 160,
444+
} as DOMRect;
445+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip(
446+
{inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: []});
447+
assert.strictEqual(proposedRect.top, 200);
448+
assert.strictEqual(proposedRect.left, 200);
449+
});
450+
451+
it('for preferred postion bottom span left', () => {
452+
const currentPopoverRect = {
453+
height: 80,
454+
width: 160,
455+
} as DOMRect;
456+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({
457+
inspectorViewRect,
458+
anchorRect,
459+
currentPopoverRect,
460+
preferredPositions: [Tooltips.Tooltip.PositionOption.BOTTOM_SPAN_LEFT]
461+
});
462+
assert.strictEqual(proposedRect.top, 200);
463+
assert.strictEqual(proposedRect.left, 240);
464+
});
465+
466+
it('uses 2nd option from default order if 1st is impossible', () => {
467+
const currentPopoverRect = {
468+
height: 80,
469+
width: 350,
470+
} as DOMRect;
471+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip(
472+
{inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: []});
473+
assert.strictEqual(proposedRect.top, 200);
474+
assert.strictEqual(proposedRect.left, 50);
475+
});
476+
477+
it('uses 3rd option from default order if first 2 are impossible', () => {
478+
const currentPopoverRect = {
479+
height: 95,
480+
width: 160,
481+
} as DOMRect;
482+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip(
483+
{inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: []});
484+
assert.strictEqual(proposedRect.top, 5);
485+
assert.strictEqual(proposedRect.left, 200);
486+
});
487+
488+
it('uses 4th option from default order if first 3 are impossible', () => {
489+
const currentPopoverRect = {
490+
height: 95,
491+
width: 350,
492+
} as DOMRect;
493+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip(
494+
{inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: []});
495+
assert.strictEqual(proposedRect.top, 5);
496+
assert.strictEqual(proposedRect.left, 50);
497+
});
498+
499+
it('uses 4th option from preferred order if first 3 are impossible', () => {
500+
const currentPopoverRect = {
501+
height: 95,
502+
width: 350,
503+
} as DOMRect;
504+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({
505+
inspectorViewRect,
506+
anchorRect,
507+
currentPopoverRect,
508+
preferredPositions:
509+
[Tooltips.Tooltip.PositionOption.BOTTOM_SPAN_LEFT, Tooltips.Tooltip.PositionOption.TOP_SPAN_LEFT]
510+
});
511+
assert.strictEqual(proposedRect.top, 5);
512+
assert.strictEqual(proposedRect.left, 50);
513+
});
514+
515+
it('moves the rect into the viewport if all 4 options are impossible', () => {
516+
const currentPopoverRect = {
517+
height: 110,
518+
width: 440,
519+
} as DOMRect;
520+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip(
521+
{inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: []});
522+
assert.strictEqual(proposedRect.top, 0);
523+
assert.strictEqual(proposedRect.left, 60);
524+
});
525+
526+
it('for anchors in a corner of the viewport', () => {
527+
const anchorBottomLeftCorner = {
528+
top: 190,
529+
bottom: 290,
530+
height: 100,
531+
left: 0,
532+
right: 100,
533+
width: 100,
534+
} as DOMRect;
535+
const currentPopoverRect = {
536+
height: 100,
537+
width: 200,
538+
} as DOMRect;
539+
const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip(
540+
{inspectorViewRect, anchorRect: anchorBottomLeftCorner, currentPopoverRect, preferredPositions: []});
541+
assert.strictEqual(proposedRect.top, 90);
542+
assert.strictEqual(proposedRect.left, 0);
543+
});
544+
545+
it('moves a simple tooltip into the viewport', () => {
546+
const currentPopoverRect = {
547+
height: 95,
548+
width: 410,
549+
} as DOMRect;
550+
const proposedRect =
551+
Tooltips.Tooltip.proposedRectForSimpleTooltip({inspectorViewRect, anchorRect, currentPopoverRect});
552+
assert.strictEqual(proposedRect.top, 5);
553+
assert.strictEqual(proposedRect.left, 90);
554+
});
555+
});
421556
});

front_end/ui/components/tooltips/Tooltip.ts

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface PositioningParams {
2121
currentPopoverRect: DOMRect;
2222
}
2323

24-
enum PositionOption {
24+
export enum PositionOption {
2525
BOTTOM_SPAN_RIGHT = 'bottom-span-right',
2626
BOTTOM_SPAN_LEFT = 'bottom-span-left',
2727
TOP_SPAN_RIGHT = 'top-span-right',
@@ -66,29 +66,32 @@ const positioningUtils = {
6666
};
6767
},
6868
// Adjusts proposed rect so that the resulting popover is always inside the inspector view bounds.
69-
insetAdjustedRect:
70-
({inspectorViewRect, anchorRect, currentPopoverRect, proposedRect}:
71-
{inspectorViewRect: DOMRect, anchorRect: DOMRect, currentPopoverRect: DOMRect, proposedRect: ProposedRect}):
72-
ProposedRect => {
73-
if (inspectorViewRect.left > proposedRect.left) {
74-
proposedRect.left = inspectorViewRect.left;
75-
}
76-
77-
if (inspectorViewRect.right < proposedRect.left + currentPopoverRect.width) {
78-
proposedRect.left = inspectorViewRect.right - currentPopoverRect.width;
79-
}
80-
81-
if (proposedRect.top + currentPopoverRect.height > inspectorViewRect.bottom) {
82-
proposedRect.top = anchorRect.top - currentPopoverRect.height;
83-
}
84-
return proposedRect;
85-
},
69+
insetAdjustedRect: ({inspectorViewRect, currentPopoverRect, proposedRect}:
70+
{inspectorViewRect: DOMRect, currentPopoverRect: DOMRect, proposedRect: ProposedRect}):
71+
ProposedRect => {
72+
if (inspectorViewRect.left > proposedRect.left) {
73+
proposedRect.left = inspectorViewRect.left;
74+
}
75+
76+
if (inspectorViewRect.right < proposedRect.left + currentPopoverRect.width) {
77+
proposedRect.left = inspectorViewRect.right - currentPopoverRect.width;
78+
}
79+
80+
if (proposedRect.top < inspectorViewRect.top) {
81+
proposedRect.top = inspectorViewRect.top;
82+
}
83+
84+
if (proposedRect.top + currentPopoverRect.height > inspectorViewRect.bottom) {
85+
proposedRect.top = inspectorViewRect.bottom - currentPopoverRect.height;
86+
}
87+
return proposedRect;
88+
},
8689
isInBounds: ({inspectorViewRect, currentPopoverRect, proposedRect}:
8790
{inspectorViewRect: DOMRect, currentPopoverRect: DOMRect, proposedRect: ProposedRect}): boolean => {
88-
return inspectorViewRect.left < proposedRect.left &&
89-
proposedRect.left + currentPopoverRect.width < inspectorViewRect.right &&
90-
inspectorViewRect.top < proposedRect.top &&
91-
proposedRect.top + currentPopoverRect.height < inspectorViewRect.bottom;
91+
return inspectorViewRect.left <= proposedRect.left &&
92+
proposedRect.left + currentPopoverRect.width <= inspectorViewRect.right &&
93+
inspectorViewRect.top <= proposedRect.top &&
94+
proposedRect.top + currentPopoverRect.height <= inspectorViewRect.bottom;
9295
},
9396
isSameRect: (rect1: DOMRect|null, rect2: DOMRect|null): boolean => {
9497
if (!rect1 || !rect2) {
@@ -100,7 +103,7 @@ const positioningUtils = {
100103
}
101104
};
102105

103-
const proposedRectForRichTooltip = ({inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions}: {
106+
export const proposedRectForRichTooltip = ({inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions}: {
104107
inspectorViewRect: DOMRect,
105108
anchorRect: DOMRect,
106109
currentPopoverRect: DOMRect,
@@ -116,53 +119,74 @@ const proposedRectForRichTooltip = ({inspectorViewRect, anchorRect, currentPopov
116119
]),
117120
];
118121

119-
// Tries the positioning options in the order given by `uniqueOrder`.
120-
// If none of them work out, we default to showing the tooltip in the bottom right and adjust
121-
// its insets so that the tooltip is inside the inspector view bounds.
122-
for (const positionOption of uniqueOrder) {
123-
let proposedRect;
122+
const getProposedRectForPositionOption = (positionOption: PositionOption): ProposedRect => {
124123
switch (positionOption) {
125124
case PositionOption.BOTTOM_SPAN_RIGHT:
126-
proposedRect = positioningUtils.bottomSpanRight({anchorRect, currentPopoverRect});
127-
break;
125+
return positioningUtils.bottomSpanRight({anchorRect, currentPopoverRect});
128126
case PositionOption.BOTTOM_SPAN_LEFT:
129-
proposedRect = positioningUtils.bottomSpanLeft({anchorRect, currentPopoverRect});
130-
break;
127+
return positioningUtils.bottomSpanLeft({anchorRect, currentPopoverRect});
131128
case PositionOption.TOP_SPAN_RIGHT:
132-
proposedRect = positioningUtils.topSpanRight({anchorRect, currentPopoverRect});
133-
break;
129+
return positioningUtils.topSpanRight({anchorRect, currentPopoverRect});
134130
case PositionOption.TOP_SPAN_LEFT:
135-
proposedRect = positioningUtils.topSpanLeft({anchorRect, currentPopoverRect});
131+
return positioningUtils.topSpanLeft({anchorRect, currentPopoverRect});
136132
}
133+
};
134+
135+
// Tries the positioning options in the order given by `uniqueOrder`.
136+
for (const positionOption of uniqueOrder) {
137+
const proposedRect = getProposedRectForPositionOption(positionOption);
137138
if (positioningUtils.isInBounds({inspectorViewRect, currentPopoverRect, proposedRect})) {
138139
return proposedRect;
139140
}
140141
}
141142

142-
// If none of the options work above, we position to bottom right
143-
// and adjust the insets so that it does not go out of bounds.
144-
const proposedRect = positioningUtils.bottomSpanRight({anchorRect, currentPopoverRect});
145-
return positioningUtils.insetAdjustedRect({anchorRect, currentPopoverRect, inspectorViewRect, proposedRect});
143+
// If none of the options above work, we decide between top or bottom by which
144+
// option is fewer vertical pixels out of the viewport. We pick left/right
145+
// according to `uniqueOrder`. And finally we adjust the insets so that the
146+
// tooltip is not out of bounds.
147+
const bottomProposed = positioningUtils.bottomSpanRight({anchorRect, currentPopoverRect});
148+
const bottomVerticalOutOfBounds =
149+
Math.max(0, bottomProposed.top + currentPopoverRect.height - inspectorViewRect.bottom);
150+
const topProposed = positioningUtils.topSpanRight({anchorRect, currentPopoverRect});
151+
const topVerticalOutOfBounds = Math.max(0, inspectorViewRect.top - topProposed.top);
152+
const prefersBottom = bottomVerticalOutOfBounds <= topVerticalOutOfBounds;
153+
const fallbackOption = uniqueOrder.find(option => {
154+
if (prefersBottom) {
155+
return option === PositionOption.BOTTOM_SPAN_LEFT || option === PositionOption.BOTTOM_SPAN_RIGHT;
156+
}
157+
return option === PositionOption.TOP_SPAN_LEFT || option === PositionOption.TOP_SPAN_RIGHT;
158+
}) ??
159+
PositionOption.TOP_SPAN_RIGHT;
160+
const fallbackRect = getProposedRectForPositionOption(fallbackOption);
161+
return positioningUtils.insetAdjustedRect({currentPopoverRect, inspectorViewRect, proposedRect: fallbackRect});
146162
};
147163

148-
const proposedRectForSimpleTooltip =
164+
export const proposedRectForSimpleTooltip =
149165
({inspectorViewRect, anchorRect, currentPopoverRect}:
150166
{inspectorViewRect: DOMRect, anchorRect: DOMRect, currentPopoverRect: DOMRect}): ProposedRect => {
151167
// Default options are bottom centered & top centered.
152168
let proposedRect = positioningUtils.bottomCentered({anchorRect, currentPopoverRect});
153169
if (positioningUtils.isInBounds({inspectorViewRect, currentPopoverRect, proposedRect})) {
154170
return proposedRect;
155171
}
172+
const bottomVerticalOutOfBoundsAmount =
173+
Math.max(0, proposedRect.top + currentPopoverRect.height - inspectorViewRect.bottom);
156174

157175
proposedRect = positioningUtils.topCentered({anchorRect, currentPopoverRect});
158176
if (positioningUtils.isInBounds({inspectorViewRect, currentPopoverRect, proposedRect})) {
159177
return proposedRect;
160178
}
161-
162-
// The default options did not work out, so position it to bottom center
163-
// and adjust the insets to make sure that it does not go out of bounds.
164-
proposedRect = positioningUtils.bottomCentered({anchorRect, currentPopoverRect});
165-
return positioningUtils.insetAdjustedRect({anchorRect, currentPopoverRect, inspectorViewRect, proposedRect});
179+
const topVerticalOutOfBoundsAmount = Math.max(0, inspectorViewRect.top - proposedRect.top);
180+
181+
// The default options did not work out, so compare which option is fewer
182+
// pixels out of the viewport vertically. Pick the better option and
183+
// adjust the insets to make sure that the tooltip is not out of bounds.
184+
if (bottomVerticalOutOfBoundsAmount <= topVerticalOutOfBoundsAmount) {
185+
proposedRect = positioningUtils.bottomCentered({anchorRect, currentPopoverRect});
186+
} else {
187+
proposedRect = positioningUtils.topCentered({anchorRect, currentPopoverRect});
188+
}
189+
return positioningUtils.insetAdjustedRect({currentPopoverRect, inspectorViewRect, proposedRect});
166190
};
167191

168192
export type TooltipVariant = 'simple'|'rich';

0 commit comments

Comments
 (0)