Bug 1782524 - Reposition Feature Callout if it overlaps parent element by more than 15% r=Mardak

Differential Revision: https://phabricator.services.mozilla.com/D154649
This commit is contained in:
Meg Viar 2022-08-24 18:24:15 +00:00
Родитель 11967f6b54
Коммит ca2529a562
5 изменённых файлов: 195 добавлений и 77 удалений

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

@ -245,7 +245,12 @@ const MESSAGES = [
function _createContainer() {
let container = document.createElement("div");
container.classList.add("onboardingContainer", "featureCallout", "hidden");
container.classList.add(
"onboardingContainer",
"featureCallout",
"callout-arrow",
"hidden"
);
container.id = CONTAINER_ID;
let parent = document.querySelector(CURRENT_SCREEN?.parent_selector);
container.setAttribute("aria-describedby", `#${CONTAINER_ID} .welcome-text`);
@ -258,11 +263,20 @@ function _createContainer() {
* Set callout's position relative to parent element
*/
function _positionCallout() {
const positions = ["top", "bottom", "left", "right"];
const container = document.getElementById(CONTAINER_ID);
const parentEl = document.querySelector(CURRENT_SCREEN?.parent_selector);
// All possible arrow positions
const arrowPositions = ["top", "bottom", "end", "start"];
const arrowPosition = CURRENT_SCREEN?.content?.arrow_position || "top";
const margin = 15;
// Length of arrow pointer in pixels
const arrowLength = 12;
// Callout should overlap the parent element by
// 15% of the latter's width or height
const overlap = 0.15;
// Number of pixels that the callout should overlap the element it describes,
// including the length of the element's arrow pointer
const parentHeightOverlap = parentEl.offsetHeight * overlap - arrowLength;
const parentWidthOverlap = parentEl.offsetWidth * overlap - arrowLength;
// Is the document layout right to left?
const RTL = document.dir === "rtl";
@ -281,8 +295,10 @@ function _positionCallout() {
}
function clearPosition() {
positions.forEach(position => {
Object.keys(positioners).forEach(position => {
container.style[position] = "unset";
});
arrowPositions.forEach(position => {
if (container.classList.contains(`arrow-${position}`)) {
container.classList.remove(`arrow-${position}`);
}
@ -292,70 +308,124 @@ function _positionCallout() {
});
}
const positioners = {
top: {
availableSpace:
document.body.offsetHeight -
getOffset(parentEl).top -
parentEl.offsetHeight +
parentHeightOverlap,
neededSpace: container.offsetHeight - parentHeightOverlap,
position() {
// Point to an element above the callout
let containerTop =
getOffset(parentEl).top + parentEl.offsetHeight - parentHeightOverlap;
container.style.top = `${Math.max(
container.offsetHeight - parentHeightOverlap,
containerTop
)}px`;
centerHorizontally(container, parentEl);
container.classList.add("arrow-top");
},
},
bottom: {
availableSpace: getOffset(parentEl).top + parentHeightOverlap,
neededSpace: container.offsetHeight - parentHeightOverlap,
position() {
// Point to an element below the callout
let containerTop =
getOffset(parentEl).top -
container.offsetHeight +
parentHeightOverlap;
container.style.top = `${Math.max(0, containerTop)}px`;
centerHorizontally(container, parentEl);
container.classList.add("arrow-bottom");
},
},
right: {
availableSpace: getOffset(parentEl).left + parentHeightOverlap,
neededSpace: container.offsetWidth - parentWidthOverlap,
position() {
// Point to an element to the right of the callout
let containerLeft =
getOffset(parentEl).left - container.offsetWidth + parentWidthOverlap;
if (RTL) {
// Account for cases where the document body may be narrow than the window
containerLeft -= window.innerWidth - document.body.offsetWidth;
}
container.style.left = `${Math.max(0, containerLeft)}px`;
container.style.top = `${getOffset(parentEl).top}px`;
container.classList.add("arrow-inline-end");
},
},
left: {
availableSpace:
document.body.offsetWidth -
getOffset(parentEl).right +
parentWidthOverlap,
neededSpace: container.offsetWidth - parentWidthOverlap,
position() {
// Point to an element to the left of the callout
let containerLeft =
getOffset(parentEl).left + parentEl.offsetWidth - parentWidthOverlap;
if (RTL) {
// Account for cases where the document body may be narrow than the window
containerLeft -= window.innerWidth - document.body.offsetWidth;
}
container.style.left = `${(container.offsetWidth - parentWidthOverlap,
containerLeft)}px`;
container.style.top = `${getOffset(parentEl).top}px`;
container.classList.add("arrow-inline-start");
},
},
};
function calloutFits(position) {
// Does callout element fit in this position relative
// to the parent element without going off screen?
return (
positioners[position].availableSpace > positioners[position].neededSpace
);
}
function choosePosition() {
let position = arrowPosition;
if (!arrowPositions.includes(position)) {
// Configured arrow position is not valid
return false;
}
if (["start", "end"].includes(position)) {
if (RTL) {
position = "start" ? "right" : "left";
} else {
position = "start" ? "left" : "right";
}
}
if (calloutFits(position)) {
return position;
}
let newPosition = Object.keys(positioners)
.filter(p => p !== position)
.find(p => calloutFits(p));
// If the callout doesn't fit in any position, use the configured one.
// The callout will be adjusted to overlap the parent element so that
// the former doesn't go off screen.
return newPosition || position;
}
function centerHorizontally() {
let sideOffset = (parentEl.offsetWidth - container.offsetWidth) / 2;
let containerSide = RTL
? window.innerWidth - getOffset(parentEl).right + sideOffset
: getOffset(parentEl).left + sideOffset;
container.style[RTL ? "right" : "left"] = `${Math.max(
containerSide,
margin
)}px`;
container.style[RTL ? "right" : "left"] = `${Math.max(containerSide, 0)}px`;
}
// Position callout relative to a parent element
const positioners = {
top() {
let containerTop = getOffset(parentEl).bottom - margin;
container.style.top = `${Math.min(
window.innerHeight - container.offsetHeight - margin,
containerTop
)}px`;
centerHorizontally(container, parentEl);
container.classList.add("arrow-top");
},
// Point to an element below the callout
bottom() {
let containerTop =
getOffset(parentEl).top - container.clientHeight + margin;
container.style.top = `${Math.max(containerTop, 0)}px`;
centerHorizontally(container, parentEl);
container.classList.add("arrow-bottom");
},
// Point to an element to the right of the callout
left() {
let containerLeft = getOffset(parentEl).right - margin;
container.style.left = `${Math.min(
window.innerWidth - container.offsetWidth - margin,
containerLeft
)}px`;
container.style.top = `${getOffset(parentEl).top}px`;
container.classList.add("arrow-inline-start");
},
// Point to an element to the left of the callout
right() {
let containerLeft =
getOffset(parentEl).left - container.offsetWidth + margin;
container.style.left = `${Math.max(containerLeft, margin)}px`;
container.style.top = `${getOffset(parentEl).top}px`;
container.classList.add("arrow-inline-end");
},
};
clearPosition(container);
if (!container.classList.contains("callout-arrow")) {
container.classList.add("callout-arrow");
}
if (["start", "end"].includes(arrowPosition)) {
if (RTL) {
positioners[arrowPosition === "start" ? "right" : "left"]();
} else {
positioners[arrowPosition === "start" ? "left" : "right"]();
}
} else {
positioners[arrowPosition]();
let finalPosition = choosePosition();
if (finalPosition) {
positioners[finalPosition].position();
}
container.classList.remove("hidden");
@ -445,14 +515,16 @@ function _loadConfig(messageId) {
screens = screens.filter((s, i) => {
return document.querySelector(s.parent_selector);
});
screens[screens.length - 1].content.primary_button = finalCTA;
if (screens.length) {
screens[screens.length - 1].content.primary_button = finalCTA;
}
return screens;
}
let content = MESSAGES.find(m => m.id === messageId);
const screenId = lazy.featureTourProgress.screen;
let screenIndex;
if (content?.screens && screenId) {
if (content?.screens?.length && screenId) {
content.screens = _getRelevantScreens(content.screens);
screenIndex = content.screens.findIndex(s => s.id === screenId);
content.startScreen = screenIndex;

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

@ -11,4 +11,3 @@ prefs =
[browser_tab_pickup_list.js]
[browser_colorways_card.js]
[browser_feature_callout.js]
skip-if = true # Bug 1784343

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

@ -306,6 +306,13 @@ add_task(
);
}
);
await SpecialPowers.pushPrefEnv({
set: [
// Revert layout direction to left to right
["intl.l10n.pseudo", ""],
],
});
}
);
@ -356,10 +363,7 @@ add_task(async function feature_callout_only_highlights_existing_elements() {
add_task(async function feature_callout_arrow_class_exists() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.firefox-view.feature-tour", defaultPrefValue],
["intl.l10n.pseudo", ""],
],
set: [["browser.firefox-view.feature-tour", defaultPrefValue]],
});
await BrowserTestUtils.withNewTab(
@ -379,10 +383,7 @@ add_task(async function feature_callout_arrow_class_exists() {
add_task(async function feature_callout_arrow_is_not_flipped_on_ltr() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.firefox-view.feature-tour", defaultPrefValue],
["intl.l10n.pseudo", ""],
],
set: [["browser.firefox-view.feature-tour", defaultPrefValue]],
});
await BrowserTestUtils.withNewTab(
@ -413,10 +414,7 @@ add_task(async function feature_callout_arrow_is_not_flipped_on_ltr() {
// Tour is accessible using a screen reader and keyboard navigation
add_task(async function feature_callout_is_accessible() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.firefox-view.feature-tour", defaultPrefValue],
["intl.l10n.pseudo", ""],
],
set: [["browser.firefox-view.feature-tour", defaultPrefValue]],
});
await BrowserTestUtils.withNewTab(
@ -458,3 +456,50 @@ add_task(async function feature_callout_is_accessible() {
}
);
});
add_task(async function feature_callout_is_repositioned_if_it_does_not_fit() {
await SpecialPowers.pushPrefEnv({
set: [
[
"browser.firefox-view.feature-tour",
'{"message":"FIREFOX_VIEW_FEATURE_TOUR","screen":"FEATURE_CALLOUT_3","complete":false}',
],
],
});
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:firefoxview",
},
async browser => {
let startHeight = window.outerHeight;
let startWidth = window.outerWidth;
const { document } = browser.contentWindow;
await waitForCalloutScreen(document, ".FEATURE_CALLOUT_3");
browser.contentWindow.resizeTo(1200, 800);
ok(
document.querySelector(`${calloutSelector}.arrow-inline-end`),
"On third screen, the callout is positioned at the start of the parent element originally configured"
);
const startingTop = document.querySelector(calloutSelector).style.top;
browser.contentWindow.resizeTo(800, 800);
// Wait for callout to be repositioned
await BrowserTestUtils.waitForMutationCondition(
document.querySelector(calloutSelector),
{ attributeFilter: ["style"], attributes: true },
() => document.querySelector(calloutSelector).style.top != startingTop
);
ok(
document.querySelector(`${calloutSelector}.arrow-top`),
"On third screen at a narrower window width, the callout is positioned below the parent element"
);
browser.contentWindow.resizeTo(startWidth, startHeight);
}
);
});

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

@ -34,6 +34,7 @@ input {
.onboardingContainer.featureCallout {
position: absolute;
transition: opacity 0.5s ease;
z-index: 2147483645;
}
.onboardingContainer.featureCallout.hidden {
opacity: 0;
@ -419,9 +420,6 @@ body[lwt-newtab-brighttext] {
text-decoration: none;
background-color: var(--in-content-button-background-hover) !important;
}
.onboardingContainer .screen[pos=split] .section-main .main-content .logo-container {
text-align: start;
}
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .arrow-icon {
background: url("chrome://browser/skin/forward.svg") no-repeat right 8px center;
background-size: 12px;
@ -431,6 +429,9 @@ body[lwt-newtab-brighttext] {
background-image: url("chrome://browser/skin/back.svg");
background-position-x: left 8px;
}
.onboardingContainer .screen[pos=split] .section-main .main-content .logo-container {
text-align: start;
}
.onboardingContainer .screen[pos=split] .section-main .main-content .brand-logo {
height: 25px;
margin-block: 0;

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

@ -3,6 +3,7 @@ $max-z-index: 2147483647;
.onboardingContainer.featureCallout {
position: absolute;
transition: opacity 0.5s ease;
z-index: $max-z-index - 2;
&.hidden {
opacity: 0;