Bug 1715158 - Remove ASRouterUISurface, snippet templates, and associated tests. r=pdahiya

Depends on D193853

Differential Revision: https://phabricator.services.mozilla.com/D193854
This commit is contained in:
Mike Conley 2023-12-06 16:15:15 +00:00
parent d7aac5f2af
commit da108723df
35 changed files with 1 additions and 5719 deletions

View file

@ -1,346 +0,0 @@
/* 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/. */
import { MESSAGE_TYPE_HASH as msg } from "common/ActorConstants.sys.mjs";
import { actionTypes as at } from "common/Actions.sys.mjs";
import { ASRouterUtils } from "./asrouter-utils";
import { generateBundles } from "./rich-text-strings";
import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
import { LocalizationProvider, ReactLocalization } from "@fluent/react";
import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
import React from "react";
import ReactDOM from "react-dom";
import { SnippetsTemplates } from "./templates/template-manifest";
const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
// Note: nextProps/prevProps refer to props passed to <ImpressionsWrapper />, not <ASRouterUISurface />
function shouldSendImpressionOnUpdate(nextProps, prevProps) {
return (
nextProps.message.id &&
(!prevProps.message || prevProps.message.id !== nextProps.message.id)
);
}
export class ASRouterUISurface extends React.PureComponent {
constructor(props) {
super(props);
this.sendClick = this.sendClick.bind(this);
this.sendImpression = this.sendImpression.bind(this);
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
this.onUserAction = this.onUserAction.bind(this);
this.fetchFlowParams = this.fetchFlowParams.bind(this);
this.onBlockSelected = this.onBlockSelected.bind(this);
this.onBlockById = this.onBlockById.bind(this);
this.onDismiss = this.onDismiss.bind(this);
this.onMessageFromParent = this.onMessageFromParent.bind(this);
this.state = { message: {} };
if (props.document) {
this.footerPortal = props.document.getElementById(
"footer-asrouter-container"
);
}
}
async fetchFlowParams(params = {}) {
let result = {};
const { fxaEndpoint } = this.props;
if (!fxaEndpoint) {
const err =
"Tried to fetch flow params before fxaEndpoint pref was ready";
console.error(err);
}
try {
const urlObj = new URL(fxaEndpoint);
urlObj.pathname = "metrics-flow";
Object.keys(params).forEach(key => {
urlObj.searchParams.append(key, params[key]);
});
const response = await fetch(urlObj.toString(), { credentials: "omit" });
if (response.status === 200) {
const { deviceId, flowId, flowBeginTime } = await response.json();
result = { deviceId, flowId, flowBeginTime };
} else {
console.error("Non-200 response", response);
}
} catch (error) {
console.error(error);
}
return result;
}
sendUserActionTelemetry(extraProps = {}) {
const { message } = this.state;
const eventType = `${message.provider}_user_event`;
const source = extraProps.id;
delete extraProps.id;
ASRouterUtils.sendTelemetry({
source,
message_id: message.id,
action: eventType,
...extraProps,
});
}
sendImpression(extraProps) {
if (this.state.message.provider === "preview") {
return Promise.resolve();
}
this.sendUserActionTelemetry({ event: "IMPRESSION", ...extraProps });
return ASRouterUtils.sendMessage({
type: msg.IMPRESSION,
data: this.state.message,
});
}
// If link has a `metric` data attribute send it as part of the `event_context`
// telemetry field which can have arbitrary values.
// Used for router messages with links as part of the content.
sendClick(event) {
const { dataset } = event.target;
const metric = {
event_context: dataset.metric,
// Used for the `source` of the event. Needed to differentiate
// from other snippet or onboarding events that may occur.
id: "NEWTAB_FOOTER_BAR_CONTENT",
};
const { entrypoint_name, entrypoint_value } = dataset;
// Assign the snippet referral for the action
const entrypoint = entrypoint_name
? new URLSearchParams([[entrypoint_name, entrypoint_value]]).toString()
: entrypoint_value;
const action = {
type: dataset.action,
data: {
args: dataset.args,
...(entrypoint && { entrypoint }),
},
};
if (action.type) {
ASRouterUtils.executeAction(action);
}
if (
!this.state.message.content.do_not_autoblock &&
!dataset.do_not_autoblock
) {
this.onBlockById(this.state.message.id);
}
if (this.state.message.provider !== "preview") {
this.sendUserActionTelemetry({ event: "CLICK_BUTTON", ...metric });
}
}
onBlockSelected(options) {
return this.onBlockById(this.state.message.id, {
...options,
campaign: this.state.message.campaign,
});
}
onBlockById(id, options) {
return ASRouterUtils.blockById(id, options).then(clearAll => {
if (clearAll) {
this.setState({ message: {} });
}
});
}
onDismiss() {
this.clearMessage(this.state.message.id);
}
// Blocking a snippet by id blocks the entire campaign
// so when clearing we use the two values interchangeably
clearMessage(idOrCampaign) {
if (
idOrCampaign === this.state.message.id ||
idOrCampaign === this.state.message.campaign
) {
this.setState({ message: {} });
}
}
clearProvider(id) {
if (this.state.message.provider === id) {
this.setState({ message: {} });
}
}
onMessageFromParent({ type, data }) {
// These only exists due to onPrefChange events in ASRouter
switch (type) {
case "ClearMessages": {
data.forEach(id => this.clearMessage(id));
break;
}
case "ClearProviders": {
data.forEach(id => this.clearProvider(id));
break;
}
case "EnterSnippetsPreviewMode": {
this.props.dispatch({ type: at.SNIPPETS_PREVIEW_MODE });
break;
}
}
}
requestMessage(endpoint) {
ASRouterUtils.sendMessage({
type: "NEWTAB_MESSAGE_REQUEST",
data: { endpoint },
}).then(state => this.setState(state));
}
componentWillMount() {
const endpoint = ASRouterUtils.getPreviewEndpoint();
if (endpoint && endpoint.theme === "dark") {
global.window.dispatchEvent(
new CustomEvent("LightweightTheme:Set", {
detail: { data: NEWTAB_DARK_THEME },
})
);
}
if (endpoint && endpoint.dir === "rtl") {
//Set `dir = rtl` on the HTML
this.props.document.dir = "rtl";
}
ASRouterUtils.addListener(this.onMessageFromParent);
this.requestMessage(endpoint);
}
componentWillUnmount() {
ASRouterUtils.removeListener(this.onMessageFromParent);
}
componentDidUpdate(prevProps, prevState) {
if (
prevProps.adminContent &&
JSON.stringify(prevProps.adminContent) !==
JSON.stringify(this.props.adminContent)
) {
this.updateContent();
}
if (prevState.message.id !== this.state.message.id) {
const main = global.window.document.querySelector("main");
if (main) {
if (this.state.message.id) {
main.classList.add("has-snippet");
} else {
main.classList.remove("has-snippet");
}
}
}
}
updateContent() {
this.setState({
...this.props.adminContent,
});
}
async getMonitorUrl({ url, flowRequestParams = {} }) {
const flowValues = await this.fetchFlowParams(flowRequestParams);
// Note that flowParams are actually added dynamically on the page
const urlObj = new URL(url);
["deviceId", "flowId", "flowBeginTime"].forEach(key => {
if (key in flowValues) {
urlObj.searchParams.append(key, flowValues[key]);
}
});
return urlObj.toString();
}
async onUserAction(action) {
switch (action.type) {
// This needs to be handled locally because its
case "ENABLE_FIREFOX_MONITOR":
const url = await this.getMonitorUrl(action.data.args);
ASRouterUtils.executeAction({ type: "OPEN_URL", data: { args: url } });
break;
default:
ASRouterUtils.executeAction(action);
}
}
renderSnippets() {
const { message } = this.state;
if (!SnippetsTemplates[message.template]) {
return null;
}
const SnippetComponent = SnippetsTemplates[message.template];
const { content } = message;
return (
<ImpressionsWrapper
id="NEWTAB_FOOTER_BAR"
message={this.state.message}
sendImpression={this.sendImpression}
shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}
// This helps with testing
document={this.props.document}
>
<LocalizationProvider
l10n={new ReactLocalization(generateBundles(content))}
>
<SnippetComponent
{...this.state.message}
UISurface="NEWTAB_FOOTER_BAR"
onBlock={this.onBlockSelected}
onDismiss={this.onDismiss}
onAction={this.onUserAction}
sendClick={this.sendClick}
sendUserActionTelemetry={this.sendUserActionTelemetry}
/>
</LocalizationProvider>
</ImpressionsWrapper>
);
}
renderPreviewBanner() {
if (this.state.message.provider !== "preview") {
return null;
}
return (
<div className="snippets-preview-banner">
<span className="icon icon-small-spacer icon-info" />
<span>Preview Purposes Only</span>
</div>
);
}
render() {
const { message } = this.state;
if (!message.id) {
return null;
}
const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(
message.template
);
return shouldRenderBelowSearch ? (
// Render special below search snippets in place;
<div className="below-search-snippet-wrapper">
{this.renderSnippets()}
</div>
) : (
// For regular snippets etc. we should render everything in our footer
// container.
ReactDOM.createPortal(
<>
{this.renderPreviewBanner()}
{this.renderSnippets()}
</>,
this.footerPortal
)
);
}
}
ASRouterUISurface.defaultProps = { document: global.document };

View file

@ -74,41 +74,6 @@ export const ASRouterUtils = {
return ASRouterUtils.sendMessage(ac.ASRouterUserEvent(ping)); return ASRouterUtils.sendMessage(ac.ASRouterUserEvent(ping));
}, },
getPreviewEndpoint() { getPreviewEndpoint() {
if (
global.document &&
global.document.location &&
global.document.location.href.includes("endpoint")
) {
const params = new URLSearchParams(
global.document.location.href.slice(
global.document.location.href.indexOf("endpoint")
)
);
try {
const endpoint = new URL(params.get("endpoint"));
return {
url: endpoint.href,
snippetId: params.get("snippetId"),
theme: this.getPreviewTheme(),
dir: this.getPreviewDir(),
};
} catch (e) {}
}
return null; return null;
}, },
getPreviewTheme() {
return new URLSearchParams(
global.document.location.href.slice(
global.document.location.href.indexOf("theme")
)
).get("theme");
},
getPreviewDir() {
return new URLSearchParams(
global.document.location.href.slice(
global.document.location.href.indexOf("dir")
)
).get("dir");
},
}; };

View file

@ -1,86 +0,0 @@
/* 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/. */
import { Localized } from "@fluent/react";
import React from "react";
import { RICH_TEXT_KEYS } from "../../rich-text-strings";
import { safeURI } from "../../template-utils";
// Elements allowed in snippet content
const ALLOWED_TAGS = {
b: <b />,
i: <i />,
u: <u />,
strong: <strong />,
em: <em />,
br: <br />,
};
/**
* Transform an object (tag name: {url}) into (tag name: anchor) where the url
* is used as href, in order to render links inside a Fluent.Localized component.
*/
export function convertLinks(
links,
sendClick,
doNotAutoBlock,
openNewWindow = false
) {
if (links) {
return Object.keys(links).reduce((acc, linkTag) => {
const { action } = links[linkTag];
// Setting the value to false will not include the attribute in the anchor
const url = action ? false : safeURI(links[linkTag].url);
acc[linkTag] = (
// eslint was getting a false positive caused by the dynamic injection
// of content.
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
href={url}
target={openNewWindow ? "_blank" : ""}
data-metric={links[linkTag].metric}
data-action={action}
data-args={links[linkTag].args}
data-do_not_autoblock={doNotAutoBlock}
data-entrypoint_name={links[linkTag].entrypoint_name}
data-entrypoint_value={links[linkTag].entrypoint_value}
rel="noreferrer"
onClick={sendClick}
/>
);
return acc;
}, {});
}
return null;
}
/**
* Message wrapper used to sanitize markup and render HTML.
*/
export function RichText(props) {
if (!RICH_TEXT_KEYS.includes(props.localization_id)) {
throw new Error(
`ASRouter: ${props.localization_id} is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js`
);
}
return (
<Localized
id={props.localization_id}
elems={{
...ALLOWED_TAGS,
...props.customElements,
...convertLinks(
props.links,
props.sendClick,
props.doNotAutoBlock,
props.openNewWindow
),
}}
>
<span>{props.text}</span>
</Localized>
);
}

View file

@ -1,121 +0,0 @@
/* 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/. */
import React from "react";
export class SnippetBase extends React.PureComponent {
constructor(props) {
super(props);
this.onBlockClicked = this.onBlockClicked.bind(this);
this.onDismissClicked = this.onDismissClicked.bind(this);
this.setBlockButtonRef = this.setBlockButtonRef.bind(this);
this.onBlockButtonMouseEnter = this.onBlockButtonMouseEnter.bind(this);
this.onBlockButtonMouseLeave = this.onBlockButtonMouseLeave.bind(this);
this.state = { blockButtonHover: false };
}
componentDidMount() {
if (this.blockButtonRef) {
this.blockButtonRef.addEventListener(
"mouseenter",
this.onBlockButtonMouseEnter
);
this.blockButtonRef.addEventListener(
"mouseleave",
this.onBlockButtonMouseLeave
);
}
}
componentWillUnmount() {
if (this.blockButtonRef) {
this.blockButtonRef.removeEventListener(
"mouseenter",
this.onBlockButtonMouseEnter
);
this.blockButtonRef.removeEventListener(
"mouseleave",
this.onBlockButtonMouseLeave
);
}
}
setBlockButtonRef(element) {
this.blockButtonRef = element;
}
onBlockButtonMouseEnter() {
this.setState({ blockButtonHover: true });
}
onBlockButtonMouseLeave() {
this.setState({ blockButtonHover: false });
}
onBlockClicked() {
if (this.props.provider !== "preview") {
this.props.sendUserActionTelemetry({
event: "BLOCK",
id: this.props.UISurface,
});
}
this.props.onBlock();
}
onDismissClicked() {
if (this.props.provider !== "preview") {
this.props.sendUserActionTelemetry({
event: "DISMISS",
id: this.props.UISurface,
});
}
this.props.onDismiss();
}
renderDismissButton() {
if (this.props.footerDismiss) {
return (
<div className="footer">
<div className="footer-content">
<button
className="ASRouterButton secondary"
onClick={this.onDismissClicked}
>
{this.props.content.scene2_dismiss_button_text}
</button>
</div>
</div>
);
}
const label = this.props.content.block_button_text || "Remove this";
return (
<button
className="blockButton"
title={label}
aria-label={label}
onClick={this.onBlockClicked}
ref={this.setBlockButtonRef}
/>
);
}
render() {
const { props } = this;
const { blockButtonHover } = this.state;
const containerClassName = `SnippetBaseContainer${
props.className ? ` ${props.className}` : ""
}${blockButtonHover ? " active" : ""}`;
return (
<div className={containerClassName} style={this.props.textStyle}>
<div className="innerWrapper">{props.children}</div>
{this.renderDismissButton()}
</div>
);
}
}

