зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
11967f6b54
Коммит
ca2529a562
|
@ -245,7 +245,12 @@ const MESSAGES = [
|
||||||
|
|
||||||
function _createContainer() {
|
function _createContainer() {
|
||||||
let container = document.createElement("div");
|
let container = document.createElement("div");
|
||||||
container.classList.add("onboardingContainer", "featureCallout", "hidden");
|
container.classList.add(
|
||||||
|
"onboardingContainer",
|
||||||
|
"featureCallout",
|
||||||
|
"callout-arrow",
|
||||||
|
"hidden"
|
||||||
|
);
|
||||||
container.id = CONTAINER_ID;
|
container.id = CONTAINER_ID;
|
||||||
let parent = document.querySelector(CURRENT_SCREEN?.parent_selector);
|
let parent = document.querySelector(CURRENT_SCREEN?.parent_selector);
|
||||||
container.setAttribute("aria-describedby", `#${CONTAINER_ID} .welcome-text`);
|
container.setAttribute("aria-describedby", `#${CONTAINER_ID} .welcome-text`);
|
||||||
|
@ -258,11 +263,20 @@ function _createContainer() {
|
||||||
* Set callout's position relative to parent element
|
* Set callout's position relative to parent element
|
||||||
*/
|
*/
|
||||||
function _positionCallout() {
|
function _positionCallout() {
|
||||||
const positions = ["top", "bottom", "left", "right"];
|
|
||||||
const container = document.getElementById(CONTAINER_ID);
|
const container = document.getElementById(CONTAINER_ID);
|
||||||
const parentEl = document.querySelector(CURRENT_SCREEN?.parent_selector);
|
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 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?
|
// Is the document layout right to left?
|
||||||
const RTL = document.dir === "rtl";
|
const RTL = document.dir === "rtl";
|
||||||
|
|
||||||
|
@ -281,8 +295,10 @@ function _positionCallout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPosition() {
|
function clearPosition() {
|
||||||
positions.forEach(position => {
|
Object.keys(positioners).forEach(position => {
|
||||||
container.style[position] = "unset";
|
container.style[position] = "unset";
|
||||||
|
});
|
||||||
|
arrowPositions.forEach(position => {
|
||||||
if (container.classList.contains(`arrow-${position}`)) {
|
if (container.classList.contains(`arrow-${position}`)) {
|
||||||
container.classList.remove(`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() {
|
function centerHorizontally() {
|
||||||
let sideOffset = (parentEl.offsetWidth - container.offsetWidth) / 2;
|
let sideOffset = (parentEl.offsetWidth - container.offsetWidth) / 2;
|
||||||
let containerSide = RTL
|
let containerSide = RTL
|
||||||
? window.innerWidth - getOffset(parentEl).right + sideOffset
|
? window.innerWidth - getOffset(parentEl).right + sideOffset
|
||||||
: getOffset(parentEl).left + sideOffset;
|
: getOffset(parentEl).left + sideOffset;
|
||||||
container.style[RTL ? "right" : "left"] = `${Math.max(
|
container.style[RTL ? "right" : "left"] = `${Math.max(containerSide, 0)}px`;
|
||||||
containerSide,
|
|
||||||
margin
|
|
||||||
)}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);
|
clearPosition(container);
|
||||||
|
|
||||||
if (!container.classList.contains("callout-arrow")) {
|
let finalPosition = choosePosition();
|
||||||
container.classList.add("callout-arrow");
|
if (finalPosition) {
|
||||||
}
|
positioners[finalPosition].position();
|
||||||
|
|
||||||
if (["start", "end"].includes(arrowPosition)) {
|
|
||||||
if (RTL) {
|
|
||||||
positioners[arrowPosition === "start" ? "right" : "left"]();
|
|
||||||
} else {
|
|
||||||
positioners[arrowPosition === "start" ? "left" : "right"]();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
positioners[arrowPosition]();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
container.classList.remove("hidden");
|
container.classList.remove("hidden");
|
||||||
|
@ -445,14 +515,16 @@ function _loadConfig(messageId) {
|
||||||
screens = screens.filter((s, i) => {
|
screens = screens.filter((s, i) => {
|
||||||
return document.querySelector(s.parent_selector);
|
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;
|
return screens;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = MESSAGES.find(m => m.id === messageId);
|
let content = MESSAGES.find(m => m.id === messageId);
|
||||||
const screenId = lazy.featureTourProgress.screen;
|
const screenId = lazy.featureTourProgress.screen;
|
||||||
let screenIndex;
|
let screenIndex;
|
||||||
if (content?.screens && screenId) {
|
if (content?.screens?.length && screenId) {
|
||||||
content.screens = _getRelevantScreens(content.screens);
|
content.screens = _getRelevantScreens(content.screens);
|
||||||
screenIndex = content.screens.findIndex(s => s.id === screenId);
|
screenIndex = content.screens.findIndex(s => s.id === screenId);
|
||||||
content.startScreen = screenIndex;
|
content.startScreen = screenIndex;
|
||||||
|
|
|
@ -11,4 +11,3 @@ prefs =
|
||||||
[browser_tab_pickup_list.js]
|
[browser_tab_pickup_list.js]
|
||||||
[browser_colorways_card.js]
|
[browser_colorways_card.js]
|
||||||
[browser_feature_callout.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() {
|
add_task(async function feature_callout_arrow_class_exists() {
|
||||||
await SpecialPowers.pushPrefEnv({
|
await SpecialPowers.pushPrefEnv({
|
||||||
set: [
|
set: [["browser.firefox-view.feature-tour", defaultPrefValue]],
|
||||||
["browser.firefox-view.feature-tour", defaultPrefValue],
|
|
||||||
["intl.l10n.pseudo", ""],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await BrowserTestUtils.withNewTab(
|
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() {
|
add_task(async function feature_callout_arrow_is_not_flipped_on_ltr() {
|
||||||
await SpecialPowers.pushPrefEnv({
|
await SpecialPowers.pushPrefEnv({
|
||||||
set: [
|
set: [["browser.firefox-view.feature-tour", defaultPrefValue]],
|
||||||
["browser.firefox-view.feature-tour", defaultPrefValue],
|
|
||||||
["intl.l10n.pseudo", ""],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await BrowserTestUtils.withNewTab(
|
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
|
// Tour is accessible using a screen reader and keyboard navigation
|
||||||
add_task(async function feature_callout_is_accessible() {
|
add_task(async function feature_callout_is_accessible() {
|
||||||
await SpecialPowers.pushPrefEnv({
|
await SpecialPowers.pushPrefEnv({
|
||||||
set: [
|
set: [["browser.firefox-view.feature-tour", defaultPrefValue]],
|
||||||
["browser.firefox-view.feature-tour", defaultPrefValue],
|
|
||||||
["intl.l10n.pseudo", ""],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await BrowserTestUtils.withNewTab(
|
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 {
|
.onboardingContainer.featureCallout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
|
z-index: 2147483645;
|
||||||
}
|
}
|
||||||
.onboardingContainer.featureCallout.hidden {
|
.onboardingContainer.featureCallout.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -419,9 +420,6 @@ body[lwt-newtab-brighttext] {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--in-content-button-background-hover) !important;
|
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 {
|
.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: url("chrome://browser/skin/forward.svg") no-repeat right 8px center;
|
||||||
background-size: 12px;
|
background-size: 12px;
|
||||||
|
@ -431,6 +429,9 @@ body[lwt-newtab-brighttext] {
|
||||||
background-image: url("chrome://browser/skin/back.svg");
|
background-image: url("chrome://browser/skin/back.svg");
|
||||||
background-position-x: left 8px;
|
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 {
|
.onboardingContainer .screen[pos=split] .section-main .main-content .brand-logo {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
|
|
|
@ -3,6 +3,7 @@ $max-z-index: 2147483647;
|
||||||
.onboardingContainer.featureCallout {
|
.onboardingContainer.featureCallout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
|
z-index: $max-z-index - 2;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
Загрузка…
Ссылка в новой задаче