Bug 1574334 - Add lazy cards, story engagements and bug fixes to New Tab Page r=pdahiya,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D42229

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ed Lee 2019-08-16 05:54:16 +00:00
Родитель 3aec893f88
Коммит 5ff7943b7d
29 изменённых файлов: 483 добавлений и 125 удалений

Просмотреть файл

@ -38,6 +38,10 @@
&:active { &:active {
background-color: $grey-90-30; background-color: $grey-90-30;
} }
&:focus {
box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;
}
} }
} }

Просмотреть файл

@ -0,0 +1,141 @@
# User Actions
A subset of actions are available to messages via fields like `button_action` for snippets, or `primary_action` for CFRs.
## Usage
For snippets, you should add the action type in `button_action` and any additional parameters in `button_action_args. For example:
```json
{
"button_action": "OPEN_ABOUT_PAGE",
"button_action_args": "config"
}
```
## Available Actions
### `OPEN_APPLICATIONS_MENU`
* args: (none)
Opens the applications menu.
### `OPEN_PRIVATE_BROWSER_WINDOW`
* args: (none)
Opens a new private browsing window.
### `OPEN_URL`
* args: `string` (a url)
Opens a given url.
Example:
```json
{
"button_action": "OPEN_URL",
"button_action_args": "https://foo.com"
}
```
### `OPEN_ABOUT_PAGE`
* args: `string` (a valid about page without the `about:` prefix)
Opens a given about page
Example:
```json
{
"button_action": "OPEN_ABOUT_PAGE",
"button_action_args": "config"
}
```
### `OPEN_PREFERENCES_PAGE`
* args: `string` (a category accessible via a `#`)
Opens `about:preferences` with an optional category accessible via a `#` in the URL (e.g. `about:preferences#home`).
Example:
```json
{
"button_action": "OPEN_PREFERENCES_PAGE",
"button_action_args": "home"
}
```
### `SHOW_FIREFOX_ACCOUNTS`
* args: (none)
Opens Firefox accounts sign-up page. Encodes some information that the origin was from snippets by default.
### `PIN_CURRENT_TAB`
* args: (none)
Pins the currently focused tab.
### `ENABLE_FIREFOX_MONITOR`
* args:
```ts
{
url: string;
flowRequestParams: {
entrypoint: string;
utm_term: string;
form_type: string;
}
}
```
Opens an oauth flow to enable Firefox Monitor at a given `url` and adds Firefox metrics that user given a set of `flowRequestParams`.
### `url`
The URL should start with `https://monitor.firefox.com/oauth/init` and add various metrics tags as search params, including:
* `utm_source`
* `utm_campaign`
* `form_type`
* `entrypoint`
You should verify the values of these search params with whoever is doing the data analysis (e.g. Leif Oines).
### `flowRequestParams`
These params are used by Firefox to add information specific to that individual user to the final oauth URL. You should include:
* `entrypoint`
* `utm_term`
* `form_type`
The `entrypoint` and `form_type` values should match the encoded values in your `url`.
You should verify the values with whoever is doing the data analysis (e.g. Leif Oines).
### Example
```json
{
"button_action": "ENABLE_FIREFOX_MONITOR",
"button_action_args": {
"url": "https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab",
"flowRequestParams": {
"entrypoint": "snippets",
"utm_term": "monitor",
"form_type": "email"
}
}
}
```

Просмотреть файл

