Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
feat(tooltip): Adding side positioning options for plain tooltips.
Browse files Browse the repository at this point in the history
Side positioning is subject to the same validation rules as the other positions (that is, if the user specifies SIDE_START positioning but this would result in the tooltip colliding with the viewport, this positioning is ignored in favor of an option that would not collide with the viewport).

However, side positioning is *not* considered if the tooltip is left to detect positioning on its own -- it must be specified by the user.

PiperOrigin-RevId: 454919379
  • Loading branch information
sayris authored and copybara-github committed Jun 14, 2022
1 parent b18a873 commit ba9c296
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 32 deletions.
6 changes: 6 additions & 0 deletions packages/mdc-tooltip/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,18 @@ enum XPosition {
// Note: CENTER is not valid for rich tooltips.
CENTER = 2,
END = 3,
// SIDE_XXX positioning is only valid for plain tooltips.
SIDE_START = 4,
SIDE_END = 5,
}

enum YPosition {
DETECTED = 0,
ABOVE = 1,
BELOW = 2,
// SIDE positioning is only valid for plain tooltips with either SIDE_START or
// SIDE_END x positioning.
SIDE = 3,
}

/**
Expand Down
87 changes: 59 additions & 28 deletions packages/mdc-tooltip/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,6 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
this.anchorRect = this.adapter.getAnchorBoundingRect();
this.parentRect = this.adapter.getParentBoundingRect();
this.richTooltip ? this.positionRichTooltip() : this.positionPlainTooltip();

this.adapter.registerAnchorEventHandler('blur', this.anchorBlurHandler);
this.adapter.registerDocumentEventHandler(
'click', this.documentClickHandler);
Expand Down Expand Up @@ -640,12 +639,13 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
* Returns the distance value and a string indicating the x-axis transform-
* origin that should be used when animating the tooltip.
*/
private calculateXTooltipDistance(
anchorRect: DOMRect,
tooltipWidth: number): ({distance: number, xTransformOrigin: string}) {
private calculateXTooltipDistance(anchorRect: DOMRect, tooltipWidth: number):
({distance: number, xTransformOrigin: string}) {
const isLTR = !this.adapter.isRTL();
let startPos, endPos, centerPos: number|undefined;
let startTransformOrigin, endTransformOrigin: string;
let startPos: number|undefined, endPos: number|undefined,
centerPos: number|undefined, sideStartPos: number|undefined,
sideEndPos: number|undefined;
let startTransformOrigin: string, endTransformOrigin: string;
if (this.richTooltip) {
startPos = isLTR ? anchorRect.left - tooltipWidth : anchorRect.right;
endPos = isLTR ? anchorRect.right : anchorRect.left - tooltipWidth;
Expand All @@ -657,27 +657,55 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
endPos = isLTR ? anchorRect.right - tooltipWidth : anchorRect.left;
centerPos = anchorRect.left + (anchorRect.width - tooltipWidth) / 2;

const sideLeftAligned = anchorRect.left - (tooltipWidth + this.anchorGap);
const sideRightAligned = anchorRect.right + this.anchorGap;
sideStartPos = isLTR ? sideLeftAligned : sideRightAligned;
sideEndPos = isLTR ? sideRightAligned : sideLeftAligned;

startTransformOrigin = isLTR ? strings.LEFT : strings.RIGHT;
endTransformOrigin = isLTR ? strings.RIGHT : strings.LEFT;
}
// For plain tooltips, centerPos is defined
const plainTooltipPosOptions = [startPos, centerPos!, endPos];

// Side positioning should only be considered if it is specified by the
// client.
if (this.xTooltipPos === XPosition.SIDE_START) {
plainTooltipPosOptions.push(sideStartPos!);
} else if (this.xTooltipPos === XPosition.SIDE_END) {
plainTooltipPosOptions.push(sideEndPos!);
}

const positionOptions = this.richTooltip ?
this.determineValidPositionOptions(startPos, endPos) :
// For plain tooltips, centerPos is defined
this.determineValidPositionOptions(centerPos!, startPos, endPos);
this.determineValidPositionOptions(...plainTooltipPosOptions);

if (this.xTooltipPos === XPosition.START && positionOptions.has(startPos)) {
return {distance: startPos, xTransformOrigin: startTransformOrigin};
}
if (this.xTooltipPos === XPosition.END && positionOptions.has(endPos)) {
} else if (
this.xTooltipPos === XPosition.END && positionOptions.has(endPos)) {
return {distance: endPos, xTransformOrigin: endTransformOrigin};
}
if (this.xTooltipPos === XPosition.CENTER &&
} else if (
this.xTooltipPos === XPosition.CENTER &&
positionOptions.has(centerPos)) {
// This code path is only executed if calculating the distance for plain
// tooltips. In this instance, centerPos will always be defined, so we can
// safely assert that the returned value is non-null/undefined.
return {distance: centerPos!, xTransformOrigin: strings.CENTER};
} else if (
this.xTooltipPos === XPosition.SIDE_START &&
positionOptions.has(sideStartPos)) {
// This code path is only executed if calculating the distance for plain
// tooltips. In this instance, sideStartPos will always be defined, so we
// can safely assert that the returned value is non-null/undefined.
return {distance: sideStartPos!, xTransformOrigin: endTransformOrigin};
} else if (
this.xTooltipPos === XPosition.SIDE_END &&
positionOptions.has(sideEndPos)) {
// This code path is only executed if calculating the distance for plain
// tooltips. In this instance, sideEndPos will always be defined, so we
// can safely assert that the returned value is non-null/undefined.
return {distance: sideEndPos!, xTransformOrigin: startTransformOrigin};
}

// If no user position is supplied, rich tooltips default to end pos, then
Expand Down Expand Up @@ -768,8 +796,15 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
anchorRect: DOMRect, tooltipHeight: number) {
const belowYPos = anchorRect.bottom + this.anchorGap;
const aboveYPos = anchorRect.top - (this.anchorGap + tooltipHeight);
const yPositionOptions =
this.determineValidYPositionOptions(aboveYPos, belowYPos);
const anchorMidpoint = anchorRect.top + anchorRect.height / 2;
const sideYPos = anchorMidpoint - (tooltipHeight / 2);
const posOptions = [aboveYPos, belowYPos];
if (this.yTooltipPos === YPosition.SIDE) {
// Side positioning should only be considered if it is specified by the
// client.
posOptions.push(sideYPos);
}
const yPositionOptions = this.determineValidYPositionOptions(...posOptions);

if (this.yTooltipPos === YPosition.ABOVE &&
yPositionOptions.has(aboveYPos)) {
Expand All @@ -778,6 +813,9 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
this.yTooltipPos === YPosition.BELOW &&
yPositionOptions.has(belowYPos)) {
return {distance: belowYPos, yTransformOrigin: strings.TOP};
} else if (
this.yTooltipPos === YPosition.SIDE && yPositionOptions.has(sideYPos)) {
return {distance: sideYPos, yTransformOrigin: strings.CENTER};
}

if (yPositionOptions.has(belowYPos)) {
Expand Down Expand Up @@ -805,23 +843,16 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
* position, if all possible alignments violate the threshold, then the
* returned Set contains values that keep the tooltip within the viewport.
*/
private determineValidYPositionOptions(
aboveAnchorPos: number, belowAnchorPos: number) {
private determineValidYPositionOptions(...positions: number[]) {
const posWithinThreshold = new Set();
const posWithinViewport = new Set();

if (this.yPositionHonorsViewportThreshold(aboveAnchorPos)) {
posWithinThreshold.add(aboveAnchorPos);
} else if (this.yPositionDoesntCollideWithViewport(aboveAnchorPos)) {
posWithinViewport.add(aboveAnchorPos);
}

if (this.yPositionHonorsViewportThreshold(belowAnchorPos)) {
posWithinThreshold.add(belowAnchorPos);
} else if (this.yPositionDoesntCollideWithViewport(belowAnchorPos)) {
posWithinViewport.add(belowAnchorPos);
for (const position of positions) {
if (this.yPositionHonorsViewportThreshold(position)) {
posWithinThreshold.add(position);
} else if (this.yPositionDoesntCollideWithViewport(position)) {
posWithinViewport.add(position);
}
}

return posWithinThreshold.size ? posWithinThreshold : posWithinViewport;
}

Expand Down
80 changes: 76 additions & 4 deletions packages/mdc-tooltip/test/foundation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1947,8 +1947,7 @@ describe('MDCTooltipFoundation', () => {
it('ignores user specification if positioning violates threshold (BELOW alignment instead of ABOVE)',
() => {
const anchorBoundingRect =
{top: 40, bottom: 70, left: 450, right: 500, width: 50} as
DOMRect;
{top: 40, bottom: 70, left: 450, right: 500, width: 50} as DOMRect;
const tooltipSize = {width: 100, height: 30};
const expectedTooltipTop =
anchorBoundingRect.bottom + numbers.BOUNDED_ANCHOR_GAP;
Expand All @@ -1968,8 +1967,7 @@ describe('MDCTooltipFoundation', () => {
it('allows users to specify a position within viewport if threshold cannot be maintained (ABOVE alignment instead of BELOW)',
() => {
const anchorBoundingRect =
{top: 40, bottom: 70, left: 450, right: 500, width: 50} as
DOMRect;
{top: 40, bottom: 70, left: 450, right: 500, width: 50} as DOMRect;
const tooltipSize = {width: 100, height: 30};
const expectedTooltipTop = anchorBoundingRect.top -
(numbers.BOUNDED_ANCHOR_GAP + tooltipSize.height);
Expand All @@ -1986,6 +1984,80 @@ describe('MDCTooltipFoundation', () => {
.toHaveBeenCalledWith('top', `${expectedTooltipTop}px`);
});

it('properly calculates tooltip position SIDE_END', () => {
const anchorBoundingRect =
{top: 100, bottom: 140, left: 0, right: 100, width: 100, height: 40};
const tooltipSize = {width: 50, height: 30};

const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
mockAdapter.getViewportHeight.and.returnValue(500);
mockAdapter.getViewportWidth.and.returnValue(500);
mockAdapter.getAnchorBoundingRect.and.returnValue(anchorBoundingRect);
mockAdapter.getTooltipSize.and.returnValue(tooltipSize);

foundation.setTooltipPosition(
{xPos: XPosition.SIDE_END, yPos: YPosition.SIDE});
foundation.show();
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('top', '105px');
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('left', '104px');
});

it('properly calculates tooltip position SIDE_START in RTL', () => {
const anchorBoundingRect =
{top: 100, bottom: 140, left: 0, right: 100, width: 100, height: 40};
const tooltipSize = {width: 50, height: 30};

const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
mockAdapter.getViewportHeight.and.returnValue(500);
mockAdapter.getViewportWidth.and.returnValue(500);
mockAdapter.getAnchorBoundingRect.and.returnValue(anchorBoundingRect);
mockAdapter.getTooltipSize.and.returnValue(tooltipSize);
mockAdapter.isRTL.and.returnValue(true);

foundation.setTooltipPosition(
{xPos: XPosition.SIDE_START, yPos: YPosition.SIDE});
foundation.show();
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('top', '105px');
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('left', '104px');
});

it('properly calculates tooltip position SIDE_START', () => {
const anchorBoundingRect =
{top: 100, bottom: 140, left: 100, right: 200, width: 100, height: 40};
const tooltipSize = {width: 50, height: 30};

const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
mockAdapter.getViewportHeight.and.returnValue(500);
mockAdapter.getViewportWidth.and.returnValue(500);
mockAdapter.getAnchorBoundingRect.and.returnValue(anchorBoundingRect);
mockAdapter.getTooltipSize.and.returnValue(tooltipSize);

foundation.setTooltipPosition(
{xPos: XPosition.SIDE_START, yPos: YPosition.SIDE});
foundation.show();
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('top', '105px');
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('left', '46px');
});

it('properly calculates tooltip position SIDE_END in RTL', () => {
const anchorBoundingRect =
{top: 100, bottom: 140, left: 100, right: 200, width: 100, height: 40};
const tooltipSize = {width: 50, height: 30};

const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
mockAdapter.getViewportHeight.and.returnValue(500);
mockAdapter.getViewportWidth.and.returnValue(500);
mockAdapter.getAnchorBoundingRect.and.returnValue(anchorBoundingRect);
mockAdapter.getTooltipSize.and.returnValue(tooltipSize);
mockAdapter.isRTL.and.returnValue(true);

foundation.setTooltipPosition(
{xPos: XPosition.SIDE_END, yPos: YPosition.SIDE});
foundation.show();
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('top', '105px');
expect(mockAdapter.setStyleProperty).toHaveBeenCalledWith('left', '46px');
});

it('#destroy clears showTimeout', () => {
const {foundation} = setUpFoundationTest(MDCTooltipFoundation);

Expand Down

0 comments on commit ba9c296

Please sign in to comment.