Bug 1640027 - Add Zap component that allows zap style to be added to headings r=pdahiya,fluent-reviewers
authoremcminn <emcminn@mozilla.com>
Wed, 24 Jun 2020 21:41:14 +0000
changeset 537259 41883b5892bcd3e393328bf179879d906ab30d82
parent 537258 016fda991c64287801e0c2184c00c395f4aa0427
child 537260 e738e5bbcfc07e916df1bf4f67f7836d2ecdbe7c
push id37540
push usercbrindusan@mozilla.com
push dateThu, 25 Jun 2020 09:44:52 +0000
treeherdermozilla-central@db74cdf9afe7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspdahiya, fluent-reviewers
bugs1640027
milestone79.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1640027 - Add Zap component that allows zap style to be added to headings r=pdahiya,fluent-reviewers Differential Revision: https://phabricator.services.mozilla.com/D80514
browser/components/newtab/.eslintrc.js
browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
browser/components/newtab/aboutwelcome/content/aboutwelcome.css
browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx
browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/data/content/assets/zap.svg
browser/components/newtab/test/browser/browser_aboutwelcome_multistage.js
browser/components/newtab/test/unit/content-src/components/MSLocalized.test.jsx
browser/locales/en-US/browser/newtab/onboarding.ftl
--- a/browser/components/newtab/.eslintrc.js
+++ b/browser/components/newtab/.eslintrc.js
@@ -34,16 +34,17 @@ module.exports = {
     "plugin:prettier/recommended", // require("eslint-plugin-prettier")
     "prettier/react", // require("eslint-config-prettier")
   ],
   overrides: [
     {
       // These files use fluent-dom to insert content
       files: [
         "content-src/aboutwelcome/components/HeroText.jsx",
+        "content-src/aboutwelcome/components/Zap.jsx",
         "content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx",
         "content-src/asrouter/templates/OnboardingMessage/**",
         "content-src/asrouter/templates/FirstRun/**",
         "content-src/asrouter/templates/Trailhead/**",
         "content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt.jsx",
         "content-src/asrouter/components/FxASignupForm/FxASignupForm.jsx",
         "content-src/components/TopSites/**",
         "content-src/components/MoreRecommendations/MoreRecommendations.jsx",
--- a/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
+++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
@@ -96,20 +96,20 @@
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__);
 /* harmony import */ var _components_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
-/* harmony import */ var _components_HeroText__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7);
-/* harmony import */ var _components_FxCards__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(8);
+/* harmony import */ var _components_HeroText__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
+/* harmony import */ var _components_FxCards__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
 /* harmony import */ var _components_MSLocalized__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4);
-/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
+/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(6);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 /* 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/. */
 
 
 
@@ -264,35 +264,37 @@ module.exports = ReactDOM;
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MultiStageAboutWelcome", function() { return MultiStageAboutWelcome; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "WelcomeScreen", function() { return WelcomeScreen; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
-/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
-/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
+/* harmony import */ var _Zap__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
+/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
+/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7);
 /* 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/. */
 
 
 
 
+
 const DEFAULT_SITES = ["youtube-com", "facebook-com", "amazon", "reddit-com", "wikipedia-org", "twitter-com"].map(site => ({
   icon: `resource://activity-stream/data/content/tippytop/images/${site}@2x.png`
 }));
 const MultiStageAboutWelcome = props => {
   const [index, setScreenIndex] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(0);
   Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(() => {
     // Send impression ping when respective screen first renders
     props.screens.forEach(screen => {
       if (index === screen.order) {
-        _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].sendImpressionTelemetry(`${props.message_id}_${screen.id}`);
+        _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].sendImpressionTelemetry(`${props.message_id}_${screen.id}`);
       }
     }); // Remember that a new screen has loaded for browser navigation
 
     if (index > window.history.state) {
       window.history.pushState(index, "");
     }
   }, [index]);
   Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(() => {
@@ -309,35 +311,35 @@ const MultiStageAboutWelcome = props => 
   }, []);
   const [flowParams, setFlowParams] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(null);
   const {
     metricsFlowUri
   } = props;
   Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(() => {
     (async () => {
       if (metricsFlowUri) {
-        setFlowParams((await _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].fetchFlowParams(metricsFlowUri)));
+        setFlowParams((await _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].fetchFlowParams(metricsFlowUri)));
       }
     })();
   }, [metricsFlowUri]); // Transition to next screen, opening about:home on last screen button CTA
 
