зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1516272 - Add search hand-off, discovery stream and bug fixes to Activity Stream r=k88hudson
Differential Revision: https://phabricator.services.mozilla.com/D15307 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
cb1e8ac83e
Коммит
3d659c0a32
|
@ -6,6 +6,8 @@ support-files =
|
|||
searchSuggestionEngine.xml
|
||||
POSTSearchEngine.xml
|
||||
dummy_page.html
|
||||
prefs =
|
||||
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
|
||||
|
||||
[browser_aboutCertError.js]
|
||||
[browser_aboutCertError_telemetry.js]
|
||||
|
|
|
@ -33,15 +33,20 @@ for (const type of [
|
|||
"AS_ROUTER_TELEMETRY_USER_EVENT",
|
||||
"BLOCK_URL",
|
||||
"BOOKMARK_URL",
|
||||
"CONTENT_LAYOUT",
|
||||
"COPY_DOWNLOAD_LINK",
|
||||
"DELETE_BOOKMARK_BY_ID",
|
||||
"DELETE_FROM_POCKET",
|
||||
"DELETE_HISTORY_URL",
|
||||
"DIALOG_CANCEL",
|
||||
"DIALOG_OPEN",
|
||||
"DISCOVERY_STREAM_CONFIG_CHANGE",
|
||||
"DISCOVERY_STREAM_CONFIG_SETUP",
|
||||
"DISCOVERY_STREAM_LAYOUT_UPDATE",
|
||||
"DOWNLOAD_CHANGED",
|
||||
"FILL_SEARCH_TERM",
|
||||
"FOCUS_SEARCH",
|
||||
"HANDOFF_SEARCH_TO_AWESOMEBAR",
|
||||
"HIDE_SEARCH",
|
||||
"INIT",
|
||||
"MIGRATION_CANCEL",
|
||||
"MIGRATION_COMPLETED",
|
||||
|
@ -91,6 +96,7 @@ for (const type of [
|
|||
"SET_PREF",
|
||||
"SHOW_DOWNLOAD_FILE",
|
||||
"SHOW_FIREFOX_ACCOUNTS",
|
||||
"SHOW_SEARCH",
|
||||
"SKIPPED_SIGNIN",
|
||||
"SNIPPETS_BLOCKLIST_CLEARED",
|
||||
"SNIPPETS_BLOCKLIST_UPDATED",
|
||||
|
|
|
@ -47,7 +47,18 @@ const INITIAL_STATE = {
|
|||
pocketCta: {},
|
||||
waitingForSpoc: true,
|
||||
},
|
||||
Layout: [],
|
||||
// This is the new pocket configurable layout state.
|
||||
DiscoveryStream: {
|
||||
// This is a JSON-parsed copy of the discoverystream.config pref value.
|
||||
config: {enabled: false, layout_endpoint: ""},
|
||||
layout: [],
|
||||
},
|
||||
Search: {
|
||||
// Pretend the search box is focused after handing off to AwesomeBar.
|
||||
focus: false,
|
||||
// Hide the search box after handing off to AwesomeBar and user starts typing.
|
||||
hide: false,
|
||||
},
|
||||
};
|
||||
|
||||
function App(prevState = INITIAL_STATE.App, action) {
|
||||
|
@ -430,10 +441,27 @@ function Pocket(prevState = INITIAL_STATE.Pocket, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function Layout(prevState = INITIAL_STATE.Layout, action) {
|
||||
function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
|
||||
switch (action.type) {
|
||||
case at.CONTENT_LAYOUT:
|
||||
return action.data;
|
||||
case at.DISCOVERY_STREAM_CONFIG_CHANGE:
|
||||
// The reason this is a separate action is so it doesn't trigger a listener update on init
|
||||
case at.DISCOVERY_STREAM_CONFIG_SETUP:
|
||||
return {...prevState, config: action.data || {}};
|
||||
case at.DISCOVERY_STREAM_LAYOUT_UPDATE:
|
||||
return {...prevState, layout: action.data || []};
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
function Search(prevState = INITIAL_STATE.Search, action) {
|
||||
switch (action.type) {
|
||||
case at.HIDE_SEARCH:
|
||||
return Object.assign({...prevState, hide: true});
|
||||
case at.FOCUS_SEARCH:
|
||||
return Object.assign({...prevState, focus: true});
|
||||
case at.SHOW_SEARCH:
|
||||
return Object.assign({...prevState, hide: false, focus: false});
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
|
@ -443,6 +471,23 @@ this.INITIAL_STATE = INITIAL_STATE;
|
|||
this.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;
|
||||
this.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;
|
||||
|
||||
this.reducers = {TopSites, App, ASRouter, Snippets, Prefs, Dialog, Sections, Pocket, Layout};
|
||||
this.reducers = {
|
||||
TopSites,
|
||||
App,
|
||||
ASRouter,
|
||||
Snippets,
|
||||
Prefs,
|
||||
Dialog,
|
||||
Sections,
|
||||
Pocket,
|
||||
DiscoveryStream,
|
||||
Search,
|
||||
};
|
||||
|
||||
const EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];
|
||||
const EXPORTED_SYMBOLS = [
|
||||
"reducers",
|
||||
"INITIAL_STATE",
|
||||
"insertPinned",
|
||||
"TOP_SITES_DEFAULT_ROWS",
|
||||
"TOP_SITES_MAX_SITES_PER_ROW",
|
||||
];
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {ASRouterUtils} from "../../asrouter/asrouter-content";
|
||||
import {connect} from "react-redux";
|
||||
import {ModalOverlay} from "../../asrouter/components/ModalOverlay/ModalOverlay";
|
||||
import React from "react";
|
||||
import {SimpleHashRouter} from "./SimpleHashRouter";
|
||||
|
||||
export class ASRouterAdmin extends React.PureComponent {
|
||||
export class ASRouterAdminInner extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
|
@ -374,6 +376,23 @@ export class ASRouterAdmin extends React.PureComponent {
|
|||
ASRouterUtils.sendMessage({type: "FORCE_ATTRIBUTION", data: this.state.attributionParameters});
|
||||
}
|
||||
|
||||
renderPocketStory(story) {
|
||||
return (<tr className="message-item" key={story.guid}>
|
||||
<td className="message-id"><span>{story.guid} <br /></span></td>
|
||||
<td className="message-summary">
|
||||
<pre>{JSON.stringify(story, null, 2)}</pre>
|
||||
</td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
renderPocketStories() {
|
||||
const {rows} = this.props.Sections.find(Section => Section.id === "topstories") || {};
|
||||
|
||||
return (<table><tbody>
|
||||
{rows && rows.map(story => this.renderPocketStory(story))}
|
||||
</tbody></table>);
|
||||
}
|
||||
|
||||
renderAttributionParamers() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -399,9 +418,45 @@ export class ASRouterAdmin extends React.PureComponent {
|
|||
</div>);
|
||||
}
|
||||
|
||||
getSection() {
|
||||
const [section] = this.props.location.routes;
|
||||
switch (section) {
|
||||
case "targeting":
|
||||
return (<React.Fragment>
|
||||
<h2>Targeting Utilities</h2>
|
||||
<button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
|
||||
{this.renderTargetingParameters()}
|
||||
{this.renderAttributionParamers()}
|
||||
</React.Fragment>);
|
||||
case "pocket":
|
||||
return (<React.Fragment>
|
||||
<h2>Pocket</h2>
|
||||
{this.renderPocketStories()}
|
||||
</React.Fragment>);
|
||||
default:
|
||||
return (<React.Fragment>
|
||||
<h2>Message Providers <button title="Restore all provider settings that ship with Firefox" className="button" onClick={this.resetPref}>Restorear default prefs</button></h2>
|
||||
{this.state.providers ? this.renderProviders() : null}
|
||||
<h2>Messages</h2>
|
||||
{this.renderMessageFilter()}
|
||||
{this.renderMessages()}
|
||||
{this.renderPasteModal()}
|
||||
</React.Fragment>);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="asrouter-admin outer-wrapper">
|
||||
return (<div className="asrouter-admin">
|
||||
<aside className="sidebar">
|
||||
<ul>
|
||||
<li><a href="#devtools">General</a></li>
|
||||
<li><a href="#devtools-targeting">Targeting</a></li>
|
||||
<li><a href="#devtools-pocket">Pocket</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
<main className="main-panel">
|
||||
<h1>AS Router Admin</h1>
|
||||
|
||||
<p className="helpLink">
|
||||
<span className="icon icon-small-spacer icon-info" />
|
||||
{" "}
|
||||
|
@ -409,17 +464,12 @@ export class ASRouterAdmin extends React.PureComponent {
|
|||
Need help using these tools? Check out our <a target="blank" href="https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/docs/debugging-docs.md">documentation</a>
|
||||
</span>
|
||||
</p>
|
||||
<h2>Targeting Utilities</h2>
|
||||
<button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
|
||||
<h2>Message Providers <button title="Restore all provider settings that ship with Firefox" className="button" onClick={this.resetPref}>Restore default prefs</button></h2>
|
||||
|
||||
{this.state.providers ? this.renderProviders() : null}
|
||||
<h2>Messages</h2>
|
||||
{this.renderMessageFilter()}
|
||||
{this.renderMessages()}
|
||||
{this.renderPasteModal()}
|
||||
{this.renderTargetingParameters()}
|
||||
{this.renderAttributionParamers()}
|
||||
{this.getSection()}
|
||||
</main>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export const _ASRouterAdmin = props => (<SimpleHashRouter><ASRouterAdminInner {...props} /></SimpleHashRouter>);
|
||||
export const ASRouterAdmin = connect(state => ({Sections: state.Sections}))(_ASRouterAdmin);
|
||||
|
|
|
@ -2,12 +2,32 @@
|
|||
.asrouter-admin {
|
||||
$border-color: var(--newtab-border-secondary-color);
|
||||
$monospace: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;
|
||||
max-width: 996px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
// Reset .outer-wrapper styles
|
||||
display: inherit;
|
||||
padding: 0 0 92px;
|
||||
|
||||
display: flex;
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
width: 240px;
|
||||
padding: 30px 20px;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li a {
|
||||
padding: 10px 34px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background: $grey-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-weight: 200;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
|
||||
export class SimpleHashRouter extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onHashChange = this.onHashChange.bind(this);
|
||||
this.state = {hash: global.location.hash};
|
||||
}
|
||||
|
||||
onHashChange() {
|
||||
this.setState({hash: global.location.hash});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
global.addEventListener("hashchange", this.onHashChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
global.removeEventListener("hashchange", this.onHashChange);
|
||||
}
|
||||
|
||||
render() {
|
||||
const [, ...routes] = this.state.hash.replace("#asrouter", "").split("-");
|
||||
return React.cloneElement(this.props.children, {
|
||||
location: {
|
||||
hash: this.state.hash,
|
||||
routes,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import {addLocaleData, injectIntl, IntlProvider} from "react-intl";
|
|||
import {ASRouterAdmin} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
|
||||
import {ConfirmDialog} from "content-src/components/ConfirmDialog/ConfirmDialog";
|
||||
import {connect} from "react-redux";
|
||||
import {DiscoveryStreamBase} from "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase";
|
||||
import {ErrorBoundary} from "content-src/components/ErrorBoundary/ErrorBoundary";
|
||||
import {ManualMigration} from "content-src/components/ManualMigration/ManualMigration";
|
||||
import {PrerenderData} from "common/PrerenderData.jsm";
|
||||
|
@ -82,10 +83,11 @@ export class _Base extends React.PureComponent {
|
|||
|
||||
const prefs = props.Prefs.values;
|
||||
if (prefs["asrouter.devtoolsEnabled"]) {
|
||||
if (window.location.hash === "#asrouter") {
|
||||
if (window.location.hash.startsWith("#asrouter") ||
|
||||
window.location.hash.startsWith("#devtools")) {
|
||||
return (<ASRouterAdmin />);
|
||||
}
|
||||
console.log("ASRouter devtools enabled. To access visit %cabout:newtab#asrouter", "font-weight: bold"); // eslint-disable-line no-console
|
||||
console.log("Activity Stream devtools enabled. To access visit %cabout:newtab#devtools", "font-weight: bold"); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (!props.isPrerendered && !initialized) {
|
||||
|
@ -138,6 +140,8 @@ export class BaseContent extends React.PureComponent {
|
|||
|
||||
const shouldBeFixedToTop = PrerenderData.arePrefsValid(name => prefs[name]);
|
||||
const noSectionsEnabled = !prefs["feeds.topsites"] && props.Sections.filter(section => section.enabled).length === 0;
|
||||
const isDiscoveryStream = props.DiscoveryStream.config && props.DiscoveryStream.config.enabled;
|
||||
const searchHandoffEnabled = prefs["improvesearch.handoffToAwesomebar"];
|
||||
|
||||
const outerClassName = [
|
||||
"outer-wrapper",
|
||||
|
@ -153,17 +157,17 @@ export class BaseContent extends React.PureComponent {
|
|||
{prefs.showSearch &&
|
||||
<div className="non-collapsible-section">
|
||||
<ErrorBoundary>
|
||||
<Search showLogo={noSectionsEnabled} />
|
||||
<Search showLogo={noSectionsEnabled} handoffEnabled={searchHandoffEnabled} {...props.Search} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
<div className={`body-wrapper${(initialized ? " on" : "")}`}>
|
||||
{!prefs.migrationExpired &&
|
||||
{!isDiscoveryStream && !prefs.migrationExpired &&
|
||||
<div className="non-collapsible-section">
|
||||
<ManualMigration />
|
||||
</div>
|
||||
}
|
||||
<Sections />
|
||||
{isDiscoveryStream ? <DiscoveryStreamBase /> : <Sections />}
|
||||
<PrefsButton onClick={this.openPreferences} />
|
||||
</div>
|
||||
<ConfirmDialog />
|
||||
|
@ -173,4 +177,10 @@ export class BaseContent extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
export const Base = connect(state => ({App: state.App, Prefs: state.Prefs, Sections: state.Sections}))(_Base);
|
||||
export const Base = connect(state => ({
|
||||
App: state.App,
|
||||
Prefs: state.Prefs,
|
||||
Sections: state.Sections,
|
||||
DiscoveryStream: state.DiscoveryStream,
|
||||
Search: state.Search,
|
||||
}))(_Base);
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import {connect} from "react-redux";
|
||||
import React from "react";
|
||||
|
||||
export class _DiscoveryStreamBase extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="discovery-stream layout">
|
||||
{this.props.DiscoveryStream.layout.map((section, sectionIndex) => (
|
||||
<div key={`section-${sectionIndex}`} className={`column column-${section.width}`}>
|
||||
{section.components.map((component, componentIndex) => (
|
||||
<div key={`component-${componentIndex}`}>
|
||||
<div>{component.type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DiscoveryStreamBase = connect(state => ({DiscoveryStream: state.DiscoveryStream}))(_DiscoveryStreamBase);
|
|
@ -0,0 +1,20 @@
|
|||
.discovery-stream.layout {
|
||||
$columns: 12;
|
||||
display: grid;
|
||||
grid-template-columns: repeat($columns, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
|
||||
.column {
|
||||
border: 1px solid $black;
|
||||
}
|
||||
|
||||
@while $columns > 0 {
|
||||
.column-#{$columns} {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span $columns;
|
||||
}
|
||||
|
||||
$columns: $columns - 1;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/* globals ContentSearchUIController */
|
||||
"use strict";
|
||||
|
||||
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
|
||||
import {FormattedMessage, injectIntl} from "react-intl";
|
||||
import {actionCreators as ac} from "common/Actions.jsm";
|
||||
import {connect} from "react-redux";
|
||||
import {IS_NEWTAB} from "content-src/lib/constants";
|
||||
import React from "react";
|
||||
|
@ -10,7 +10,8 @@ import React from "react";
|
|||
export class _Search extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onSearchClick = this.onSearchClick.bind(this);
|
||||
this.onSearchHandoffClick = this.onSearchHandoffClick.bind(this);
|
||||
this.onInputMount = this.onInputMount.bind(this);
|
||||
}
|
||||
|
||||
|
@ -21,10 +22,26 @@ export class _Search extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
onClick(event) {
|
||||
onSearchClick(event) {
|
||||
window.gContentSearchController.search(event);
|
||||
}
|
||||
|
||||
onSearchHandoffClick(event) {
|
||||
// When search hand-off is enabled, we render a big button that is styled to
|
||||
// look like a search textbox. If the button is clicked with the mouse, we style
|
||||
// the button as if it was a focused search box and show a fake cursor but
|
||||
// really focus the awesomebar without the focus styles.
|
||||
// If the button is clicked from the keyboard, we focus the awesomebar normally.
|
||||
// This is to minimize confusion with users navigating with the keyboard and
|
||||
// users using assistive technologoy.
|
||||
const isKeyboardClick = event.clientX === 0 && event.clientY === 0;
|
||||
const hiddenFocus = !isKeyboardClick;
|
||||
this.props.dispatch(ac.OnlyToMain({type: at.HANDOFF_SEARCH_TO_AWESOMEBAR, data: {hiddenFocus}}));
|
||||
this.props.dispatch({type: at.FOCUS_SEARCH});
|
||||
|
||||
// TODO: Send a telemetry ping. BUG 1514732
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
delete window.gContentSearchController;
|
||||
}
|
||||
|
@ -63,13 +80,20 @@ export class _Search extends React.PureComponent {
|
|||
* in order to execute searches in various tests
|
||||
*/
|
||||
render() {
|
||||
return (<div className="search-wrapper">
|
||||
const wrapperClassName = [
|
||||
"search-wrapper",
|
||||
this.props.hide && "search-hidden",
|
||||
this.props.focus && "search-active",
|
||||
].filter(v => v).join(" ");
|
||||
|
||||
return (<div className={wrapperClassName}>
|
||||
{this.props.showLogo &&
|
||||
<div className="logo-and-wordmark">
|
||||
<div className="logo" />
|
||||
<div className="wordmark" />
|
||||
</div>
|
||||
}
|
||||
{!this.props.handoffEnabled &&
|
||||
<div className="search-inner-wrapper">
|
||||
<label htmlFor="newtab-search-text" className="search-label">
|
||||
<span className="sr-only"><FormattedMessage id="search_web_placeholder" /></span>
|
||||
|
@ -84,11 +108,32 @@ export class _Search extends React.PureComponent {
|
|||
<button
|
||||
id="searchSubmit"
|
||||
className="search-button"
|
||||
onClick={this.onClick}
|
||||
onClick={this.onSearchClick}
|
||||
title={this.props.intl.formatMessage({id: "search_button"})}>
|
||||
<span className="sr-only"><FormattedMessage id="search_button" /></span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{this.props.handoffEnabled &&
|
||||
<div className="search-inner-wrapper">
|
||||
<button
|
||||
className="search-handoff-button"
|
||||
onClick={this.onSearchHandoffClick}
|
||||
title={this.props.intl.formatMessage({id: "search_web_placeholder"})}>
|
||||
<div className="fake-textbox">{this.props.intl.formatMessage({id: "search_web_placeholder"})}</div>
|
||||
<div className="fake-caret" />
|
||||
<div className="fake-button" />
|
||||
</button>
|
||||
{/*
|
||||
This dummy and hidden input below is so we can load ContentSearchUIController.
|
||||
Why? It sets --newtab-search-icon for us and it isn't trivial to port over.
|
||||
*/}
|
||||
<input
|
||||
type="search"
|
||||
style={{display: "none"}}
|
||||
ref={this.onInputMount} />
|
||||
</div>
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
.search-wrapper {
|
||||
$search-height: 48px;
|
||||
$search-icon-size: 24px;
|
||||
$search-icon-padding: 12px;
|
||||
$search-icon-width: 2 * $search-icon-padding + $search-icon-size -2;
|
||||
$search-button-width: 48px;
|
||||
$glyph-forward: url('chrome://browser/skin/forward.svg');
|
||||
$search-height: 48px;
|
||||
$search-icon-size: 24px;
|
||||
$search-icon-padding: 12px;
|
||||
$search-icon-width: 2 * $search-icon-padding + $search-icon-size -2;
|
||||
$search-button-width: 48px;
|
||||
$glyph-forward: url('chrome://browser/skin/forward.svg');
|
||||
|
||||
.search-wrapper {
|
||||
padding: 34px 0 64px;
|
||||
|
||||
@media (max-height: 700px) {
|
||||
|
@ -137,6 +137,85 @@
|
|||
}
|
||||
}
|
||||
|
||||
.search-handoff-button {
|
||||
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) $search-icon-padding center no-repeat;
|
||||
background-size: $search-icon-size;
|
||||
border: solid 1px var(--newtab-search-border-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: $shadow-secondary, 0 0 0 1px $black-15;
|
||||
cursor: text;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
padding-inline-end: 48px;
|
||||
padding-inline-start: 46px;
|
||||
opacity: 1;
|
||||
transition: opacity 500ms;
|
||||
width: 100%;
|
||||
|
||||
&:dir(rtl) {
|
||||
background-position-x: right $search-icon-padding;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow-secondary, 0 0 0 1px $black-25;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
.search-active & {
|
||||
border: $input-border-active;
|
||||
box-shadow: var(--newtab-textbox-focus-boxshadow);
|
||||
}
|
||||
|
||||
.search-hidden & {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.fake-textbox {
|
||||
opacity: 0.54;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fake-caret {
|
||||
animation: caret-animation 1.3s steps(5, start) infinite;
|
||||
background: var(--newtab-text-primary-color);
|
||||
display: none;
|
||||
inset-inline-start: 47px;
|
||||
height: 17px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
width: 1px;
|
||||
|
||||
@keyframes caret-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.search-active & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.fake-button {
|
||||
background: $glyph-forward no-repeat center center;
|
||||
background-size: 16px 16px;
|
||||
border: 0;
|
||||
border-radius: 0 $border-radius $border-radius 0;
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--newtab-search-icon-color);
|
||||
height: 100%;
|
||||
inset-inline-end: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: $search-button-width;
|
||||
|
||||
&:dir(rtl) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 701px) {
|
||||
.fixed-search {
|
||||
main {
|
||||
|
@ -172,6 +251,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-handoff-button {
|
||||
background-position-x: $search-icon-padding;
|
||||
background-size: $search-icon-size;
|
||||
|
||||
&:dir(rtl) {
|
||||
background-position-x: right $search-icon-padding;
|
||||
}
|
||||
|
||||
.fake-caret {
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -306,26 +306,7 @@ export class _Sections extends React.PureComponent {
|
|||
return sections;
|
||||
}
|
||||
|
||||
renderLayout() {
|
||||
return (
|
||||
<div className="sections-list layout">
|
||||
{this.props.Layout.map((section, sectionIndex) => (
|
||||
<div key={`section-${sectionIndex}`} className={`column column-${section.width}`}>
|
||||
{section.components.map((component, componentIndex) => (
|
||||
<div key={`component-${componentIndex}`}>
|
||||
<div>{component.type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.Layout && this.props.Layout.length) {
|
||||
return this.renderLayout();
|
||||
}
|
||||
return (
|
||||
<div className="sections-list">
|
||||
{this.renderSections()}
|
||||
|
@ -334,4 +315,4 @@ export class _Sections extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
export const Sections = connect(state => ({Sections: state.Sections, Prefs: state.Prefs, Layout: state.Layout}))(_Sections);
|
||||
export const Sections = connect(state => ({Sections: state.Sections, Prefs: state.Prefs}))(_Sections);
|
||||
|
|
|
@ -1,25 +1,4 @@
|
|||
.sections-list {
|
||||
&.layout {
|
||||
$columns: 12;
|
||||
display: grid;
|
||||
grid-template-columns: repeat($columns, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
|
||||
.column {
|
||||
border: 1px solid $black;
|
||||
}
|
||||
|
||||
@while $columns > 0 {
|
||||
.column-#{$columns} {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span $columns;
|
||||
}
|
||||
|
||||
$columns: $columns - 1;
|
||||
}
|
||||
}
|
||||
|
||||
.section-list {
|
||||
display: grid;
|
||||
grid-gap: $base-gutter;
|
||||
|
|
|
@ -143,6 +143,7 @@ input {
|
|||
@import '../components/ASRouterAdmin/ASRouterAdmin';
|
||||
@import '../components/PocketLoggedInCta/PocketLoggedInCta';
|
||||
@import '../components/MoreRecommendations/MoreRecommendations';
|
||||
@import '../components/DiscoveryStreamBase/DiscoveryStreamBase';
|
||||
|
||||
// AS Router
|
||||
@import '../asrouter/components/Button/Button';
|
||||
|
|
|
@ -846,50 +846,6 @@ main {
|
|||
opacity: 1;
|
||||
transform: translateY(0); } }
|
||||
|
||||
.sections-list.layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px; }
|
||||
.sections-list.layout .column {
|
||||
border: 1px solid #000; }
|
||||
.sections-list.layout .column-12 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 12; }
|
||||
.sections-list.layout .column-11 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 11; }
|
||||
.sections-list.layout .column-10 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 10; }
|
||||
.sections-list.layout .column-9 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 9; }
|
||||
.sections-list.layout .column-8 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 8; }
|
||||
.sections-list.layout .column-7 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 7; }
|
||||
.sections-list.layout .column-6 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 6; }
|
||||
.sections-list.layout .column-5 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 5; }
|
||||
.sections-list.layout .column-4 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 4; }
|
||||
.sections-list.layout .column-3 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 3; }
|
||||
.sections-list.layout .column-2 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 2; }
|
||||
.sections-list.layout .column-1 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 1; }
|
||||
|
||||
.sections-list .section-list {
|
||||
display: grid;
|
||||
grid-gap: 32px;
|
||||
|
@ -1092,6 +1048,64 @@ main {
|
|||
.search-wrapper .search-button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.search-handoff-button {
|
||||
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
|
||||
background-size: 24px;
|
||||
border: solid 1px var(--newtab-search-border-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
cursor: text;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
padding-inline-end: 48px;
|
||||
padding-inline-start: 46px;
|
||||
opacity: 1;
|
||||
transition: opacity 500ms;
|
||||
width: 100%; }
|
||||
.search-handoff-button:dir(rtl) {
|
||||
background-position-x: right 12px; }
|
||||
.search-handoff-button:hover {
|
||||
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.25); }
|
||||
.search-handoff-button:focus,
|
||||
.search-active .search-handoff-button {
|
||||
border: 1px solid var(--newtab-textbox-focus-color);
|
||||
box-shadow: var(--newtab-textbox-focus-boxshadow); }
|
||||
.search-hidden .search-handoff-button {
|
||||
opacity: 0;
|
||||
visibility: hidden; }
|
||||
.search-handoff-button .fake-textbox {
|
||||
opacity: 0.54;
|
||||
text-align: left; }
|
||||
.search-handoff-button .fake-caret {
|
||||
animation: caret-animation 1.3s steps(5, start) infinite;
|
||||
background: var(--newtab-text-primary-color);
|
||||
display: none;
|
||||
inset-inline-start: 47px;
|
||||
height: 17px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
width: 1px; }
|
||||
|
||||
@keyframes caret-animation {
|
||||
to {
|
||||
visibility: hidden; } }
|
||||
.search-active .search-handoff-button .fake-caret {
|
||||
display: block; }
|
||||
.search-handoff-button .fake-button {
|
||||
background: url("chrome://browser/skin/forward.svg") no-repeat center center;
|
||||
background-size: 16px 16px;
|
||||
border: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--newtab-search-icon-color);
|
||||
height: 100%;
|
||||
inset-inline-end: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: 48px; }
|
||||
.search-handoff-button .fake-button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
@media (min-height: 701px) {
|
||||
.fixed-search main {
|
||||
padding-top: 146px; }
|
||||
|
@ -1111,7 +1125,14 @@ main {
|
|||
background-position-x: 16px;
|
||||
background-size: 16px; }
|
||||
.fixed-search .search-wrapper input:dir(rtl) {
|
||||
background-position-x: right 16px; } }
|
||||
background-position-x: right 16px; }
|
||||
.fixed-search .search-handoff-button {
|
||||
background-position-x: 12px;
|
||||
background-size: 24px; }
|
||||
.fixed-search .search-handoff-button:dir(rtl) {
|
||||
background-position-x: right 12px; }
|
||||
.fixed-search .search-handoff-button .fake-caret {
|
||||
top: 10px; } }
|
||||
|
||||
.contentSearchSuggestionTable {
|
||||
background-color: var(--newtab-search-dropdown-color);
|
||||
|
@ -1610,11 +1631,22 @@ main {
|
|||
display: none; } }
|
||||
|
||||
.asrouter-admin {
|
||||
max-width: 996px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
display: inherit;
|
||||
padding: 0 0 92px; }
|
||||
display: flex; }
|
||||
.asrouter-admin .sidebar {
|
||||
position: fixed;
|
||||
width: 240px;
|
||||
padding: 30px 20px; }
|
||||
.asrouter-admin .sidebar ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none; }
|
||||
.asrouter-admin .sidebar li a {
|
||||
padding: 10px 34px;
|
||||
display: block; }
|
||||
.asrouter-admin .sidebar li a:hover {
|
||||
background: #EDEDF0; }
|
||||
.asrouter-admin h1 {
|
||||
font-weight: 200;
|
||||
font-size: 32px; }
|
||||
|
@ -1726,6 +1758,50 @@ main {
|
|||
.more-recommendations:dir(rtl)::after {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.discovery-stream.layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px; }
|
||||
.discovery-stream.layout .column {
|
||||
border: 1px solid #000; }
|
||||
.discovery-stream.layout .column-12 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 12; }
|
||||
.discovery-stream.layout .column-11 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 11; }
|
||||
.discovery-stream.layout .column-10 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 10; }
|
||||
.discovery-stream.layout .column-9 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 9; }
|
||||
.discovery-stream.layout .column-8 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 8; }
|
||||
.discovery-stream.layout .column-7 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 7; }
|
||||
.discovery-stream.layout .column-6 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 6; }
|
||||
.discovery-stream.layout .column-5 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 5; }
|
||||
.discovery-stream.layout .column-4 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 4; }
|
||||
.discovery-stream.layout .column-3 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 3; }
|
||||
.discovery-stream.layout .column-2 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 2; }
|
||||
.discovery-stream.layout .column-1 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 1; }
|
||||
|
||||
.ASRouterButton {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -849,50 +849,6 @@ main {
|
|||
opacity: 1;
|
||||
transform: translateY(0); } }
|
||||
|
||||
.sections-list.layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px; }
|
||||
.sections-list.layout .column {
|
||||
border: 1px solid #000; }
|
||||
.sections-list.layout .column-12 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 12; }
|
||||
.sections-list.layout .column-11 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 11; }
|
||||
.sections-list.layout .column-10 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 10; }
|
||||
.sections-list.layout .column-9 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 9; }
|
||||
.sections-list.layout .column-8 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 8; }
|
||||
.sections-list.layout .column-7 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 7; }
|
||||
.sections-list.layout .column-6 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 6; }
|
||||
.sections-list.layout .column-5 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 5; }
|
||||
.sections-list.layout .column-4 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 4; }
|
||||
.sections-list.layout .column-3 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 3; }
|
||||
.sections-list.layout .column-2 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 2; }
|
||||
.sections-list.layout .column-1 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 1; }
|
||||
|
||||
.sections-list .section-list {
|
||||
display: grid;
|
||||
grid-gap: 32px;
|
||||
|
@ -1095,6 +1051,64 @@ main {
|
|||
.search-wrapper .search-button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.search-handoff-button {
|
||||
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
|
||||
background-size: 24px;
|
||||
border: solid 1px var(--newtab-search-border-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
cursor: text;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
padding-inline-end: 48px;
|
||||
padding-inline-start: 46px;
|
||||
opacity: 1;
|
||||
transition: opacity 500ms;
|
||||
width: 100%; }
|
||||
.search-handoff-button:dir(rtl) {
|
||||
background-position-x: right 12px; }
|
||||
.search-handoff-button:hover {
|
||||
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.25); }
|
||||
.search-handoff-button:focus,
|
||||
.search-active .search-handoff-button {
|
||||
border: 1px solid var(--newtab-textbox-focus-color);
|
||||
box-shadow: var(--newtab-textbox-focus-boxshadow); }
|
||||
.search-hidden .search-handoff-button {
|
||||
opacity: 0;
|
||||
visibility: hidden; }
|
||||
.search-handoff-button .fake-textbox {
|
||||
opacity: 0.54;
|
||||
text-align: left; }
|
||||
.search-handoff-button .fake-caret {
|
||||
animation: caret-animation 1.3s steps(5, start) infinite;
|
||||
background: var(--newtab-text-primary-color);
|
||||
display: none;
|
||||
inset-inline-start: 47px;
|
||||
height: 17px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
width: 1px; }
|
||||
|
||||
@keyframes caret-animation {
|
||||
to {
|
||||
visibility: hidden; } }
|
||||
.search-active .search-handoff-button .fake-caret {
|
||||
display: block; }
|
||||
.search-handoff-button .fake-button {
|
||||
background: url("chrome://browser/skin/forward.svg") no-repeat center center;
|
||||
background-size: 16px 16px;
|
||||
border: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--newtab-search-icon-color);
|
||||
height: 100%;
|
||||
inset-inline-end: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: 48px; }
|
||||
.search-handoff-button .fake-button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
@media (min-height: 701px) {
|
||||
.fixed-search main {
|
||||
padding-top: 146px; }
|
||||
|
@ -1114,7 +1128,14 @@ main {
|
|||
background-position-x: 16px;
|
||||
background-size: 16px; }
|
||||
.fixed-search .search-wrapper input:dir(rtl) {
|
||||
background-position-x: right 16px; } }
|
||||
background-position-x: right 16px; }
|
||||
.fixed-search .search-handoff-button {
|
||||
background-position-x: 12px;
|
||||
background-size: 24px; }
|
||||
.fixed-search .search-handoff-button:dir(rtl) {
|
||||
background-position-x: right 12px; }
|
||||
.fixed-search .search-handoff-button .fake-caret {
|
||||
top: 10px; } }
|
||||
|
||||
.contentSearchSuggestionTable {
|
||||
background-color: var(--newtab-search-dropdown-color);
|
||||
|
@ -1613,11 +1634,22 @@ main {
|
|||
display: none; } }
|
||||
|
||||
.asrouter-admin {
|
||||
max-width: 996px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
display: inherit;
|
||||
padding: 0 0 92px; }
|
||||
display: flex; }
|
||||
.asrouter-admin .sidebar {
|
||||
position: fixed;
|
||||
width: 240px;
|
||||
padding: 30px 20px; }
|
||||
.asrouter-admin .sidebar ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none; }
|
||||
.asrouter-admin .sidebar li a {
|
||||
padding: 10px 34px;
|
||||
display: block; }
|
||||
.asrouter-admin .sidebar li a:hover {
|
||||
background: #EDEDF0; }
|
||||
.asrouter-admin h1 {
|
||||
font-weight: 200;
|
||||
font-size: 32px; }
|
||||
|
@ -1729,6 +1761,50 @@ main {
|
|||
.more-recommendations:dir(rtl)::after {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.discovery-stream.layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px; }
|
||||
.discovery-stream.layout .column {
|
||||
border: 1px solid #000; }
|
||||
.discovery-stream.layout .column-12 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 12; }
|
||||
.discovery-stream.layout .column-11 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 11; }
|
||||
.discovery-stream.layout .column-10 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 10; }
|
||||
.discovery-stream.layout .column-9 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 9; }
|
||||
.discovery-stream.layout .column-8 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 8; }
|
||||
.discovery-stream.layout .column-7 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 7; }
|
||||
.discovery-stream.layout .column-6 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 6; }
|
||||
.discovery-stream.layout .column-5 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 5; }
|
||||
.discovery-stream.layout .column-4 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 4; }
|
||||
.discovery-stream.layout .column-3 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 3; }
|
||||
.discovery-stream.layout .column-2 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 2; }
|
||||
.discovery-stream.layout .column-1 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 1; }
|
||||
|
||||
.ASRouterButton {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -846,50 +846,6 @@ main {
|
|||
opacity: 1;
|
||||
transform: translateY(0); } }
|
||||
|
||||
.sections-list.layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px; }
|
||||
.sections-list.layout .column {
|
||||
border: 1px solid #000; }
|
||||
.sections-list.layout .column-12 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 12; }
|
||||
.sections-list.layout .column-11 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 11; }
|
||||
.sections-list.layout .column-10 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 10; }
|
||||
.sections-list.layout .column-9 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 9; }
|
||||
.sections-list.layout .column-8 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 8; }
|
||||
.sections-list.layout .column-7 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 7; }
|
||||
.sections-list.layout .column-6 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 6; }
|
||||
.sections-list.layout .column-5 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 5; }
|
||||
.sections-list.layout .column-4 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 4; }
|
||||
.sections-list.layout .column-3 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 3; }
|
||||
.sections-list.layout .column-2 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 2; }
|
||||
.sections-list.layout .column-1 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 1; }
|
||||
|
||||
.sections-list .section-list {
|
||||
display: grid;
|
||||
grid-gap: 32px;
|
||||
|
@ -1092,6 +1048,64 @@ main {
|
|||
.search-wrapper .search-button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.search-handoff-button {
|
||||
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
|
||||
background-size: 24px;
|
||||
border: solid 1px var(--newtab-search-border-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
cursor: text;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
padding-inline-end: 48px;
|
||||
padding-inline-start: 46px;
|
||||
opacity: 1;
|
||||
transition: opacity 500ms;
|
||||
width: 100%; }
|
||||
.search-handoff-button:dir(rtl) {
|
||||
background-position-x: right 12px; }
|
||||
.search-handoff-button:hover {
|
||||
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.25); }
|
||||
.search-handoff-button:focus,
|
||||
.search-active .search-handoff-button {
|
||||
border: 1px solid var(--newtab-textbox-focus-color);
|
||||
box-shadow: var(--newtab-textbox-focus-boxshadow); }
|
||||
.search-hidden .search-handoff-button {
|
||||
opacity: 0;
|
||||
visibility: hidden; }
|
||||
.search-handoff-button .fake-textbox {
|
||||
opacity: 0.54;
|
||||
text-align: left; }
|
||||
.search-handoff-button .fake-caret {
|
||||
animation: caret-animation 1.3s steps(5, start) infinite;
|
||||
background: var(--newtab-text-primary-color);
|
||||
display: none;
|
||||
inset-inline-start: 47px;
|
||||
height: 17px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
width: 1px; }
|
||||
|
||||
@keyframes caret-animation {
|
||||
to {
|
||||
visibility: hidden; } }
|
||||
.search-active .search-handoff-button .fake-caret {
|
||||
display: block; }
|
||||
.search-handoff-button .fake-button {
|
||||
background: url("chrome://browser/skin/forward.svg") no-repeat center center;
|
||||
background-size: 16px 16px;
|
||||
border: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--newtab-search-icon-color);
|
||||
height: 100%;
|
||||
inset-inline-end: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: 48px; }
|
||||
.search-handoff-button .fake-button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
@media (min-height: 701px) {
|
||||
.fixed-search main {
|
||||
padding-top: 146px; }
|
||||
|
@ -1111,7 +1125,14 @@ main {
|
|||
background-position-x: 16px;
|
||||
background-size: 16px; }
|
||||
.fixed-search .search-wrapper input:dir(rtl) {
|
||||
background-position-x: right 16px; } }
|
||||
background-position-x: right 16px; }
|
||||
.fixed-search .search-handoff-button {
|
||||
background-position-x: 12px;
|
||||
background-size: 24px; }
|
||||
.fixed-search .search-handoff-button:dir(rtl) {
|
||||
background-position-x: right 12px; }
|
||||
.fixed-search .search-handoff-button .fake-caret {
|
||||
top: 10px; } }
|
||||
|
||||
.contentSearchSuggestionTable {
|
||||
background-color: var(--newtab-search-dropdown-color);
|
||||
|
@ -1610,11 +1631,22 @@ main {
|
|||
display: none; } }
|
||||
|
||||
.asrouter-admin {
|
||||
max-width: 996px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
display: inherit;
|
||||
padding: 0 0 92px; }
|
||||
display: flex; }
|
||||
.asrouter-admin .sidebar {
|
||||
position: fixed;
|
||||
width: 240px;
|
||||
padding: 30px 20px; }
|
||||
.asrouter-admin .sidebar ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none; }
|
||||
.asrouter-admin .sidebar li a {
|
||||
padding: 10px 34px;
|
||||
display: block; }
|
||||
.asrouter-admin .sidebar li a:hover {
|
||||
background: #EDEDF0; }
|
||||
.asrouter-admin h1 {
|
||||
font-weight: 200;
|
||||
font-size: 32px; }
|
||||
|
@ -1726,6 +1758,50 @@ main {
|
|||
.more-recommendations:dir(rtl)::after {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.discovery-stream.layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px; }
|
||||
.discovery-stream.layout .column {
|
||||
border: 1px solid #000; }
|
||||
.discovery-stream.layout .column-12 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 12; }
|
||||
.discovery-stream.layout .column-11 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 11; }
|
||||
.discovery-stream.layout .column-10 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 10; }
|
||||
.discovery-stream.layout .column-9 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 9; }
|
||||
.discovery-stream.layout .column-8 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 8; }
|
||||
.discovery-stream.layout .column-7 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 7; }
|
||||
.discovery-stream.layout .column-6 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 6; }
|
||||
.discovery-stream.layout .column-5 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 5; }
|
||||
.discovery-stream.layout .column-4 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 4; }
|
||||
.discovery-stream.layout .column-3 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 3; }
|
||||
.discovery-stream.layout .column-2 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 2; }
|
||||
.discovery-stream.layout .column-1 {
|
||||
grid-column-start: auto;
|
||||
grid-column-end: span 1; }
|
||||
|
||||
.ASRouterButton {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -29,6 +29,7 @@ const {TopSitesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopSit
|
|||
const {TopStoriesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
|
||||
const {HighlightsFeed} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm", {});
|
||||
const {ASRouterFeed} = ChromeUtils.import("resource://activity-stream/lib/ASRouterFeed.jsm", {});
|
||||
const {DiscoveryStreamFeed} = ChromeUtils.import("resource://activity-stream/lib/DiscoveryStreamFeed.jsm", {});
|
||||
|
||||
const DEFAULT_SITES = new Map([
|
||||
// This first item is the global list fallback for any unexpected geos
|
||||
|
@ -192,6 +193,10 @@ const PREFS_CONFIG = new Map([
|
|||
title: "A comma-delimited list of search shortcuts that have previously been pinned",
|
||||
value: "",
|
||||
}],
|
||||
["improvesearch.handoffToAwesomebar", {
|
||||
title: "Should the search box handoff to the Awesomebar?",
|
||||
value: true,
|
||||
}],
|
||||
["asrouter.devtoolsEnabled", {
|
||||
title: "Are the asrouter devtools enabled?",
|
||||
value: false,
|
||||
|
@ -210,6 +215,14 @@ const PREFS_CONFIG = new Map([
|
|||
}),
|
||||
}],
|
||||
// See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
|
||||
["discoverystream.config", {
|
||||
title: "Configuration for the new pocket new tab",
|
||||
value: JSON.stringify({
|
||||
enabled: false,
|
||||
// Set this to https://gist.githubusercontent.com/ScottDowne/164995d9535b4203846048bdee29d169/raw/0cf538411e6ee898eb116208d70842c62c8d52f1/spoc.json to test
|
||||
layout_endpoint: "",
|
||||
}),
|
||||
}],
|
||||
]);
|
||||
|
||||
// Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
|
||||
|
@ -306,6 +319,12 @@ const FEEDS_DATA = [
|
|||
title: "Handles AS Router messages, such as snippets and onboaridng",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: "discoverystreamfeed",
|
||||
factory: () => new DiscoveryStreamFeed(),
|
||||
title: "Handles new pocket ui for the new tab page",
|
||||
value: true,
|
||||
},
|
||||
];
|
||||
|
||||
const FEEDS_CONFIG = new Map();
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm", {});
|
||||
|
||||
const CACHE_KEY = "discovery_stream";
|
||||
const LAYOUT_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
|
||||
const CONFIG_PREF_NAME = "browser.newtabpage.activity-stream.discoverystream.config";
|
||||
|
||||
this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
|
||||
constructor() {
|
||||
// Internal state for checking if we've intialized all our data
|
||||
this.loaded = false;
|
||||
|
||||
// Persistent cache for remote endpoint data.
|
||||
this.cache = new PersistentCache(CACHE_KEY, true);
|
||||
// Internal in-memory cache for parsing json prefs.
|
||||
this._prefCache = {};
|
||||
}
|
||||
|
||||
get config() {
|
||||
if (this._prefCache.config) {
|
||||
return this._prefCache.config;
|
||||
}
|
||||
try {
|
||||
this._prefCache.config = JSON.parse(Services.prefs.getStringPref(CONFIG_PREF_NAME, ""));
|
||||
} catch (e) {
|
||||
// istanbul ignore next
|
||||
this._prefCache.config = {};
|
||||
// istanbul ignore next
|
||||
Cu.reportError(`Could not parse preference. Try resetting ${CONFIG_PREF_NAME} in about:config.`);
|
||||
}
|
||||
return this._prefCache.config;
|
||||
}
|
||||
|
||||
setupPrefs() {
|
||||
Services.prefs.addObserver(CONFIG_PREF_NAME, this);
|
||||
// Send the initial state of the pref on our reducer
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_CONFIG_SETUP, data: this.config}));
|
||||
}
|
||||
|
||||
uninitPrefs() {
|
||||
Services.prefs.removeObserver(CONFIG_PREF_NAME, this);
|
||||
// Reset in-memory cache
|
||||
this._prefCache = {};
|
||||
}
|
||||
|
||||
observe(aSubject, aTopic, aPrefName) {
|
||||
if (aPrefName === CONFIG_PREF_NAME) {
|
||||
this._prefCache.config = null;
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_CONFIG_CHANGE, data: this.config}));
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLayout() {
|
||||
const endpoint = this.config.layout_endpoint;
|
||||
if (!endpoint) {
|
||||
Cu.reportError("No endpoint configured for pocket, so could not fetch layout");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(endpoint, {credentials: "omit"});
|
||||
if (!response.ok) {
|
||||
// istanbul ignore next
|
||||
throw new Error(`Stories endpoint returned unexpected status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
// istanbul ignore next
|
||||
Cu.reportError(`Failed to fetch layout: ${error.message}`);
|
||||
}
|
||||
// istanbul ignore next
|
||||
return null;
|
||||
}
|
||||
|
||||
async loadCachedData() {
|
||||
const cachedData = await this.cache.get() || {};
|
||||
|
||||
let {layout: layoutResponse} = cachedData;
|
||||
if (!layoutResponse || !(Date.now() - layoutResponse._timestamp > LAYOUT_UPDATE_TIME)) {
|
||||
layoutResponse = await this.fetchLayout();
|
||||
if (layoutResponse && layoutResponse.layout) {
|
||||
layoutResponse._timestamp = Date.now();
|
||||
await this.cache.set("layout", layoutResponse);
|
||||
} else {
|
||||
Cu.reportError("No response for response.layout prop");
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutResponse && layoutResponse.layout) {
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: layoutResponse.layout}));
|
||||
}
|
||||
}
|
||||
|
||||
async enable() {
|
||||
await this.loadCachedData();
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async disable() {
|
||||
// Clear cache
|
||||
await this.cache.set("layout", {});
|
||||
// Reset reducer
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: []}));
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
async onPrefChange() {
|
||||
// Load data from all endpoints if our config.enabled = true.
|
||||
if (this.config.enabled) {
|
||||
await this.enable();
|
||||
}
|
||||
|
||||
// Clear state and relevant listeners if config.enabled = false.
|
||||
if (this.loaded && !this.config.enabled) {
|
||||
await this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
async onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
// During the initialization of Firefox:
|
||||
// 1. Set-up listeners and initialize the redux state for config;
|
||||
this.setupPrefs();
|
||||
// 2. If config.enabled is true, start loading data.
|
||||
if (this.config.enabled) {
|
||||
await this.enable();
|
||||
}
|
||||
break;
|
||||
case at.DISCOVERY_STREAM_CONFIG_CHANGE:
|
||||
// When the config pref changes, load or unload data as needed.
|
||||
await this.onPrefChange();
|
||||
break;
|
||||
case at.UNINIT:
|
||||
// When this feed is shutting down:
|
||||
this.uninitPrefs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const EXPORTED_SYMBOLS = ["DiscoveryStreamFeed"];
|
|
@ -283,6 +283,38 @@ class PlacesFeed {
|
|||
_target.browser.ownerGlobal.gURLBar.search(`${data.label} `);
|
||||
}
|
||||
|
||||
handoffSearchToAwesomebar({_target, data, meta}) {
|
||||
const urlBar = _target.browser.ownerGlobal.gURLBar;
|
||||
|
||||
if (!data.hiddenFocus) {
|
||||
// Do a normal focus of awesomebar and reset the in content search (remove fake focus styles).
|
||||
urlBar.focus();
|
||||
this.store.dispatch(ac.OnlyToOneContent({type: at.SHOW_SEARCH}, meta.fromTarget));
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus the awesomebar without the style changes.
|
||||
urlBar.hiddenFocus();
|
||||
const onKeydown = () => {
|
||||
// Once the user starts typing, we want to hide the in content search box
|
||||
// and show the focus styles on the awesomebar.
|
||||
this.store.dispatch(ac.OnlyToOneContent({type: at.HIDE_SEARCH}, meta.fromTarget));
|
||||
urlBar.removeHiddenFocus();
|
||||
urlBar.removeEventListener("keydown", onKeydown);
|
||||
};
|
||||
const onDone = () => {
|
||||
// When done, let's cleanup everything.
|
||||
this.store.dispatch(ac.OnlyToOneContent({type: at.SHOW_SEARCH}, meta.fromTarget));
|
||||
urlBar.removeHiddenFocus();
|
||||
urlBar.removeEventListener("keydown", onKeydown);
|
||||
urlBar.removeEventListener("mousedown", onDone);
|
||||
urlBar.removeEventListener("blur", onDone);
|
||||
};
|
||||
urlBar.addEventListener("keydown", onKeydown);
|
||||
urlBar.addEventListener("mousedown", onDone);
|
||||
urlBar.addEventListener("blur", onDone);
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
|
@ -323,6 +355,9 @@ class PlacesFeed {
|
|||
case at.FILL_SEARCH_TERM:
|
||||
this.fillSearchTopSiteTerm(action);
|
||||
break;
|
||||
case at.HANDOFF_SEARCH_TO_AWESOMEBAR:
|
||||
this.handoffSearchToAwesomebar(action);
|
||||
break;
|
||||
case at.OPEN_LINK: {
|
||||
this.openLink(action);
|
||||
break;
|
||||
|
|
|
@ -112,13 +112,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : ac.AlsoToPreloaded(action));
|
||||
}
|
||||
|
||||
maybeDispatchLayoutUpdate(data, shouldBroadcast) {
|
||||
if (data && data.length) {
|
||||
const action = {type: at.CONTENT_LAYOUT, data};
|
||||
this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : ac.AlsoToPreloaded(action));
|
||||
}
|
||||
}
|
||||
|
||||
doContentUpdate(shouldBroadcast) {
|
||||
let updateProps = {};
|
||||
if (this.stories) {
|
||||
|
@ -181,7 +174,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
}
|
||||
|
||||
const body = await response.json();
|
||||
this.maybeDispatchLayoutUpdate(body.layout);
|
||||
this.updateSettings(body.settings);
|
||||
this.stories = this.rotate(this.transform(body.recommendations));
|
||||
this.cleanUpTopRecImpressionPref();
|
||||
|
@ -202,9 +194,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
async loadCachedData() {
|
||||
const data = await this.cache.get();
|
||||
let stories = data.stories && data.stories.recommendations;
|
||||
let layout = data.stories && data.stories.layout;
|
||||
let topics = data.topics && data.topics.topics;
|
||||
this.maybeDispatchLayoutUpdate(layout);
|
||||
|
||||
let affinities = data.domainAffinities;
|
||||
if (this.personalized && affinities && affinities.scores) {
|
||||
|
|
|
@ -66,5 +66,15 @@ window.gActivityStreamPrerenderedState = {
|
|||
"pocketCta": {},
|
||||
"waitingForSpoc": true
|
||||
},
|
||||
"Layout": []
|
||||
"DiscoveryStream": {
|
||||
"config": {
|
||||
"enabled": false,
|
||||
"layout_endpoint": ""
|
||||
},
|
||||
"layout": []
|
||||
},
|
||||
"Search": {
|
||||
"focus": false,
|
||||
"hide": false
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
"use strict";
|
||||
|
||||
test_newtab(function test_render_search() {
|
||||
let search = content.document.getElementById("newtab-search-text");
|
||||
ok(search, "Got the search box");
|
||||
isnot(search.placeholder, "search_web_placeholder", "Search box is localized");
|
||||
test_newtab({
|
||||
async before({pushPrefs}) {
|
||||
await pushPrefs(["browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false]);
|
||||
},
|
||||
test: function test_render_search() {
|
||||
let search = content.document.getElementById("newtab-search-text");
|
||||
ok(search, "Got the search box");
|
||||
isnot(search.placeholder, "search_web_placeholder", "Search box is localized");
|
||||
},
|
||||
});
|
||||
|
||||
test_newtab({
|
||||
async before({pushPrefs}) {
|
||||
await pushPrefs(["browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true]);
|
||||
},
|
||||
test: function test_render_search_handoff() {
|
||||
let search = content.document.querySelector(".search-handoff-button");
|
||||
ok(search, "Got the search handoff button");
|
||||
},
|
||||
});
|
||||
|
||||
test_newtab(function test_render_topsites() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {INITIAL_STATE, insertPinned, reducers} from "common/Reducers.jsm";
|
||||
const {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket, Layout} = reducers;
|
||||
const {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket, DiscoveryStream, Search} = reducers;
|
||||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
|
||||
describe("Reducers", () => {
|
||||
|
@ -655,13 +655,35 @@ describe("Reducers", () => {
|
|||
assert.equal(state.pocketCta.useCta, data.use_cta);
|
||||
});
|
||||
});
|
||||
describe("Layout", () => {
|
||||
describe("DiscoveryStream", () => {
|
||||
it("should return INITIAL_STATE by default", () => {
|
||||
assert.equal(Layout(undefined, {type: "some_action"}), INITIAL_STATE.Layout);
|
||||
assert.equal(DiscoveryStream(undefined, {type: "some_action"}), INITIAL_STATE.DiscoveryStream);
|
||||
});
|
||||
it("should set layout data with layout.type CONTENT_LAYOUT", () => {
|
||||
const state = Layout(undefined, {type: at.CONTENT_LAYOUT, data: ["test"]});
|
||||
assert.equal(state[0], "test");
|
||||
it("should set layout data with DISCOVERY_STREAM_LAYOUT_UPDATE", () => {
|
||||
const state = DiscoveryStream(undefined, {type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: ["test"]});
|
||||
assert.equal(state.layout[0], "test");
|
||||
});
|
||||
it("should set config data with DISCOVERY_STREAM_CONFIG_CHANGE", () => {
|
||||
const state = DiscoveryStream(undefined, {type: at.DISCOVERY_STREAM_CONFIG_CHANGE, data: {enabled: true}});
|
||||
assert.deepEqual(state.config, {enabled: true});
|
||||
});
|
||||
});
|
||||
describe("Search", () => {
|
||||
it("should return INITIAL_STATE by default", () => {
|
||||
assert.equal(Search(undefined, {type: "some_action"}), INITIAL_STATE.Search);
|
||||
});
|
||||
it("should set hide to true on HIDE_SEARCH", () => {
|
||||
const nextState = Search(undefined, {type: "HIDE_SEARCH"});
|
||||
assert.propertyVal(nextState, "hide", true);
|
||||
});
|
||||
it("should set focus to true on FOCUS_SEARCH", () => {
|
||||
const nextState = Search(undefined, {type: "FOCUS_SEARCH"});
|
||||
assert.propertyVal(nextState, "focus", true);
|
||||
});
|
||||
it("should set focus and hide to false on SHOW_SEARCH", () => {
|
||||
const nextState = Search(undefined, {type: "SHOW_SEARCH"});
|
||||
assert.propertyVal(nextState, "focus", false);
|
||||
assert.propertyVal(nextState, "hide", false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ASRouterAdmin} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
|
||||
import {ASRouterAdminInner} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
|
||||
import {GlobalOverrider} from "test/unit/utils";
|
||||
import React from "react";
|
||||
import {shallow} from "enzyme";
|
||||
|
@ -9,6 +9,7 @@ describe("ASRouterAdmin", () => {
|
|||
let sendMessageStub;
|
||||
let addListenerStub;
|
||||
let removeListenerStub;
|
||||
let wrapper;
|
||||
let FAKE_PROVIDER_PREF = [{
|
||||
enabled: true,
|
||||
id: "snippets_local_testing",
|
||||
|
@ -32,37 +33,43 @@ describe("ASRouterAdmin", () => {
|
|||
globals.set("RPMSendAsyncMessage", sendMessageStub);
|
||||
globals.set("RPMAddMessageListener", addListenerStub);
|
||||
globals.set("RPMRemoveMessageListener", removeListenerStub);
|
||||
|
||||
wrapper = shallow(<ASRouterAdminInner location={{routes: [""]}} />);
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
globals.restore();
|
||||
});
|
||||
it("should render ASRouterAdmin component", () => {
|
||||
const wrapper = shallow(<ASRouterAdmin />);
|
||||
assert.ok(wrapper.exists());
|
||||
});
|
||||
it("should send ADMIN_CONNECT_STATE on mount", () => {
|
||||
shallow(<ASRouterAdmin />);
|
||||
|
||||
assert.calledOnce(sendMessageStub);
|
||||
assert.propertyVal(sendMessageStub.firstCall.args[1], "type", "ADMIN_CONNECT_STATE");
|
||||
});
|
||||
it("should set a listener on mount", () => {
|
||||
const wrapper = shallow(<ASRouterAdmin />);
|
||||
|
||||
assert.calledOnce(addListenerStub);
|
||||
assert.calledWithExactly(addListenerStub, sinon.match.string, wrapper.instance().onMessage);
|
||||
});
|
||||
it("should remove listener on unmount", () => {
|
||||
const wrapper = shallow(<ASRouterAdmin />);
|
||||
wrapper.unmount();
|
||||
|
||||
assert.calledOnce(removeListenerStub);
|
||||
});
|
||||
describe("#getSection", () => {
|
||||
it("should render a message provider section by default", () => {
|
||||
assert.equal(wrapper.find("h2").at(1).text(), "Messages");
|
||||
});
|
||||
it("should render a targeting section for targeting route", () => {
|
||||
wrapper = shallow(<ASRouterAdminInner location={{routes: ["targeting"]}} />);
|
||||
assert.equal(wrapper.find("h2").at(0).text(), "Targeting Utilities");
|
||||
});
|
||||
it("should render a pocket section for pocket route", () => {
|
||||
wrapper = shallow(<ASRouterAdminInner location={{routes: ["pocket"]}} Sections={[]} />);
|
||||
assert.equal(wrapper.find("h2").at(0).text(), "Pocket");
|
||||
});
|
||||
});
|
||||
describe("#render", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ASRouterAdmin />);
|
||||
wrapper.setState({
|
||||
providerPrefs: [],
|
||||
providers: [],
|
||||
|
|
|
@ -5,7 +5,7 @@ import {Search} from "content-src/components/Search/Search";
|
|||
import {shallow} from "enzyme";
|
||||
|
||||
describe("<Base>", () => {
|
||||
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], dispatch: () => {}};
|
||||
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], DiscoveryStream: {config: {enabled: false}}, dispatch: () => {}};
|
||||
|
||||
it("should render Base component", () => {
|
||||
const wrapper = shallow(<Base {...DEFAULT_PROPS} />);
|
||||
|
@ -27,7 +27,7 @@ describe("<Base>", () => {
|
|||
});
|
||||
|
||||
describe("<BaseContent>", () => {
|
||||
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], dispatch: () => {}};
|
||||
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], DiscoveryStream: {config: {enabled: false}}, dispatch: () => {}};
|
||||
|
||||
it("should render an ErrorBoundary with a Search child", () => {
|
||||
const searchEnabledProps =
|
||||
|
|
|
@ -73,4 +73,34 @@ describe("<Search>", () => {
|
|||
assert.isUserEventAction(action);
|
||||
assert.propertyVal(action.data, "event", "SEARCH");
|
||||
});
|
||||
|
||||
describe("Search Hand-off", () => {
|
||||
it("should render a Search element when hand-off is enabled", () => {
|
||||
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} handoffEnabled={true} />);
|
||||
assert.ok(wrapper.exists());
|
||||
assert.equal(wrapper.find(".search-handoff-button").length, 1);
|
||||
});
|
||||
it("should hand-off search when button is clicked with mouse", () => {
|
||||
const dispatch = sinon.spy();
|
||||
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />);
|
||||
wrapper.instance().onSearchHandoffClick({clientX: 101, clientY: 102});
|
||||
assert.calledWith(dispatch, {
|
||||
data: {hiddenFocus: true},
|
||||
meta: {from: "ActivityStream:Content", skipLocal: true, to: "ActivityStream:Main"},
|
||||
type: "HANDOFF_SEARCH_TO_AWESOMEBAR",
|
||||
});
|
||||
assert.calledWith(dispatch, {type: "FOCUS_SEARCH"});
|
||||
});
|
||||
it("should hand-off search when button is clicked with keyboard", () => {
|
||||
const dispatch = sinon.spy();
|
||||
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />);
|
||||
wrapper.instance().onSearchHandoffClick({clientX: 0, clientY: 0});
|
||||
assert.calledWith(dispatch, {
|
||||
data: {hiddenFocus: false},
|
||||
meta: {from: "ActivityStream:Content", skipLocal: true, to: "ActivityStream:Main"},
|
||||
type: "HANDOFF_SEARCH_TO_AWESOMEBAR",
|
||||
});
|
||||
assert.calledWith(dispatch, {type: "FOCUS_SEARCH"});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import {combineReducers, createStore} from "redux";
|
||||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
import {DiscoveryStreamFeed} from "lib/DiscoveryStreamFeed.jsm";
|
||||
import {reducers} from "common/Reducers.jsm";
|
||||
|
||||
const CONFIG_PREF_NAME = "browser.newtabpage.activity-stream.discoverystream.config";
|
||||
|
||||
describe("DiscoveryStreamFeed", () => {
|
||||
let feed;
|
||||
let sandbox;
|
||||
let configPrefStub;
|
||||
let fetchStub;
|
||||
|
||||
beforeEach(() => {
|
||||
// Pref
|
||||
sandbox = sinon.createSandbox();
|
||||
configPrefStub = sandbox.stub(global.Services.prefs, "getStringPref")
|
||||
.withArgs(CONFIG_PREF_NAME)
|
||||
.returns(JSON.stringify({enabled: false, layout_endpoint: "foo.com"}));
|
||||
|
||||
// Fetch
|
||||
fetchStub = sandbox.stub(global, "fetch");
|
||||
|
||||
// Feed
|
||||
feed = new DiscoveryStreamFeed();
|
||||
feed.store = createStore(combineReducers(reducers));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("#observe", () => {
|
||||
it("should update state.DiscoveryStream.config when the pref changes", async () => {
|
||||
configPrefStub.returns(JSON.stringify({enabled: true, layout_endpoint: "foo"}));
|
||||
|
||||
feed.observe(null, null, CONFIG_PREF_NAME);
|
||||
|
||||
assert.deepEqual(feed.store.getState().DiscoveryStream.config, {enabled: true, layout_endpoint: "foo"});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#loadCachedData", () => {
|
||||
it("should fetch data and populate the cache if it is empty", async () => {
|
||||
const resp = {layout: ["foo", "bar"]};
|
||||
const fakeCache = {};
|
||||
sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
|
||||
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
|
||||
|
||||
fetchStub.resolves({ok: true, json: () => Promise.resolve(resp)});
|
||||
|
||||
await feed.loadCachedData();
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledWith(feed.cache.set, "layout", resp);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onAction: INIT", () => {
|
||||
it("should be .loaded=false before initialization", () => {
|
||||
assert.isFalse(feed.loaded);
|
||||
});
|
||||
it("should load data, add pref observer, and set .loaded=true if config.enabled is true", async () => {
|
||||
configPrefStub.returns(JSON.stringify({enabled: true}));
|
||||
sandbox.stub(feed, "loadCachedData").returns(Promise.resolve());
|
||||
sandbox.stub(global.Services.prefs, "addObserver");
|
||||
|
||||
await feed.onAction({type: at.INIT});
|
||||
|
||||
assert.calledOnce(feed.loadCachedData);
|
||||
assert.calledWith(global.Services.prefs.addObserver, CONFIG_PREF_NAME, feed);
|
||||
assert.isTrue(feed.loaded);
|
||||
});
|
||||
});
|
||||
describe("#onAction: DISCOVERY_STREAM_CONFIG_CHANGE", () => {
|
||||
it("should call this.loadCachedData if config.enabled changes to true ", async () => {
|
||||
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
|
||||
// First initialize
|
||||
await feed.onAction({type: at.INIT});
|
||||
assert.isFalse(feed.loaded);
|
||||
|
||||
// force clear cached pref value
|
||||
feed._prefCache = {};
|
||||
configPrefStub.returns(JSON.stringify({enabled: true}));
|
||||
|
||||
sandbox.stub(feed, "loadCachedData").returns(Promise.resolve());
|
||||
await feed.onAction({type: at.DISCOVERY_STREAM_CONFIG_CHANGE});
|
||||
|
||||
assert.calledOnce(feed.loadCachedData);
|
||||
assert.isTrue(feed.loaded);
|
||||
});
|
||||
it("should call this.loadCachedData if config.enabled changes to true ", async () => {
|
||||
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
|
||||
// force clear cached pref value
|
||||
feed._prefCache = {};
|
||||
configPrefStub.returns(JSON.stringify({enabled: true}));
|
||||
|
||||
await feed.onAction({type: at.INIT});
|
||||
assert.isTrue(feed.loaded);
|
||||
|
||||
feed._prefCache = {};
|
||||
configPrefStub.returns(JSON.stringify({enabled: false}));
|
||||
sandbox.stub(feed, "loadCachedData").returns(Promise.resolve());
|
||||
await feed.onAction({type: at.DISCOVERY_STREAM_CONFIG_CHANGE});
|
||||
|
||||
assert.notCalled(feed.loadCachedData);
|
||||
assert.isFalse(feed.loaded);
|
||||
});
|
||||
});
|
||||
describe("#onAction: UNINIT", () => {
|
||||
it("should remove pref listeners", async () => {
|
||||
sandbox.stub(global.Services.prefs, "removeObserver");
|
||||
|
||||
await feed.onAction({type: at.UNINIT});
|
||||
assert.calledWith(global.Services.prefs.removeObserver, CONFIG_PREF_NAME, feed);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -306,6 +306,95 @@ describe("PlacesFeed", () => {
|
|||
await feed.saveToPocket(action.data.site, action._target.browser);
|
||||
assert.notCalled(feed.store.dispatch);
|
||||
});
|
||||
it("should call handoffSearchToAwesomebar on HANDOFF_SEARCH_TO_AWESOMEBAR", () => {
|
||||
const action = {
|
||||
type: at.HANDOFF_SEARCH_TO_AWESOMEBAR,
|
||||
data: {hiddenFocus: false},
|
||||
meta: {fromTarget: {}},
|
||||
_target: {browser: {ownerGlobal: {gURLBar: {focus: () => {}}}}},
|
||||
};
|
||||
sinon.stub(feed, "handoffSearchToAwesomebar");
|
||||
feed.onAction(action);
|
||||
assert.calledWith(feed.handoffSearchToAwesomebar, action);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handoffSearchToAwesomebar", () => {
|
||||
let fakeUrlBar;
|
||||
let listeners;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeUrlBar = {
|
||||
focus: sinon.spy(),
|
||||
hiddenFocus: sinon.spy(),
|
||||
removeHiddenFocus: sinon.spy(),
|
||||
addEventListener: (ev, cb) => {
|
||||
listeners[ev] = cb;
|
||||
},
|
||||
removeEventListener: sinon.spy(),
|
||||
};
|
||||
listeners = {};
|
||||
});
|
||||
it("should properly handle hiddenFocus=false", () => {
|
||||
feed.handoffSearchToAwesomebar({
|
||||
_target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
|
||||
data: {hiddenFocus: false},
|
||||
meta: {fromTarget: {}},
|
||||
});
|
||||
assert.calledOnce(fakeUrlBar.focus);
|
||||
assert.notCalled(fakeUrlBar.hiddenFocus);
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.calledWith(feed.store.dispatch, {
|
||||
meta: {
|
||||
from: "ActivityStream:Main",
|
||||
skipMain: true,
|
||||
to: "ActivityStream:Content",
|
||||
toTarget: {},
|
||||
},
|
||||
type: "SHOW_SEARCH",
|
||||
});
|
||||
});
|
||||
it("should properly handle hiddenFocus=true", () => {
|
||||
feed.handoffSearchToAwesomebar({
|
||||
_target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
|
||||
data: {hiddenFocus: true},
|
||||
meta: {fromTarget: {}},
|
||||
});
|
||||
assert.calledOnce(fakeUrlBar.hiddenFocus);
|
||||
assert.notCalled(fakeUrlBar.focus);
|
||||
assert.notCalled(feed.store.dispatch);
|
||||
|
||||
// Now call keydown listener.
|
||||
feed.store.dispatch.resetHistory();
|
||||
listeners.keydown();
|
||||
assert.calledOnce(fakeUrlBar.removeHiddenFocus);
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.calledWith(feed.store.dispatch, {
|
||||
meta: {
|
||||
from: "ActivityStream:Main",
|
||||
skipMain: true,
|
||||
to: "ActivityStream:Content",
|
||||
toTarget: {},
|
||||
},
|
||||
type: "HIDE_SEARCH",
|
||||
});
|
||||
|
||||
// And then call blur listener.
|
||||
fakeUrlBar.removeHiddenFocus.resetHistory();
|
||||
feed.store.dispatch.resetHistory();
|
||||
listeners.blur();
|
||||
assert.calledOnce(fakeUrlBar.removeHiddenFocus);
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.calledWith(feed.store.dispatch, {
|
||||
meta: {
|
||||
from: "ActivityStream:Main",
|
||||
skipMain: true,
|
||||
to: "ActivityStream:Content",
|
||||
toTarget: {},
|
||||
},
|
||||
type: "SHOW_SEARCH",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#observe", () => {
|
||||
|
|
|
@ -1460,40 +1460,4 @@ describe("Top Stories Feed", () => {
|
|||
assert.calledOnce(instance.uninit);
|
||||
assert.calledOnce(instance.init);
|
||||
});
|
||||
describe("#layout", () => {
|
||||
it("should call maybeDispatchLayoutUpdate from fetchStories", async () => {
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
|
||||
const response = {
|
||||
"layout": [1, 2],
|
||||
};
|
||||
globals.set("fetch", fetchStub);
|
||||
fetchStub.resolves({ok: true, status: 200, json: () => Promise.resolve(response)});
|
||||
sinon.spy(instance, "maybeDispatchLayoutUpdate");
|
||||
|
||||
await instance.fetchStories();
|
||||
assert.calledOnce(instance.maybeDispatchLayoutUpdate);
|
||||
assert.calledWith(instance.maybeDispatchLayoutUpdate, [1, 2]);
|
||||
});
|
||||
it("should call maybeDispatchLayoutUpdate from loadCachedData", async () => {
|
||||
sinon.spy(instance, "maybeDispatchLayoutUpdate");
|
||||
instance.cache.get = () => ({stories: {layout: [2, 3]}});
|
||||
|
||||
await instance.loadCachedData();
|
||||
assert.calledOnce(instance.maybeDispatchLayoutUpdate);
|
||||
assert.calledWith(instance.maybeDispatchLayoutUpdate, [2, 3]);
|
||||
});
|
||||
it("should call dispatch from maybeDispatchLayoutUpdate with available data", () => {
|
||||
instance.maybeDispatchLayoutUpdate([1, 2]);
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
const [action] = instance.store.dispatch.firstCall.args;
|
||||
assert.equal(action.type, "CONTENT_LAYOUT");
|
||||
assert.deepEqual(action.data, [1, 2]);
|
||||
});
|
||||
it("should not call dispatch from maybeDispatchLayoutUpdate with no available data", () => {
|
||||
instance.maybeDispatchLayoutUpdate([]);
|
||||
assert.notCalled(instance.store.dispatch);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
[DEFAULT]
|
||||
prefs = dom.sidebar.enabled=true
|
||||
prefs =
|
||||
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
|
||||
dom.sidebar.enabled=true
|
||||
support-files =
|
||||
426329.xml
|
||||
483086-1.xml
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
prefs =
|
||||
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
|
||||
|
||||
[browser_BrowserErrorReporter.js]
|
||||
skip-if = (verify && !debug && (os == 'mac' || os == 'win'))
|
||||
|
|
Загрузка…
Ссылка в новой задаче