Bug 1616280 - Use shadow DOM to hide Remote L10n translations for from local translations r=k88hudson

Differential Revision: https://phabricator.services.mozilla.com/D63895
This commit is contained in:
Andrei Oprea 2020-04-18 11:24:44 +00:00
Родитель af4a04d2c5
Коммит d88ff7e8b9
7 изменённых файлов: 246 добавлений и 211 удалений

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

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
const { RemoteL10n } = ChromeUtils.import(
"resource://activity-stream/lib/RemoteL10n.jsm"
);
class MozTextParagraph extends HTMLElement {
constructor() {
super();
this._content = null;
}
get fluentAttributeValues() {
const attributes = {};
for (let name of this.getAttributeNames()) {
if (name.startsWith("fluent-variable-")) {
attributes[name.replace(/^fluent-variable-/, "")] = this.getAttribute(
name
);
}
}
return attributes;
}
render() {
if (this.getAttribute("fluent-remote-id") && this._content) {
RemoteL10n.l10n.setAttributes(
this._content,
this.getAttribute("fluent-remote-id"),
this.fluentAttributeValues
);
}
}
static get observedAttributes() {
return ["fluent-remote-id"];
}
attributeChangedCallback(name, oldValue, newValue) {
this.render();
}
connectedCallback() {
if (this.shadowRoot) {
this.render();
return;
}
const shadowRoot = this.attachShadow({ mode: "open" });
this._content = document.createElement("span");
shadowRoot.appendChild(this._content);
this.render();
RemoteL10n.l10n.translateFragment(this._content);
}
}
customElements.define("remote-text", MozTextParagraph);
}

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