View file

@ -1,117 +0,0 @@
.SnippetBaseContainer {
position: fixed;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
background-color: var(--newtab-background-color-secondary);
color: var(--newtab-text-primary-color);
font-size: 14px;
line-height: 20px;
border-top: 1px solid transparent;
box-shadow: $shadow-secondary;
display: flex;
align-items: center;
a {
cursor: pointer;
color: var(--newtab-primary-action-background);
&:hover {
text-decoration: underline;
}
[lwt-newtab-brighttext] & {
font-weight: bold;
}
}
input {
&[type='checkbox'] {
margin-inline-start: 0;
}
}
.innerWrapper {
margin: 0 auto;
display: flex;
align-items: center;
padding: 12px $section-horizontal-padding;
// This is to account for the block button on smaller screens
padding-inline-end: 36px;
max-width: $wrapper-max-width-large + ($section-horizontal-padding * 2);
@media (min-width: $break-point-large) {
padding-inline-end: $section-horizontal-padding;
}
@media (min-width: $break-point-widest) {
max-width: $wrapper-max-width-widest + ($section-horizontal-padding * 2);
}
}
.blockButton {
display: none;
background: none;
border: 0;
position: absolute;
top: 20px;
inset-inline-end: 12px;
height: 16px;
width: 16px;
background-image: url('chrome://global/skin/icons/close.svg');
-moz-context-properties: fill;
color: inherit;
fill: currentColor;
opacity: 0.5;
margin-top: -8px;
padding: 0;
cursor: pointer;
}
&:hover .blockButton {
display: block;
}
.icon {
height: 42px;
width: 42px;
margin-inline-end: 12px;
flex-shrink: 0;
}
}
.snippets-preview-banner {
font-size: 15px;
line-height: 42px;
color: var(--newtab-text-primary-color);
background: var(--newtab-background-color-secondary);
text-align: center;
position: absolute;
top: 0;
width: 100%;
span {
vertical-align: middle;
}
}
// We show snippet icons for both themes and conditionally hide
// based on which theme is currently active
body {
&:not([lwt-newtab-brighttext]) {
.icon-dark-theme,
.icon.icon-dark-theme,
.scene2Icon .icon-dark-theme {
display: none;
}
}
&[lwt-newtab-brighttext] {
.icon-light-theme,
.icon.icon-light-theme,
.scene2Icon .icon-light-theme {
display: none;
}
}
}

View file

@ -1,44 +0,0 @@
/* 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/. */
import { FluentBundle, FluentResource } from "@fluent/bundle";
/**
* Properties that allow rich text MUST be added to this list.
* key: the localization_id that should be used
* value: a property or array of properties on the message.content object
*/
const RICH_TEXT_CONFIG = {
text: ["text", "scene1_text"],
success_text: "success_text",
error_text: "error_text",
scene2_text: "scene2_text",
amo_html: "amo_html",
privacy_html: "scene2_privacy_html",
disclaimer_html: "scene2_disclaimer_html",
};
export const RICH_TEXT_KEYS = Object.keys(RICH_TEXT_CONFIG);
/**
* Generates an array of messages suitable for fluent's localization provider
* including all needed strings for rich text.
* @param {object} content A .content object from an ASR message (i.e. message.content)
* @returns {FluentBundle[]} A array containing the fluent message context
*/
export function generateBundles(content) {
const bundle = new FluentBundle("en-US");
RICH_TEXT_KEYS.forEach(key => {
const attrs = RICH_TEXT_CONFIG[key];
const attrsToTry = Array.isArray(attrs) ? [...attrs] : [attrs];
let string = "";
while (!string && attrsToTry.length) {
const attr = attrsToTry.pop();
string = content[attr];
}
bundle.addResource(new FluentResource(`${key} = ${string}`));
});
return [bundle];
}

View file

@ -1,153 +0,0 @@
/* 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/. */
import React from "react";
import { SimpleSnippet } from "../SimpleSnippet/SimpleSnippet";
class EOYSnippetBase extends React.PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
/**
* setFrequencyValue - `frequency` form parameter value should be `monthly`
* if `monthly-checkbox` is selected or `single` otherwise
*/
setFrequencyValue() {
const frequencyCheckbox = this.refs.form.querySelector("#monthly-checkbox");
if (frequencyCheckbox.checked) {
this.refs.form.querySelector("[name='frequency']").value = "monthly";
}
}
handleSubmit(event) {
event.preventDefault();
this.props.sendClick(event);
this.setFrequencyValue();
if (!this.props.content.do_not_autoblock) {
this.props.onBlock();
}
this.refs.form.submit();
}
renderDonations() {
const fieldNames = ["first", "second", "third", "fourth"];
const numberFormat = new Intl.NumberFormat(
this.props.content.locale || navigator.language,
{
style: "currency",
currency: this.props.content.currency_code,
minimumFractionDigits: 0,
}
);
// Default to `second` button
const { selected_button } = this.props.content;
const btnStyle = {
color: this.props.content.button_color,
backgroundColor: this.props.content.button_background_color,
};
const donationURLParams = [];
const paramsStartIndex = this.props.content.donation_form_url.indexOf("?");
for (const entry of new URLSearchParams(
this.props.content.donation_form_url.slice(paramsStartIndex)
).entries()) {
donationURLParams.push(entry);
}
return (
<form
className="EOYSnippetForm"
action={this.props.content.donation_form_url}
method={this.props.form_method}
onSubmit={this.handleSubmit}
data-metric="EOYSnippetForm"
ref="form"
>
{donationURLParams.map(([key, value], idx) => (
<input type="hidden" name={key} value={value} key={idx} />
))}
{fieldNames.map((field, idx) => {
const button_name = `donation_amount_${field}`;
const amount = this.props.content[button_name];
return (
<React.Fragment key={idx}>
<input
type="radio"
name="amount"
value={amount}
id={field}
defaultChecked={button_name === selected_button}
/>
<label htmlFor={field} className="donation-amount">
{numberFormat.format(amount)}
</label>
</React.Fragment>
);
})}
<div className="monthly-checkbox-container">
<input id="monthly-checkbox" type="checkbox" />
<label htmlFor="monthly-checkbox">
{this.props.content.monthly_checkbox_label_text}
</label>
</div>
<input type="hidden" name="frequency" value="single" />
<input
type="hidden"
name="currency"
value={this.props.content.currency_code}
/>
<input
type="hidden"
name="presets"
value={fieldNames.map(
field => this.props.content[`donation_amount_${field}`]
)}
/>
<button
style={btnStyle}
type="submit"
className="ASRouterButton primary donation-form-url"
>
{this.props.content.button_label}
</button>
</form>
);
}
render() {
const textStyle = {
color: this.props.content.text_color,
backgroundColor: this.props.content.background_color,
};
const customElement = (
<em style={{ backgroundColor: this.props.content.highlight_color }} />
);
return (
<SimpleSnippet
{...this.props}
className={this.props.content.test}
customElements={{ em: customElement }}
textStyle={textStyle}
extraContent={this.renderDonations()}
/>
);
}
}
export const EOYSnippet = props => {
const extendedContent = {
monthly_checkbox_label_text: "Make my donation monthly",
locale: "en-US",
currency_code: "usd",
selected_button: "donation_amount_second",
...props.content,
};
return (
<EOYSnippetBase {...props} content={extendedContent} form_method="GET" />
);
};

View file

@ -1,171 +0,0 @@
{
"title": "EOYSnippet",
"description": "Fundraising Snippet",
"version": "1.1.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"donation_form_url": {
"type": "string",
"description": "Url to the donation form."
},
"currency_code": {
"type": "string",
"description": "The code for the currency. Examle gbp, cad, usd.",
"default": "usd"
},
"locale": {
"type": "string",
"description": "String for the locale code.",
"default": "en-US"
},
"text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"text_color": {
"type": "string",
"description": "Modify the text message color"
},
"background_color": {
"type": "string",
"description": "Snippet background color."
},
"highlight_color": {
"type": "string",
"description": "Paragraph em highlight color."
},
"donation_amount_first": {
"type": "number",
"description": "First button amount."
},
"donation_amount_second": {
"type": "number",
"description": "Second button amount."
},
"donation_amount_third": {
"type": "number",
"description": "Third button amount."
},
"donation_amount_fourth": {
"type": "number",
"description": "Fourth button amount."
},
"selected_button": {
"type": "string",
"description": "Default donation_amount_second. Donation amount button that's selected by default.",
"default": "donation_amount_second"
},
"icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"icon_dark_theme": {
"type": "string",
"description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
},
"icon_alt_text": {
"type": "string",
"description": "Alt text for accessibility",
"default": ""
},
"title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{ "description": "Snippet title displayed before snippet text" }
]
},
"title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"title_icon_dark_theme": {
"type": "string",
"description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
},
"button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
]
},
"button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"block_button_text": {
"type": "string",
"description": "Tooltip text used for dismiss button."
},
"monthly_checkbox_label_text": {
"type": "string",
"description": "Label text for monthly checkbox.",
"default": "Make my donation monthly"
},
"test": {
"type": "string",
"description": "Different styles for the snippet. Options are bold and takeover."
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked"
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
},
"args": {
"type": "string",
"description": "Additional parameters for link action, example which specific menu the button should open"
}
}
}
},
"additionalProperties": false,
"required": [
"text",
"donation_form_url",
"donation_amount_first",
"donation_amount_second",
"donation_amount_third",
"donation_amount_fourth",
"button_label",
"currency_code"
],
"dependencies": {
"button_color": ["button_label"],
"button_background_color": ["button_label"]
}
}

View file

@ -1,55 +0,0 @@
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center;
.donation-amount,
.donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px;
}
.donation-amount {
color: var(--newtab-text-primary-color);
margin-inline-end: 18px;
border: $input-border;
padding: 5px 14px;
background: var(--newtab-background-color-secondary);
cursor: pointer;
}
input {
&[type='radio'] {
opacity: 0;
margin-inline-end: -18px;
&:checked + .donation-amount {
// Use a text color for the background to achieve an inverted look.
background: var(--newtab-text-secondary-color);
color: var(--newtab-background-color-secondary);
border: $border-secondary;
}
// accessibility
&:checked:focus + .donation-amount,
&:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-primary-action-background);
}
}
}
.monthly-checkbox-container {
display: flex;
width: 100%;
}
.donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex;
}
}

View file

@ -1,38 +0,0 @@
/* 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/. */
import React from "react";
import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx";
export const FXASignupSnippet = props => {
const userAgent = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./);
const firefox_version = userAgent ? parseInt(userAgent[1], 10) : 0;
const extendedContent = {
scene1_button_label: "Learn more",
retry_button_label: "Try again",
scene2_email_placeholder_text: "Your email here",
scene2_button_label: "Sign me up",
scene2_dismiss_button_text: "Dismiss",
...props.content,
hidden_inputs: {
action: "email",
context: "fx_desktop_v3",
entrypoint: "snippets",
utm_source: "snippet",
utm_content: firefox_version,
utm_campaign: props.content.utm_campaign,
utm_term: props.content.utm_term,
...props.content.hidden_inputs,
},
};
return (
<SubmitFormSnippet
{...props}
content={extendedContent}
form_action={"https://accounts.firefox.com/"}
form_method="GET"
/>
);
};

View file

@ -1,196 +0,0 @@
{
"title": "FXASignupSnippet",
"description": "A snippet template for FxA sign up/sign in",
"version": "1.2.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"scene1_title": {
"allof": [
{ "$ref": "#/definitions/plainText" },
{ "description": "snippet title displayed before snippet text" }
]
},
"scene1_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_section_title_icon": {
"type": "string",
"description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_icon_dark_theme": {
"type": "string",
"description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_text": {
"type": "string",
"description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display."
},
"scene1_section_title_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, scene1_section_title_text links to this" }
]
},
"scene2_title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Title displayed before text in scene 2. Should be plain text."
}
]
},
"scene2_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"scene1_icon_dark_theme": {
"type": "string",
"description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
},
"scene1_title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene1_title_icon_dark_theme": {
"type": "string",
"description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene2_email_placeholder_text": {
"type": "string",
"description": "Value to show while input is empty.",
"default": "Your email here"
},
"scene2_button_label": {
"type": "string",
"description": "Label for form submit button",
"default": "Sign me up"
},
"scene2_dismiss_button_text": {
"type": "string",
"description": "Label for the dismiss button when the sign-up form is expanded.",
"default": "Dismiss"
},
"hidden_inputs": {
"type": "object",
"description": "Each entry represents a hidden input, key is used as value for the name property.",
"properties": {
"action": {
"type": "string",
"enum": ["email"]
},
"context": {
"type": "string",
"enum": ["fx_desktop_v3"]
},
"entrypoint": {
"type": "string",
"enum": ["snippets"]
},
"utm_content": {
"type": "number",
"description": "Firefox version number"
},
"utm_source": {
"type": "string",
"enum": ["snippet"]
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"additionalProperties": false
}
},
"scene1_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
],
"default": "Learn more"
},
"scene1_button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"scene1_button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"retry_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for the button in the event of a submission error/failure."
}
],
"default": "Try again"
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked",
"default": false
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
}
}
}
},
"additionalProperties": false,
"required": ["scene1_text", "scene2_text", "scene1_button_label"],
"dependencies": {
"scene1_button_color": ["scene1_button_label"],
"scene1_button_background_color": ["scene1_button_label"]
}
}

View file

@ -1,34 +0,0 @@
/* 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/. */
import React from "react";
import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx";
export const NewsletterSnippet = props => {
const extendedContent = {
scene1_button_label: "Learn more",
retry_button_label: "Try again",
scene2_email_placeholder_text: "Your email here",
scene2_button_label: "Sign me up",
scene2_dismiss_button_text: "Dismiss",
scene2_newsletter: "mozilla-foundation",
...props.content,
hidden_inputs: {
newsletters: props.content.scene2_newsletter || "mozilla-foundation",
fmt: "H",
lang: props.content.locale || "en-US",
source_url: `https://snippets.mozilla.com/show/${props.id}`,
...props.content.hidden_inputs,
},
};
return (
<SubmitFormSnippet
{...props}
content={extendedContent}
form_action={"https://basket.mozilla.org/subscribe.json"}
form_method="POST"
/>
);
};

View file