-  const handleTransition = index < props.screens.length ? Object(react__WEBPACK_IMPORTED_MODULE_0__["useCallback"])(() => setScreenIndex(prevState => prevState + 1), []) : _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].handleUserAction({
+  const handleTransition = index < props.screens.length ? Object(react__WEBPACK_IMPORTED_MODULE_0__["useCallback"])(() => setScreenIndex(prevState => prevState + 1), []) : _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].handleUserAction({
     type: "OPEN_ABOUT_PAGE",
     data: {
       args: "home",
       where: "current"
     }
   });
   const useImportable = props.message_id.includes("IMPORTABLE");
   const [topSites, setTopSites] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(DEFAULT_SITES);
   Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(() => {
     (async () => {
       const importable = JSON.parse((await window.AWGetImportableSites()));
       const showImportable = useImportable && importable.length >= 5;
-      _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].sendImpressionTelemetry(`${props.message_id}_SITES`, {
+      _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].sendImpressionTelemetry(`${props.message_id}_SITES`, {
         display: showImportable ? "importable" : "static",
         importable: importable.length
       });
       setTopSites(showImportable ? importable : DEFAULT_SITES);
     })();
   }, [useImportable]);
   return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_0___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
     className: `multistageContainer`
@@ -362,28 +364,28 @@ class WelcomeScreen extends react__WEBPA
   }
 
   handleOpenURL(action, flowParams, UTMTerm) {
     let {
       type,
       data
     } = action;
     let url = new URL(data.args);
-    Object(_asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_3__["addUtmParams"])(url, `aboutwelcome-${UTMTerm}-screen`);
+    Object(_asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_4__["addUtmParams"])(url, `aboutwelcome-${UTMTerm}-screen`);
 
     if (action.addFlowParams && flowParams) {
       url.searchParams.append("device_id", flowParams.deviceId);
       url.searchParams.append("flow_id", flowParams.flowId);
       url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
     }
 
     data = { ...data,
       args: url.toString()
     };
-    _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].handleUserAction({
+    _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].handleUserAction({
       type,
       data
     });
   }
 
   highlightTheme(theme) {
     const themes = document.querySelectorAll("button.theme");
     themes.forEach(function (element) {
@@ -401,29 +403,29 @@ class WelcomeScreen extends react__WEBPA
     } = this;
     let targetContent = props.content[event.currentTarget.value] || props.content.tiles;
 
     if (!(targetContent && targetContent.action)) {
       return;
     } // Send telemetry before waiting on actions
 
 
-    _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].sendActionTelemetry(props.messageId, event.currentTarget.value);
+    _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].sendActionTelemetry(props.messageId, event.currentTarget.value);
     let {
       action
     } = targetContent;
 
     if (action.type === "OPEN_URL") {
       this.handleOpenURL(action, props.flowParams, props.UTMTerm);
     } else if (action.type) {
-      _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].handleUserAction(action); // Wait until migration closes to complete the action
+      _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].handleUserAction(action); // Wait until migration closes to complete the action
 
       if (action.type === "SHOW_MIGRATION_WIZARD") {
         await window.AWWaitForMigrationClose();
-        _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].sendActionTelemetry(props.messageId, "migrate_close");
+        _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].sendActionTelemetry(props.messageId, "migrate_close");
       }
     } // A special tiles.action.theme value indicates we should use the event's value vs provided value.
 
 
     if (action.theme) {
       this.highlightTheme(event.currentTarget.value);
       window.AWSelectTheme(action.theme === "<event>" ? event.currentTarget.value : action.theme);
     }