@ -26,6 +26,7 @@ browser.jar:
res/activity-stream/data/content/tippytop/ (./data/content/tippytop/*)
res/activity-stream/data/content/activity-stream.bundle.js (./data/content/activity-stream.bundle.js)
res/activity-stream/data/content/newtab-render.js (./data/content/newtab-render.js)
res/activity-stream/data/custom-elements/ (./components/CustomElements/*)
#ifdef XP_MACOSX
res/activity-stream/css/activity-stream.css (./css/activity-stream-mac.css)
#elifdef XP_WIN

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

@ -10,7 +10,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
EveryWindow: "resource:///modules/EveryWindow.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
RemoteL10n: "resource://activity-stream/lib/RemoteL10n.jsm",
});
XPCOMUtils.defineLazyServiceGetter(
this,
@ -108,6 +107,15 @@ class _ToolbarPanelHub {
win.MozXULElement.insertFTLIfNeeded("browser/branding/sync-brand.ftl");
}
maybeLoadCustomElement(win) {
if (!win.customElements.get("remote-text")) {
Services.scriptloader.loadSubScript(
"resource://activity-stream/data/custom-elements/paragraph.js",
win
);
}
}
// Turns on the Appmenu (hamburger menu) button for all open windows and future windows.
async enableAppmenuButton() {
if ((await this.messages).length) {
@ -170,6 +178,7 @@ class _ToolbarPanelHub {
// Render what's new messages into the panel.
async renderMessages(win, doc, containerId, options = {}) {
this.maybeLoadCustomElement(win);
const messages =
(options.force && options.messages) ||
(await this.messages).sort(this._sortWhatsNewMessages);
@ -178,17 +187,13 @@ class _ToolbarPanelHub {
if (messages) {
// Targeting attribute state might have changed making new messages
// available and old messages invalid, we need to refresh
for (const prevMessageEl of container.querySelectorAll(
".whatsNew-message"
)) {
container.removeChild(prevMessageEl);
}
this.removeMessages(win, containerId);
let previousDate = 0;
// Get and store any variable part of the message content
this.state.contentArguments = await this._contentArguments();
for (let message of messages) {
container.appendChild(
await this._createMessageElements(win, doc, message, previousDate)
this._createMessageElements(win, doc, message, previousDate)
);
previousDate = message.content.published_date;
}
@ -267,15 +272,15 @@ class _ToolbarPanelHub {
});
}
async _createMessageElements(win, doc, message, previousDate) {
_createMessageElements(win, doc, message, previousDate) {
const { content } = message;
const messageEl = await this._createElement(doc, "div");
const messageEl = this._createElement(doc, "div");
messageEl.classList.add("whatsNew-message");
// Only render date if it is different from the one rendered before.
if (content.published_date !== previousDate) {
messageEl.appendChild(
await this._createElement(doc, "p", {
this._createElement(doc, "p", {
classList: "whatsNew-message-date",
content: new Date(content.published_date).toLocaleDateString(
"default",
@ -289,24 +294,28 @@ class _ToolbarPanelHub {
);
}
const wrapperEl = await this._createElement(doc, "button");
const wrapperEl = this._createElement(doc, "button");
wrapperEl.doCommand = () => this._dispatchUserAction(win, message);
wrapperEl.classList.add("whatsNew-message-body");
messageEl.appendChild(wrapperEl);
if (content.icon_url) {
wrapperEl.classList.add("has-icon");
const iconEl = await this._createElement(doc, "img");
const iconEl = this._createElement(doc, "img");
iconEl.src = content.icon_url;
iconEl.classList.add("whatsNew-message-icon");
await this._setTextAttribute(iconEl, "alt", content.icon_alt);
if (content.icon_alt && content.icon_alt.string_id) {
doc.l10n.setAttributes(iconEl, content.icon_alt.string_id);
} else {
iconEl.setAttribute("alt", content.icon_alt);
}
wrapperEl.appendChild(iconEl);
}
wrapperEl.appendChild(await this._createMessageContent(win, doc, content));
wrapperEl.appendChild(this._createMessageContent(win, doc, content));
if (content.link_text) {
const anchorEl = await this._createElement(doc, "a", {
const anchorEl = this._createElement(doc, "a", {
classList: "text-link",
content: content.link_text,
});
@ -323,11 +332,11 @@ class _ToolbarPanelHub {
/**
* Return message title (optional subtitle) and body
*/
async _createMessageContent(win, doc, content) {
_createMessageContent(win, doc, content) {
const wrapperEl = new win.DocumentFragment();
wrapperEl.appendChild(
await this._createElement(doc, "h2", {
this._createElement(doc, "h2", {
classList: "whatsNew-message-title",
content: content.title,
})
@ -335,14 +344,14 @@ class _ToolbarPanelHub {
switch (content.layout) {
case "tracking-protections":
await wrapperEl.appendChild(
await this._createElement(doc, "h4", {
wrapperEl.appendChild(
this._createElement(doc, "h4", {
classList: "whatsNew-message-subtitle",
content: content.subtitle,
})
);
wrapperEl.appendChild(
await this._createElement(doc, "h2", {
this._createElement(doc, "h2", {
classList: "whatsNew-message-title-large",
content: this.state.contentArguments[
content.layout_title_content_variable
@ -353,32 +362,40 @@ class _ToolbarPanelHub {
}
wrapperEl.appendChild(
await this._createElement(doc, "p", { content: content.body })
this._createElement(doc, "p", {
content: content.body,
classList: "whatsNew-message-content",
})
);
return wrapperEl;
}
async _createHeroElement(win, doc, message) {
const messageEl = await this._createElement(doc, "div");
_createHeroElement(win, doc, message) {
this.maybeLoadCustomElement(win);
const messageEl = this._createElement(doc, "div");
messageEl.setAttribute("id", "protections-popup-message");
messageEl.classList.add("whatsNew-hero-message");
const wrapperEl = await this._createElement(doc, "div");
const wrapperEl = this._createElement(doc, "div");
wrapperEl.classList.add("whatsNew-message-body");
messageEl.appendChild(wrapperEl);
wrapperEl.appendChild(
await this._createElement(doc, "h2", {
this._createElement(doc, "h2", {
classList: "whatsNew-message-title",
content: message.content.title,
})
);
wrapperEl.appendChild(
await this._createElement(doc, "p", { content: message.content.body })
this._createElement(doc, "p", {
classList: "protections-popup-content",
content: message.content.body,
})
);
if (message.content.link_text) {
let linkEl = await this._createElement(doc, "a", {
let linkEl = this._createElement(doc, "a", {
classList: "text-link",
content: message.content.link_text,
});
@ -392,14 +409,17 @@ class _ToolbarPanelHub {
return messageEl;
}
async _createElement(doc, elem, options = {}) {
const node = doc.createElementNS("http://www.w3.org/1999/xhtml", elem);
_createElement(doc, elem, options = {}) {
let node;
if (options.content && options.content.string_id) {
node = doc.createElement("remote-text");
} else {
node = doc.createElementNS("http://www.w3.org/1999/xhtml", elem);
}
if (options.classList) {
node.classList.add(options.classList);
}
if (options.content) {
await this._setString(node, options.content);
}
this._setString(node, options.content);
return node;
}
@ -447,41 +467,19 @@ class _ToolbarPanelHub {
// If `string_id` is present it means we are relying on fluent for translations.
// Otherwise, we have a vanilla string.
async _setString(el, stringObj) {
_setString(el, stringObj) {
if (stringObj && stringObj.string_id) {
const [{ value }] = await RemoteL10n.l10n.formatMessages([
{
id: stringObj.string_id,
// Pass all available arguments to Fluent
args: this.state.contentArguments,
},
]);
el.textContent = value;
for (let [fluentId, value] of Object.entries(
this.state.contentArguments || {}
)) {
el.setAttribute(`fluent-variable-${fluentId}`, value);
}
el.setAttribute("fluent-remote-id", stringObj.string_id);
} else {
el.textContent = stringObj;
}
}
// If `string_id` is present it means we are relying on fluent for translations.
// Otherwise, we have a vanilla string.
async _setTextAttribute(el, attr, stringObj) {
if (stringObj && stringObj.string_id) {
const [{ attributes }] = await RemoteL10n.l10n.formatMessages([
{
id: stringObj.string_id,
// Pass all available arguments to Fluent
args: this.state.contentArguments,
},
]);
if (attributes) {
const { value } = attributes.find(({ name }) => name === attr);
el.setAttribute(attr, value);
}
} else {
el.setAttribute(attr, stringObj);
}
}
async _showAppmenuButton(win) {
this.maybeInsertFTL(win);
await this._showElement(
@ -505,10 +503,9 @@ class _ToolbarPanelHub {
this._hideElement(win.browser.ownerDocument, TOOLBAR_BUTTON_ID);
}
async _showElement(document, id, string_id) {
_showElement(document, id, string_id) {
const el = document.getElementById(id);
await this._setTextAttribute(el, "label", { string_id });
await this._setTextAttribute(el, "tooltiptext", { string_id });
document.l10n.setAttributes(el, string_id);
el.removeAttribute("hidden");
}
@ -568,7 +565,7 @@ class _ToolbarPanelHub {
triggerId: "protectionsPanelOpen",
});
if (message) {
const messageEl = await this._createHeroElement(win, doc, message);
const messageEl = this._createHeroElement(win, doc, message);
container.appendChild(messageEl);
infoButton.addEventListener("click", toggleMessage);
this.sendUserEventTelemetry(win, "IMPRESSION", message);

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

@ -77,6 +77,14 @@ add_task(async function test_with_rs_messages() {
"The message container was not populated with the expected number of msgs"
);
await BrowserTestUtils.waitForCondition(
() =>
document.querySelector(
"#PanelUI-whatsNew-message-container .whatsNew-message-body remote-text"
).shadowRoot.innerHTML,
"Ensure messages have content"
);
UITour.hideMenu(window, "appMenu");
// Clean up and remove messages
ToolbarPanelHub.disableAppmenuButton();

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

@ -12,6 +12,7 @@ describe("ToolbarPanelHub", () => {
let fakeWindow;
let fakeElementById;
let createdElements = [];
let createdCustomElements = [];
let eventListeners = {};
let addObserverStub;
let removeObserverStub;
@ -24,6 +25,7 @@ describe("ToolbarPanelHub", () => {
let getEventsByDateRangeStub;
let handleUserActionStub;
let defaultSearchStub;
let scriptloaderStub;
beforeEach(async () => {
sandbox = sinon.createSandbox();
@ -56,10 +58,34 @@ describe("ToolbarPanelHub", () => {
},
appendChild: sandbox.stub(),
setAttribute: sandbox.stub(),
textContent: "",
};
createdElements.push(element);
return element;
},
createElement: tagName => {
const element = {
tagName,
classList: {},
addEventListener: (ev, fn) => {
eventListeners[ev] = fn;
},
appendChild: sandbox.stub(),
setAttribute: sandbox.stub(),
textContent: "",
};
element.classList.add = sandbox.stub();
element.classList.includes = className =>
element.classList.add.firstCall.args[0] === className;
createdCustomElements.push(element);
return element;
},
l10n: {
translateElements: sandbox.stub(),
translateFragment: sandbox.stub(),
formatMessages: sandbox.stub().resolves([{}]),
setAttributes: sandbox.stub(),
},
};
fakeWindow = {
// eslint-disable-next-line object-shorthand
@ -79,11 +105,13 @@ describe("ToolbarPanelHub", () => {
panel: fakeElementById,
whatsNewPanel: fakeElementById,
},
customElements: { get: sandbox.stub() },
};
everyWindowStub = {
registerCallback: sandbox.stub(),
unregisterCallback: sandbox.stub(),
};
scriptloaderStub = { loadSubScript: sandbox.stub() };
addObserverStub = sandbox.stub();
removeObserverStub = sandbox.stub();
getBoolPrefStub = sandbox.stub();
@ -108,6 +136,7 @@ describe("ToolbarPanelHub", () => {
setBoolPref: setBoolPrefStub,
},
search: defaultSearchStub,
scriptloader: scriptloaderStub,
},
PrivateBrowsingUtils: {
isBrowserPrivate: isBrowserPrivateStub,
@ -116,13 +145,6 @@ describe("ToolbarPanelHub", () => {
getEarliestRecordedDate: getEarliestRecordedDateStub,
getEventsByDateRange: getEventsByDateRangeStub,
},
RemoteL10n: {
l10n: {
translateElements: sandbox.stub(),
translateFragment: sandbox.stub(),
formatMessages: sandbox.stub().resolves([{}]),
},
},
});
});
afterEach(() => {
@ -131,6 +153,7 @@ describe("ToolbarPanelHub", () => {
globals.restore();
eventListeners = {};
createdElements = [];
createdCustomElements = [];
});
it("should create an instance", () => {
assert.ok(instance);
@ -290,6 +313,32 @@ describe("ToolbarPanelHub", () => {
handleUserAction: handleUserActionStub,
});
});
it("should have correct state", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.returns(messages);
const ev1 = sandbox.stub();
ev1.withArgs("type").returns(1); // tracker
ev1.withArgs("count").returns(4);
const ev2 = sandbox.stub();
ev2.withArgs("type").returns(4); // fingerprinter
ev2.withArgs("count").returns(3);
getEventsByDateRangeStub.returns([
{ getResultByName: ev1 },
{ getResultByName: ev2 },
]);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.propertyVal(instance.state.contentArguments, "trackerCount", 4);
assert.propertyVal(
instance.state.contentArguments,
"fingerprinterCount",
3
);
});
it("should render messages to the panel on renderMessages()", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
@ -311,19 +360,30 @@ describe("ToolbarPanelHub", () => {
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
for (let message of messages) {
assert.ok(createdElements.find(el => el.tagName === "h2"));
assert.ok(
createdCustomElements.find(el =>
el.classList.includes("whatsNew-message-title")
)
);
if (message.content.layout === "tracking-protections") {
assert.ok(createdElements.find(el => el.tagName === "h4"));
assert.ok(
createdCustomElements.find(el =>
el.classList.includes("whatsNew-message-subtitle")
)
);
}
if (message.id === "WHATS_NEW_FINGERPRINTER_COUNTER_72") {
assert.ok(createdElements.find(el => el.tagName === "h4"));
assert.ok(
createdElements.find(
el => el.tagName === "h2" && el.textContent === 3
)
);
}
assert.ok(createdElements.find(el => el.tagName === "p"));
assert.ok(
createdCustomElements.find(el =>
el.classList.includes("whatsNew-message-content")
)
);
}
// Call the click handler to make coverage happy.
eventListeners.mouseup();
@ -333,17 +393,18 @@ describe("ToolbarPanelHub", () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
const removeStub = sandbox.stub();
fakeElementById.querySelectorAll.onCall(0).returns([]);
fakeElementById.querySelectorAll.onCall(1).returns(["a", "b", "c"]);
fakeElementById.querySelectorAll
.onCall(1)
.returns([{ remove: removeStub }, { remove: removeStub }]);
getMessagesStub.returns(messages);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledThrice(fakeElementById.removeChild);
assert.equal(fakeElementById.removeChild.firstCall.args[0], "a");
assert.equal(fakeElementById.removeChild.secondCall.args[0], "b");
assert.calledTwice(removeStub);
});
it("should sort based on order field value", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
@ -385,38 +446,7 @@ describe("ToolbarPanelHub", () => {
"Firefox Send Logo"
);
});
it("should accept fluent ids for image attributes", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.id === "WHATS_NEW_70_1"
);
messages[0].content.icon_alt = { string_id: "foo" };
getMessagesStub.returns(messages);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [
{
id: "foo",
args: instance.state.contentArguments,
},
]);
});
it("handle fluent attributes", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.id === "WHATS_NEW_70_1"
);
messages[0].content.icon_alt = { string_id: "foo" };
getMessagesStub.returns(messages);
global.RemoteL10n.l10n.formatMessages
.withArgs([{ id: "foo", args: sinon.match.object }])
.resolves([{ attributes: [{ name: "alt", value: "bar" }] }]);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
const imgEl = createdElements.find(e => e.tagName === "img");
assert.calledWithExactly(imgEl.setAttribute, "alt", "bar");
});
it("should accept fluent ids for elements attributes", async () => {
it("should set state values as data-attribute", async () => {
const [message] = (await PanelTestProvider.getMessages()).filter(
m =>
m.template === "whatsnew_panel_message" &&
@ -427,102 +457,13 @@ describe("ToolbarPanelHub", () => {
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [
{
id: message.content.subtitle.string_id,
args: instance.state.contentArguments,
},
]);
});
it("should correctly compute blocker trackers and date", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
// Currently this.state.contentArguments has 9 different entries
assert.callCount(createdCustomElements[0].setAttribute, 9);
assert.calledWithExactly(
createdCustomElements[0].setAttribute,
"fluent-variable-searchEngineName",
defaultSearchStub.defaultEngine.name
);
getMessagesStub.returns(messages);
const ev1 = sandbox.stub();
ev1.withArgs("type").returns(2); // cookie
ev1.withArgs("count").returns(4);
const ev2 = sandbox.stub();
ev2.withArgs("type").returns(2); // cookie
ev2.withArgs("count").returns(3);
getEventsByDateRangeStub.returns([
{ getResultByName: ev1 },
{ getResultByName: ev2 },
]);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [
{
id: sinon.match.string,
args: {
blockedCount: 7,
earliestDate: getEarliestRecordedDateStub(),
cookieCount: 7,
cryptominerCount: 0,
socialCount: 0,
trackerCount: 0,
fingerprinterCount: 0,
searchEngineName: Services.search.defaultEngine.name,
},
},
]);
});
it("should correctly compute event counts per type", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.returns(messages);
const ev1 = sandbox.stub();
ev1.withArgs("type").returns(1); // tracker
ev1.withArgs("count").returns(4);
const ev2 = sandbox.stub();
ev2.withArgs("type").returns(4); // fingerprinter
ev2.withArgs("count").returns(3);
getEventsByDateRangeStub.returns([
{ getResultByName: ev1 },
{ getResultByName: ev2 },
]);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [
{
id: sinon.match.string,
args: {
blockedCount: 7,
earliestDate: getEarliestRecordedDateStub(),
trackerCount: 4,
fingerprinterCount: 3,
cookieCount: 0,
cryptominerCount: 0,
socialCount: 0,
searchEngineName: Services.search.defaultEngine.name,
},
},
]);
});
it("should fallback to undefined search engine name", async () => {
globals.set("Services", {
...global.Services,
search: { defaultEngine: null },
});
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.returns(messages);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [
{
id: sinon.match.string,
args: {
...instance.state.contentArguments,
searchEngineName: "undefined",
},
},
]);
});
it("should only render unique dates (no duplicates)", async () => {
const messages = (await PanelTestProvider.getMessages()).filter(
@ -745,7 +686,6 @@ describe("ToolbarPanelHub", () => {
it("should call removeMessages when forcing a message to show", () => {
instance.forceShowMessage(browser, messages);
assert.calledOnce(removeMessagesSpy);
assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);
});
it("should call renderMessages when forcing a message to show", () => {

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

@ -345,6 +345,7 @@ const TEST_GLOBAL = {
},
ww: { registerNotification() {}, unregisterNotification() {} },
appinfo: { appBuildID: "20180710100040", version: "69.0a1" },
scriptloader: { loadSubScript: () => {} },
},
XPCOMUtils: {
defineLazyGetter(object, name, f) {

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

@ -1854,6 +1854,11 @@ toolbarpaletteitem[place="menu-panel"] > .subviewbutton-nav::after {
text-decoration: underline;
}
#protections-popup-message .protections-popup-content {
display: block;
margin: 12px 0;
}
panelview[mainview] #PanelUI-whatsNew-content {
height: 43em;
}
@ -1868,13 +1873,23 @@ panelview[mainview] #PanelUI-whatsNew-content {
padding: 0;
}
#PanelUI-whatsNew .whatsNew-message:not(:first-child)::before {
/* The following 2 rules show a 1 pixel line separator between What's New
* messages while at the same time ensuring that the first message (which has
* a date header) will not show the separator
*/
#PanelUI-whatsNew .whatsNew-message-body::before {
content: "";
display: block;
height: 1px;
width: 104%;
margin-inline-start: -2%;
background: var(--panel-separator-color);
}
#PanelUI-whatsNew .whatsNew-message-date + .whatsNew-message-body::before {
display: none;
}
#PanelUI-whatsNew .whatsNew-message-date {
font-size: .85em;
margin: 0 -12px;
@ -1908,17 +1923,18 @@ panelview[mainview] #PanelUI-whatsNew-content {
inset-inline-end: 6px;
height: 32px;
position: absolute;
top: 10px;
top: 16px;
width: 32px;
}
#PanelUI-whatsNew .whatsNew-message-title,
#protections-popup-message .whatsNew-message-title {
display: block;
padding-inline-end: 46px;
font-size: 1.3em;
font-weight: 600;
line-height: 1.4em;
margin: 2px 0;
margin: 8px 0 2px;
}
#PanelUI-whatsNew .whatsNew-message-title-large {
@ -1934,6 +1950,11 @@ panelview[mainview] #PanelUI-whatsNew-content {
font-weight: normal;
}
#PanelUI-whatsNew .whatsNew-message-content {
display: block;
margin: 13px 0;
}
#PanelUI-whatsNew .text-link {
background: none;
border: 0;