@ -1,186 +0,0 @@
{
"title": "NewsletterSnippet",
"description": "A snippet template for send to device mobile download",
"version": "1.2.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"locale": {
"type": "string",
"description": "Two to five character string for the locale code",
"default": "en-US"
},
"scene1_title": {
"allof": [
{ "$ref": "#/definitions/plainText" },
{ "description": "snippet title displayed before snippet text" }
]
},
"scene1_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_section_title_icon": {
"type": "string",
"description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_icon_dark_theme": {
"type": "string",
"description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_text": {
"type": "string",
"description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display."
},
"scene1_section_title_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, scene1_section_title_text links to this" }
]
},
"scene2_title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Title displayed before text in scene 2. Should be plain text."
}
]
},
"scene2_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"scene1_icon_dark_theme": {
"type": "string",
"description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
},
"scene1_title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene1_title_icon_dark_theme": {
"type": "string",
"description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene2_email_placeholder_text": {
"type": "string",
"description": "Value to show while input is empty.",
"default": "Your email here"
},
"scene2_button_label": {
"type": "string",
"description": "Label for form submit button",
"default": "Sign me up"
},
"scene2_privacy_html": {
"type": "string",
"description": "(send to device) Html for disclaimer and link underneath input box."
},
"scene2_dismiss_button_text": {
"type": "string",
"description": "Label for the dismiss button when the sign-up form is expanded.",
"default": "Dismiss"
},
"hidden_inputs": {
"type": "object",
"description": "Each entry represents a hidden input, key is used as value for the name property.",
"properties": {
"fmt": {
"type": "string",
"description": "",
"default": "H"
}
}
},
"scene1_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
],
"default": "Learn more"
},
"scene1_button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"scene1_button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"retry_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for the button in the event of a submission error/failure."
}
],
"default": "Try again"
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked",
"default": false
},
"success_text": {
"type": "string",
"description": "Message shown on successful registration."
},
"error_text": {
"type": "string",
"description": "Message shown if registration failed."
},
"scene2_newsletter": {
"type": "string",
"description": "Newsletter/basket id user is subscribing to.",
"default": "mozilla-foundation"
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
}
}
}
},
"additionalProperties": false,
"required": ["scene1_text", "scene2_text", "scene1_button_label"],
"dependencies": {
"scene1_button_color": ["scene1_button_label"],
"scene1_button_background_color": ["scene1_button_label"]
}
}

View file

@ -1,76 +0,0 @@
/* 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/. */
import { isEmailOrPhoneNumber } from "./isEmailOrPhoneNumber";
import React from "react";
import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx";
function validateInput(value, content) {
const type = isEmailOrPhoneNumber(value, content);
return type ? "" : "Must be an email or a phone number.";
}
function processFormData(input, message) {
const { content } = message;
const type = content.include_sms
? isEmailOrPhoneNumber(input.value, content)
: "email";
const formData = new FormData();
let url;
if (type === "phone") {
url = "https://basket.mozilla.org/news/subscribe_sms/";
formData.append("mobile_number", input.value);
formData.append("msg_name", content.message_id_sms);
formData.append("country", content.country);
} else if (type === "email") {
url = "https://basket.mozilla.org/news/subscribe/";
formData.append("email", input.value);
formData.append("newsletters", content.message_id_email);
formData.append(
"source_url",
encodeURIComponent(`https://snippets.mozilla.com/show/${message.id}`)
);
}
formData.append("lang", content.locale);
return { formData, url };
}
function addDefaultValues(props) {
return {
...props,
content: {
scene1_button_label: "Learn more",
retry_button_label: "Try again",
scene2_dismiss_button_text: "Dismiss",
scene2_button_label: "Send",
scene2_input_placeholder: "Your email here",
locale: "en-US",
country: "us",
message_id_email: "",
include_sms: false,
...props.content,
},
};
}
export const SendToDeviceSnippet = props => {
const propsWithDefaults = addDefaultValues(props);
return (
<SubmitFormSnippet
{...propsWithDefaults}
form_method="POST"
className="send_to_device_snippet"
inputType={propsWithDefaults.content.include_sms ? "text" : "email"}
validateInput={
propsWithDefaults.content.include_sms ? validateInput : null
}
processFormData={processFormData}
/>
);
};
export const SendToDeviceScene2Snippet = props => {
return <SendToDeviceSnippet expandedAlt={true} {...props} />;
};

View file

@ -1,243 +0,0 @@
{
"title": "SendToDeviceSnippet",
"description": "A snippet template for send to device mobile download",
"version": "1.2.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"locale": {
"type": "string",
"description": "Two to five character string for the locale code",
"default": "en-US"
},
"country": {
"type": "string",
"description": "Two character string for the country code (used for SMS)",
"default": "us"
},
"scene1_title": {
"allof": [
{ "$ref": "#/definitions/plainText" },
{ "description": "snippet title displayed before snippet text" }
]
},
"scene1_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_section_title_icon": {
"type": "string",
"description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_icon_dark_theme": {
"type": "string",
"description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_text": {
"type": "string",
"description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display."
},
"scene1_section_title_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, scene1_section_title_text links to this" }
]
},
"scene2_title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Title displayed before text in scene 2. Should be plain text."
}
]
},
"scene2_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"scene1_icon_dark_theme": {
"type": "string",
"description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
},
"scene2_icon": {
"type": "string",
"description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."
},
"scene2_icon_dark_theme": {
"type": "string",
"description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
},
"scene1_title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene1_title_icon_dark_theme": {
"type": "string",
"description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene2_button_label": {
"type": "string",
"description": "Label for form submit button",
"default": "Send"
},
"scene2_input_placeholder": {
"type": "string",
"description": "(send to device) Value to show while input is empty.",
"default": "Your email here"
},
"scene2_disclaimer_html": {
"type": "string",
"description": "(send to device) Html for disclaimer and link underneath input box."
},
"scene2_dismiss_button_text": {
"type": "string",
"description": "Label for the dismiss button when the sign-up form is expanded.",
"default": "Dismiss"
},
"hidden_inputs": {
"type": "object",
"description": "Each entry represents a hidden input, key is used as value for the name property.",
"properties": {
"action": {
"type": "string",
"enum": ["email"]
},
"context": {
"type": "string",
"enum": ["fx_desktop_v3"]
},
"entrypoint": {
"type": "string",
"enum": ["snippets"]
},
"utm_content": {
"type": "string",
"description": "Firefox version number"
},
"utm_source": {
"type": "string",
"enum": ["snippet"]
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"additionalProperties": false
}
},
"scene1_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
],
"default": "Learn more"
},
"scene1_button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"scene1_button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"retry_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for the button in the event of a submission error/failure."
}
],
"default": "Try again"
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked",
"default": false
},
"success_title": {
"type": "string",
"description": "(send to device) Title shown before text on successful registration."
},
"success_text": {
"type": "string",
"description": "Message shown on successful registration."
},
"error_text": {
"type": "string",
"description": "Message shown if registration failed."
},
"include_sms": {
"type": "boolean",
"description": "(send to device) Allow users to send an SMS message with the form?",
"default": false
},
"message_id_sms": {
"type": "string",
"description": "(send to device) Newsletter/basket id representing the SMS message to be sent."
},
"message_id_email": {
"type": "string",
"description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
}
}
}
},
"additionalProperties": false,
"required": ["scene1_text", "scene2_text", "scene1_button_label"],
"dependencies": {
"scene1_button_color": ["scene1_button_label"],
"scene1_button_background_color": ["scene1_button_label"]
}
}

View file

@ -1,39 +0,0 @@
/* 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/. */
/**
* Checks if a given string is an email or phone number or neither
* @param {string} val The user input
* @param {ASRMessageContent} content .content property on ASR message
* @returns {"email"|"phone"|""} The type of the input
*/
export function isEmailOrPhoneNumber(val, content) {
const { locale } = content;
// http://emailregex.com/
const email_re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const check_email = email_re.test(val);
let check_phone; // depends on locale
switch (locale) {
case "en-US":
case "en-CA":
// allow 10-11 digits in case user wants to enter country code
check_phone = val.length >= 10 && val.length <= 11 && !isNaN(val);
break;
case "de":
// allow between 2 and 12 digits for german phone numbers
check_phone = val.length >= 2 && val.length <= 12 && !isNaN(val);
break;
// this case should never be hit, but good to have a fallback just in case
default:
check_phone = !isNaN(val);
break;
}
if (check_email) {
return "email";
} else if (check_phone) {
return "phone";
}
return "";
}

View file

@ -1,133 +0,0 @@
/* 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/. */
import React from "react";
import { Button } from "../../components/Button/Button";
import { RichText } from "../../components/RichText/RichText";
import { safeURI } from "../../template-utils";
import { SnippetBase } from "../../components/SnippetBase/SnippetBase";
const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
// Alt text placeholder in case the prop from the server isn't available
const ICON_ALT_TEXT = "";
export class SimpleBelowSearchSnippet extends React.PureComponent {
constructor(props) {
super(props);
this.onButtonClick = this.onButtonClick.bind(this);
}
renderText() {
const { props } = this;
return props.content.text ? (
<RichText
text={props.content.text}
customElements={this.props.customElements}
localization_id="text"
links={props.content.links}
sendClick={props.sendClick}
/>
) : null;
}
renderTitle() {
const { title } = this.props.content;
return title ? (
<h3 className={"title title-inline"}>
{title}
<br />
</h3>
) : null;
}
async onButtonClick() {
if (this.props.provider !== "preview") {
this.props.sendUserActionTelemetry({
event: "CLICK_BUTTON",
id: this.props.UISurface,
});
}
const { button_url } = this.props.content;
// If button_url is defined handle it as OPEN_URL action
const type = this.props.content.button_action || (button_url && "OPEN_URL");
await this.props.onAction({
type,
data: { args: this.props.content.button_action_args || button_url },
});
if (!this.props.content.do_not_autoblock) {
this.props.onBlock();
}
}
_shouldRenderButton() {
return (
this.props.content.button_action ||
this.props.onButtonClick ||
this.props.content.button_url
);
}
renderButton() {
const { props } = this;
if (!this._shouldRenderButton()) {
return null;
}
return (
<Button
onClick={props.onButtonClick || this.onButtonClick}
color={props.content.button_color}
backgroundColor={props.content.button_background_color}
>
{props.content.button_label}
</Button>
);
}
render() {
const { props } = this;
let className = "SimpleBelowSearchSnippet";
let containerName = "below-search-snippet";
if (props.className) {
className += ` ${props.className}`;
}
if (this._shouldRenderButton()) {
className += " withButton";
containerName += " withButton";
}
return (
<div className={containerName}>
<div className="snippet-hover-wrapper">
<SnippetBase
{...props}
className={className}
textStyle={this.props.textStyle}
>
<img
src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}
className="icon icon-light-theme"
alt={props.content.icon_alt_text || ICON_ALT_TEXT}
/>
<img
src={
safeURI(props.content.icon_dark_theme || props.content.icon) ||
DEFAULT_ICON_PATH
}
className="icon icon-dark-theme"
alt={props.content.icon_alt_text || ICON_ALT_TEXT}
/>
<div className="textContainer">
{this.renderTitle()}
<p className="body">{this.renderText()}</p>
{this.props.extraContent}
</div>
{<div className="buttonContainer">{this.renderButton()}</div>}
</SnippetBase>
</div>
</div>
);
}
}

View file

@ -1,114 +0,0 @@
{
"title": "SimpleBelowSearchSnippet",
"description": "A simple template with an icon, rich text and an optional button. It gets inserted below the Activity Stream search box.",
"version": "1.2.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{ "description": "Snippet title displayed before snippet text" }
]
},
"text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"icon_dark_theme": {
"type": "string",
"description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
},
"icon_alt_text": {
"type": "string",
"description": "Alt text describing icon for screen readers",
"default": ""
},
"block_button_text": {
"type": "string",
"description": "Tooltip text used for dismiss button.",
"default": "Remove this"
},
"button_action": {
"type": "string",
"description": "The type of action the button should trigger."
},
"button_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, button_label links to this" }
]
},
"button_action_args": {
"description": "Additional parameters for button action, example which specific menu the button should open"
},
"button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
]
},
"button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA link has been clicked"
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
},
"args": {
"type": "string",
"description": "Additional parameters for link action, example which specific menu the button should open"
}
}
}
},
"additionalProperties": false,
"required": ["text"],
"dependencies": {
"button_action": ["button_label"],
"button_url": ["button_label"],
"button_color": ["button_label"],
"button_background_color": ["button_label"]
}
}

View file

@ -1,191 +0,0 @@
/* stylelint-disable max-nesting-depth */
.below-search-snippet {
margin: 0 auto 16px;
&.withButton {
margin: auto;
min-height: 60px;
background-color: transparent;
.snippet-hover-wrapper {
min-height: 60px;
border-radius: 4px;
&:hover {
background-color: var(--newtab-element-hover-color);
.blockButton {
display: block;
opacity: 1;
}
}
}
}
}
.SimpleBelowSearchSnippet {
background-color: transparent;
border: 0;
box-shadow: none;
position: relative;
margin: auto;
z-index: auto;
@media (min-width: $break-point-large) {
width: 736px;
}
&.active {
background-color: var(--newtab-element-hover-color);
border-radius: 4px;
}
.innerWrapper {
align-items: center;
background-color: transparent;
border-radius: 4px;
box-shadow: $shadow-card;
flex-direction: column;
padding: 16px;
text-align: center;
width: 100%;
@media (min-width: $break-point-medium) {
align-items: flex-start;
background-color: transparent;
border-radius: 4px;
box-shadow: none;
flex-direction: row;
padding: 0;
text-align: inherit;
width: 696px;
}
@media (width <= 865px) {
margin-inline-start: 0;
}
// There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 610px.
@media (max-width: $break-point-medium - 1px) {
margin: auto;
}
}
.blockButton {
display: block;
inset-inline-end: 10px;
opacity: 1;
top: 50%;
&:focus {
box-shadow: $shadow-primary;
border-radius: 2px;
}
}
.title {
font-size: inherit;
margin: 0;
}
.title-inline {
display: inline;
}
.textContainer {
margin: 10px;
margin-inline-start: 0;
padding-inline-end: 20px;
}
.icon {
margin-top: 8px;
margin-inline-start: 12px;
height: 32px;
width: 32px;
@media (min-width: $break-point-medium) {
height: 24px;
width: 24px;
}
@media (max-width: $break-point-medium) {
margin: auto;
}
}
&.withButton {
line-height: 20px;
margin-bottom: 10px;
min-height: 60px;
background-color: transparent;
.innerWrapper {
// There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px.
@media (max-width: $break-point-widest + 1px) {
margin: 0 40px;
}
}
.blockButton {
display: block;
inset-inline-end: -10%;
opacity: 0;
margin: auto;
top: unset;
&:focus {
opacity: 1;
box-shadow: none;
}
// There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px.
@media (max-width: $break-point-widest + 1px) {
inset-inline-end: 2%;
}
}
.icon {
width: 42px;
height: 42px;
flex-shrink: 0;
margin: auto 0;
margin-inline-end: 10px;
@media (max-width: $break-point-medium) {
margin: auto;
}
}
.buttonContainer {
margin: auto;
margin-inline-end: 0;
@media (max-width: $break-point-medium) {
margin: auto;
}
}
}
button {
@media (max-width: $break-point-medium) {
margin: auto;
}
}
.body {
display: inline;
position: sticky;
transform: translateY(-50%);
margin: 8px 0 0;
@media (min-width: $break-point-medium) {
margin: 12px 0;
}
a {
font-weight: 600;
}
}
}

View file