@@ -511,19 +513,20 @@ class WelcomeScreen extends react__WEBPA
     } = this.props;
     const hasSecondaryTopCTA = content.secondary_button && content.secondary_button.position === "top";
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("main", {
       className: `screen ${this.props.id}`
     }, hasSecondaryTopCTA ? this.renderSecondaryCTA("top") : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: `brand-logo ${hasSecondaryTopCTA ? "cta-top" : ""}`
     }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: "welcome-text"
-    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_Zap__WEBPACK_IMPORTED_MODULE_2__["Zap"], {
+      hasZap: content.zap,
       text: content.title
-    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", null)), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
+    }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
       text: content.subtitle
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", null))), content.tiles ? this.renderTiles() : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
       text: content.primary_button ? content.primary_button.label : null
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
       className: "primary",
       value: "primary_button",
       onClick: this.handleAction
     }))), content.secondary_button && content.secondary_button.position !== "top" ? this.renderSecondaryCTA() : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
@@ -582,25 +585,78 @@ const Localized = ({
   if (typeof text === "object" && text[MS_STRING_PROP]) {
     props = { ...props
     };
     props["data-l10n-id"] = text[MS_STRING_PROP];
   } else if (typeof text === "string") {
     textNode = text;
   }
 
-  return children ? react__WEBPACK_IMPORTED_MODULE_0___default.a.cloneElement(children, props, textNode) : react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", props, textNode);
+  if (!children) {
+    return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", props, textNode);
+  } else if (textNode) {
+    return react__WEBPACK_IMPORTED_MODULE_0___default.a.cloneElement(children, props, textNode);
+  }
+
+  return react__WEBPACK_IMPORTED_MODULE_0___default.a.cloneElement(children, props);
 };
 
 /***/ }),
 /* 5 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Zap", function() { return Zap; });
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* 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/. */
+
+
+const MS_STRING_PROP = "string_id";
+const Zap = props => {
+  if (!props.text) {
+    return null;
+  }
+
+  if (props.hasZap) {
+    if (typeof props.text === "object" && props.text[MS_STRING_PROP]) {
+      return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
+        text: props.text
+      }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
+        className: "welcomeZap"
+      }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", {
+        "data-l10n-name": "zap"
+      })));
+    } else if (typeof props.text === "string") {
+      // Parse string to zap style last word of the props.text
+      let titleArray = props.text.split(" ");
+      let lastWord = `${titleArray.pop()}`;
+      return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
+        className: "welcomeZap"
+      }, titleArray.join(" ").concat(" "), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", null, lastWord));
+    }
+  } else {
+    return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
+      text: props.text
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", null));
+  }
+
+  return null;
+};
+
+/***/ }),
+/* 6 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AboutWelcomeUtils", function() { return AboutWelcomeUtils; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DEFAULT_WELCOME_CONTENT", function() { return DEFAULT_WELCOME_CONTENT; });
 /* 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/. */
 const AboutWelcomeUtils = {
   handleUserAction(action) {
     window.AWSendToParent("SPECIAL_ACTION", action);
@@ -747,17 +803,17 @@ const DEFAULT_WELCOME_CONTENT = {
     },
     id: "TRAILHEAD_CARD_4",
     order: 3,
     blockOnClick: true
   }]
 };
 
 /***/ }),
-/* 6 */
+/* 7 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BASE_PARAMS", function() { return BASE_PARAMS; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "addUtmParams", function() { return addUtmParams; });
 /* 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,
@@ -788,17 +844,17 @@ function addUtmParams(url, utmTerm) {
   Object.keys(BASE_PARAMS).forEach(key => {
     returnUrl.searchParams.append(key, BASE_PARAMS[key]);
   });
   returnUrl.searchParams.append("utm_term", utmTerm);
   return returnUrl;
 }
 
 /***/ }),
