summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/content-src/asrouter/templates/SimpleSnippet')
-rw-r--r--browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx222
-rw-r--r--browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json159
-rw-r--r--browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss131
3 files changed, 512 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
new file mode 100644
index 0000000000..96570e2dbd
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
@@ -0,0 +1,222 @@
+/* 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>
+ );
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
new file mode 100644
index 0000000000..4970b124af
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
@@ -0,0 +1,159 @@
+{
+ "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"]
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss
new file mode 100644
index 0000000000..1ee83a5cc9
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss
@@ -0,0 +1,131 @@
+$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;
+ }
+ }
+}