Bug 1794651 - Adding custom position override to feature callout r=mviar

Differential Revision: https://phabricator.services.mozilla.com/D159556
This commit is contained in:
Jason Prickett 2022-11-02 17:32:08 +00:00
Родитель 5318c8b0a3
Коммит e9b58109c1
2 изменённых файлов: 133 добавлений и 20 удалений

Просмотреть файл

@ -112,7 +112,7 @@ function _createContainer() {
// Don't render the callout if the parent element is not present.
// This means the message was misconfigured, mistargeted, or the
// content of the parent page is not as expected.
if (!parent) {
if (!parent && !CURRENT_SCREEN?.content.callout_position_override) {
return false;
}
@ -160,8 +160,11 @@ function _positionCallout() {
overlap -= arrowWidth;
// Is the document layout right to left?
const RTL = document.dir === "rtl";
const customPosition = CURRENT_SCREEN?.content.callout_position_override;
if (!container || !parentEl) {
// Early exit if the container doesn't exist,
// or if we're missing a parent element and don't have a custom callout position
if (!container || (!parentEl && !customPosition)) {
return;
}
@ -189,39 +192,96 @@ function _positionCallout() {
});
}
function addArrowPositionClassToContainer(finalArrowPosition) {
let className;
switch (finalArrowPosition) {
case "bottom":
className = "arrow-bottom";
break;
case "left":
className = "arrow-inline-start";
break;
case "right":
className = "arrow-inline-end";
break;
case "top-start":
className = RTL ? "arrow-top-end" : "arrow-top-start";
break;
case "top-end":
className = RTL ? "arrow-top-start" : "arrow-top-end";
break;
case "top":
default:
className = "arrow-top";
break;
}
container.classList.add(className);
}
function overridePosition() {
// We override _every_ positioner here, because we want to manually set all
// container.style.positions in every positioner's "position" function
// regardless of the actual arrow position
for (const position in positioners) {
positioners[position].position = function() {
if (customPosition.top) {
container.style.top = customPosition.top;
}
if (customPosition.left) {
container.style.left = customPosition.left;
}
if (customPosition.right) {
container.style.right = customPosition.right;
}
if (customPosition.bottom) {
container.style.bottom = customPosition.bottom;
}
};
}
}
const positioners = {
// availableSpace should be the space between the edge of the page in the assumed direction
// and the edge of the parent (with the callout being intended to fit between those two edges)
// while needed space should be the space necessary to fit the callout container
top: {
availableSpace:
document.documentElement.clientHeight -
getOffset(parentEl).top -
parentEl.clientHeight,
availableSpace() {
return (
document.documentElement.clientHeight -
getOffset(parentEl).top -
parentEl.clientHeight
);
},
neededSpace: container.clientHeight - overlap,
position() {
// Point to an element above the callout
let containerTop =
getOffset(parentEl).top + parentEl.clientHeight - overlap;
container.style.top = `${Math.max(0, containerTop)}px`;
container.classList.add("arrow-top");
centerHorizontally(container, parentEl);
},
},
bottom: {
availableSpace: getOffset(parentEl).top,
availableSpace() {
return getOffset(parentEl).top;
},
neededSpace: container.clientHeight - overlap,
position() {
// Point to an element below the callout
let containerTop =
getOffset(parentEl).top - container.clientHeight + overlap;
container.style.top = `${Math.max(0, containerTop)}px`;
container.classList.add("arrow-bottom");
centerHorizontally(container, parentEl);
},
},
right: {
availableSpace: getOffset(parentEl).left,
availableSpace() {
return getOffset(parentEl).left;
},
neededSpace: container.clientWidth - overlap,
position() {
// Point to an element to the right of the callout
@ -229,12 +289,12 @@ function _positionCallout() {
getOffset(parentEl).left - container.clientWidth + overlap;
container.style.left = `${Math.max(0, containerLeft)}px`;
container.style.top = `${getOffset(parentEl).top}px`;
container.classList.add("arrow-inline-end");
},
},
left: {
availableSpace:
document.documentElement.clientWidth - getOffset(parentEl).right,
availableSpace() {
return document.documentElement.clientWidth - getOffset(parentEl).right;
},
neededSpace: container.clientWidth - overlap,
position() {
// Point to an element to the left of the callout
@ -242,17 +302,24 @@ function _positionCallout() {
getOffset(parentEl).left + parentEl.clientWidth - overlap;
container.style.left = `${Math.max(0, containerLeft)}px`;
container.style.top = `${getOffset(parentEl).top}px`;
container.classList.add("arrow-inline-start");
},
},
"top-end": {
availableSpace() {
document.documentElement.clientHeight -
getOffset(parentEl).top -
parentEl.clientHeight;
},
neededSpace: container.clientHeight - overlap,
position() {
// Point to an element above and at the end of the callout
let containerTop =
getOffset(parentEl).top + parentEl.clientHeight - overlap;
container.style.top = `${Math.max(0, containerTop)}px`;
container.style.top = `${Math.max(
container.clientHeight - overlap,
containerTop
)}px`;
alignEnd(container, parentEl);
container.classList.add(RTL ? "arrow-top-start" : "arrow-top-end");
},
},
};
@ -265,7 +332,7 @@ function _positionCallout() {
// not the alignment of the arrow along the edge of the callout
let edgePosition = position.split("-")[0];
return (
positioners[edgePosition].availableSpace >
positioners[edgePosition].availableSpace() >
positioners[edgePosition].neededSpace
);
}
@ -283,7 +350,8 @@ function _positionCallout() {
// at an element to the right of itself, while in RTL layouts it is pointing to the left of itself
position = RTL ^ (position === "start") ? "left" : "right";
}
if (calloutFits(position)) {
// If we're overriding the position, we don't need to sort for available space
if (customPosition || calloutFits(position)) {
return position;
}
let sortedPositions = Object.keys(positioners)
@ -291,8 +359,8 @@ function _positionCallout() {
.filter(calloutFits)
.sort((a, b) => {
return (
positioners[b].availableSpace - positioners[b].neededSpace >
positioners[a].availableSpace - positioners[a].neededSpace
positioners[b].availableSpace() - positioners[b].neededSpace >
positioners[a].availableSpace() - positioners[a].neededSpace
);
});
// If the callout doesn't fit in any position, use the configured one.
@ -322,9 +390,16 @@ function _positionCallout() {
clearPosition(container);
if (customPosition) {
// We override the position functions with new functions here,
// but they don't actually get executed in the override function
overridePosition();
}
let finalPosition = choosePosition();
if (finalPosition) {
positioners[finalPosition].position();
addArrowPositionClassToContainer(finalPosition);
}
container.classList.remove("hidden");

Просмотреть файл

@ -297,3 +297,41 @@ add_task(
sandbox.restore();
}
);
add_task(
async function feature_callout_custom_position_override_properties_are_applied() {
const testMessage = {
message: FeatureCalloutMessages.getMessages().find(
m => m.id === "FIREFOX_VIEW_FEATURE_TOUR_1"
),
};
testMessage.message.content.screens[0].content.callout_position_override = {
top: "500px",
left: "500px",
};
const sandbox = sinon.createSandbox();
const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage");
sendTriggerStub.resolves(testMessage);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:firefoxview",
},
async browser => {
const { document } = browser.contentWindow;
await waitForCalloutScreen(document, 1);
let container = document.querySelector(calloutSelector);
let containerLeft = container.getBoundingClientRect().left;
let containerTop = container.getBoundingClientRect().top;
ok(
containerLeft === 500 && containerTop === 500,
"Feature callout container has a top position of 500, and left position of 500"
);
}
);
sandbox.restore();
}
);