@ -29,6 +29,7 @@
.blockButton { .blockButton {
display: block; display: block;
opacity: 1;
// larger inset if discovery stream is enabled. // larger inset if discovery stream is enabled.
.ds-outer-wrapper-breakpoint-override & { .ds-outer-wrapper-breakpoint-override & {
@ -105,6 +106,11 @@
inset-inline-end: 20px; inset-inline-end: 20px;
opacity: 1; opacity: 1;
top: 50%; top: 50%;
&:focus {
box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;
border-radius: 2px;
}
} }
.title { .title {
@ -149,12 +155,17 @@
background-color: transparent; background-color: transparent;
.blockButton { .blockButton {
display: none; display: block;
inset-inline-end: -15%; inset-inline-end: -15%;
opacity: 1; opacity: 0;
margin: auto; margin: auto;
top: unset; top: unset;
&:focus {
opacity: 1;
box-shadow: none;
}
@media (max-width: 1120px) { @media (max-width: 1120px) {
inset-inline-end: 2%; inset-inline-end: 2%;
} }

Просмотреть файл

@ -36,6 +36,7 @@ export class CardGrid extends React.PureComponent {
pocket_id={rec.pocket_id} pocket_id={rec.pocket_id}
context_type={rec.context_type} context_type={rec.context_type}
bookmarkGuid={rec.bookmarkGuid} bookmarkGuid={rec.bookmarkGuid}
engagement={rec.engagement}
cta={rec.cta} cta={rec.cta}
cta_variant={this.props.cta_variant} cta_variant={this.props.cta_variant}
/> />

Просмотреть файл

@ -17,6 +17,7 @@ export const DefaultMeta = ({
context, context,
context_type, context_type,
cta, cta,
engagement,
}) => ( }) => (
<div className="meta"> <div className="meta">
<div className="info-wrap"> <div className="info-wrap">
@ -29,7 +30,11 @@ export const DefaultMeta = ({
</div> </div>
)} )}
</div> </div>
<DSContextFooter context_type={context_type} context={context} /> <DSContextFooter
context_type={context_type}
context={context}
engagement={engagement}
/>
</div> </div>
); );
@ -40,6 +45,7 @@ export const VariantMeta = ({
context, context,
context_type, context_type,
cta, cta,
engagement,
sponsor, sponsor,
}) => ( }) => (
<div className="meta"> <div className="meta">
@ -51,9 +57,13 @@ export const VariantMeta = ({
<header className="title clamp">{title}</header> <header className="title clamp">{title}</header>
{excerpt && <p className="excerpt clamp">{excerpt}</p>} {excerpt && <p className="excerpt clamp">{excerpt}</p>}
</div> </div>
{cta && <button className="button cta-button">{cta}</button>} {context && cta && <button className="button cta-button">{cta}</button>}
{!context && ( {!context && (
<DSContextFooter context_type={context_type} context={context} /> <DSContextFooter
context_type={context_type}
context={context}
engagement={engagement}
/>
)} )}
</div> </div>
); );
@ -63,6 +73,13 @@ export class DSCard extends React.PureComponent {
super(props); super(props);
this.onLinkClick = this.onLinkClick.bind(this); this.onLinkClick = this.onLinkClick.bind(this);
this.setPlaceholderRef = element => {
this.placholderElement = element;
};
this.state = {
isSeen: false,
};
} }
onLinkClick(event) { onLinkClick(event) {
@ -93,9 +110,45 @@ export class DSCard extends React.PureComponent {
} }
} }
onSeen(entries) {
if (this.state) {
const entry = entries.find(e => e.isIntersecting);
if (entry) {
if (this.placholderElement) {
this.observer.unobserve(this.placholderElement);
}
// Stop observing since element has been seen
this.setState({
isSeen: true,
});
}
}
}
componentDidMount() {
if (this.placholderElement) {
this.observer = new IntersectionObserver(this.onSeen.bind(this));
this.observer.observe(this.placholderElement);
}
}
componentWillUnmount() {
// Remove observer on unmount
if (this.observer && this.placholderElement) {
this.observer.unobserve(this.placholderElement);
}
}
render() { render() {
if (this.props.placeholder || !this.state.isSeen) {
return (
<div className="ds-card placeholder" ref={this.setPlaceholderRef} />
);
}
return ( return (
<div className={`ds-card${this.props.placeholder ? " placeholder" : ""}`}> <div className="ds-card">
<SafeAnchor <SafeAnchor
className="ds-card-link" className="ds-card-link"
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
@ -116,6 +169,7 @@ export class DSCard extends React.PureComponent {
excerpt={this.props.excerpt} excerpt={this.props.excerpt}
context={this.props.context} context={this.props.context}
context_type={this.props.context_type} context_type={this.props.context_type}
engagement={this.props.engagement}
cta={this.props.cta} cta={this.props.cta}
sponsor={this.props.sponsor} sponsor={this.props.sponsor}
/> />
@ -126,6 +180,7 @@ export class DSCard extends React.PureComponent {
title={this.props.title} title={this.props.title}
excerpt={this.props.excerpt} excerpt={this.props.excerpt}
context={this.props.context} context={this.props.context}
engagement={this.props.engagement}
context_type={this.props.context_type} context_type={this.props.context_type}
cta={this.props.cta} cta={this.props.cta}
/> />
@ -145,20 +200,18 @@ export class DSCard extends React.PureComponent {
source={this.props.type} source={this.props.type}
/> />
</SafeAnchor> </SafeAnchor>
{!this.props.placeholder && ( <DSLinkMenu
<DSLinkMenu id={this.props.id}
id={this.props.id} index={this.props.pos}
index={this.props.pos} dispatch={this.props.dispatch}
dispatch={this.props.dispatch} url={this.props.url}
url={this.props.url} title={this.props.title}
title={this.props.title} source={this.props.source}
source={this.props.source} type={this.props.type}
type={this.props.type} pocket_id={this.props.pocket_id}
pocket_id={this.props.pocket_id} shim={this.props.shim}
shim={this.props.shim} bookmarkGuid={this.props.bookmarkGuid}
bookmarkGuid={this.props.bookmarkGuid} />
/>
)}
</div> </div>
); );
} }

Просмотреть файл

@ -13,14 +13,7 @@ $excerpt-line-height: 20;
background: transparent; background: transparent;
box-shadow: inset $inner-box-shadow; box-shadow: inset $inner-box-shadow;
border-radius: 4px; border-radius: 4px;
min-height: 300px;
.ds-card-link {
cursor: default;
}
.img-wrapper {
opacity: 0;
}
} }
.img-wrapper { .img-wrapper {

Просмотреть файл

@ -21,20 +21,24 @@ export const StatusMessage = ({ icon, fluentID }) => (
export class DSContextFooter extends React.PureComponent { export class DSContextFooter extends React.PureComponent {
render() { render() {
const { context, context_type } = this.props; const { context, context_type, engagement } = this.props;
const { icon, fluentID } = cardContextTypes[context_type] || {}; const { icon, fluentID } = cardContextTypes[context_type] || {};
return ( return (
<div className="story-footer"> <div className="story-footer">
{context && <p className="story-sponsored-label clamp">{context}</p>} {context && <p className="story-sponsored-label clamp">{context}</p>}
<TransitionGroup component={null}> <TransitionGroup component={null}>
{!context && context_type && ( {!context && (context_type || engagement) && (
<CSSTransition <CSSTransition
key={fluentID} key={fluentID}
timeout={ANIMATION_DURATION} timeout={ANIMATION_DURATION}
classNames="story-animate" classNames="story-animate"
> >
<StatusMessage icon={icon} fluentID={fluentID} /> {engagement && !context_type ? (
<div className="story-view-count">{engagement}</div>
) : (
<StatusMessage icon={icon} fluentID={fluentID} />
)}
</CSSTransition> </CSSTransition>
)} )}
</TransitionGroup> </TransitionGroup>

Просмотреть файл

@ -8,6 +8,7 @@ $status-dark-green: #7C6;
position: relative; position: relative;
.story-sponsored-label, .story-sponsored-label,
.story-view-count,
.status-message { .status-message {
@include dark-theme-only { @include dark-theme-only {
color: $grey-40; color: $grey-40;

Просмотреть файл

@ -78,6 +78,7 @@ export class Hero extends React.PureComponent {
source={rec.domain} source={rec.domain}
pocket_id={rec.pocket_id} pocket_id={rec.pocket_id}
bookmarkGuid={rec.bookmarkGuid} bookmarkGuid={rec.bookmarkGuid}
engagement={rec.engagement}
/> />
) )
); );
@ -112,6 +113,7 @@ export class Hero extends React.PureComponent {
<DSContextFooter <DSContextFooter
context={heroRec.context} context={heroRec.context}
context_type={heroRec.context_type} context_type={heroRec.context_type}
engagement={heroRec.engagement}
/> />
</div> </div>
<ImpressionStats <ImpressionStats

Просмотреть файл

@ -45,6 +45,7 @@ $card-header-in-hero-line-height: 20;
.ds-card.placeholder { .ds-card.placeholder {
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 20px; padding-bottom: 20px;
min-height: 180px;
} }
.img-wrapper { .img-wrapper {

Просмотреть файл

@ -81,6 +81,7 @@ export class ListItem extends React.PureComponent {
<DSContextFooter <DSContextFooter
context={this.props.context} context={this.props.context}
context_type={this.props.context_type} context_type={this.props.context_type}
engagement={this.props.engagement}
/> />
</div> </div>
<DSImage <DSImage
@ -159,6 +160,7 @@ export function _List(props) {
url={rec.url} url={rec.url}
pocket_id={rec.pocket_id} pocket_id={rec.pocket_id}
bookmarkGuid={rec.bookmarkGuid} bookmarkGuid={rec.bookmarkGuid}
engagement={rec.engagement}
/> />
) )
); );

Просмотреть файл

@ -1991,7 +1991,8 @@ main {
margin: 0 0 12px; } margin: 0 0 12px; }
.ds-hero .ds-card.placeholder { .ds-hero .ds-card.placeholder {
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 20px; } padding-bottom: 20px;
min-height: 180px; }
.ds-hero .img-wrapper { .ds-hero .img-wrapper {
margin: 0 0 12px; } margin: 0 0 12px; }
.ds-hero .ds-hero-item { .ds-hero .ds-hero-item {
@ -2635,11 +2636,8 @@ main {
.ds-card.placeholder { .ds-card.placeholder {
background: transparent; background: transparent;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color); box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
border-radius: 4px; } border-radius: 4px;
.ds-card.placeholder .ds-card-link { min-height: 300px; }
cursor: default; }
.ds-card.placeholder .img-wrapper {
opacity: 0; }
.ds-card .img-wrapper { .ds-card .img-wrapper {
width: 100%; } width: 100%; }
.ds-card .img { .ds-card .img {
@ -2783,12 +2781,14 @@ main {
margin-top: 12px; margin-top: 12px;
position: relative; } position: relative; }
.story-footer .story-sponsored-label, .story-footer .story-sponsored-label,
.story-footer .story-view-count,
.story-footer .status-message { .story-footer .status-message {
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
font-size: 13px; font-size: 13px;
line-height: 24px; line-height: 24px;
color: #737373; } color: #737373; }
[lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext] [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
.story-footer .story-view-count, [lwt-newtab-brighttext]
.story-footer .status-message { .story-footer .status-message {
color: #B1B1B3; } color: #B1B1B3; }
.story-footer .status-message { .story-footer .status-message {
@ -3003,6 +3003,8 @@ main {
background-color: rgba(12, 12, 13, 0.2); } background-color: rgba(12, 12, 13, 0.2); }
.ASRouterButton.secondary:active { .ASRouterButton.secondary:active {
background-color: rgba(12, 12, 13, 0.3); } background-color: rgba(12, 12, 13, 0.3); }
.ASRouterButton.secondary:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
[lwt-newtab-brighttext] .secondary { [lwt-newtab-brighttext] .secondary {
background-color: rgba(249, 249, 250, 0.1); } background-color: rgba(249, 249, 250, 0.1); }
@ -3313,7 +3315,8 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.below-search-snippet.withButton .snippet-hover-wrapper:hover { .below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color); } background-color: var(--newtab-element-hover-color); }
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton { .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block; } display: block;
opacity: 1; }
.ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton { .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
inset-inline-end: -8%; } inset-inline-end: -8%; }
@media (max-width: 865px) { @media (max-width: 865px) {
@ -3371,6 +3374,9 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
inset-inline-end: 20px; inset-inline-end: 20px;
opacity: 1; opacity: 1;
top: 50%; } top: 50%; }
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
border-radius: 2px; }
.SimpleBelowSearchSnippet .title { .SimpleBelowSearchSnippet .title {
font-size: inherit; font-size: inherit;
margin: 0; } margin: 0; }
@ -3397,11 +3403,14 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-height: 60px; min-height: 60px;
background-color: transparent; } background-color: transparent; }
.SimpleBelowSearchSnippet.withButton .blockButton { .SimpleBelowSearchSnippet.withButton .blockButton {
display: none; display: block;
inset-inline-end: -15%; inset-inline-end: -15%;
opacity: 1; opacity: 0;
margin: auto; margin: auto;
top: unset; } top: unset; }
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none; }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.SimpleBelowSearchSnippet.withButton .blockButton { .SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%; } } inset-inline-end: 2%; } }

Просмотреть файл

@ -1994,7 +1994,8 @@ main {
margin: 0 0 12px; } margin: 0 0 12px; }
.ds-hero .ds-card.placeholder { .ds-hero .ds-card.placeholder {
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 20px; } padding-bottom: 20px;
min-height: 180px; }
.ds-hero .img-wrapper { .ds-hero .img-wrapper {
margin: 0 0 12px; } margin: 0 0 12px; }
.ds-hero .ds-hero-item { .ds-hero .ds-hero-item {
@ -2638,11 +2639,8 @@ main {
.ds-card.placeholder { .ds-card.placeholder {
background: transparent; background: transparent;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color); box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
border-radius: 4px; } border-radius: 4px;
.ds-card.placeholder .ds-card-link { min-height: 300px; }
cursor: default; }
.ds-card.placeholder .img-wrapper {
opacity: 0; }
.ds-card .img-wrapper { .ds-card .img-wrapper {
width: 100%; } width: 100%; }
.ds-card .img { .ds-card .img {
@ -2786,12 +2784,14 @@ main {
margin-top: 12px; margin-top: 12px;
position: relative; } position: relative; }
.story-footer .story-sponsored-label, .story-footer .story-sponsored-label,
.story-footer .story-view-count,
.story-footer .status-message { .story-footer .status-message {
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
font-size: 13px; font-size: 13px;
line-height: 24px; line-height: 24px;
color: #737373; } color: #737373; }
[lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext] [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
.story-footer .story-view-count, [lwt-newtab-brighttext]
.story-footer .status-message { .story-footer .status-message {
color: #B1B1B3; } color: #B1B1B3; }
.story-footer .status-message { .story-footer .status-message {
@ -3006,6 +3006,8 @@ main {
background-color: rgba(12, 12, 13, 0.2); } background-color: rgba(12, 12, 13, 0.2); }
.ASRouterButton.secondary:active { .ASRouterButton.secondary:active {
background-color: rgba(12, 12, 13, 0.3); } background-color: rgba(12, 12, 13, 0.3); }
.ASRouterButton.secondary:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
[lwt-newtab-brighttext] .secondary { [lwt-newtab-brighttext] .secondary {
background-color: rgba(249, 249, 250, 0.1); } background-color: rgba(249, 249, 250, 0.1); }
@ -3316,7 +3318,8 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.below-search-snippet.withButton .snippet-hover-wrapper:hover { .below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color); } background-color: var(--newtab-element-hover-color); }
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton { .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block; } display: block;
opacity: 1; }
.ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton { .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
inset-inline-end: -8%; } inset-inline-end: -8%; }
@media (max-width: 865px) { @media (max-width: 865px) {
@ -3374,6 +3377,9 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
inset-inline-end: 20px; inset-inline-end: 20px;
opacity: 1; opacity: 1;
top: 50%; } top: 50%; }
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
border-radius: 2px; }
.SimpleBelowSearchSnippet .title { .SimpleBelowSearchSnippet .title {
font-size: inherit; font-size: inherit;
margin: 0; } margin: 0; }
@ -3400,11 +3406,14 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-height: 60px; min-height: 60px;
background-color: transparent; } background-color: transparent; }
.SimpleBelowSearchSnippet.withButton .blockButton { .SimpleBelowSearchSnippet.withButton .blockButton {
display: none; display: block;
inset-inline-end: -15%; inset-inline-end: -15%;
opacity: 1; opacity: 0;
margin: auto; margin: auto;
top: unset; } top: unset; }
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none; }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.SimpleBelowSearchSnippet.withButton .blockButton { .SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%; } } inset-inline-end: 2%; } }

Просмотреть файл

@ -1991,7 +1991,8 @@ main {
margin: 0 0 12px; } margin: 0 0 12px; }
.ds-hero .ds-card.placeholder { .ds-hero .ds-card.placeholder {
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 20px; } padding-bottom: 20px;
min-height: 180px; }
.ds-hero .img-wrapper { .ds-hero .img-wrapper {
margin: 0 0 12px; } margin: 0 0 12px; }
.ds-hero .ds-hero-item { .ds-hero .ds-hero-item {
@ -2635,11 +2636,8 @@ main {
.ds-card.placeholder { .ds-card.placeholder {
background: transparent; background: transparent;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color); box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
border-radius: 4px; } border-radius: 4px;
.ds-card.placeholder .ds-card-link { min-height: 300px; }
cursor: default; }
.ds-card.placeholder .img-wrapper {
opacity: 0; }
.ds-card .img-wrapper { .ds-card .img-wrapper {
width: 100%; } width: 100%; }
.ds-card .img { .ds-card .img {
@ -2783,12 +2781,14 @@ main {
margin-top: 12px; margin-top: 12px;
position: relative; } position: relative; }
.story-footer .story-sponsored-label, .story-footer .story-sponsored-label,
.story-footer .story-view-count,
.story-footer .status-message { .story-footer .status-message {
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
font-size: 13px; font-size: 13px;
line-height: 24px; line-height: 24px;
color: #737373; } color: #737373; }
[lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext] [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
.story-footer .story-view-count, [lwt-newtab-brighttext]
.story-footer .status-message { .story-footer .status-message {
color: #B1B1B3; } color: #B1B1B3; }
.story-footer .status-message { .story-footer .status-message {
@ -3003,6 +3003,8 @@ main {
background-color: rgba(12, 12, 13, 0.2); } background-color: rgba(12, 12, 13, 0.2); }
.ASRouterButton.secondary:active { .ASRouterButton.secondary:active {
background-color: rgba(12, 12, 13, 0.3); } background-color: rgba(12, 12, 13, 0.3); }
.ASRouterButton.secondary:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
[lwt-newtab-brighttext] .secondary { [lwt-newtab-brighttext] .secondary {
background-color: rgba(249, 249, 250, 0.1); } background-color: rgba(249, 249, 250, 0.1); }
@ -3313,7 +3315,8 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.below-search-snippet.withButton .snippet-hover-wrapper:hover { .below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color); } background-color: var(--newtab-element-hover-color); }
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton { .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block; } display: block;
opacity: 1; }
.ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton { .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
inset-inline-end: -8%; } inset-inline-end: -8%; }
@media (max-width: 865px) { @media (max-width: 865px) {
@ -3371,6 +3374,9 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
inset-inline-end: 20px; inset-inline-end: 20px;
opacity: 1; opacity: 1;
top: 50%; } top: 50%; }
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
border-radius: 2px; }
.SimpleBelowSearchSnippet .title { .SimpleBelowSearchSnippet .title {
font-size: inherit; font-size: inherit;
margin: 0; } margin: 0; }
@ -3397,11 +3403,14 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-height: 60px; min-height: 60px;
background-color: transparent; } background-color: transparent; }
.SimpleBelowSearchSnippet.withButton .blockButton { .SimpleBelowSearchSnippet.withButton .blockButton {
display: none; display: block;
inset-inline-end: -15%; inset-inline-end: -15%;
opacity: 1; opacity: 0;
margin: auto; margin: auto;
top: unset; } top: unset; }
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none; }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.SimpleBelowSearchSnippet.withButton .blockButton { .SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%; } } inset-inline-end: 2%; } }

Просмотреть файл

@ -7895,7 +7895,8 @@ class DSContextFooter_DSContextFooter extends external_React_default.a.PureCompo
render() { render() {
const { const {
context, context,
context_type context_type,
engagement
} = this.props; } = this.props;
const { const {
icon, icon,
@ -7907,11 +7908,13 @@ class DSContextFooter_DSContextFooter extends external_React_default.a.PureCompo
className: "story-sponsored-label clamp" className: "story-sponsored-label clamp"
}, context), external_React_default.a.createElement(external_ReactTransitionGroup_["TransitionGroup"], { }, context), external_React_default.a.createElement(external_ReactTransitionGroup_["TransitionGroup"], {
component: null component: null
}, !context && context_type && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], { }, !context && (context_type || engagement) && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], {
key: fluentID, key: fluentID,
timeout: ANIMATION_DURATION, timeout: ANIMATION_DURATION,
classNames: "story-animate" classNames: "story-animate"
}, external_React_default.a.createElement(StatusMessage, { }, engagement && !context_type ? external_React_default.a.createElement("div", {
className: "story-view-count"
}, engagement) : external_React_default.a.createElement(StatusMessage, {
icon: icon, icon: icon,
fluentID: fluentID fluentID: fluentID
})))); }))));
@ -7935,7 +7938,8 @@ const DefaultMeta = ({
excerpt, excerpt,
context, context,
context_type, context_type,
cta cta,
engagement
}) => external_React_default.a.createElement("div", { }) => external_React_default.a.createElement("div", {
className: "meta" className: "meta"
}, external_React_default.a.createElement("div", { }, external_React_default.a.createElement("div", {
@ -7952,7 +7956,8 @@ const DefaultMeta = ({
tabIndex: "0" tabIndex: "0"
}, cta)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, { }, cta)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context_type: context_type, context_type: context_type,
context: context context: context,
engagement: engagement
})); }));
const VariantMeta = ({ const VariantMeta = ({
source, source,
@ -7961,6 +7966,7 @@ const VariantMeta = ({
context, context,
context_type, context_type,
cta, cta,
engagement,
sponsor sponsor
}) => external_React_default.a.createElement("div", { }) => external_React_default.a.createElement("div", {
className: "meta" className: "meta"
@ -7972,16 +7978,25 @@ const VariantMeta = ({
className: "title clamp" className: "title clamp"
}, title), excerpt && external_React_default.a.createElement("p", { }, title), excerpt && external_React_default.a.createElement("p", {
className: "excerpt clamp" className: "excerpt clamp"
}, excerpt)), cta && external_React_default.a.createElement("button", { }, excerpt)), context && cta && external_React_default.a.createElement("button", {
className: "button cta-button" className: "button cta-button"
}, cta), !context && external_React_default.a.createElement(DSContextFooter_DSContextFooter, { }, cta), !context && external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context_type: context_type, context_type: context_type,
context: context context: context,
engagement: engagement
})); }));
class DSCard_DSCard extends external_React_default.a.PureComponent { class DSCard_DSCard extends external_React_default.a.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.onLinkClick = this.onLinkClick.bind(this); this.onLinkClick = this.onLinkClick.bind(this);
this.setPlaceholderRef = element => {
this.placholderElement = element;
};
this.state = {
isSeen: false
};
} }
onLinkClick(event) { onLinkClick(event) {
@ -8005,9 +8020,47 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
} }
} }
onSeen(entries) {
if (this.state) {
const entry = entries.find(e => e.isIntersecting);
if (entry) {
if (this.placholderElement) {
this.observer.unobserve(this.placholderElement);
} // Stop observing since element has been seen
this.setState({
isSeen: true
});
}
}
}
componentDidMount() {
if (this.placholderElement) {
this.observer = new IntersectionObserver(this.onSeen.bind(this));
this.observer.observe(this.placholderElement);
}
}
componentWillUnmount() {
// Remove observer on unmount
if (this.observer && this.placholderElement) {
this.observer.unobserve(this.placholderElement);
}
}
render() { render() {
if (this.props.placeholder || !this.state.isSeen) {
return external_React_default.a.createElement("div", {
className: "ds-card placeholder",
ref: this.setPlaceholderRef
});
}
return external_React_default.a.createElement("div", { return external_React_default.a.createElement("div", {
className: `ds-card${this.props.placeholder ? " placeholder" : ""}` className: "ds-card"
}, external_React_default.a.createElement(SafeAnchor_SafeAnchor, { }, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
className: "ds-card-link", className: "ds-card-link",
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
@ -8025,6 +8078,7 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
excerpt: this.props.excerpt, excerpt: this.props.excerpt,
context: this.props.context, context: this.props.context,
context_type: this.props.context_type, context_type: this.props.context_type,
engagement: this.props.engagement,
cta: this.props.cta, cta: this.props.cta,
sponsor: this.props.sponsor sponsor: this.props.sponsor
}), !this.props.cta_variant && external_React_default.a.createElement(DefaultMeta, { }), !this.props.cta_variant && external_React_default.a.createElement(DefaultMeta, {
@ -8032,6 +8086,7 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
title: this.props.title, title: this.props.title,
excerpt: this.props.excerpt, excerpt: this.props.excerpt,
context: this.props.context, context: this.props.context,
engagement: this.props.engagement,
context_type: this.props.context_type, context_type: this.props.context_type,
cta: this.props.cta cta: this.props.cta
}), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], { }), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
@ -8045,7 +8100,7 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
}], }],
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
source: this.props.type source: this.props.type
})), !this.props.placeholder && external_React_default.a.createElement(DSLinkMenu_DSLinkMenu, { })), external_React_default.a.createElement(DSLinkMenu_DSLinkMenu, {
id: this.props.id, id: this.props.id,
index: this.props.pos, index: this.props.pos,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
@ -8192,6 +8247,7 @@ class CardGrid_CardGrid extends external_React_default.a.PureComponent {
pocket_id: rec.pocket_id, pocket_id: rec.pocket_id,
context_type: rec.context_type, context_type: rec.context_type,
bookmarkGuid: rec.bookmarkGuid, bookmarkGuid: rec.bookmarkGuid,
engagement: rec.engagement,
cta: rec.cta, cta: rec.cta,
cta_variant: this.props.cta_variant cta_variant: this.props.cta_variant
})); }));
@ -8336,7 +8392,8 @@ class List_ListItem extends external_React_default.a.PureComponent {
className: "ds-list-item-excerpt clamp" className: "ds-list-item-excerpt clamp"
}, this.props.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, { }, this.props.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context: this.props.context, context: this.props.context,
context_type: this.props.context_type context_type: this.props.context_type,
engagement: this.props.engagement
})), external_React_default.a.createElement(DSImage_DSImage, { })), external_React_default.a.createElement(DSImage_DSImage, {
extraClassNames: "ds-list-image", extraClassNames: "ds-list-image",
source: this.props.image_src, source: this.props.image_src,
@ -8400,7 +8457,8 @@ function _List(props) {
type: props.type, type: props.type,
url: rec.url, url: rec.url,
pocket_id: rec.pocket_id, pocket_id: rec.pocket_id,
bookmarkGuid: rec.bookmarkGuid bookmarkGuid: rec.bookmarkGuid,
engagement: rec.engagement
})); }));
} }
@ -8513,7 +8571,8 @@ class Hero_Hero extends external_React_default.a.PureComponent {
context_type: rec.context_type, context_type: rec.context_type,
source: rec.domain, source: rec.domain,
pocket_id: rec.pocket_id, pocket_id: rec.pocket_id,
bookmarkGuid: rec.bookmarkGuid bookmarkGuid: rec.bookmarkGuid,
engagement: rec.engagement
})); }));
} }
@ -8548,7 +8607,8 @@ class Hero_Hero extends external_React_default.a.PureComponent {
className: "excerpt clamp" className: "excerpt clamp"
}, heroRec.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, { }, heroRec.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context: heroRec.context, context: heroRec.context,
context_type: heroRec.context_type context_type: heroRec.context_type,
engagement: heroRec.engagement
})), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], { })), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
campaignId: heroRec.campaign_id, campaignId: heroRec.campaign_id,
rows: [{ rows: [{

Просмотреть файл

@ -399,10 +399,9 @@ const ONBOARDING_MESSAGES = () => [
id: "PROTECTIONS_PANEL_1", id: "PROTECTIONS_PANEL_1",
template: "protections_panel", template: "protections_panel",
content: { content: {
title: "Browse without being followed", title: { string_id: "cfr-protections-panel-header" },
body: body: { string_id: "cfr-protections-panel-body" },
"Keep your data to yourself. Firefox protects you from many of the most common trackers that follow what you do online.", link_text: { string_id: "cfr-protections-panel-link-text" },
link_text: "Learn more",
cta_url: `${Services.urlFormatter.formatURLPref( cta_url: `${Services.urlFormatter.formatURLPref(
"app.support.baseURL" "app.support.baseURL"
)}etp-promotions?as=u&utm_source=inproduct`, )}etp-promotions?as=u&utm_source=inproduct`,

Просмотреть файл

@ -48,6 +48,11 @@ ChromeUtils.defineModuleGetter(
"clearInterval", "clearInterval",
"resource://gre/modules/Timer.jsm" "resource://gre/modules/Timer.jsm"
); );
ChromeUtils.defineModuleGetter(
this,
"requestIdleCallback",
"resource://gre/modules/Timer.jsm"
);
// Frequency at which to check for new messages // Frequency at which to check for new messages
const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000; const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
@ -257,17 +262,17 @@ class _ToolbarBadgeHub {
} }
registerBadgeToAllWindows(message) { registerBadgeToAllWindows(message) {
// Impression should be added when the badge becomes visible
this._addImpression(message);
// Send a telemetry ping when adding the notification badge
this.sendUserEventTelemetry("IMPRESSION", message);
if (message.template === "update_action") { if (message.template === "update_action") {
this.executeAction({ ...message.content.action, message_id: message.id }); this.executeAction({ ...message.content.action, message_id: message.id });
// No badge to set only an action to execute // No badge to set only an action to execute
return; return;
} }
// Impression should be added when the badge becomes visible
this._addImpression(message);
// Send a telemetry ping when adding the notification badge
this.sendUserEventTelemetry("IMPRESSION", message);
EveryWindow.registerCallback( EveryWindow.registerCallback(
this.id, this.id,
win => { win => {
@ -297,7 +302,7 @@ class _ToolbarBadgeHub {
if (message.content.delay) { if (message.content.delay) {
this.state.showBadgeTimeoutId = setTimeout(() => { this.state.showBadgeTimeoutId = setTimeout(() => {
this.registerBadgeToAllWindows(message); requestIdleCallback(() => this.registerBadgeToAllWindows(message));
}, message.content.delay); }, message.content.delay);
} else { } else {
this.registerBadgeToAllWindows(message); this.registerBadgeToAllWindows(message);

Просмотреть файл

@ -378,6 +378,8 @@ class _ToolbarPanelHub {
*/ */
async insertProtectionPanelMessage(event) { async insertProtectionPanelMessage(event) {
const win = event.target.ownerGlobal; const win = event.target.ownerGlobal;
this.maybeInsertFTL(win);
const doc = event.target.ownerDocument; const doc = event.target.ownerDocument;
const container = doc.getElementById("messaging-system-message-container"); const container = doc.getElementById("messaging-system-message-container");
const infoButton = doc.getElementById("protections-popup-info-button"); const infoButton = doc.getElementById("protections-popup-info-button");

Просмотреть файл

@ -78,6 +78,12 @@ cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
.aria-label = Close button .aria-label = Close button
.title = Close .title = Close
## Protections panel
cfr-protections-panel-header = Browse without being followed
cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel ## What's New toolbar button and panel
cfr-whatsnew-button = cfr-whatsnew-button =

Просмотреть файл

@ -11,7 +11,6 @@
## avoid breaking quoted text). ## avoid breaking quoted text).
onboarding-button-label-learn-more = Learn More onboarding-button-label-learn-more = Learn More
onboarding-button-label-try-now = Try It Now
onboarding-button-label-get-started = Get Started onboarding-button-label-get-started = Get Started
## Welcome modal dialog strings ## Welcome modal dialog strings
@ -76,21 +75,6 @@ onboarding-benefit-privacy-text = Everything we do honors our Personal Data Prom
## Each message has a title and a description of what the browser feature is. ## Each message has a title and a description of what the browser feature is.
## Each message also has an associated button for the user to try the feature. ## Each message also has an associated button for the user to try the feature.
## The string for the button is found above, in the UI strings section ## The string for the button is found above, in the UI strings section
onboarding-private-browsing-title = Private Browsing
onboarding-private-browsing-text = Browse by yourself. Private Browsing with Content Blocking blocks online trackers that follow you around the web.
onboarding-screenshots-title = Screenshots
onboarding-screenshots-text = Take, save and share screenshots - without leaving { -brand-short-name }. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.
onboarding-addons-title = Add-ons
onboarding-addons-text = Add even more features that make { -brand-short-name } work harder for you. Compare prices, check the weather or express your personality with a custom theme.
onboarding-ghostery-title = Ghostery
onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
# Note: "Sync" in this case is a generic verb, as in "to synchronize"
onboarding-fxa-title = Sync
onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
onboarding-tracking-protection-title2 = Protection From Tracking onboarding-tracking-protection-title2 = Protection From Tracking
onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web. onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web.

Просмотреть файл

@ -16,7 +16,7 @@ const FAKE_TRIPLETS = [
text: { string_id: "onboarding-private-browsing-text" }, text: { string_id: "onboarding-private-browsing-text" },
icon: "icon", icon: "icon",
primary_button: { primary_button: {
label: { string_id: "onboarding-button-label-try-now" }, label: { string_id: "onboarding-button-label-get-started" },
action: { action: {
type: "OPEN_URL", type: "OPEN_URL",
data: { args: "https://example.com/" }, data: { args: "https://example.com/" },

Просмотреть файл

@ -22,7 +22,7 @@ const L10N_CONTENT = {
text: { string_id: "onboarding-private-browsing-text" }, text: { string_id: "onboarding-private-browsing-text" },
icon: "icon", icon: "icon",
primary_button: { primary_button: {
label: { string_id: "onboarding-button-label-try-now" }, label: { string_id: "onboarding-button-label-get-started" },
action: { type: "SOME_TYPE" }, action: { type: "SOME_TYPE" },
}, },
}; };

Просмотреть файл

@ -11,7 +11,7 @@ export const CARDS = [
text: { string_id: "onboarding-private-browsing-text" }, text: { string_id: "onboarding-private-browsing-text" },
icon: "icon", icon: "icon",
primary_button: { primary_button: {
label: { string_id: "onboarding-button-label-try-now" }, label: { string_id: "onboarding-button-label-get-started" },
action: { action: {
type: "OPEN_URL", type: "OPEN_URL",
data: { args: "https://example.com/" }, data: { args: "https://example.com/" },

Просмотреть файл

@ -11,7 +11,7 @@ const CARDS = [
text: { string_id: "onboarding-private-browsing-text" }, text: { string_id: "onboarding-private-browsing-text" },
icon: "icon", icon: "icon",
primary_button: { primary_button: {
label: { string_id: "onboarding-button-label-try-now" }, label: { string_id: "onboarding-button-label-get-started" },
action: { action: {
type: "OPEN_URL", type: "OPEN_URL",
data: { args: "https://example.com/" }, data: { args: "https://example.com/" },

Просмотреть файл

@ -20,6 +20,7 @@ describe("<DSCard>", () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallow(<DSCard />); wrapper = shallow(<DSCard />);
wrapper.setState({ isSeen: true });
sandbox = sinon.createSandbox(); sandbox = sinon.createSandbox();
}); });
@ -79,6 +80,7 @@ describe("<DSCard>", () => {
it("should render badges for pocket, bookmark when not a spoc element ", () => { it("should render badges for pocket, bookmark when not a spoc element ", () => {
wrapper = mount(<DSCard context_type="bookmark" />); wrapper = mount(<DSCard context_type="bookmark" />);
wrapper.setState({ isSeen: true });
const contextFooter = wrapper.find(DSContextFooter); const contextFooter = wrapper.find(DSContextFooter);
assert.lengthOf(contextFooter.find(StatusMessage), 1); assert.lengthOf(contextFooter.find(StatusMessage), 1);
@ -87,6 +89,7 @@ describe("<DSCard>", () => {
it("should render Sponsored Context for a spoc element", () => { it("should render Sponsored Context for a spoc element", () => {
const context = "Sponsored by Foo"; const context = "Sponsored by Foo";
wrapper = mount(<DSCard context_type="bookmark" context={context} />); wrapper = mount(<DSCard context_type="bookmark" context={context} />);
wrapper.setState({ isSeen: true });
const contextFooter = wrapper.find(DSContextFooter); const contextFooter = wrapper.find(DSContextFooter);
assert.lengthOf(contextFooter.find(StatusMessage), 0); assert.lengthOf(contextFooter.find(StatusMessage), 0);
@ -99,6 +102,7 @@ describe("<DSCard>", () => {
beforeEach(() => { beforeEach(() => {
dispatch = sandbox.stub(); dispatch = sandbox.stub();
wrapper = shallow(<DSCard dispatch={dispatch} />); wrapper = shallow(<DSCard dispatch={dispatch} />);
wrapper.setState({ isSeen: true });
}); });
it("should call dispatch with the correct events", () => { it("should call dispatch with the correct events", () => {
@ -160,6 +164,7 @@ describe("<DSCard>", () => {
describe("DSCard with CTA", () => { describe("DSCard with CTA", () => {
beforeEach(() => { beforeEach(() => {
wrapper = mount(<DSCard />); wrapper = mount(<DSCard />);
wrapper.setState({ isSeen: true });
}); });
it("should render Default Meta", () => { it("should render Default Meta", () => {
@ -178,9 +183,19 @@ describe("<DSCard>", () => {
assert.equal(meta.find(".cta-link").text(), "test"); assert.equal(meta.find(".cta-link").text(), "test");
}); });
it("should render cta-button when item has cta and cta button variant is true", () => { it("should not render cta-button for non spoc content", () => {
wrapper.setProps({ cta: "test", cta_variant: true }); wrapper.setProps({ cta: "test", cta_variant: true });
const meta = wrapper.find(VariantMeta); const meta = wrapper.find(VariantMeta);
assert.lengthOf(meta.find(".cta-button"), 0);
});
it("should render cta-button when item has cta and cta button variant is true and is spoc", () => {
wrapper.setProps({
cta: "test",
cta_variant: true,
context: "Sponsored by Foo",
});
const meta = wrapper.find(VariantMeta);
assert.equal(meta.find(".cta-button").text(), "test"); assert.equal(meta.find(".cta-button").text(), "test");
}); });
@ -207,6 +222,44 @@ describe("<DSCard>", () => {
assert.equal(meta.find(".source").text(), "Test · Sponsored"); assert.equal(meta.find(".source").text(), "Test · Sponsored");
}); });
}); });
describe("DSCard with Intersection Observer", () => {
beforeEach(() => {
wrapper = shallow(<DSCard />);
});
it("should render card when seen", () => {
let card = wrapper.find("div.ds-card.placeholder");
assert.lengthOf(card, 1);
wrapper.instance().observer = {
unobserve: sandbox.stub(),
};
wrapper.instance().placholderElement = "element";
wrapper.instance().onSeen([
{
isIntersecting: true,
},
]);
assert.isTrue(wrapper.instance().state.isSeen);
card = wrapper.find("div.ds-card.placeholder");
assert.lengthOf(card, 0);
assert.lengthOf(wrapper.find(SafeAnchor), 1);
assert.calledOnce(wrapper.instance().observer.unobserve);
assert.calledWith(wrapper.instance().observer.unobserve, "element");
});
it("should setup proper placholder ref for isSeen", () => {
wrapper.instance().setPlaceholderRef("element");
assert.equal(wrapper.instance().placholderElement, "element");
});
it("should setup observer on componentDidMount", () => {
wrapper = mount(<DSCard />);
assert.isTrue(!!wrapper.instance().observer);
});
});
}); });
describe("<PlaceholderDSCard> component", () => { describe("<PlaceholderDSCard> component", () => {
@ -221,21 +274,21 @@ describe("<PlaceholderDSCard> component", () => {
it("should contain placeholder div", () => { it("should contain placeholder div", () => {
const wrapper = shallow(<DSCard placeholder={true} />); const wrapper = shallow(<DSCard placeholder={true} />);
wrapper.setState({ isSeen: true });
const card = wrapper.find("div.ds-card.placeholder"); const card = wrapper.find("div.ds-card.placeholder");
assert.lengthOf(card, 1); assert.lengthOf(card, 1);
}); });
it("should not be clickable", () => { it("should not be clickable", () => {
const wrapper = shallow(<DSCard placeholder={true} />); const wrapper = shallow(<DSCard placeholder={true} />);
wrapper.setState({ isSeen: true });
const anchor = wrapper.find("SafeAnchor.ds-card-link"); const anchor = wrapper.find("SafeAnchor.ds-card-link");
assert.lengthOf(anchor, 1); assert.lengthOf(anchor, 0);
const linkClick = anchor.prop("onLinkClick");
assert.isUndefined(linkClick);
}); });
it("should not have context menu", () => { it("should not have context menu", () => {
const wrapper = shallow(<DSCard placeholder={true} />); const wrapper = shallow(<DSCard placeholder={true} />);
wrapper.setState({ isSeen: true });
const linkMenu = wrapper.find(DSLinkMenu); const linkMenu = wrapper.find(DSLinkMenu);
assert.lengthOf(linkMenu, 0); assert.lengthOf(linkMenu, 0);
}); });

Просмотреть файл

@ -12,6 +12,7 @@ describe("<DSContextFooter>", () => {
const bookmarkBadge = "bookmark"; const bookmarkBadge = "bookmark";
const removeBookmarkBadge = "removedBookmark"; const removeBookmarkBadge = "removedBookmark";
const context = "Sponsored by Babel"; const context = "Sponsored by Babel";
const engagement = "Popular";
beforeEach(() => { beforeEach(() => {
wrapper = mount(<DSContextFooter />); wrapper = mount(<DSContextFooter />);
@ -26,19 +27,32 @@ describe("<DSContextFooter>", () => {
assert.isTrue(wrapper.exists()); assert.isTrue(wrapper.exists());
assert.isOk(wrapper.find(".story-footer")); assert.isOk(wrapper.find(".story-footer"));
}); });
it("should render an engagement status if no badge and spoc passed", () => {
wrapper = mount(<DSContextFooter engagement={engagement} />);
const engagementLabel = wrapper.find(".story-view-count");
assert.equal(engagementLabel.text(), engagement);
});
it("should render a badge if a proper badge prop is passed", () => { it("should render a badge if a proper badge prop is passed", () => {
wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />); wrapper = mount(
<DSContextFooter context_type={bookmarkBadge} engagement={engagement} />
);
const { fluentID } = cardContextTypes[bookmarkBadge]; const { fluentID } = cardContextTypes[bookmarkBadge];
assert.lengthOf(wrapper.find(".story-view-count"), 0);
const statusLabel = wrapper.find(".story-context-label"); const statusLabel = wrapper.find(".story-context-label");
assert.isOk(statusLabel);
assert.equal(statusLabel.prop("data-l10n-id"), fluentID); assert.equal(statusLabel.prop("data-l10n-id"), fluentID);
}); });
it("should only render a sponsored context if pass a sponsored context", async () => { it("should only render a sponsored context if pass a sponsored context", async () => {
wrapper = mount( wrapper = mount(
<DSContextFooter context_type={bookmarkBadge} context={context} /> <DSContextFooter
context_type={bookmarkBadge}
context={context}
engagement={engagement}
/>
); );
assert.lengthOf(wrapper.find(".story-view-count"), 0);
assert.lengthOf(wrapper.find(StatusMessage), 0); assert.lengthOf(wrapper.find(StatusMessage), 0);
assert.equal(wrapper.find(".story-sponsored-label").text(), context); assert.equal(wrapper.find(".story-sponsored-label").text(), context);
}); });

Просмотреть файл

@ -23,6 +23,7 @@ describe("ToolbarBadgeHub", () => {
let getStringPrefStub; let getStringPrefStub;
let clearUserPrefStub; let clearUserPrefStub;
let setStringPrefStub; let setStringPrefStub;
let requestIdleCallbackStub;
beforeEach(async () => { beforeEach(async () => {
globals = new GlobalOverrider(); globals = new GlobalOverrider();
sandbox = sinon.createSandbox(); sandbox = sinon.createSandbox();
@ -67,7 +68,9 @@ describe("ToolbarBadgeHub", () => {
getStringPrefStub = sandbox.stub(); getStringPrefStub = sandbox.stub();
clearUserPrefStub = sandbox.stub(); clearUserPrefStub = sandbox.stub();
setStringPrefStub = sandbox.stub(); setStringPrefStub = sandbox.stub();
requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
globals.set({ globals.set({
requestIdleCallback: requestIdleCallbackStub,
EveryWindow: everyWindowStub, EveryWindow: everyWindowStub,
PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub }, PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
setTimeout: setTimeoutStub, setTimeout: setTimeoutStub,
@ -562,6 +565,8 @@ describe("ToolbarBadgeHub", () => {
instance.registerBadgeToAllWindows, instance.registerBadgeToAllWindows,
msg_with_delay msg_with_delay
); );
// Delayed actions should be executed inside requestIdleCallback
assert.calledOnce(requestIdleCallbackStub);
}); });
}); });
describe("#sendUserEventTelemetry", () => { describe("#sendUserEventTelemetry", () => {

Просмотреть файл

@ -78,6 +78,12 @@ cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
.aria-label = Close button .aria-label = Close button
.title = Close .title = Close
## Protections panel
cfr-protections-panel-header = Browse without being followed
cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel ## What's New toolbar button and panel
cfr-whatsnew-button = cfr-whatsnew-button =

Просмотреть файл

@ -11,7 +11,6 @@
## avoid breaking quoted text). ## avoid breaking quoted text).
onboarding-button-label-learn-more = Learn More onboarding-button-label-learn-more = Learn More
onboarding-button-label-try-now = Try It Now
onboarding-button-label-get-started = Get Started onboarding-button-label-get-started = Get Started
## Welcome modal dialog strings ## Welcome modal dialog strings
@ -76,21 +75,6 @@ onboarding-benefit-privacy-text = Everything we do honors our Personal Data Prom
## Each message has a title and a description of what the browser feature is. ## Each message has a title and a description of what the browser feature is.
## Each message also has an associated button for the user to try the feature. ## Each message also has an associated button for the user to try the feature.
## The string for the button is found above, in the UI strings section ## The string for the button is found above, in the UI strings section
onboarding-private-browsing-title = Private Browsing
onboarding-private-browsing-text = Browse by yourself. Private Browsing with Content Blocking blocks online trackers that follow you around the web.
onboarding-screenshots-title = Screenshots
onboarding-screenshots-text = Take, save and share screenshots - without leaving { -brand-short-name }. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.
onboarding-addons-title = Add-ons
onboarding-addons-text = Add even more features that make { -brand-short-name } work harder for you. Compare prices, check the weather or express your personality with a custom theme.
onboarding-ghostery-title = Ghostery
onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
# Note: "Sync" in this case is a generic verb, as in "to synchronize"
onboarding-fxa-title = Sync
onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
onboarding-tracking-protection-title2 = Protection From Tracking onboarding-tracking-protection-title2 = Protection From Tracking
onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web. onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web.