@ -1,222 +0,0 @@
/* 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/. */
import { Button } from "../../components/Button/Button";
import ConditionalWrapper from "../../components/ConditionalWrapper/ConditionalWrapper";
import React from "react";
import { RichText } from "../../components/RichText/RichText";
import { safeURI } from "../../template-utils";
import { SnippetBase } from "../../components/SnippetBase/SnippetBase";
const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
// Alt text placeholder in case the prop from the server isn't available
const ICON_ALT_TEXT = "";
export class SimpleSnippet extends React.PureComponent {
constructor(props) {
super(props);
this.onButtonClick = this.onButtonClick.bind(this);
}
onButtonClick() {
if (this.props.provider !== "preview") {
this.props.sendUserActionTelemetry({
event: "CLICK_BUTTON",
id: this.props.UISurface,
});
}
const { button_url, button_entrypoint_value, button_entrypoint_name } =
this.props.content;
// If button_url is defined handle it as OPEN_URL action
const type = this.props.content.button_action || (button_url && "OPEN_URL");
// Assign the snippet referral for the action
const entrypoint = button_entrypoint_name
? new URLSearchParams([
[button_entrypoint_name, button_entrypoint_value],
]).toString()
: button_entrypoint_value;
this.props.onAction({
type,
data: {
args: this.props.content.button_action_args || button_url,
...(entrypoint && { entrypoint }),
},
});
if (!this.props.content.do_not_autoblock) {
this.props.onBlock();
}
}
_shouldRenderButton() {
return (
this.props.content.button_action ||
this.props.onButtonClick ||
this.props.content.button_url
);
}
renderTitle() {
const { title } = this.props.content;
return title ? (
<h3
className={`title ${this._shouldRenderButton() ? "title-inline" : ""}`}
>
{this.renderTitleIcon()} {title}
</h3>
) : null;
}
renderTitleIcon() {
const titleIconLight = safeURI(this.props.content.title_icon);
const titleIconDark = safeURI(
this.props.content.title_icon_dark_theme || this.props.content.title_icon
);
if (!titleIconLight) {
return null;
}
return (
<React.Fragment>
<span
className="titleIcon icon-light-theme"
style={{ backgroundImage: `url("${titleIconLight}")` }}
/>
<span
className="titleIcon icon-dark-theme"
style={{ backgroundImage: `url("${titleIconDark}")` }}
/>
</React.Fragment>
);
}
renderButton() {
const { props } = this;
if (!this._shouldRenderButton()) {
return null;
}
return (
<Button
onClick={props.onButtonClick || this.onButtonClick}
color={props.content.button_color}
backgroundColor={props.content.button_background_color}
>
{props.content.button_label}
</Button>
);
}
renderText() {
const { props } = this;
return (
<RichText
text={props.content.text}
customElements={this.props.customElements}
localization_id="text"
links={props.content.links}
sendClick={props.sendClick}
/>
);
}
wrapSectionHeader(url) {
return function (children) {
return <a href={url}>{children}</a>;
};
}
wrapSnippetContent(children) {
return <div className="innerContentWrapper">{children}</div>;
}
renderSectionHeader() {
const { props } = this;
// an icon and text must be specified to render the section header
if (props.content.section_title_icon && props.content.section_title_text) {
const sectionTitleIconLight = safeURI(props.content.section_title_icon);
const sectionTitleIconDark = safeURI(
props.content.section_title_icon_dark_theme ||
props.content.section_title_icon
);
const sectionTitleURL = props.content.section_title_url;
return (
<div className="section-header">
<h3 className="section-title">
<ConditionalWrapper
condition={sectionTitleURL}
wrap={this.wrapSectionHeader(sectionTitleURL)}
>
<span
className="icon icon-small-spacer icon-light-theme"
style={{ backgroundImage: `url("${sectionTitleIconLight}")` }}
/>
<span
className="icon icon-small-spacer icon-dark-theme"
style={{ backgroundImage: `url("${sectionTitleIconDark}")` }}
/>
<span className="section-title-text">
{props.content.section_title_text}
</span>
</ConditionalWrapper>
</h3>
</div>
);
}
return null;
}
render() {
const { props } = this;
const sectionHeader = this.renderSectionHeader();
let className = "SimpleSnippet";
if (props.className) {
className += ` ${props.className}`;
}
if (props.content.tall) {
className += " tall";
}
if (sectionHeader) {
className += " has-section-header";
}
return (
<div className="snippet-hover-wrapper">
<SnippetBase
{...props}
className={className}
textStyle={this.props.textStyle}
>
{sectionHeader}
<ConditionalWrapper
condition={sectionHeader}
wrap={this.wrapSnippetContent}
>
<img
src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}
className="icon icon-light-theme"
alt={props.content.icon_alt_text || ICON_ALT_TEXT}
/>
<img
src={
safeURI(props.content.icon_dark_theme || props.content.icon) ||
DEFAULT_ICON_PATH
}
className="icon icon-dark-theme"
alt={props.content.icon_alt_text || ICON_ALT_TEXT}
/>
<div>
{this.renderTitle()} <p className="body">{this.renderText()}</p>
{this.props.extraContent}
</div>
{<div>{this.renderButton()}</div>}
</ConditionalWrapper>
</SnippetBase>
</div>
);
}
}

View file

@ -1,159 +0,0 @@
{
"title": "SimpleSnippet",
"description": "A simple template with an icon, text, and optional button.",
"version": "1.1.2",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{ "description": "Snippet title displayed before snippet text" }
]
},
"text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"icon_dark_theme": {
"type": "string",
"description": "Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."
},
"icon_alt_text": {
"type": "string",
"description": "Alt text describing icon for screen readers",
"default": ""
},
"title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"title_icon_dark_theme": {
"type": "string",
"description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
},
"title_icon_alt_text": {
"type": "string",
"description": "Alt text describing title icon for screen readers",
"default": ""
},
"button_action": {
"type": "string",
"description": "The type of action the button should trigger."
},
"button_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, button_label links to this" }
]
},
"button_action_args": {
"description": "Additional parameters for button action, example which specific menu the button should open"
},
"button_entrypoint_value": {
"description": "String used for telemetry attribution of clicks",
"type": "string"
},
"button_entrypoint_name": {
"description": "String used for telemetry attribution of clicks",
"type": "string"
},
"button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
]
},
"button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"block_button_text": {
"type": "string",
"description": "Tooltip text used for dismiss button.",
"default": "Remove this"
},
"tall": {
"type": "boolean",
"description": "To be used by fundraising only, increases height to roughly 120px. Defaults to false."
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked"
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
},
"args": {
"type": "string",
"description": "Additional parameters for link action, example which specific menu the button should open"
}
}
},
"section_title_icon": {
"type": "string",
"description": "Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."
},
"section_title_icon_dark_theme": {
"type": "string",
"description": "Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."
},
"section_title_text": {
"type": "string",
"description": "Section title text. section_title_icon must also be specified to display."
},
"section_title_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, section_title_text links to this" }
]
}
},
"additionalProperties": false,
"required": ["text"],
"dependencies": {
"button_action": ["button_label"],
"button_url": ["button_label"],
"button_color": ["button_label"],
"button_background_color": ["button_label"],
"section_title_url": ["section_title_text"],
"button_entrypoint_name": ["button_entrypoint_value"]
}
}

View file

@ -1,131 +0,0 @@
$section-header-height: 30px;
$icon-width: 54px; // width of primary icon + margin
.SimpleSnippet {
&.tall {
padding: 27px 0;
}
p em {
color: var(--newtab-text-emphasis-text-color);
font-style: normal;
background: var(--newtab-text-emphasis-background);
}
&.bold {
height: 176px;
.body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px;
}
.icon {
width: 71px;
height: 71px;
}
}
&.takeover {
height: 344px;
.body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px;
}
.icon {
width: 79px;
height: 79px;
}
}
.title {
font-size: inherit;
margin: 0;
}
.title-inline {
display: inline;
}
.titleIcon {
background-repeat: no-repeat;
background-size: 14px;
background-position: center;
height: 16px;
width: 16px;
margin-top: 2px;
margin-inline-end: 2px;
display: inline-block;
vertical-align: top;
}
.body {
display: inline;
margin: 0;
}
&.tall .icon {
margin-inline-end: 20px;
}
&.bold,
&.takeover {
.donation-form-url,
.donation-amount {
padding-block: 8px;
}
.icon {
margin-inline-end: 20px;
}
}
.icon {
align-self: flex-start;
}
&.has-section-header .innerWrapper {
// account for section header being 100% width
flex-wrap: wrap;
padding-top: 7px;
}
// wrapper div added if section-header is displayed that allows icon/text/button
// to squish instead of wrapping. this is effectively replicating layout behavior
// when section-header is *not* present.
.innerContentWrapper {
align-items: center;
display: flex;
}
.section-header {
flex: 0 0 100%;
margin-bottom: 10px;
}
.section-title {
// color should match that of 'Recommended by Pocket' and 'Highlights' in newtab page
color: var(--newtab-text-primary-color);
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.icon {
height: 16px;
margin-inline-end: 6px;
margin-top: -2px;
width: 16px;
}
}
}

View file

@ -1,167 +0,0 @@
{
"title": "SubmitFormSnippet",
"description": "A template with two states: a SimpleSnippet and another that contains a form",
"version": "1.2.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"locale": {
"type": "string",
"description": "Two to five character string for the locale code"
},
"country": {
"type": "string",
"description": "Two character string for the country code (used for SMS)"
},
"section_title_icon": {
"type": "string",
"description": "Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."
},
"section_title_icon_dark_theme": {
"type": "string",
"description": "Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."
},
"section_title_text": {
"type": "string",
"description": "Section title text. section_title_icon must also be specified to display."
},
"section_title_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, section_title_text links to this" }
]
},
"scene2_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"form_action": {
"type": "string",
"description": "Endpoint to submit form data."
},
"success_title": {
"type": "string",
"description": "(send to device) Title shown before text on successful registration."
},
"success_text": {
"type": "string",
"description": "Message shown on successful registration."
},
"error_text": {
"type": "string",
"description": "Message shown if registration failed."
},
"scene2_email_placeholder_text": {
"type": "string",
"description": "Value to show while input is empty."
},
"scene2_input_placeholder": {
"type": "string",
"description": "(send to device) Value to show while input is empty."
},
"scene2_button_label": {
"type": "string",
"description": "Label for form submit button"
},
"scene2_privacy_html": {
"type": "string",
"description": "Information about how the form data is used."
},
"scene2_disclaimer_html": {
"type": "string",
"description": "(send to device) Html for disclaimer and link underneath input box."
},
"scene2_icon": {
"type": "string",
"description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
},
"scene2_icon_dark_theme": {
"type": "string",
"description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."
},
"scene2_icon_alt_text": {
"type": "string",
"description": "Alt text describing scene2 icon for screen readers",
"default": ""
},
"scene2_newsletter": {
"type": "string",
"description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'."
},
"hidden_inputs": {
"type": "object",
"description": "Each entry represents a hidden input, key is used as value for the name property."
},
"retry_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for the button in the event of a submission error/failure."
}
],
"default": "Try again"
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked"
},
"include_sms": {
"type": "boolean",
"description": "(send to device) Allow users to send an SMS message with the form?"
},
"message_id_sms": {
"type": "string",
"description": "(send to device) Newsletter/basket id representing the SMS message to be sent."
},
"message_id_email": {
"type": "string",
"description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
}
}
}
},
"additionalProperties": false,
"required": ["scene2_text"],
"dependencies": {
"section_title_icon": ["section_title_text"],
"section_title_icon_dark_theme": ["section_title_text"]
}
}

View file

