diff options
Diffstat (limited to 'browser/components/newtab/content-src')
28 files changed, 498 insertions, 96 deletions
diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx index 0580267f26..20402b09f5 100644 --- a/browser/components/newtab/content-src/components/Base/Base.jsx +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -165,6 +165,7 @@ export class BaseContent extends React.PureComponent { const { App } = props; const { initialized, customizeMenuVisible } = App; const prefs = props.Prefs.values; + const { pocketConfig } = prefs; const isDiscoveryStream = props.DiscoveryStream.config && props.DiscoveryStream.config.enabled; @@ -172,6 +173,14 @@ export class BaseContent extends React.PureComponent { section => section.id !== "topstories" ); + let spocMessageVariant = ""; + if ( + props.App.locale?.startsWith("en-") && + pocketConfig?.spocMessageVariant === "variant-c" + ) { + spocMessageVariant = pocketConfig.spocMessageVariant; + } + const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; const noSectionsEnabled = @@ -218,6 +227,7 @@ export class BaseContent extends React.PureComponent { pocketRegion={pocketRegion} mayHaveSponsoredTopSites={mayHaveSponsoredTopSites} mayHaveSponsoredStories={mayHaveSponsoredStories} + spocMessageVariant={spocMessageVariant} showing={customizeMenuVisible} /> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions*/} @@ -239,7 +249,10 @@ export class BaseContent extends React.PureComponent { <div className={`body-wrapper${initialized ? " on" : ""}`}> {isDiscoveryStream ? ( <ErrorBoundary className="borderless-error"> - <DiscoveryStreamBase locale={props.App.locale} /> + <DiscoveryStreamBase + locale={props.App.locale} + mayHaveSponsoredStories={mayHaveSponsoredStories} + /> </ErrorBoundary> ) : ( <Sections /> diff --git a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx index 679e8e137f..98bf88fbea 100644 --- a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx +++ b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx @@ -4,6 +4,7 @@ import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary"; import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; +import { SponsoredContentHighlight } from "../DiscoveryStreamComponents/FeatureHighlight/SponsoredContentHighlight"; import React from "react"; import { connect } from "react-redux"; @@ -48,7 +49,14 @@ export class _CollapsibleSection extends React.PureComponent { render() { const { isAnimating, maxHeight, menuButtonHover, showContextMenu } = this.state; - const { id, collapsed, learnMore, title, subTitle } = this.props; + const { + id, + collapsed, + learnMore, + title, + subTitle, + mayHaveSponsoredStories, + } = this.props; const active = menuButtonHover || showContextMenu; let bodyStyle; if (isAnimating && !collapsed) { @@ -91,6 +99,13 @@ export class _CollapsibleSection extends React.PureComponent { <FluentOrText message={subTitle} /> </span> )} + {mayHaveSponsoredStories && + this.props.spocMessageVariant === "variant-a" && ( + <SponsoredContentHighlight + position="inset-block-start inset-inline-start" + dispatch={this.props.dispatch} + /> + )} </h3> </div> <ErrorBoundary className="section-body-fallback"> diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx index 0364f5386a..58df7d012b 100644 --- a/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx @@ -16,7 +16,7 @@ export class ContextMenuButton extends React.PureComponent { this.onUpdate = this.onUpdate.bind(this); } - openContextMenu(isKeyBoard, event) { + openContextMenu(isKeyBoard) { if (this.props.onUpdate) { this.props.onUpdate(true); } diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx index 57ed935e93..298dedcee5 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -4,6 +4,7 @@ import React from "react"; import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { SafeAnchor } from "../../DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; export class ContentSection extends React.PureComponent { constructor(props) { @@ -96,6 +97,7 @@ export class ContentSection extends React.PureComponent { mayHaveSponsoredStories, mayHaveRecentSaves, openPreferences, + spocMessageVariant, } = this.props; const { topSitesEnabled, @@ -254,6 +256,23 @@ export class ContentSection extends React.PureComponent { </label> </div> + {pocketRegion && + mayHaveSponsoredStories && + spocMessageVariant === "variant-c" && ( + <div className="sponsored-content-info"> + <div className="icon icon-help"></div> + <div> + Sponsored content supports our mission to build a better web.{" "} + <SafeAnchor + dispatch={this.props.dispatch} + url="https://support.mozilla.org/kb/pocket-sponsored-stories-new-tabs" + > + Find out how + </SafeAnchor> + </div> + </div> + )} + <span className="divider" role="separator"></span> <div> diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx index 3d33f6fde7..54dcd550c4 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx @@ -71,6 +71,7 @@ export class _CustomizeMenu extends React.PureComponent { mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites} mayHaveSponsoredStories={this.props.mayHaveSponsoredStories} mayHaveRecentSaves={this.props.DiscoveryStream.recentSavesEnabled} + spocMessageVariant={this.props.spocMessageVariant} dispatch={this.props.dispatch} /> </div> diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss index f534b8701b..579e455a3f 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss +++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss @@ -143,6 +143,10 @@ border-radius: 4px; appearance: none; background-color: var(--newtab-element-secondary-color); + + &:hover { + background-color: var(--newtab-element-secondary-hover-color); + } } .sponsored-checkbox:checked { @@ -151,6 +155,14 @@ background: url('chrome://global/skin/icons/check.svg') center no-repeat; background-color: var(--newtab-primary-action-background); + &:hover { + background-color: var(--newtab-primary-element-hover-color); + } + + &:active { + background-color: var(--newtab-primary-element-active-color); + } + @media (forced-colors: active) { fill: $black; } @@ -178,6 +190,10 @@ background-origin: content-box; background-color: var(--newtab-background-color-secondary); + &:hover { + background-color: var(--newtab-element-secondary-hover-color); + } + &:dir(rtl) { background-position-x: left; } @@ -201,6 +217,25 @@ } } + .sponsored-content-info { + display: flex; + gap: var(--space-small); + font-size: var(--font-size-small); + border-radius: var(--border-radius-medium); + background-color: var(--newtab-element-secondary-color); + padding: var(--space-small) var(--space-large); + + .icon-help { + flex-shrink: 0; + color: var(--color-accent-primary); + height: 20px; + } + + a { + color: var(--newtab-primary-action-background); + } + } + .divider { border-top: 1px var(--newtab-border-color) solid; margin: 0 -16px; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx index 0112013391..3c31a5a29f 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx @@ -143,7 +143,7 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { ); } - restorePrefDefaults(event) { + restorePrefDefaults() { this.props.dispatch( ac.OnlyToMain({ type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS, diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx index dff122b366..0f0ee51ab9 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx @@ -110,7 +110,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { }); } - renderComponent(component, embedWidth) { + renderComponent(component) { switch (component.type) { case "Highlights": return <Highlights />; @@ -196,6 +196,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { onboardingExperience={component.properties.onboardingExperience} ctaButtonSponsors={component.properties.ctaButtonSponsors} ctaButtonVariant={component.properties.ctaButtonVariant} + spocMessageVariant={component.properties.spocMessageVariant} editorsPicksHeader={component.properties.editorsPicksHeader} recentSavesEnabled={this.props.DiscoveryStream.recentSavesEnabled} hideDescriptions={this.props.DiscoveryStream.hideDescriptions} @@ -218,7 +219,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { } render() { - const { locale } = this.props; + const { locale, mayHaveSponsoredStories } = this.props; // Select layout render data by adding spocs and position to recommendations const { layoutRender } = selectLayoutRender({ state: this.props.DiscoveryStream, @@ -322,6 +323,8 @@ export class _DiscoveryStreamBase extends React.PureComponent { showPrefName={topStories.pref.feed} title={sectionTitle} subTitle={subTitle} + mayHaveSponsoredStories={mayHaveSponsoredStories} + spocMessageVariant={message?.properties?.spocMessageVariant} eventSource="CARDGRID" > {this.renderLayout(layoutRender)} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx index f13e0eb7ed..cf00361df2 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx @@ -31,11 +31,7 @@ export function DSSubHeader({ children }) { ); } -export function OnboardingExperience({ - children, - dispatch, - windowObj = global, -}) { +export function OnboardingExperience({ dispatch, windowObj = global }) { const [dismissed, setDismissed] = useState(false); const [maxHeight, setMaxHeight] = useState(null); const heightElement = useRef(null); @@ -330,6 +326,7 @@ export class _CardGrid extends React.PureComponent { onboardingExperience, ctaButtonSponsors, ctaButtonVariant, + spocMessageVariant, widgets, recentSavesEnabled, hideDescriptions, @@ -378,6 +375,7 @@ export class _CardGrid extends React.PureComponent { saveToPocketCard={saveToPocketCard} ctaButtonSponsors={ctaButtonSponsors} ctaButtonVariant={ctaButtonVariant} + spocMessageVariant={spocMessageVariant} recommendation_id={rec.recommendation_id} /> ) diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx index 6aef56fb33..f3e1eab503 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -85,8 +85,9 @@ export const DefaultMeta = ({ sponsor, sponsored_by_override, saveToPocketCard, - isRecentSave, ctaButtonVariant, + dispatch, + spocMessageVariant, }) => ( <div className="meta"> <div className="info-wrap"> @@ -100,9 +101,7 @@ export const DefaultMeta = ({ sponsored_by_override={sponsored_by_override} /> )} - <header title={title} className="title clamp"> - {title} - </header> + <header className="title clamp">{title}</header> {excerpt && <p className="excerpt clamp">{excerpt}</p>} </div> {!newSponsoredLabel && ( @@ -113,6 +112,8 @@ export const DefaultMeta = ({ sponsored_by_override={sponsored_by_override} cta_button_variant={ctaButtonVariant} source={source} + dispatch={dispatch} + spocMessageVariant={spocMessageVariant} /> )} {/* Sponsored label is normally in the way of any message. @@ -183,7 +184,7 @@ export class _DSCard extends React.PureComponent { ]; } - onLinkClick(event) { + onLinkClick() { if (this.props.dispatch) { this.props.dispatch( ac.DiscoveryStreamUserEvent({ @@ -223,7 +224,7 @@ export class _DSCard extends React.PureComponent { } } - onSaveClick(event) { + onSaveClick() { if (this.props.dispatch) { this.props.dispatch( ac.AlsoToMain({ @@ -408,43 +409,28 @@ export class _DSCard extends React.PureComponent { }; return ( - <div + <article className={`ds-card ${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName}`} ref={this.setContextMenuButtonHostRef} > + <div className="img-wrapper"> + <DSImage + extraClassNames="img" + source={this.props.image_src} + rawSource={this.props.raw_image_src} + sizes={this.dsImageSizes} + url={this.props.url} + title={this.props.title} + isRecentSave={isRecentSave} + /> + </div> <SafeAnchor className="ds-card-link" dispatch={this.props.dispatch} onLinkClick={!this.props.placeholder ? this.onLinkClick : undefined} url={this.props.url} + title={this.props.title} > - <div className="img-wrapper"> - <DSImage - extraClassNames="img" - source={this.props.image_src} - rawSource={this.props.raw_image_src} - sizes={this.dsImageSizes} - url={this.props.url} - title={this.props.title} - isRecentSave={isRecentSave} - /> - </div> - {ctaButtonVariant === "variant-b" && ( - <div className="cta-header">Shop Now</div> - )} - <DefaultMeta - source={source} - title={this.props.title} - excerpt={excerpt} - newSponsoredLabel={newSponsoredLabel} - timeToRead={timeToRead} - context={this.props.context} - context_type={this.props.context_type} - sponsor={this.props.sponsor} - sponsored_by_override={this.props.sponsored_by_override} - saveToPocketCard={saveToPocketCard} - ctaButtonVariant={ctaButtonVariant} - /> <ImpressionStats flightId={this.props.flightId} rows={[ @@ -461,6 +447,24 @@ export class _DSCard extends React.PureComponent { source={this.props.type} /> </SafeAnchor> + {ctaButtonVariant === "variant-b" && ( + <div className="cta-header">Shop Now</div> + )} + <DefaultMeta + source={source} + title={this.props.title} + excerpt={excerpt} + newSponsoredLabel={newSponsoredLabel} + timeToRead={timeToRead} + context={this.props.context} + context_type={this.props.context_type} + sponsor={this.props.sponsor} + sponsored_by_override={this.props.sponsored_by_override} + saveToPocketCard={saveToPocketCard} + ctaButtonVariant={ctaButtonVariant} + dispatch={this.props.dispatch} + spocMessageVariant={this.props.spocMessageVariant} + /> {saveToPocketCard && ( <div className="card-stp-button-hover-background"> <div className="card-stp-button-position-wrapper"> @@ -512,7 +516,7 @@ export class _DSCard extends React.PureComponent { isRecentSave={isRecentSave} /> )} - </div> + </article> ); } } @@ -526,4 +530,4 @@ export const DSCard = connect(state => ({ DiscoveryStream: state.DiscoveryStream, }))(_DSCard); -export const PlaceholderDSCard = props => <DSCard placeholder={true} />; +export const PlaceholderDSCard = () => <DSCard placeholder={true} />; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss index 92afedff26..9004e609df 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss @@ -118,6 +118,16 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); transform: scale(1); } } + + header { + color: var(--newtab-primary-action-background); + } + } + + &:active { + header { + color: var(--newtab-primary-element-active-color); + } } .img { @@ -131,31 +141,15 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); } .ds-card-link { + position: absolute; height: 100%; - display: flex; - flex-direction: column; + width: 100%; text-decoration: none; - &:hover { - header { - color: var(--newtab-primary-action-background); - } - } - &:focus { @include ds-focus; transition: none; - - header { - color: var(--newtab-primary-action-background); - } - } - - &:active { - header { - color: var(--newtab-primary-element-active-color); - } } } @@ -255,7 +249,7 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); flex-wrap: wrap; justify-content: space-between; align-items: center; - column-gap: var(--space-small); + gap: 0 var(--space-small); margin-top: 0; } diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx index 5c7e79685e..6c0641cfc1 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx @@ -3,6 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { cardContextTypes } from "../../Card/types.js"; +import { SponsoredContentHighlight } from "../FeatureHighlight/SponsoredContentHighlight"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; import React from "react"; @@ -82,6 +83,8 @@ export class DSContextFooter extends React.PureComponent { sponsored_by_override, cta_button_variant, source, + spocMessageVariant, + dispatch, } = this.props; const sponsorLabel = SponsorLabel({ @@ -119,6 +122,12 @@ export class DSContextFooter extends React.PureComponent { return ( <div className="story-footer"> {sponsorLabel} + {sponsorLabel && spocMessageVariant === "variant-b" && ( + <SponsoredContentHighlight + dispatch={dispatch} + position="inset-block-end inset-inline-start" + /> + )} {dsMessageLabel} </div> ); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss index c23bb1c661..b28b498682 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss @@ -2,7 +2,10 @@ color: var(--newtab-text-secondary-color); inset-inline-start: 0; margin-top: 12px; + display: flex; + gap: var(--space-large); position: relative; + pointer-events: none; .story-sponsored-label span { display: inline-block; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx index f342c9829b..b251fb0401 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx @@ -17,7 +17,7 @@ export class DSPrivacyModal extends React.PureComponent { this.onManageLinkClick = this.onManageLinkClick.bind(this); } - onLearnLinkClick(event) { + onLearnLinkClick() { this.props.dispatch( ac.DiscoveryStreamUserEvent({ event: "CLICK_PRIVACY_INFO", @@ -26,7 +26,7 @@ export class DSPrivacyModal extends React.PureComponent { ); } - onManageLinkClick(event) { + onManageLinkClick() { this.props.dispatch(ac.OnlyToMain({ type: at.SETTINGS_OPEN })); } @@ -50,7 +50,7 @@ export class DSPrivacyModal extends React.PureComponent { className="modal-link modal-link-privacy" data-l10n-id="newtab-privacy-modal-link" onClick={this.onLearnLinkClick} - href="https://help.getpocket.com/article/1142-firefox-new-tab-recommendations-faq" + href="https://support.mozilla.org/kb/pocket-recommendations-firefox-new-tab" /> <button className="modal-link modal-link-manage" diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx new file mode 100644 index 0000000000..792be40ba3 --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx @@ -0,0 +1,74 @@ +/* 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, useRef, useEffect } from "react"; +import { actionCreators as ac } from "common/Actions.sys.mjs"; + +export function FeatureHighlight({ + message, + icon, + toggle, + position = "top-left", + title, + ariaLabel, + feature = "FEATURE_HIGHLIGHT_DEFAULT", + dispatch = () => {}, + windowObj = global, +}) { + const [opened, setOpened] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleOutsideClick = e => { + if (!ref?.current?.contains(e.target)) { + setOpened(false); + } + }; + + windowObj.document.addEventListener("click", handleOutsideClick); + return () => { + windowObj.document.removeEventListener("click", handleOutsideClick); + }; + }, [windowObj]); + + const onToggleClick = useCallback(() => { + if (!opened) { + dispatch( + ac.DiscoveryStreamUserEvent({ + event: "CLICK", + source: "FEATURE_HIGHLIGHT", + value: { + feature, + }, + }) + ); + } + setOpened(!opened); + }, [dispatch, feature, opened]); + + const openedClassname = opened ? `opened` : `closed`; + return ( + <div ref={ref} className="feature-highlight"> + <button + title={title} + aria-haspopup="true" + aria-label={ariaLabel} + className="toggle-button" + onClick={onToggleClick} + > + {toggle} + </button> + <div className={`feature-highlight-modal ${position} ${openedClassname}`}> + <div className="message-icon">{icon}</div> + <p>{message}</p> + <button + title="Dismiss" + aria-label="Close sponsored content more info popup" + className="icon icon-dismiss" + onClick={() => setOpened(false)} + ></button> + </div> + </div> + ); +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/SponsoredContentHighlight.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/SponsoredContentHighlight.jsx new file mode 100644 index 0000000000..8fdd03be6d --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/SponsoredContentHighlight.jsx @@ -0,0 +1,34 @@ +/* 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 { FeatureHighlight } from "./FeatureHighlight"; +import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; +import React from "react"; + +export function SponsoredContentHighlight({ position, dispatch }) { + return ( + <div className="sponsored-content-highlight"> + <FeatureHighlight + position={position} + ariaLabel="Sponsored content supports our mission to build a better web." + title="Sponsored content more info" + feature="SPONSORED_CONTENT_INFO" + dispatch={dispatch} + message={ + <span> + Sponsored content supports our mission to build a better web.{" "} + <SafeAnchor + dispatch={dispatch} + url="https://support.mozilla.org/kb/pocket-sponsored-stories-new-tabs" + > + Find out how + </SafeAnchor> + </span> + } + icon={<div className="sponsored-message-icon"></div>} + toggle={<div className="icon icon-help"></div>} + /> + </div> + ); +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss new file mode 100644 index 0000000000..c0fdd52f58 --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss @@ -0,0 +1,98 @@ +.feature-highlight { + position: relative; + // This is needed because in 1 case this is positioned under a link + // and in an element that's not clickable. + pointer-events: auto; + z-index: 1; + + .feature-highlight-modal { + position: absolute; + display: flex; + opacity: 0; + visibility: hidden; + cursor: default; + justify-content: space-between; + border-radius: var(--border-radius-small); + background: var(--newtab-background-color-secondary); + box-shadow: 0 2px 6px rgba(0, 0, 0, 15%); + width: 298px; + transition: opacity 0.3s, visibility 0.3s; + + .icon-dismiss { + flex-shrink: 0; + cursor: pointer; + background-size: $smaller-icon-size; + height: $smaller-icon-size; + width: $smaller-icon-size; + margin: var(--space-medium); + color: var(--icon-color); + border: none; + } + + .message-icon { + margin-block: var(--space-large); + margin-inline: var(--space-large) var(--space-medium); + } + + &.opened { + opacity: 1; + visibility: visible; + } + + &::after { + content: ''; + position: absolute; + height: 24px; + width: 24px; + background: var(--newtab-background-color-secondary); + box-shadow: 4px 4px 6px -2px rgba(0, 0, 0, 15%); + } + + &.inset-block-start { + inset-block-end: 100%; + margin-bottom: var(--space-xlarge); + + &::after { + inset-block-end: -12px; + transform: rotate(45deg); + } + } + + &.inset-block-end { + inset-block-start: 100%; + margin-top: var(--space-xlarge); + + &::after { + inset-block-start: -12px; + transform: rotate(225deg); + } + } + + &.inset-inline-start { + inset-inline-end: calc(var(--space-xxlarge) * -1); + + &::after { + inset-inline-end: calc(var(--space-xxlarge) - 12px); + } + } + + &.inset-inline-end { + inset-inline-start: calc(var(--space-xxlarge) * -1); + + &::after { + inset-inline-start: calc(var(--space-xxlarge) - 12px); + } + } + + p { + font-size: var(--font-size-small); + font-weight: normal; + margin: var(--space-large) 0; + } + } + + .toggle-button { + border: none; + padding: 0; + } +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_SponsoredContentHighlight.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_SponsoredContentHighlight.scss new file mode 100644 index 0000000000..1dc024b7c0 --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_SponsoredContentHighlight.scss @@ -0,0 +1,33 @@ +.sponsored-content-highlight { + float: inline-end; + + .sponsored-message-icon { + background-image: url('chrome://activity-stream/content/data/content/assets/sponsor-message-icon.svg'); + background-size: 18px; + height: 18px; + width: 18px; + } + + .icon-help { + cursor: pointer; + height: 20px; + width: 20px; + background-size: 20px; + color: var(--icon-color); + vertical-align: text-bottom; + } + + .feature-highlight-modal { + &.inset-inline-start { + &::after { + margin-inline-end: 11px; + } + } + + &.inset-inline-end { + &::after { + margin-inline-start: 9px; + } + } + } +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx index cfbc6fe6cb..72ec94e1fe 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx @@ -55,9 +55,14 @@ export class SafeAnchor extends React.PureComponent { } render() { - const { url, className } = this.props; + const { url, className, title } = this.props; return ( - <a href={this.safeURI(url)} className={className} onClick={this.onClick}> + <a + href={this.safeURI(url)} + title={title} + className={className} + onClick={this.onClick} + > {this.props.children} </a> ); diff --git a/browser/components/newtab/content-src/components/ErrorBoundary/ErrorBoundary.jsx b/browser/components/newtab/content-src/components/ErrorBoundary/ErrorBoundary.jsx index 1834a0a521..6bf1614ce7 100644 --- a/browser/components/newtab/content-src/components/ErrorBoundary/ErrorBoundary.jsx +++ b/browser/components/newtab/content-src/components/ErrorBoundary/ErrorBoundary.jsx @@ -52,7 +52,7 @@ export class ErrorBoundary extends React.PureComponent { this.state = { hasError: false }; } - componentDidCatch(error, info) { + componentDidCatch() { this.setState({ hasError: true }); } diff --git a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx index b7f0038558..c0932104af 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx @@ -159,7 +159,10 @@ export class TopSiteLink extends React.PureComponent { // If we have tabbed to a search shortcut top site, and we click 'enter', // we should execute the onClick function. This needs to be added because // search top sites are anchor tags without an href. See bug 1483135 - if (this.props.link.searchTopSite && event.key === "Enter") { + if ( + event.key === "Enter" && + (this.props.link.searchTopSite || this.props.isAddButton) + ) { this.props.onClick(event); } } @@ -250,8 +253,15 @@ export class TopSiteLink extends React.PureComponent { } render() { - const { children, className, isDraggable, link, onClick, title } = - this.props; + const { + children, + className, + isDraggable, + link, + onClick, + title, + isAddButton, + } = this.props; const topSiteOuterClassName = `top-site-outer${ className ? ` ${className}` : "" }${link.isDragged ? " dragged" : ""}${ @@ -266,6 +276,10 @@ export class TopSiteLink extends React.PureComponent { selectedColor, } = this.calculateStyle(); + let addButtonl10n = { + "data-l10n-id": "newtab-topsites-add-shortcut-label", + }; + let draggableProps = {}; if (isDraggable) { draggableProps = { @@ -380,14 +394,14 @@ export class TopSiteLink extends React.PureComponent { : "" }`} > - <span dir="auto"> + <span dir="auto" {...(isAddButton && { ...addButtonl10n })}> {link.isPinned && <div className="icon icon-pin-small" />} {title || <br />} - <span - className="sponsored-label" - data-l10n-id="newtab-topsite-sponsored" - /> </span> + <span + className="sponsored-label" + data-l10n-id="newtab-topsite-sponsored" + /> </div> </a> {children} @@ -635,19 +649,23 @@ export class TopSitePlaceholder extends React.PureComponent { } render() { + let addButtonProps = {}; + if (this.props.isAddButton) { + addButtonProps = { + title: "newtab-topsites-add-shortcut-label", + onClick: this.onEditButtonClick, + }; + } + return ( <TopSiteLink {...this.props} - className={`placeholder ${this.props.className || ""}`} + {...(this.props.isAddButton ? { ...addButtonProps } : {})} + className={`placeholder ${this.props.className || ""} ${ + this.props.isAddButton ? "add-button" : "" + }`} isDraggable={false} - > - <button - aria-haspopup="dialog" - className="context-menu-button edit-button icon" - data-l10n-id="newtab-menu-topsites-placeholder-tooltip" - onClick={this.onEditButtonClick} - /> - </TopSiteLink> + /> ); } } @@ -758,6 +776,17 @@ export class _TopSiteList extends React.PureComponent { // Make a copy of the sites to truncate or extend to desired length let topSites = this.props.TopSites.rows.slice(); topSites.length = this.props.TopSitesRows * TOP_SITES_MAX_SITES_PER_ROW; + // if topSites do not fill an entire row add 'Add shortcut' button to array of topSites + // (there should only be one of these) + let firstPlaceholder = topSites.findIndex(Object.is.bind(null, undefined)); + // make sure placeholder exists and there already isnt a add button + if (firstPlaceholder && !topSites.includes(site => site.isAddButton)) { + topSites[firstPlaceholder] = { isAddButton: true }; + } else if (topSites.includes(site => site.isAddButton)) { + topSites.push( + topSites.splice(topSites.indexOf({ isAddButton: true }), 1)[0] + ); + } return topSites; } @@ -855,8 +884,20 @@ export class _TopSiteList extends React.PureComponent { let topSiteLink; // Use a placeholder if the link is empty or it's rendering a sponsored // tile for the about:home startup cache. - if (!link || (props.App.isForStartupCache && isSponsored(link))) { - topSiteLink = <TopSitePlaceholder {...slotProps} {...commonProps} />; + if ( + !link || + (props.App.isForStartupCache && isSponsored(link)) || + topSites[i]?.isAddButton + ) { + if (link) { + topSiteLink = ( + <TopSitePlaceholder + {...slotProps} + {...commonProps} + isAddButton={topSites[i] && topSites[i].isAddButton} + /> + ); + } } else { topSiteLink = ( <TopSite diff --git a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx index c69156c514..ba7676fd10 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx @@ -206,7 +206,7 @@ export class _TopSites extends React.PureComponent { } } -export const TopSites = connect((state, props) => ({ +export const TopSites = connect(state => ({ TopSites: state.TopSites, Prefs: state.Prefs, TopSitesRows: state.Prefs.values.topSitesRows, diff --git a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss index ff2e2df826..4e4019513d 100644 --- a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss +++ b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss @@ -246,7 +246,24 @@ $letter-fallback-color: $white; &.placeholder { .tile { - box-shadow: $inner-box-shadow; + box-shadow: $shadow-card; + cursor: default; + } + + &.add-button { + .tile { + background-color: var(--button-background-color); + + .icon-wrapper { + background-image: url('chrome://global/skin/icons/plus-20.svg'); + background-size: cover; + background-repeat: no-repeat; + height: 20px; + width: 20px; + fill: var(--icon-color); + -moz-context-properties: fill; + } + } } } diff --git a/browser/components/newtab/content-src/lib/init-store.js b/browser/components/newtab/content-src/lib/init-store.js index 20fcedc6c0..f0ab2db86a 100644 --- a/browser/components/newtab/content-src/lib/init-store.js +++ b/browser/components/newtab/content-src/lib/init-store.js @@ -44,7 +44,7 @@ function mergeStateReducer(mainReducer) { /** * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary */ -const messageMiddleware = store => next => action => { +const messageMiddleware = () => next => action => { const skipLocal = action.meta && action.meta.skipLocal; if (au.isSendToMain(action)) { RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action); diff --git a/browser/components/newtab/content-src/lib/link-menu-options.js b/browser/components/newtab/content-src/lib/link-menu-options.js index caac738170..12e47259c1 100644 --- a/browser/components/newtab/content-src/lib/link-menu-options.js +++ b/browser/components/newtab/content-src/lib/link-menu-options.js @@ -25,7 +25,7 @@ const _OpenInPrivateWindow = site => ({ export const LinkMenuOptions = { Separator: () => ({ type: "separator" }), EmptyItem: () => ({ type: "empty" }), - ShowPrivacyInfo: site => ({ + ShowPrivacyInfo: () => ({ id: "newtab-menu-show-privacy-info", icon: "info", action: { diff --git a/browser/components/newtab/content-src/lib/selectLayoutRender.js b/browser/components/newtab/content-src/lib/selectLayoutRender.js index aa3eb927d2..8ef4dd428f 100644 --- a/browser/components/newtab/content-src/lib/selectLayoutRender.js +++ b/browser/components/newtab/content-src/lib/selectLayoutRender.js @@ -2,7 +2,7 @@ * 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/. */ -export const selectLayoutRender = ({ state = {}, prefs = {}, locale = "" }) => { +export const selectLayoutRender = ({ state = {}, prefs = {} }) => { const { layout, feeds, spocs } = state; let spocIndexPlacementMap = {}; diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index 8bd3a7a397..88ed530b6a 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -166,6 +166,8 @@ input { @import '../components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal'; @import '../components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink'; @import '../components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget'; +@import '../components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight'; +@import '../components/DiscoveryStreamComponents/FeatureHighlight/SponsoredContentHighlight'; // AS Router @import '../../../asrouter/content-src/components/Button/Button'; diff --git a/browser/components/newtab/content-src/styles/_icons.scss b/browser/components/newtab/content-src/styles/_icons.scss index 4074f0a6a6..8be97ad9ae 100644 --- a/browser/components/newtab/content-src/styles/_icons.scss +++ b/browser/components/newtab/content-src/styles/_icons.scss @@ -70,6 +70,10 @@ background-image: url('chrome://global/skin/icons/info.svg'); } + &.icon-help { + background-image: url('chrome://global/skin/icons/help.svg'); + } + &.icon-new-window { @include flip-icon; |