summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet')
-rw-r--r--browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx133
-rw-r--r--browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json114
-rw-r--r--browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss190
3 files changed, 437 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
new file mode 100644
index 0000000000..2641d51e86
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
@@ -0,0 +1,133 @@
+/* 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>
+ );
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
new file mode 100644
index 0000000000..06368257f0
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
@@ -0,0 +1,114 @@
+{
+ "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"]
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
new file mode 100644
index 0000000000..9d902b4cbb
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
@@ -0,0 +1,190 @@
+
+.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 (max-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;
+ }
+ }
+}