@ -1,408 +0,0 @@
/* 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/. */
import { Button } from "../../components/Button/Button";
import React from "react";
import { RichText } from "../../components/RichText/RichText";
import { safeURI } from "../../template-utils";
import { SimpleSnippet } from "../SimpleSnippet/SimpleSnippet";
import { SnippetBase } from "../../components/SnippetBase/SnippetBase";
import ConditionalWrapper from "../../components/ConditionalWrapper/ConditionalWrapper";
// Alt text placeholder in case the prop from the server isn't available
const ICON_ALT_TEXT = "";
export class SubmitFormSnippet extends React.PureComponent {
constructor(props) {
super(props);
this.expandSnippet = this.expandSnippet.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.state = {
expanded: false,
submitAttempted: false,
signupSubmitted: false,
signupSuccess: false,
disableForm: false,
};
}
handleSubmitAttempt() {
if (!this.state.submitAttempted) {
this.setState({ submitAttempted: true });
}
}
async handleSubmit(event) {
let json;
if (this.state.disableForm) {
return;
}
event.preventDefault();
this.setState({ disableForm: true });
this.props.sendUserActionTelemetry({
event: "CLICK_BUTTON",
event_context: "conversion-subscribe-activation",
id: "NEWTAB_FOOTER_BAR_CONTENT",
});
if (this.props.form_method.toUpperCase() === "GET") {
this.props.onBlock({ preventDismiss: true });
this.refs.form.submit();
return;
}
const { url, formData } = this.props.processFormData
? this.props.processFormData(this.refs.mainInput, this.props)
: { url: this.refs.form.action, formData: new FormData(this.refs.form) };
try {
const fetchRequest = new Request(url, {
body: formData,
method: "POST",
credentials: "omit",
});
const response = await fetch(fetchRequest); // eslint-disable-line fetch-options/no-fetch-credentials
json = await response.json();
} catch (err) {
console.error(err);
}
if (json && json.status === "ok") {
this.setState({ signupSuccess: true, signupSubmitted: true });
if (!this.props.content.do_not_autoblock) {
this.props.onBlock({ preventDismiss: true });
}
this.props.sendUserActionTelemetry({
event: "CLICK_BUTTON",
event_context: "subscribe-success",
id: "NEWTAB_FOOTER_BAR_CONTENT",
});
} else {
console.error(
"There was a problem submitting the form",
json || "[No JSON response]"
);
this.setState({ signupSuccess: false, signupSubmitted: true });
this.props.sendUserActionTelemetry({
event: "CLICK_BUTTON",
event_context: "subscribe-error",
id: "NEWTAB_FOOTER_BAR_CONTENT",
});
}
this.setState({ disableForm: false });
}
expandSnippet() {
this.props.sendUserActionTelemetry({
event: "CLICK_BUTTON",
event_context: "scene1-button-learn-more",
id: this.props.UISurface,
});
this.setState({
expanded: true,
signupSuccess: false,
signupSubmitted: false,
});
}
renderHiddenFormInputs() {
const { hidden_inputs } = this.props.content;
if (!hidden_inputs) {
return null;
}
return Object.keys(hidden_inputs).map((key, idx) => (
<input key={idx} type="hidden" name={key} value={hidden_inputs[key]} />
));
}
renderDisclaimer() {
const { content } = this.props;
if (!content.scene2_disclaimer_html) {
return null;
}
return (
<p className="disclaimerText">
<RichText
text={content.scene2_disclaimer_html}
localization_id="disclaimer_html"
links={content.links}
doNotAutoBlock={true}
openNewWindow={true}
sendClick={this.props.sendClick}
/>
</p>
);
}
renderFormPrivacyNotice() {
const { content } = this.props;
if (!content.scene2_privacy_html) {
return null;
}
return (
<p className="privacyNotice">
<input
type="checkbox"
id="id_privacy"
name="privacy"
required="required"
/>
<label htmlFor="id_privacy">
<RichText
text={content.scene2_privacy_html}
localization_id="privacy_html"
links={content.links}
doNotAutoBlock={true}
openNewWindow={true}
sendClick={this.props.sendClick}
/>
</label>
</p>
);
}
renderSignupSubmitted() {
const { content } = this.props;
const isSuccess = this.state.signupSuccess;
const successTitle = isSuccess && content.success_title;
const bodyText = isSuccess
? { success_text: content.success_text }
: { error_text: content.error_text };
const retryButtonText = content.retry_button_label;
return (
<SnippetBase {...this.props}>
<div className="submissionStatus">
{successTitle ? (
<h2 className="submitStatusTitle">{successTitle}</h2>
) : null}
<p>
<RichText
{...bodyText}
localization_id={isSuccess ? "success_text" : "error_text"}
/>
{isSuccess ? null : (
<Button onClick={this.expandSnippet}>{retryButtonText}</Button>
)}
</p>
</div>
</SnippetBase>
);
}
onInputChange(event) {
if (!this.props.validateInput) {
return;
}
const hasError = this.props.validateInput(
event.target.value,
this.props.content
);
event.target.setCustomValidity(hasError);
}
wrapSectionHeader(url) {
return function (children) {
return <a href={url}>{children}</a>;
};
}
renderInput() {
const placholder =
this.props.content.scene2_email_placeholder_text ||
this.props.content.scene2_input_placeholder;
return (
<input
ref="mainInput"
type={this.props.inputType || "email"}
className={`mainInput${this.state.submitAttempted ? "" : " clean"}`}
name="email"
required={true}
placeholder={placholder}
onChange={this.props.validateInput ? this.onInputChange : null}
/>
);
}
renderForm() {
return (
<form
action={this.props.form_action}
method={this.props.form_method}
onSubmit={this.handleSubmit}
ref="form"
>
{this.renderHiddenFormInputs()}
<div>
{this.renderInput()}
<button
type="submit"
className="ASRouterButton primary"
onClick={this.handleSubmitAttempt}
ref="formSubmitBtn"
>
{this.props.content.scene2_button_label}
</button>
</div>
{this.renderFormPrivacyNotice() || this.renderDisclaimer()}
</form>
);
}
renderScene2Icon() {
const { content } = this.props;
if (!content.scene2_icon) {
return null;
}
return (
<div className="scene2Icon">
<img
src={safeURI(content.scene2_icon)}
className="icon-light-theme"
alt={content.scene2_icon_alt_text || ICON_ALT_TEXT}
/>
<img
src={safeURI(content.scene2_icon_dark_theme || content.scene2_icon)}
className="icon-dark-theme"
alt={content.scene2_icon_alt_text || ICON_ALT_TEXT}
/>
</div>
);
}
renderSignupView() {
const { content } = this.props;
const containerClass = `SubmitFormSnippet ${this.props.className}`;
return (
<SnippetBase
{...this.props}
className={containerClass}
footerDismiss={true}
>
{this.renderScene2Icon()}
<div className="message">
<p>
{content.scene2_title && (
<h3 className="scene2Title">{content.scene2_title}</h3>
)}{" "}
{content.scene2_text && (
<RichText
scene2_text={content.scene2_text}
localization_id="scene2_text"
/>
)}
</p>
</div>
{this.renderForm()}
</SnippetBase>
);
}
renderSectionHeader() {
const { props } = this;
// an icon and text must be specified to render the section header
if (props.content.section_title_icon && props.content.section_title_text) {
const sectionTitleIconLight = safeURI(props.content.section_title_icon);
const sectionTitleIconDark = safeURI(
props.content.section_title_icon_dark_theme ||
props.content.section_title_icon
);
const sectionTitleURL = props.content.section_title_url;
return (
<div className="section-header">
<h3 className="section-title">
<ConditionalWrapper
wrap={this.wrapSectionHeader(sectionTitleURL)}
condition={sectionTitleURL}
>
<span
className="icon icon-small-spacer icon-light-theme"
style={{ backgroundImage: `url("${sectionTitleIconLight}")` }}
/>
<span
className="icon icon-small-spacer icon-dark-theme"
style={{ backgroundImage: `url("${sectionTitleIconDark}")` }}
/>
<span className="section-title-text">
{props.content.section_title_text}
</span>
</ConditionalWrapper>
</h3>
</div>
);
}
return null;
}
renderSignupViewAlt() {
const { content } = this.props;
const containerClass = `SubmitFormSnippet ${this.props.className} scene2Alt`;
return (
<SnippetBase
{...this.props}
className={containerClass}
// Don't show bottom dismiss button
footerDismiss={false}
>
{this.renderSectionHeader()}
{this.renderScene2Icon()}
<div className="message">
<p>
{content.scene2_text && (
<RichText
scene2_text={content.scene2_text}
localization_id="scene2_text"
/>
)}
</p>
{this.renderForm()}
</div>
</SnippetBase>
);
}
getFirstSceneContent() {
return Object.keys(this.props.content)
.filter(key => key.includes("scene1"))
.reduce((acc, key) => {
acc[key.substr(7)] = this.props.content[key];
return acc;
}, {});
}
render() {
const content = { ...this.props.content, ...this.getFirstSceneContent() };
if (this.state.signupSubmitted) {
return this.renderSignupSubmitted();
}
// Render only scene 2 (signup view). Must check before `renderSignupView`
// to catch the Failure/Try again scenario where we want to return and render
// the scene again.
if (this.props.expandedAlt) {
return this.renderSignupViewAlt();
}
if (this.state.expanded) {
return this.renderSignupView();
}
return (
<SimpleSnippet
{...this.props}
content={content}
onButtonClick={this.expandSnippet}
/>
);
}
}

View file

@ -1,235 +0,0 @@
{
"title": "SubmitFormSnippet",
"description": "A template with two states: a SimpleSnippet and another that contains a form",
"version": "1.2.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"locale": {
"type": "string",
"description": "Two to five character string for the locale code"
},
"country": {
"type": "string",
"description": "Two character string for the country code (used for SMS)"
},
"scene1_title": {
"allof": [
{ "$ref": "#/definitions/plainText" },
{ "description": "snippet title displayed before snippet text" }
]
},
"scene1_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_section_title_icon": {
"type": "string",
"description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_icon_dark_theme": {
"type": "string",
"description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display."
},
"scene1_section_title_text": {
"type": "string",
"description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display."
},
"scene1_section_title_url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "A url, scene1_section_title_text links to this" }
]
},
"scene2_title": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Title displayed before text in scene 2. Should be plain text."
}
]
},
"scene2_text": {
"allOf": [
{ "$ref": "#/definitions/richText" },
{
"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"
}
]
},
"scene1_icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"scene1_icon_dark_theme": {
"type": "string",
"description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
},
"scene1_icon_alt_text": {
"type": "string",
"description": "Alt text describing scene1 icon for screen readers",
"default": ""
},
"scene1_title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene1_title_icon_dark_theme": {
"type": "string",
"description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
},
"scene1_title_icon_alt_text": {
"type": "string",
"description": "Alt text describing scene1 title icon for screen readers",
"default": ""
},
"form_action": {
"type": "string",
"description": "Endpoint to submit form data."
},
"success_title": {
"type": "string",
"description": "(send to device) Title shown before text on successful registration."
},
"success_text": {
"type": "string",
"description": "Message shown on successful registration."
},
"error_text": {
"type": "string",
"description": "Message shown if registration failed."
},
"scene2_email_placeholder_text": {
"type": "string",
"description": "Value to show while input is empty."
},
"scene2_input_placeholder": {
"type": "string",
"description": "(send to device) Value to show while input is empty."
},
"scene2_button_label": {
"type": "string",
"description": "Label for form submit button"
},
"scene2_privacy_html": {
"type": "string",
"description": "Information about how the form data is used."
},
"scene2_disclaimer_html": {
"type": "string",
"description": "(send to device) Html for disclaimer and link underneath input box."
},
"scene2_dismiss_button_text": {
"type": "string",
"description": "Label for the dismiss button when the sign-up form is expanded."
},
"scene2_icon": {
"type": "string",
"description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
},
"scene2_icon_dark_theme": {
"type": "string",
"description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."
},
"scene2_icon_alt_text": {
"type": "string",
"description": "Alt text describing scene2 icon for screen readers",
"default": ""
},
"scene2_newsletter": {
"type": "string",
"description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'."
},
"hidden_inputs": {
"type": "object",
"description": "Each entry represents a hidden input, key is used as value for the name property."
},
"scene1_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."
}
]
},
"scene1_button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"scene1_button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"retry_button_label": {
"allOf": [
{ "$ref": "#/definitions/plainText" },
{
"description": "Text for the button in the event of a submission error/failure."
}
],
"default": "Try again"
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked"
},
"include_sms": {
"type": "boolean",
"description": "(send to device) Allow users to send an SMS message with the form?"
},
"message_id_sms": {
"type": "string",
"description": "(send to device) Newsletter/basket id representing the SMS message to be sent."
},
"message_id_email": {
"type": "string",
"description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{ "$ref": "#/definitions/link_url" },
{ "description": "The url where the link points to." }
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
}
}
}
},
"additionalProperties": false,
"required": ["scene1_text", "scene2_text", "scene1_button_label"],
"dependencies": {
"scene1_button_color": ["scene1_button_label"],
"scene1_button_background_color": ["scene1_button_label"]
}
}

View file

@ -1,178 +0,0 @@
/* stylelint-disable max-nesting-depth */
.SubmitFormSnippet {
flex-direction: column;
flex: 1 1 100%;
width: 100%;
.disclaimerText {
margin: 5px 0 0;
font-size: 12px;
color: var(--newtab-text-secondary-color);
}
p {
margin: 0;
}
&.send_to_device_snippet {
text-align: center;
.message {
font-size: 16px;
margin-bottom: 20px;
}
.scene2Title {
font-size: 24px;
display: block;
}
}
.ASRouterButton {
&.primary {
flex: 1 1 0;
}
}
.scene2Icon {
width: 100%;
margin-bottom: 20px;
img {
width: 98px;
display: inline-block;
}
}
.scene2Title {
font-size: inherit;
margin: 0 0 10px;
font-weight: bold;
display: inline;
}
form {
display: flex;
flex-direction: column;
width: 100%;
}
.message {
font-size: 14px;
align-self: stretch;
flex: 0 0 100%;
margin-bottom: 10px;
}
.privacyNotice {
font-size: 12px;
color: var(--newtab-text-secondary-color);
margin-top: 10px;
display: flex;
flex: 0 0 100%;
}
.innerWrapper {
// https://github.com/mozmeao/snippets/blob/2054899350590adcb3c0b0a341c782b0e2f81d0b/activity-stream/newsletter-subscribe.html#L46
max-width: 736px;
flex-wrap: wrap;
justify-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.footer {
width: 100%;
margin: 0 auto;
text-align: right;
background-color: var(--newtab-background-color);
padding: 10px 0;
.footer-content {
margin: 0 auto;
max-width: 768px;
width: 100%;
text-align: right;
[dir='rtl'] & {
text-align: left;
}
}
}
input {
&.mainInput {
border-radius: 2px;
background-color: var(--newtab-background-color-secondary);
border: $input-border;
padding: 0 8px;
height: 100%;
font-size: 14px;
width: 50%;
&.clean {
&:invalid,
&:required {
box-shadow: none;
}
}
&:focus {
border: $input-border-active;
box-shadow: var(--newtab-textbox-focus-boxshadow);
}
}
}
&.scene2Alt {
text-align: start;
.scene2Icon {
flex: 1;
margin-bottom: 0;
}
.message {
flex: 5;
margin-bottom: 0;
p {
margin-bottom: 10px;
}
}
.section-header {
width: 100%;
.icon {
width: 16px;
height: 16px;
}
}
.section-title {
font-size: 13px;
}
.section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.innerWrapper {
padding: 0 0 16px;
}
}
}
.submissionStatus {
text-align: center;
font-size: 14px;
padding: 20px 0;
.submitStatusTitle {
font-size: 20px;
}
}

View file

@ -1,24 +0,0 @@
/* 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/. */
import { EOYSnippet } from "./EOYSnippet/EOYSnippet";
import { FXASignupSnippet } from "./FXASignupSnippet/FXASignupSnippet";
import { NewsletterSnippet } from "./NewsletterSnippet/NewsletterSnippet";
import {
SendToDeviceSnippet,
SendToDeviceScene2Snippet,
} from "./SendToDeviceSnippet/SendToDeviceSnippet";
import { SimpleBelowSearchSnippet } from "./SimpleBelowSearchSnippet/SimpleBelowSearchSnippet";
import { SimpleSnippet } from "./SimpleSnippet/SimpleSnippet";
// Key names matching schema name of templates
export const SnippetsTemplates = {
simple_snippet: SimpleSnippet,
newsletter_snippet: NewsletterSnippet,
fxa_signup_snippet: FXASignupSnippet,
send_to_device_snippet: SendToDeviceSnippet,
send_to_device_scene2_snippet: SendToDeviceScene2Snippet,
eoy_snippet: EOYSnippet,
simple_below_search_snippet: SimpleBelowSearchSnippet,
};

View file

@ -168,10 +168,5 @@ input {
// AS Router // AS Router
@import '../asrouter/components/Button/Button'; @import '../asrouter/components/Button/Button';
@import '../asrouter/components/SnippetBase/SnippetBase';
@import '../asrouter/components/ModalOverlay/ModalOverlay'; @import '../asrouter/components/ModalOverlay/ModalOverlay';
@import '../asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet';
@import '../asrouter/templates/SimpleSnippet/SimpleSnippet';
@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';
@import '../asrouter/templates/EOYSnippet/EOYSnippet';
// stylelint-enable no-invalid-position-at-import-rule // stylelint-enable no-invalid-position-at-import-rule

View file

@ -57,8 +57,6 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15;
--newtab-status-error: #{$red-60}; --newtab-status-error: #{$red-60};
--newtab-inner-box-shadow-color: #{$black-10}; --newtab-inner-box-shadow-color: #{$black-10};
--newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent); --newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent);
--newtab-text-emphasis-background: #{$yellow-50};
--newtab-text-emphasis-text-color: #{$grey-90};
@include textbox-focus(var(--newtab-primary-action-background)); @include textbox-focus(var(--newtab-primary-action-background));

View file