-/* 7 */
+/* 8 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "HeroText", function() { return HeroText; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
@@ -815,27 +871,27 @@ const HeroText = props => {
   })), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
     text: props.subtitle
   }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", {
     className: "welcome-subtitle"
   })));
 };
 
 /***/ }),
-/* 8 */
+/* 9 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FxCards", function() { return FxCards; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
-/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
-/* harmony import */ var _asrouter_templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
-/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5);
+/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7);
+/* harmony import */ var _asrouter_templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
+/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 /* 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/. */
 
 
 
@@ -911,17 +967,17 @@ class FxCards extends react__WEBPACK_IMP
       onAction: this.onCardAction,
       UISurface: "ABOUT_WELCOME"
     }, card)))));
   }
 
 }
 
 /***/ }),
-/* 9 */
+/* 10 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OnboardingCard", function() { return OnboardingCard; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* harmony import */ var _aboutwelcome_components_MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
--- a/browser/components/newtab/aboutwelcome/content/aboutwelcome.css
+++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.css
@@ -271,16 +271,25 @@ body {
     flex-flow: column nowrap;
     height: 100%; }
   .multistageContainer .brand-logo {
     background: url("chrome://branding/content/about-logo.svg") top center/112px no-repeat;
     padding: 112px 0 20px;
     margin-top: 60px; }
     .multistageContainer .brand-logo.cta-top {
       margin-top: 25px; }
+  .multistageContainer .welcomeZap span {
+    display: inline-block;
+    font-weight: bold;
+    background-image: url("../data/content/assets/zap.svg");
+    background-repeat: no-repeat;
+    background-size: 100%;
+    content: '';
+    background-position-y: bottom;
+    padding-bottom: 8px; }
   .multistageContainer .welcome-text {
     display: flex;
     flex-direction: column;
     justify-content: center;
     align-items: center;
     margin-bottom: 20px; }
     .multistageContainer .welcome-text h1,
     .multistageContainer .welcome-text h2 {
--- a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
+++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
@@ -262,16 +262,27 @@ body {
     padding: $logo-size 0 20px;
     margin-top: 60px;
 
     &.cta-top {
       margin-top: 25px;
     }
   }
 
+  .welcomeZap span {
+    display: inline-block;
+    font-weight: bold;
+    background-image: url('../data/content/assets/zap.svg');
+    background-repeat: no-repeat;
+    background-size: 100%;
+    content: '';
+    background-position-y: bottom;
+    padding-bottom: 8px;
+  }
+
   .welcome-text {
     display: flex;
     flex-direction: column;
     justify-content: center;
     align-items: center;
     margin-bottom: 20px;
 
     h1,
--- a/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx
+++ b/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx
@@ -35,12 +35,16 @@ export const Localized = ({ text, childr
   let textNode;
 
   if (typeof text === "object" && text[MS_STRING_PROP]) {
     props = { ...props };
     props["data-l10n-id"] = text[MS_STRING_PROP];
   } else if (typeof text === "string") {
     textNode = text;
   }
-  return children
-    ? React.cloneElement(children, props, textNode)
-    : React.createElement("span", props, textNode);
+
+  if (!children) {
+    return React.createElement("span", props, textNode);
+  } else if (textNode) {
+    return React.cloneElement(children, props, textNode);
+  }
+  return React.cloneElement(children, props);
 };
--- a/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
+++ b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
@@ -1,14 +1,15 @@
 /* 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, { useState, useCallback, useEffect } from "react";
 import { Localized } from "./MSLocalized";
+import { Zap } from "./Zap";
 import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils";
 import { addUtmParams } from "../../asrouter/templates/FirstRun/addUtmParams";
 
 const DEFAULT_SITES = [
   "youtube-com",
   "facebook-com",
   "amazon",
   "reddit-com",
@@ -255,19 +256,17 @@ export class WelcomeScreen extends React
     const { content } = this.props;
     const hasSecondaryTopCTA =
       content.secondary_button && content.secondary_button.position === "top";
     return (
       <main className={`screen ${this.props.id}`}>
         {hasSecondaryTopCTA ? this.renderSecondaryCTA("top") : null}
         <div className={`brand-logo ${hasSecondaryTopCTA ? "cta-top" : ""}`} />
         <div className="welcome-text">
-          <Localized text={content.title}>
-            <h1 />
-          </Localized>
+          <Zap hasZap={content.zap} text={content.title} />
           <Localized text={content.subtitle}>
             <h2 />
           </Localized>
         </div>
         {content.tiles ? this.renderTiles() : null}
         <div>
           <Localized
             text={content.primary_button ? content.primary_button.label : null}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx
@@ -0,0 +1,42 @@
+/* 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 { Localized } from "./MSLocalized";
+const MS_STRING_PROP = "string_id";
+
+export const Zap = props => {
+  if (!props.text) {
+    return null;
+  }
+
+  if (props.hasZap) {
+    if (typeof props.text === "object" && props.text[MS_STRING_PROP]) {
+      return (
+        <Localized text={props.text}>
+          <h1 className="welcomeZap">
+            <span data-l10n-name="zap" />
+          </h1>
+        </Localized>
+      );
+    } else if (typeof props.text === "string") {
+      // Parse string to zap style last word of the props.text
+      let titleArray = props.text.split(" ");
+      let lastWord = `${titleArray.pop()}`;
+      return (
+        <h1 className="welcomeZap">
+          {titleArray.join(" ").concat(" ")}
+          <span>{lastWord}</span>
+        </h1>
+      );
+    }
+  } else {
+    return (
+      <Localized text={props.text}>
+        <h1 />
+      </Localized>
+    );
+  }
+  return null;
+};
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -16523,17 +16523,23 @@ const Localized = ({
   if (typeof text === "object" && text[MS_STRING_PROP]) {
     props = { ...props
     };
     props["data-l10n-id"] = text[MS_STRING_PROP];
   } else if (typeof text === "string") {
     textNode = text;
   }
 
-  return children ? external_React_default.a.cloneElement(children, props, textNode) : external_React_default.a.createElement("span", props, textNode);
+  if (!children) {
+    return external_React_default.a.createElement("span", props, textNode);
+  } else if (textNode) {
+    return external_React_default.a.cloneElement(children, props, textNode);
+  }
+
+  return external_React_default.a.cloneElement(children, props);
 };
 // CONCATENATED MODULE: ./content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OnboardingCard", function() { return OnboardingMessage_OnboardingCard; });
 /* 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/. */
 
 
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/zap.svg
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+<svg width="108" height="9" viewBox="0 0 108 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 4.83004C6.10256 2.84324 25.0154 0.657752 38.3077 3.04192C54.9231 6.02212 93.0769 9.59836 106 4.234" stroke="url(#paint0_linear)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear" x1="2" y1="5.6009" x2="97.9111" y2="5.6009" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FF9100"/>
+<stop offset="0.489583" stop-color="#F10366"/>
+<stop offset="1" stop-color="#6173FF"/>
+</linearGradient>
+</defs>
+</svg>
--- a/browser/components/newtab/test/browser/browser_aboutwelcome_multistage.js
+++ b/browser/components/newtab/test/browser/browser_aboutwelcome_multistage.js
@@ -6,16 +6,17 @@ const ABOUT_WELCOME_OVERRIDE_CONTENT_PRE
 
 const TEST_MULTISTAGE_CONTENT = {
   id: "multi-stage-welcome",
   screens: [
     {
       id: "AW_STEP1",
       order: 0,
       content: {
+        zap: true,
         title: "Step 1",
         tiles: {
           type: "theme",
           action: {
             theme: "<event>",
           },
           data: [
             {
@@ -175,33 +176,34 @@ add_task(async function test_Multistage_
 
   await test_screen_content(
     browser,
     "multistage step 1",
     // Expected selectors:
     [
       "div.multistageContainer",
       "main.AW_STEP1",
+      "h1.welcomeZap",
       "div.secondary-cta.top",
       "button.secondary",
       "button.theme",
       "div.indicator.current",
     ],
     // Unexpected selectors:
     ["main.AW_STEP2", "main.AW_STEP3"]
   );
 
   await onButtonClick(browser, "button.primary");
   await test_screen_content(
     browser,
     "multistage step 2",
     // Expected selectors:
     ["div.multistageContainer", "main.AW_STEP2", "button.secondary"],
     // Unexpected selectors:
-    ["main.AW_STEP1", "main.AW_STEP3", "div.secondary-cta.top"]
+    ["main.AW_STEP1", "main.AW_STEP3", "div.secondary-cta.top", "h1.welcomeZap"]
   );
   await onButtonClick(browser, "button.primary");
   await test_screen_content(
     browser,
     "multistage step 3",
     // Expected selectors:
     [
       "div.multistageContainer",
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/MSLocalized.test.jsx
@@ -0,0 +1,46 @@
+import { Localized } from "content-src/aboutwelcome/components/MSLocalized";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<MSLocalized>", () => {
+  it("should render span with no children", () => {
+    const shallowWrapper = shallow(<Localized text="test" />);
+
+    assert.ok(shallowWrapper.find("span"));
+    assert.equal(shallowWrapper.text(), "test");
+  });
+  it("should render span when using string_id with no children", () => {
+    const shallowWrapper = shallow(
+      <Localized text={{ string_id: "test_id" }} />
+    );
+
+    assert.ok(shallowWrapper.find("span[data-l10n-id='test_id']"));
+  });
+  it("should render text inside child", () => {
+    const shallowWrapper = shallow(
+      <Localized text="test">
+        <div />
+      </Localized>
+    );
+    assert.ok(shallowWrapper.find("div").text(), "test");
+  });
+  it("should use l10n id on child", () => {
+    const shallowWrapper = shallow(
+      <Localized text={{ string_id: "test_id" }}>
+        <div />
+      </Localized>
+    );
+    assert.ok(shallowWrapper.find("div[data-l10n-id='test_id']"));
+  });
+  it("should keep original children", () => {
+    const shallowWrapper = shallow(
+      <Localized text={{ string_id: "test_id" }}>
+        <h1>
+          <span data-l10n-name="test" />
+        </h1>
+      </Localized>
+    );
+
+    assert.ok(shallowWrapper.find("span[data-l10n-name='test']"));
+  });
+});
--- a/browser/locales/en-US/browser/newtab/onboarding.ftl
+++ b/browser/locales/en-US/browser/newtab/onboarding.ftl
@@ -11,16 +11,20 @@
 ## avoid breaking quoted text).
 
 onboarding-button-label-learn-more = Learn More
 onboarding-button-label-get-started = Get Started
 
 ## Welcome modal dialog strings
 
 onboarding-welcome-header = Welcome to { -brand-short-name }
+
+# The <span data-l10n-name="zap"></span> in this string allows a "zap" underline style to be
+# automatically added to the text inside it. { -brand-short-name } should stay inside the span.
+onboarding-multistage-welcome-header = Welcome to <span data-l10n-name="zap">{ -brand-short-name }</span>
 onboarding-welcome-body = You’ve got the browser.<br/>Meet the rest of { -brand-product-name }.
 onboarding-welcome-learn-more = Learn more about the benefits.
 onboarding-welcome-modal-get-body = You’ve got the browser.<br/>Now get the most out of { -brand-product-name }.
 onboarding-welcome-modal-supercharge-body = Supercharge your privacy protection.
 onboarding-welcome-modal-privacy-body = You’ve got the browser. Let’s add more privacy protection.
 onboarding-welcome-modal-family-learn-more = Learn about the { -brand-product-name } family of products.
 onboarding-welcome-form-header = Start Here