@ -57,8 +57,6 @@ input {
--newtab-status-error: #D70022; --newtab-status-error: #D70022;
--newtab-inner-box-shadow-color: rgba(0, 0, 0, 0.1); --newtab-inner-box-shadow-color: rgba(0, 0, 0, 0.1);
--newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent); --newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent);
--newtab-text-emphasis-background: #FFE900;
--newtab-text-emphasis-text-color: #0C0C0D;
--newtab-textbox-focus-color: var(--newtab-primary-action-background); --newtab-textbox-focus-color: var(--newtab-primary-action-background);
--newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3); --newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3);
--newtab-button-secondary-color: inherit; --newtab-button-secondary-color: inherit;
@ -4136,105 +4134,6 @@ main.has-snippet {
transition: box-shadow 150ms; transition: box-shadow 150ms;
} }
.SnippetBaseContainer {
position: fixed;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
background-color: var(--newtab-background-color-secondary);
color: var(--newtab-text-primary-color);
font-size: 14px;
line-height: 20px;
border-top: 1px solid transparent;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2);
display: flex;
align-items: center;
}
.SnippetBaseContainer a {
cursor: pointer;
color: var(--newtab-primary-action-background);
}
.SnippetBaseContainer a:hover {
text-decoration: underline;
}
[lwt-newtab-brighttext] .SnippetBaseContainer a {
font-weight: bold;
}
.SnippetBaseContainer input[type=checkbox] {
margin-inline-start: 0;
}
.SnippetBaseContainer .innerWrapper {
margin: 0 auto;
display: flex;
align-items: center;
padding: 12px 25px;
padding-inline-end: 36px;
max-width: 836px;
}
@media (min-width: 866px) {
.SnippetBaseContainer .innerWrapper {
padding-inline-end: 25px;
}
}
@media (min-width: 1122px) {
.SnippetBaseContainer .innerWrapper {
max-width: 1092px;
}
}
.SnippetBaseContainer .blockButton {
display: none;
background: none;
border: 0;
position: absolute;
top: 20px;
inset-inline-end: 12px;
height: 16px;
width: 16px;
background-image: url("chrome://global/skin/icons/close.svg");
-moz-context-properties: fill;
color: inherit;
fill: currentColor;
opacity: 0.5;
margin-top: -8px;
padding: 0;
cursor: pointer;
}
.SnippetBaseContainer:hover .blockButton {
display: block;
}
.SnippetBaseContainer .icon {
height: 42px;
width: 42px;
margin-inline-end: 12px;
flex-shrink: 0;
}
.snippets-preview-banner {
font-size: 15px;
line-height: 42px;
color: var(--newtab-text-primary-color);
background: var(--newtab-background-color-secondary);
text-align: center;
position: absolute;
top: 0;
width: 100%;
}
.snippets-preview-banner span {
vertical-align: middle;
}
body:not([lwt-newtab-brighttext]) .icon-dark-theme,
body:not([lwt-newtab-brighttext]) .icon.icon-dark-theme,
body:not([lwt-newtab-brighttext]) .scene2Icon .icon-dark-theme {
display: none;
}
body[lwt-newtab-brighttext] .icon-light-theme,
body[lwt-newtab-brighttext] .icon.icon-light-theme,
body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
display: none;
}
.activity-stream.modal-open { .activity-stream.modal-open {
overflow: hidden; overflow: hidden;
} }
@ -4328,464 +4227,3 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
box-shadow: 0 0 0 5px var(--newtab-element-secondary-color); box-shadow: 0 0 0 5px var(--newtab-element-secondary-color);
transition: box-shadow 150ms; transition: box-shadow 150ms;
} }
/* stylelint-disable max-nesting-depth */
.below-search-snippet {
margin: 0 auto 16px;
}
.below-search-snippet.withButton {
margin: auto;
min-height: 60px;
background-color: transparent;
}
.below-search-snippet.withButton .snippet-hover-wrapper {
min-height: 60px;
border-radius: 4px;
}
.below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color);
}
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block;
opacity: 1;
}
.SimpleBelowSearchSnippet {
background-color: transparent;
border: 0;
box-shadow: none;
position: relative;
margin: auto;
z-index: auto;
}
@media (min-width: 866px) {
.SimpleBelowSearchSnippet {
width: 736px;
}
}
.SimpleBelowSearchSnippet.active {
background-color: var(--newtab-element-hover-color);
border-radius: 4px;
}
.SimpleBelowSearchSnippet .innerWrapper {
align-items: center;
background-color: transparent;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
flex-direction: column;
padding: 16px;
text-align: center;
width: 100%;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .innerWrapper {
align-items: flex-start;
background-color: transparent;
border-radius: 4px;
box-shadow: none;
flex-direction: row;
padding: 0;
text-align: inherit;
width: 696px;
}
}
@media (width <= 865px) {
.SimpleBelowSearchSnippet .innerWrapper {
margin-inline-start: 0;
}
}
@media (max-width: 609px) {
.SimpleBelowSearchSnippet .innerWrapper {
margin: auto;
}
}
.SimpleBelowSearchSnippet .blockButton {
display: block;
inset-inline-end: 10px;
opacity: 1;
top: 50%;
}
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 5px var(--newtab-element-secondary-color);
border-radius: 2px;
}
.SimpleBelowSearchSnippet .title {
font-size: inherit;
margin: 0;
}
.SimpleBelowSearchSnippet .title-inline {
display: inline;
}
.SimpleBelowSearchSnippet .textContainer {
margin: 10px;
margin-inline-start: 0;
padding-inline-end: 20px;
}
.SimpleBelowSearchSnippet .icon {
margin-top: 8px;
margin-inline-start: 12px;
height: 32px;
width: 32px;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .icon {
height: 24px;
width: 24px;
}
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet .icon {
margin: auto;
}
}
.SimpleBelowSearchSnippet.withButton {
line-height: 20px;
margin-bottom: 10px;
min-height: 60px;
background-color: transparent;
}
@media (max-width: 1123px) {
.SimpleBelowSearchSnippet.withButton .innerWrapper {
margin: 0 40px;
}
}
.SimpleBelowSearchSnippet.withButton .blockButton {
display: block;
inset-inline-end: -10%;
opacity: 0;
margin: auto;
top: unset;
}
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none;
}
@media (max-width: 1123px) {
.SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%;
}
}
.SimpleBelowSearchSnippet.withButton .icon {
width: 42px;
height: 42px;
flex-shrink: 0;
margin: auto 0;
margin-inline-end: 10px;
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet.withButton .icon {
margin: auto;
}
}
.SimpleBelowSearchSnippet.withButton .buttonContainer {
margin: auto;
margin-inline-end: 0;
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet.withButton .buttonContainer {
margin: auto;
}
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet button {
margin: auto;
}
}
.SimpleBelowSearchSnippet .body {
display: inline;
position: sticky;
transform: translateY(-50%);
margin: 8px 0 0;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .body {
margin: 12px 0;
}
}
.SimpleBelowSearchSnippet .body a {
font-weight: 600;
}
.SimpleSnippet.tall {
padding: 27px 0;
}
.SimpleSnippet p em {
color: var(--newtab-text-emphasis-text-color);
font-style: normal;
background: var(--newtab-text-emphasis-background);
}
.SimpleSnippet.bold {
height: 176px;
}
.SimpleSnippet.bold .body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px;
}
.SimpleSnippet.bold .icon {
width: 71px;
height: 71px;
}
.SimpleSnippet.takeover {
height: 344px;
}
.SimpleSnippet.takeover .body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px;
}
.SimpleSnippet.takeover .icon {
width: 79px;
height: 79px;
}
.SimpleSnippet .title {
font-size: inherit;
margin: 0;
}
.SimpleSnippet .title-inline {
display: inline;
}
.SimpleSnippet .titleIcon {
background-repeat: no-repeat;
background-size: 14px;
background-position: center;
height: 16px;
width: 16px;
margin-top: 2px;
margin-inline-end: 2px;
display: inline-block;
vertical-align: top;
}
.SimpleSnippet .body {
display: inline;
margin: 0;
}
.SimpleSnippet.tall .icon {
margin-inline-end: 20px;
}
.SimpleSnippet.bold .donation-form-url,
.SimpleSnippet.bold .donation-amount, .SimpleSnippet.takeover .donation-form-url,
.SimpleSnippet.takeover .donation-amount {
padding-block: 8px;
}
.SimpleSnippet.bold .icon, .SimpleSnippet.takeover .icon {
margin-inline-end: 20px;
}
.SimpleSnippet .icon {
align-self: flex-start;
}
.SimpleSnippet.has-section-header .innerWrapper {
flex-wrap: wrap;
padding-top: 7px;
}
.SimpleSnippet .innerContentWrapper {
align-items: center;
display: flex;
}
.SimpleSnippet .section-header {
flex: 0 0 100%;
margin-bottom: 10px;
}
.SimpleSnippet .section-title {
color: var(--newtab-text-primary-color);
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
}
.SimpleSnippet .section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.SimpleSnippet .section-title .icon {
height: 16px;
margin-inline-end: 6px;
margin-top: -2px;
width: 16px;
}
/* stylelint-disable max-nesting-depth */
.SubmitFormSnippet {
flex-direction: column;
flex: 1 1 100%;
width: 100%;
}
.SubmitFormSnippet .disclaimerText {
margin: 5px 0 0;
font-size: 12px;
color: var(--newtab-text-secondary-color);
}
.SubmitFormSnippet p {
margin: 0;
}
.SubmitFormSnippet.send_to_device_snippet {
text-align: center;
}
.SubmitFormSnippet.send_to_device_snippet .message {
font-size: 16px;
margin-bottom: 20px;
}
.SubmitFormSnippet.send_to_device_snippet .scene2Title {
font-size: 24px;
display: block;
}
.SubmitFormSnippet .ASRouterButton.primary {
flex: 1 1 0;
}
.SubmitFormSnippet .scene2Icon {
width: 100%;
margin-bottom: 20px;
}
.SubmitFormSnippet .scene2Icon img {
width: 98px;
display: inline-block;
}
.SubmitFormSnippet .scene2Title {
font-size: inherit;
margin: 0 0 10px;
font-weight: bold;
display: inline;
}
.SubmitFormSnippet form {
display: flex;
flex-direction: column;
width: 100%;
}
.SubmitFormSnippet .message {
font-size: 14px;
align-self: stretch;
flex: 0 0 100%;
margin-bottom: 10px;
}
.SubmitFormSnippet .privacyNotice {
font-size: 12px;
color: var(--newtab-text-secondary-color);
margin-top: 10px;
display: flex;
flex: 0 0 100%;
}
.SubmitFormSnippet .innerWrapper {
max-width: 736px;
flex-wrap: wrap;
justify-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.SubmitFormSnippet .footer {
width: 100%;
margin: 0 auto;
text-align: right;
background-color: var(--newtab-background-color);
padding: 10px 0;
}
.SubmitFormSnippet .footer .footer-content {
margin: 0 auto;
max-width: 768px;
width: 100%;
text-align: right;
}
[dir=rtl] .SubmitFormSnippet .footer .footer-content {
text-align: left;
}
.SubmitFormSnippet input.mainInput {
border-radius: 2px;
background-color: var(--newtab-background-color-secondary);
border: 1px solid var(--newtab-border-color);
padding: 0 8px;
height: 100%;
font-size: 14px;
width: 50%;
}
.SubmitFormSnippet input.mainInput.clean:invalid, .SubmitFormSnippet input.mainInput.clean:required {
box-shadow: none;
}
.SubmitFormSnippet input.mainInput:focus {
border: 1px solid var(--newtab-textbox-focus-color);
box-shadow: var(--newtab-textbox-focus-boxshadow);
}
.SubmitFormSnippet.scene2Alt {
text-align: start;
}
.SubmitFormSnippet.scene2Alt .scene2Icon {
flex: 1;
margin-bottom: 0;
}
.SubmitFormSnippet.scene2Alt .message {
flex: 5;
margin-bottom: 0;
}
.SubmitFormSnippet.scene2Alt .message p {
margin-bottom: 10px;
}
.SubmitFormSnippet.scene2Alt .section-header {
width: 100%;
}
.SubmitFormSnippet.scene2Alt .section-header .icon {
width: 16px;
height: 16px;
}
.SubmitFormSnippet.scene2Alt .section-title {
font-size: 13px;
}
.SubmitFormSnippet.scene2Alt .section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.SubmitFormSnippet.scene2Alt .innerWrapper {
padding: 0 0 16px;
}
.submissionStatus {
text-align: center;
font-size: 14px;
padding: 20px 0;
}
.submissionStatus .submitStatusTitle {
font-size: 20px;
}
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center;
}
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px;
}
.EOYSnippetForm .donation-amount {
color: var(--newtab-text-primary-color);
margin-inline-end: 18px;
border: 1px solid var(--newtab-border-color);
padding: 5px 14px;
background: var(--newtab-background-color-secondary);
cursor: pointer;
}
.EOYSnippetForm input[type=radio] {
opacity: 0;
margin-inline-end: -18px;
}
.EOYSnippetForm input[type=radio]:checked + .donation-amount {
background: var(--newtab-text-secondary-color);
color: var(--newtab-background-color-secondary);
border: 1px solid var(--newtab-border-color);
}
.EOYSnippetForm input[type=radio]:checked:focus + .donation-amount, .EOYSnippetForm input[type=radio]:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-primary-action-background);
}
.EOYSnippetForm .monthly-checkbox-container {
display: flex;
width: 100%;
}
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex;
}

View file

@ -61,8 +61,6 @@ input {
--newtab-status-error: #D70022; --newtab-status-error: #D70022;
--newtab-inner-box-shadow-color: rgba(0, 0, 0, 0.1); --newtab-inner-box-shadow-color: rgba(0, 0, 0, 0.1);
--newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent); --newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent);
--newtab-text-emphasis-background: #FFE900;
--newtab-text-emphasis-text-color: #0C0C0D;
--newtab-textbox-focus-color: var(--newtab-primary-action-background); --newtab-textbox-focus-color: var(--newtab-primary-action-background);
--newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3); --newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3);
--newtab-button-secondary-color: inherit; --newtab-button-secondary-color: inherit;
@ -4140,105 +4138,6 @@ main.has-snippet {
transition: box-shadow 150ms; transition: box-shadow 150ms;
} }
.SnippetBaseContainer {
position: fixed;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
background-color: var(--newtab-background-color-secondary);
color: var(--newtab-text-primary-color);
font-size: 14px;
line-height: 20px;
border-top: 1px solid transparent;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2);
display: flex;
align-items: center;
}
.SnippetBaseContainer a {
cursor: pointer;
color: var(--newtab-primary-action-background);
}
.SnippetBaseContainer a:hover {
text-decoration: underline;
}
[lwt-newtab-brighttext] .SnippetBaseContainer a {
font-weight: bold;
}
.SnippetBaseContainer input[type=checkbox] {
margin-inline-start: 0;
}
.SnippetBaseContainer .innerWrapper {
margin: 0 auto;
display: flex;
align-items: center;
padding: 12px 25px;
padding-inline-end: 36px;
max-width: 836px;
}
@media (min-width: 866px) {
.SnippetBaseContainer .innerWrapper {
padding-inline-end: 25px;
}
}
@media (min-width: 1122px) {
.SnippetBaseContainer .innerWrapper {
max-width: 1092px;
}
}
.SnippetBaseContainer .blockButton {
display: none;
background: none;
border: 0;
position: absolute;
top: 20px;
inset-inline-end: 12px;
height: 16px;
width: 16px;
background-image: url("chrome://global/skin/icons/close.svg");
-moz-context-properties: fill;
color: inherit;
fill: currentColor;
opacity: 0.5;
margin-top: -8px;
padding: 0;
cursor: pointer;
}
.SnippetBaseContainer:hover .blockButton {
display: block;
}
.SnippetBaseContainer .icon {
height: 42px;
width: 42px;
margin-inline-end: 12px;
flex-shrink: 0;
}
.snippets-preview-banner {
font-size: 15px;
line-height: 42px;
color: var(--newtab-text-primary-color);
background: var(--newtab-background-color-secondary);
text-align: center;
position: absolute;
top: 0;
width: 100%;
}
.snippets-preview-banner span {
vertical-align: middle;
}
body:not([lwt-newtab-brighttext]) .icon-dark-theme,
body:not([lwt-newtab-brighttext]) .icon.icon-dark-theme,
body:not([lwt-newtab-brighttext]) .scene2Icon .icon-dark-theme {
display: none;
}
body[lwt-newtab-brighttext] .icon-light-theme,
body[lwt-newtab-brighttext] .icon.icon-light-theme,
body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
display: none;
}
.activity-stream.modal-open { .activity-stream.modal-open {
overflow: hidden; overflow: hidden;
} }
@ -4332,464 +4231,3 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
box-shadow: 0 0 0 5px var(--newtab-element-secondary-color); box-shadow: 0 0 0 5px var(--newtab-element-secondary-color);
transition: box-shadow 150ms; transition: box-shadow 150ms;
} }
/* stylelint-disable max-nesting-depth */
.below-search-snippet {
margin: 0 auto 16px;
}
.below-search-snippet.withButton {
margin: auto;
min-height: 60px;
background-color: transparent;
}
.below-search-snippet.withButton .snippet-hover-wrapper {
min-height: 60px;
border-radius: 4px;
}
.below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color);
}
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block;
opacity: 1;
}
.SimpleBelowSearchSnippet {
background-color: transparent;
border: 0;
box-shadow: none;
position: relative;
margin: auto;
z-index: auto;
}
@media (min-width: 866px) {
.SimpleBelowSearchSnippet {
width: 736px;
}
}
.SimpleBelowSearchSnippet.active {
background-color: var(--newtab-element-hover-color);
border-radius: 4px;
}
.SimpleBelowSearchSnippet .innerWrapper {
align-items: center;
background-color: transparent;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
flex-direction: column;
padding: 16px;
text-align: center;
width: 100%;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .innerWrapper {
align-items: flex-start;
background-color: transparent;
border-radius: 4px;
box-shadow: none;
flex-direction: row;
padding: 0;
text-align: inherit;
width: 696px;
}
}
@media (width <= 865px) {
.SimpleBelowSearchSnippet .innerWrapper {
margin-inline-start: 0;
}
}
@media (max-width: 609px) {
.SimpleBelowSearchSnippet .innerWrapper {
margin: auto;
}
}
.SimpleBelowSearchSnippet .blockButton {
display: block;
inset-inline-end: 10px;
opacity: 1;
top: 50%;
}
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 5px var(--newtab-element-secondary-color);
border-radius: 2px;
}
.SimpleBelowSearchSnippet .title {
font-size: inherit;
margin: 0;
}
.SimpleBelowSearchSnippet .title-inline {
display: inline;
}
.SimpleBelowSearchSnippet .textContainer {
margin: 10px;
margin-inline-start: 0;
padding-inline-end: 20px;
}
.SimpleBelowSearchSnippet .icon {
margin-top: 8px;
margin-inline-start: 12px;
height: 32px;
width: 32px;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .icon {
height: 24px;
width: 24px;
}
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet .icon {
margin: auto;
}
}
.SimpleBelowSearchSnippet.withButton {
line-height: 20px;
margin-bottom: 10px;
min-height: 60px;
background-color: transparent;
}
@media (max-width: 1123px) {
.SimpleBelowSearchSnippet.withButton .innerWrapper {
margin: 0 40px;
}
}
.SimpleBelowSearchSnippet.withButton .blockButton {
display: block;
inset-inline-end: -10%;
opacity: 0;
margin: auto;
top: unset;
}
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none;
}
@media (max-width: 1123px) {
.SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%;
}
}
.SimpleBelowSearchSnippet.withButton .icon {
width: 42px;
height: 42px;
flex-shrink: 0;
margin: auto 0;
margin-inline-end: 10px;
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet.withButton .icon {
margin: auto;
}
}
.SimpleBelowSearchSnippet.withButton .buttonContainer {
margin: auto;
margin-inline-end: 0;
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet.withButton .buttonContainer {
margin: auto;
}
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet button {
margin: auto;
}
}
.SimpleBelowSearchSnippet .body {
display: inline;
position: sticky;
transform: translateY(-50%);
margin: 8px 0 0;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .body {
margin: 12px 0;
}
}
.SimpleBelowSearchSnippet .body a {
font-weight: 600;
}
.SimpleSnippet.tall {
padding: 27px 0;
}
.SimpleSnippet p em {
color: var(--newtab-text-emphasis-text-color);
font-style: normal;
background: var(--newtab-text-emphasis-background);
}
.SimpleSnippet.bold {
height: 176px;
}
.SimpleSnippet.bold .body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px;
}
.SimpleSnippet.bold .icon {
width: 71px;
height: 71px;
}
.SimpleSnippet.takeover {
height: 344px;
}
.SimpleSnippet.takeover .body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px;
}
.SimpleSnippet.takeover .icon {
width: 79px;
height: 79px;
}
.SimpleSnippet .title {
font-size: inherit;
margin: 0;
}
.SimpleSnippet .title-inline {
display: inline;
}
.SimpleSnippet .titleIcon {
background-repeat: no-repeat;
background-size: 14px;
background-position: center;
height: 16px;
width: 16px;
margin-top: 2px;
margin-inline-end: 2px;
display: inline-block;
vertical-align: top;
}
.SimpleSnippet .body {
display: inline;
margin: 0;
}
.SimpleSnippet.tall .icon {
margin-inline-end: 20px;
}
.SimpleSnippet.bold .donation-form-url,
.SimpleSnippet.bold .donation-amount, .SimpleSnippet.takeover .donation-form-url,
.SimpleSnippet.takeover .donation-amount {
padding-block: 8px;
}
.SimpleSnippet.bold .icon, .SimpleSnippet.takeover .icon {
margin-inline-end: 20px;
}
.SimpleSnippet .icon {
align-self: flex-start;
}
.SimpleSnippet.has-section-header .innerWrapper {
flex-wrap: wrap;
padding-top: 7px;
}
.SimpleSnippet .innerContentWrapper {
align-items: center;
display: flex;
}
.SimpleSnippet .section-header {
flex: 0 0 100%;
margin-bottom: 10px;
}
.SimpleSnippet .section-title {
color: var(--newtab-text-primary-color);
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
}
.SimpleSnippet .section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.SimpleSnippet .section-title .icon {
height: 16px;
margin-inline-end: 6px;
margin-top: -2px;
width: 16px;
}
/* stylelint-disable max-nesting-depth */
.SubmitFormSnippet {
flex-direction: column;
flex: 1 1 100%;
width: 100%;
}
.SubmitFormSnippet .disclaimerText {
margin: 5px 0 0;
font-size: 12px;
color: var(--newtab-text-secondary-color);
}
.SubmitFormSnippet p {
margin: 0;
}
.SubmitFormSnippet.send_to_device_snippet {
text-align: center;
}
.SubmitFormSnippet.send_to_device_snippet .message {
font-size: 16px;
margin-bottom: 20px;
}
.SubmitFormSnippet.send_to_device_snippet .scene2Title {
font-size: 24px;
display: block;
}
.SubmitFormSnippet .ASRouterButton.primary {
flex: 1 1 0;
}
.SubmitFormSnippet .scene2Icon {
width: 100%;
margin-bottom: 20px;
}
.SubmitFormSnippet .scene2Icon img {
width: 98px;
display: inline-block;
}
.SubmitFormSnippet .scene2Title {
font-size: inherit;
margin: 0 0 10px;
font-weight: bold;
display: inline;
}
.SubmitFormSnippet form {
display: flex;
flex-direction: column;
width: 100%;
}
.SubmitFormSnippet .message {
font-size: 14px;
align-self: stretch;
flex: 0 0 100%;
margin-bottom: 10px;
}
.SubmitFormSnippet .privacyNotice {
font-size: 12px;
color: var(--newtab-text-secondary-color);
margin-top: 10px;
display: flex;
flex: 0 0 100%;
}
.SubmitFormSnippet .innerWrapper {
max-width: 736px;
flex-wrap: wrap;
justify-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.SubmitFormSnippet .footer {
width: 100%;
margin: 0 auto;
text-align: right;
background-color: var(--newtab-background-color);
padding: 10px 0;
}
.SubmitFormSnippet .footer .footer-content {
margin: 0 auto;
max-width: 768px;
width: 100%;
text-align: right;
}
[dir=rtl] .SubmitFormSnippet .footer .footer-content {
text-align: left;
}
.SubmitFormSnippet input.mainInput {
border-radius: 2px;
background-color: var(--newtab-background-color-secondary);
border: 1px solid var(--newtab-border-color);
padding: 0 8px;
height: 100%;
font-size: 14px;
width: 50%;
}
.SubmitFormSnippet input.mainInput.clean:invalid, .SubmitFormSnippet input.mainInput.clean:required {
box-shadow: none;
}
.SubmitFormSnippet input.mainInput:focus {
border: 1px solid var(--newtab-textbox-focus-color);
box-shadow: var(--newtab-textbox-focus-boxshadow);
}
.SubmitFormSnippet.scene2Alt {
text-align: start;
}
.SubmitFormSnippet.scene2Alt .scene2Icon {
flex: 1;
margin-bottom: 0;
}
.SubmitFormSnippet.scene2Alt .message {
flex: 5;
margin-bottom: 0;
}
.SubmitFormSnippet.scene2Alt .message p {
margin-bottom: 10px;
}
.SubmitFormSnippet.scene2Alt .section-header {
width: 100%;
}
.SubmitFormSnippet.scene2Alt .section-header .icon {
width: 16px;
height: 16px;
}
.SubmitFormSnippet.scene2Alt .section-title {
font-size: 13px;
}
.SubmitFormSnippet.scene2Alt .section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.SubmitFormSnippet.scene2Alt .innerWrapper {
padding: 0 0 16px;
}
.submissionStatus {
text-align: center;
font-size: 14px;
padding: 20px 0;
}
.submissionStatus .submitStatusTitle {
font-size: 20px;
}
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center;
}
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px;
}
.EOYSnippetForm .donation-amount {
color: var(--newtab-text-primary-color);
margin-inline-end: 18px;
border: 1px solid var(--newtab-border-color);
padding: 5px 14px;
background: var(--newtab-background-color-secondary);
cursor: pointer;
}
.EOYSnippetForm input[type=radio] {
opacity: 0;
margin-inline-end: -18px;
}
.EOYSnippetForm input[type=radio]:checked + .donation-amount {
background: var(--newtab-text-secondary-color);
color: var(--newtab-background-color-secondary);
border: 1px solid var(--newtab-border-color);
}
.EOYSnippetForm input[type=radio]:checked:focus + .donation-amount, .EOYSnippetForm input[type=radio]:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-primary-action-background);
}
.EOYSnippetForm .monthly-checkbox-container {
display: flex;
width: 100%;
}
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex;
}

View file

@ -57,8 +57,6 @@ input {
--newtab-status-error: #D70022; --newtab-status-error: #D70022;
--newtab-inner-box-shadow-color: rgba(0, 0, 0, 0.1); --newtab-inner-box-shadow-color: rgba(0, 0, 0, 0.1);
--newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent); --newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent);
--newtab-text-emphasis-background: #FFE900;
--newtab-text-emphasis-text-color: #0C0C0D;
--newtab-textbox-focus-color: var(--newtab-primary-action-background); --newtab-textbox-focus-color: var(--newtab-primary-action-background);
--newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3); --newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3);
--newtab-button-secondary-color: inherit; --newtab-button-secondary-color: inherit;
@ -4136,105 +4134,6 @@ main.has-snippet {
transition: box-shadow 150ms; transition: box-shadow 150ms;
} }
.SnippetBaseContainer {
position: fixed;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
background-color: var(--newtab-background-color-secondary);
color: var(--newtab-text-primary-color);
font-size: 14px;
line-height: 20px;
border-top: 1px solid transparent;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2);
display: flex;
align-items: center;
}
.SnippetBaseContainer a {
cursor: pointer;
color: var(--newtab-primary-action-background);
}
.SnippetBaseContainer a:hover {
text-decoration: underline;
}
[lwt-newtab-brighttext] .SnippetBaseContainer a {
font-weight: bold;
}
.SnippetBaseContainer input[type=checkbox] {
margin-inline-start: 0;
}
.SnippetBaseContainer .innerWrapper {
margin: 0 auto;
display: flex;
align-items: center;
padding: 12px 25px;
padding-inline-end: 36px;
max-width: 836px;
}
@media (min-width: 866px) {
.SnippetBaseContainer .innerWrapper {
padding-inline-end: 25px;
}
}
@media (min-width: 1122px) {
.SnippetBaseContainer .innerWrapper {
max-width: 1092px;
}
}
.SnippetBaseContainer .blockButton {
display: none;
background: none;
border: 0;
position: absolute;
top: 20px;
inset-inline-end: 12px;
height: 16px;
width: 16px;
background-image: url("chrome://global/skin/icons/close.svg");
-moz-context-properties: fill;
color: inherit;
fill: currentColor;
opacity: 0.5;
margin-top: -8px;
padding: 0;
cursor: pointer;
}
.SnippetBaseContainer:hover .blockButton {
display: block;
}
.SnippetBaseContainer .icon {
height: 42px;
width: 42px;
margin-inline-end: 12px;
flex-shrink: 0;
}
.snippets-preview-banner {
font-size: 15px;
line-height: 42px;
color: var(--newtab-text-primary-color);
background: var(--newtab-background-color-secondary);
text-align: center;
position: absolute;
top: 0;
width: 100%;
}
.snippets-preview-banner span {
vertical-align: middle;
}
body:not([lwt-newtab-brighttext]) .icon-dark-theme,
body:not([lwt-newtab-brighttext]) .icon.icon-dark-theme,
body:not([lwt-newtab-brighttext]) .scene2Icon .icon-dark-theme {
display: none;
}
body[lwt-newtab-brighttext] .icon-light-theme,
body[lwt-newtab-brighttext] .icon.icon-light-theme,
body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
display: none;
}
.activity-stream.modal-open { .activity-stream.modal-open {
overflow: hidden; overflow: hidden;
} }
@ -4328,464 +4227,3 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
box-shadow: 0 0 0 5px var(--newtab-element-secondary-color); box-shadow: 0 0 0 5px var(--newtab-element-secondary-color);
transition: box-shadow 150ms; transition: box-shadow 150ms;
} }
/* stylelint-disable max-nesting-depth */
.below-search-snippet {
margin: 0 auto 16px;
}
.below-search-snippet.withButton {
margin: auto;
min-height: 60px;
background-color: transparent;
}
.below-search-snippet.withButton .snippet-hover-wrapper {
min-height: 60px;
border-radius: 4px;
}
.below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color);
}
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block;
opacity: 1;
}
.SimpleBelowSearchSnippet {
background-color: transparent;
border: 0;
box-shadow: none;
position: relative;
margin: auto;
z-index: auto;
}
@media (min-width: 866px) {
.SimpleBelowSearchSnippet {
width: 736px;
}
}
.SimpleBelowSearchSnippet.active {
background-color: var(--newtab-element-hover-color);
border-radius: 4px;
}
.SimpleBelowSearchSnippet .innerWrapper {
align-items: center;
background-color: transparent;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
flex-direction: column;
padding: 16px;
text-align: center;
width: 100%;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .innerWrapper {
align-items: flex-start;
background-color: transparent;
border-radius: 4px;
box-shadow: none;
flex-direction: row;
padding: 0;
text-align: inherit;
width: 696px;
}
}
@media (width <= 865px) {
.SimpleBelowSearchSnippet .innerWrapper {
margin-inline-start: 0;
}
}
@media (max-width: 609px) {
.SimpleBelowSearchSnippet .innerWrapper {
margin: auto;
}
}
.SimpleBelowSearchSnippet .blockButton {
display: block;
inset-inline-end: 10px;
opacity: 1;
top: 50%;
}
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 5px var(--newtab-element-secondary-color);
border-radius: 2px;
}
.SimpleBelowSearchSnippet .title {
font-size: inherit;
margin: 0;
}
.SimpleBelowSearchSnippet .title-inline {
display: inline;
}
.SimpleBelowSearchSnippet .textContainer {
margin: 10px;
margin-inline-start: 0;
padding-inline-end: 20px;
}
.SimpleBelowSearchSnippet .icon {
margin-top: 8px;
margin-inline-start: 12px;
height: 32px;
width: 32px;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .icon {
height: 24px;
width: 24px;
}
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet .icon {
margin: auto;
}
}
.SimpleBelowSearchSnippet.withButton {
line-height: 20px;
margin-bottom: 10px;
min-height: 60px;
background-color: transparent;
}
@media (max-width: 1123px) {
.SimpleBelowSearchSnippet.withButton .innerWrapper {
margin: 0 40px;
}
}
.SimpleBelowSearchSnippet.withButton .blockButton {
display: block;
inset-inline-end: -10%;
opacity: 0;
margin: auto;
top: unset;
}
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none;
}
@media (max-width: 1123px) {
.SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%;
}
}
.SimpleBelowSearchSnippet.withButton .icon {
width: 42px;
height: 42px;
flex-shrink: 0;
margin: auto 0;
margin-inline-end: 10px;
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet.withButton .icon {
margin: auto;
}
}
.SimpleBelowSearchSnippet.withButton .buttonContainer {
margin: auto;
margin-inline-end: 0;
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet.withButton .buttonContainer {
margin: auto;
}
}
@media (max-width: 610px) {
.SimpleBelowSearchSnippet button {
margin: auto;
}
}
.SimpleBelowSearchSnippet .body {
display: inline;
position: sticky;
transform: translateY(-50%);
margin: 8px 0 0;
}
@media (min-width: 610px) {
.SimpleBelowSearchSnippet .body {
margin: 12px 0;
}
}
.SimpleBelowSearchSnippet .body a {
font-weight: 600;
}
.SimpleSnippet.tall {
padding: 27px 0;
}
.SimpleSnippet p em {
color: var(--newtab-text-emphasis-text-color);
font-style: normal;
background: var(--newtab-text-emphasis-background);
}
.SimpleSnippet.bold {
height: 176px;
}
.SimpleSnippet.bold .body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px;
}
.SimpleSnippet.bold .icon {
width: 71px;
height: 71px;
}
.SimpleSnippet.takeover {
height: 344px;
}
.SimpleSnippet.takeover .body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px;
}
.SimpleSnippet.takeover .icon {
width: 79px;
height: 79px;
}
.SimpleSnippet .title {
font-size: inherit;
margin: 0;
}
.SimpleSnippet .title-inline {
display: inline;
}
.SimpleSnippet .titleIcon {
background-repeat: no-repeat;
background-size: 14px;
background-position: center;
height: 16px;
width: 16px;
margin-top: 2px;
margin-inline-end: 2px;
display: inline-block;
vertical-align: top;
}
.SimpleSnippet .body {
display: inline;
margin: 0;
}
.SimpleSnippet.tall .icon {
margin-inline-end: 20px;
}
.SimpleSnippet.bold .donation-form-url,
.SimpleSnippet.bold .donation-amount, .SimpleSnippet.takeover .donation-form-url,
.SimpleSnippet.takeover .donation-amount {
padding-block: 8px;
}
.SimpleSnippet.bold .icon, .SimpleSnippet.takeover .icon {
margin-inline-end: 20px;
}
.SimpleSnippet .icon {
align-self: flex-start;
}
.SimpleSnippet.has-section-header .innerWrapper {
flex-wrap: wrap;
padding-top: 7px;
}
.SimpleSnippet .innerContentWrapper {
align-items: center;
display: flex;
}
.SimpleSnippet .section-header {
flex: 0 0 100%;
margin-bottom: 10px;
}
.SimpleSnippet .section-title {
color: var(--newtab-text-primary-color);
display: inline-block;
font-size: 13px;
font-weight: bold;
margin: 0;
}
.SimpleSnippet .section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.SimpleSnippet .section-title .icon {
height: 16px;
margin-inline-end: 6px;
margin-top: -2px;
width: 16px;
}
/* stylelint-disable max-nesting-depth */
.SubmitFormSnippet {
flex-direction: column;
flex: 1 1 100%;
width: 100%;
}
.SubmitFormSnippet .disclaimerText {
margin: 5px 0 0;
font-size: 12px;
color: var(--newtab-text-secondary-color);
}
.SubmitFormSnippet p {
margin: 0;
}
.SubmitFormSnippet.send_to_device_snippet {
text-align: center;
}
.SubmitFormSnippet.send_to_device_snippet .message {
font-size: 16px;
margin-bottom: 20px;
}
.SubmitFormSnippet.send_to_device_snippet .scene2Title {
font-size: 24px;
display: block;
}
.SubmitFormSnippet .ASRouterButton.primary {
flex: 1 1 0;
}
.SubmitFormSnippet .scene2Icon {
width: 100%;
margin-bottom: 20px;
}
.SubmitFormSnippet .scene2Icon img {
width: 98px;
display: inline-block;
}
.SubmitFormSnippet .scene2Title {
font-size: inherit;
margin: 0 0 10px;
font-weight: bold;
display: inline;
}
.SubmitFormSnippet form {
display: flex;
flex-direction: column;
width: 100%;
}
.SubmitFormSnippet .message {
font-size: 14px;
align-self: stretch;
flex: 0 0 100%;
margin-bottom: 10px;
}
.SubmitFormSnippet .privacyNotice {
font-size: 12px;
color: var(--newtab-text-secondary-color);
margin-top: 10px;
display: flex;
flex: 0 0 100%;
}
.SubmitFormSnippet .innerWrapper {
max-width: 736px;
flex-wrap: wrap;
justify-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.SubmitFormSnippet .footer {
width: 100%;
margin: 0 auto;
text-align: right;
background-color: var(--newtab-background-color);
padding: 10px 0;
}
.SubmitFormSnippet .footer .footer-content {
margin: 0 auto;
max-width: 768px;
width: 100%;
text-align: right;
}
[dir=rtl] .SubmitFormSnippet .footer .footer-content {
text-align: left;
}
.SubmitFormSnippet input.mainInput {
border-radius: 2px;
background-color: var(--newtab-background-color-secondary);
border: 1px solid var(--newtab-border-color);
padding: 0 8px;
height: 100%;
font-size: 14px;
width: 50%;
}
.SubmitFormSnippet input.mainInput.clean:invalid, .SubmitFormSnippet input.mainInput.clean:required {
box-shadow: none;
}
.SubmitFormSnippet input.mainInput:focus {
border: 1px solid var(--newtab-textbox-focus-color);
box-shadow: var(--newtab-textbox-focus-boxshadow);
}
.SubmitFormSnippet.scene2Alt {
text-align: start;
}
.SubmitFormSnippet.scene2Alt .scene2Icon {
flex: 1;
margin-bottom: 0;
}
.SubmitFormSnippet.scene2Alt .message {
flex: 5;
margin-bottom: 0;
}
.SubmitFormSnippet.scene2Alt .message p {
margin-bottom: 10px;
}
.SubmitFormSnippet.scene2Alt .section-header {
width: 100%;
}
.SubmitFormSnippet.scene2Alt .section-header .icon {
width: 16px;
height: 16px;
}
.SubmitFormSnippet.scene2Alt .section-title {
font-size: 13px;
}
.SubmitFormSnippet.scene2Alt .section-title a {
color: var(--newtab-text-primary-color);
font-weight: inherit;
text-decoration: none;
}
.SubmitFormSnippet.scene2Alt .innerWrapper {
padding: 0 0 16px;
}
.submissionStatus {
text-align: center;
font-size: 14px;
padding: 20px 0;
}
.submissionStatus .submitStatusTitle {
font-size: 20px;
}
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center;
}
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px;
}
.EOYSnippetForm .donation-amount {
color: var(--newtab-text-primary-color);
margin-inline-end: 18px;
border: 1px solid var(--newtab-border-color);
padding: 5px 14px;
background: var(--newtab-background-color-secondary);
cursor: pointer;
}
.EOYSnippetForm input[type=radio] {
opacity: 0;
margin-inline-end: -18px;
}
.EOYSnippetForm input[type=radio]:checked + .donation-amount {
background: var(--newtab-text-secondary-color);
color: var(--newtab-background-color-secondary);
border: 1px solid var(--newtab-border-color);
}
.EOYSnippetForm input[type=radio]:checked:focus + .donation-amount, .EOYSnippetForm input[type=radio]:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-primary-action-background);
}
.EOYSnippetForm .monthly-checkbox-container {
display: flex;
width: 100%;
}
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex;
}

View file

@ -683,29 +683,7 @@ const ASRouterUtils = {
}, },
getPreviewEndpoint() { getPreviewEndpoint() {
if (__webpack_require__.g.document && __webpack_require__.g.document.location && __webpack_require__.g.document.location.href.includes("endpoint")) {
const params = new URLSearchParams(__webpack_require__.g.document.location.href.slice(__webpack_require__.g.document.location.href.indexOf("endpoint")));
try {
const endpoint = new URL(params.get("endpoint"));
return {
url: endpoint.href,
snippetId: params.get("snippetId"),
theme: this.getPreviewTheme(),
dir: this.getPreviewDir()
};
} catch (e) {}
}
return null; return null;
},
getPreviewTheme() {
return new URLSearchParams(__webpack_require__.g.document.location.href.slice(__webpack_require__.g.document.location.href.indexOf("theme"))).get("theme");
},
getPreviewDir() {
return new URLSearchParams(__webpack_require__.g.document.location.href.slice(__webpack_require__.g.document.location.href.indexOf("dir"))).get("dir");
} }
}; };

View file

@ -98,7 +98,7 @@ module.exports = function (config) {
statements: 66, statements: 66,
lines: 66, lines: 66,
functions: 78, functions: 78,
branches: 63, branches: 50,
}, },
"lib/TelemetryFeed.jsm": { "lib/TelemetryFeed.jsm": {
statements: 98, statements: 98,

View file

@ -1,101 +0,0 @@
import {
convertLinks,
RichText,
} from "content-src/asrouter/components/RichText/RichText";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import {
Localized,
LocalizationProvider,
ReactLocalization,
} from "@fluent/react";
import { mount } from "enzyme";
import React from "react";
function mockL10nWrapper(content) {
const bundle = new FluentBundle("en-US");
for (const [id, value] of Object.entries(content)) {
if (typeof value === "string") {
bundle.addResource(new FluentResource(`${id} = ${value}`));
}
}
const l10n = new ReactLocalization([bundle]);
return {
wrappingComponent: LocalizationProvider,
wrappingComponentProps: { l10n },
};
}
describe("convertLinks", () => {
let sandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it("should return an object with anchor elements", () => {
const cta = {
url: "https://foo.com",
metric: "foo",
};
const stub = sandbox.stub();
const result = convertLinks({ cta }, stub);
assert.property(result, "cta");
assert.propertyVal(result.cta, "type", "a");
assert.propertyVal(result.cta.props, "href", cta.url);
assert.propertyVal(result.cta.props, "data-metric", cta.metric);
assert.propertyVal(result.cta.props, "onClick", stub);
});
it("should return an anchor element without href", () => {
const cta = {
url: "https://foo.com",
metric: "foo",
action: "OPEN_MENU",
args: "appMenu",
entrypoint_name: "entrypoint_name",
entrypoint_value: "entrypoint_value",
};
const stub = sandbox.stub();
const result = convertLinks({ cta }, stub);
assert.property(result, "cta");
assert.propertyVal(result.cta, "type", "a");
assert.propertyVal(result.cta.props, "href", false);
assert.propertyVal(result.cta.props, "data-metric", cta.metric);
assert.propertyVal(result.cta.props, "data-action", cta.action);
assert.propertyVal(result.cta.props, "data-args", cta.args);
assert.propertyVal(
result.cta.props,
"data-entrypoint_name",
cta.entrypoint_name
);
assert.propertyVal(
result.cta.props,
"data-entrypoint_value",
cta.entrypoint_value
);
assert.propertyVal(result.cta.props, "onClick", stub);
});
it("should follow openNewWindow prop", () => {
const cta = { url: "https://foo.com" };
const newWindow = convertLinks({ cta }, sandbox.stub(), false, true);
const sameWindow = convertLinks({ cta }, sandbox.stub(), false);
assert.propertyVal(newWindow.cta.props, "target", "_blank");
assert.propertyVal(sameWindow.cta.props, "target", "");
});
it("should allow for custom elements & styles", () => {
const wrapper = mount(
<RichText
customElements={{ em: <em style={{ color: "#f05" }} /> }}
text="<em>foo</em>"
localization_id="text"
/>,
mockL10nWrapper({ text: "<em>foo</em>" })
);
const localized = wrapper.find(Localized);
assert.propertyVal(localized.props().elems.em.props.style, "color", "#f05");
});
});