summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/components/CustomizeMenu
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/content-src/components/CustomizeMenu')
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx11
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx308
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx87
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss311
4 files changed, 717 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx
new file mode 100644
index 0000000000..522ea6841f
--- /dev/null
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+
+export class BackgroundsSection extends React.PureComponent {
+ render() {
+ return <div />;
+ }
+}
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
new file mode 100644
index 0000000000..423fd131e2
--- /dev/null
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import { actionCreators as ac } from "common/Actions.sys.mjs";
+
+export class ContentSection extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.onPreferenceSelect = this.onPreferenceSelect.bind(this);
+
+ // Refs are necessary for dynamically measuring drawer heights for slide animations
+ this.topSitesDrawerRef = React.createRef();
+ this.pocketDrawerRef = React.createRef();
+ }
+
+ inputUserEvent(eventSource, status) {
+ this.props.dispatch(
+ ac.UserEvent({
+ event: "PREF_CHANGED",
+ source: eventSource,
+ value: { status, menu_source: "CUSTOMIZE_MENU" },
+ })
+ );
+ }
+
+ onPreferenceSelect(e) {
+ let prefName = e.target.getAttribute("preference");
+ const eventSource = e.target.getAttribute("eventSource"); // TOP_SITES, TOP_STORIES, HIGHLIGHTS
+ let value;
+ if (e.target.nodeName === "SELECT") {
+ value = parseInt(e.target.value, 10);
+ } else if (e.target.nodeName === "INPUT") {
+ value = e.target.checked;
+ if (eventSource) {
+ this.inputUserEvent(eventSource, value);
+ }
+ }
+ this.props.setPref(prefName, value);
+ }
+
+ componentDidMount() {
+ this.setDrawerMargins();
+ }
+
+ componentDidUpdate() {
+ this.setDrawerMargins();
+ }
+
+ setDrawerMargins() {
+ this.setDrawerMargin(
+ `TOP_SITES`,
+ this.props.enabledSections.topSitesEnabled
+ );
+ this.setDrawerMargin(
+ `TOP_STORIES`,
+ this.props.enabledSections.pocketEnabled
+ );
+ }
+
+ setDrawerMargin(drawerID, isOpen) {
+ let drawerRef;
+
+ if (drawerID === `TOP_SITES`) {
+ drawerRef = this.topSitesDrawerRef.current;
+ } else if (drawerID === `TOP_STORIES`) {
+ drawerRef = this.pocketDrawerRef.current;
+ } else {
+ return;
+ }
+
+ let drawerHeight;
+
+ if (drawerRef) {
+ drawerHeight = parseFloat(window.getComputedStyle(drawerRef)?.height);
+
+ if (isOpen) {
+ drawerRef.style.marginTop = `0`;
+ } else {
+ drawerRef.style.marginTop = `-${drawerHeight}px`;
+ }
+ }
+ }
+
+ render() {
+ const {
+ enabledSections,
+ mayHaveSponsoredTopSites,
+ pocketRegion,
+ mayHaveSponsoredStories,
+ mayHaveRecentSaves,
+ openPreferences,
+ } = this.props;
+ const {
+ topSitesEnabled,
+ pocketEnabled,
+ highlightsEnabled,
+ showSponsoredTopSitesEnabled,
+ showSponsoredPocketEnabled,
+ showRecentSavesEnabled,
+ topSitesRowsCount,
+ } = enabledSections;
+
+ return (
+ <div className="home-section">
+ <div id="shortcuts-section" className="section">
+ <label className="switch">
+ <input
+ id="shortcuts-toggle"
+ checked={topSitesEnabled}
+ type="checkbox"
+ onChange={this.onPreferenceSelect}
+ preference="feeds.topsites"
+ aria-labelledby="custom-shortcuts-title"
+ aria-describedby="custom-shortcuts-subtitle"
+ eventSource="TOP_SITES"
+ />
+ <span className="slider" role="presentation"></span>
+ </label>
+ <div>
+ <h2 id="custom-shortcuts-title" className="title">
+ <label
+ htmlFor="shortcuts-toggle"
+ data-l10n-id="newtab-custom-shortcuts-title"
+ ></label>
+ </h2>
+ <p
+ id="custom-shortcuts-subtitle"
+ className="subtitle"
+ data-l10n-id="newtab-custom-shortcuts-subtitle"
+ ></p>
+ <div className="more-info-top-wrapper">
+ <div className="more-information" ref={this.topSitesDrawerRef}>
+ <select
+ id="row-selector"
+ className="selector"
+ name="row-count"
+ preference="topSitesRows"
+ value={topSitesRowsCount}
+ onChange={this.onPreferenceSelect}
+ disabled={!topSitesEnabled}
+ aria-labelledby="custom-shortcuts-title"
+ >
+ <option
+ value="1"
+ data-l10n-id="newtab-custom-row-selector"
+ data-l10n-args='{"num": 1}'
+ />
+ <option
+ value="2"
+ data-l10n-id="newtab-custom-row-selector"
+ data-l10n-args='{"num": 2}'
+ />
+ <option
+ value="3"
+ data-l10n-id="newtab-custom-row-selector"
+ data-l10n-args='{"num": 3}'
+ />
+ <option
+ value="4"
+ data-l10n-id="newtab-custom-row-selector"
+ data-l10n-args='{"num": 4}'
+ />
+ </select>
+ {mayHaveSponsoredTopSites && (
+ <div className="check-wrapper" role="presentation">
+ <input
+ id="sponsored-shortcuts"
+ className="sponsored-checkbox"
+ disabled={!topSitesEnabled}
+ checked={showSponsoredTopSitesEnabled}
+ type="checkbox"
+ onChange={this.onPreferenceSelect}
+ preference="showSponsoredTopSites"
+ eventSource="SPONSORED_TOP_SITES"
+ />
+ <label
+ className="sponsored"
+ htmlFor="sponsored-shortcuts"
+ data-l10n-id="newtab-custom-sponsored-sites"
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {pocketRegion && (
+ <div id="pocket-section" className="section">
+ <label className="switch">
+ <input
+ id="pocket-toggle"
+ checked={pocketEnabled}
+ type="checkbox"
+ onChange={this.onPreferenceSelect}
+ preference="feeds.section.topstories"
+ aria-labelledby="custom-pocket-title"
+ aria-describedby="custom-pocket-subtitle"
+ eventSource="TOP_STORIES"
+ />
+ <span className="slider" role="presentation"></span>
+ </label>
+ <div>
+ <h2 id="custom-pocket-title" className="title">
+ <label
+ htmlFor="pocket-toggle"
+ data-l10n-id="newtab-custom-pocket-title"
+ ></label>
+ </h2>
+ <p
+ id="custom-pocket-subtitle"
+ className="subtitle"
+ data-l10n-id="newtab-custom-pocket-subtitle"
+ />
+ {(mayHaveSponsoredStories || mayHaveRecentSaves) && (
+ <div className="more-info-pocket-wrapper">
+ <div className="more-information" ref={this.pocketDrawerRef}>
+ {mayHaveSponsoredStories && (
+ <div className="check-wrapper" role="presentation">
+ <input
+ id="sponsored-pocket"
+ className="sponsored-checkbox"
+ disabled={!pocketEnabled}
+ checked={showSponsoredPocketEnabled}
+ type="checkbox"
+ onChange={this.onPreferenceSelect}
+ preference="showSponsored"
+ eventSource="POCKET_SPOCS"
+ />
+ <label
+ className="sponsored"
+ htmlFor="sponsored-pocket"
+ data-l10n-id="newtab-custom-pocket-sponsored"
+ />
+ </div>
+ )}
+ {mayHaveRecentSaves && (
+ <div className="check-wrapper" role="presentation">
+ <input
+ id="recent-saves-pocket"
+ className="sponsored-checkbox"
+ disabled={!pocketEnabled}
+ checked={showRecentSavesEnabled}
+ type="checkbox"
+ onChange={this.onPreferenceSelect}
+ preference="showRecentSaves"
+ eventSource="POCKET_RECENT_SAVES"
+ />
+ <label
+ className="sponsored"
+ htmlFor="recent-saves-pocket"
+ data-l10n-id="newtab-custom-pocket-show-recent-saves"
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+
+ <div id="recent-section" className="section">
+ <label className="switch">
+ <input
+ id="highlights-toggle"
+ checked={highlightsEnabled}
+ type="checkbox"
+ onChange={this.onPreferenceSelect}
+ preference="feeds.section.highlights"
+ eventSource="HIGHLIGHTS"
+ aria-labelledby="custom-recent-title"
+ aria-describedby="custom-recent-subtitle"
+ />
+ <span className="slider" role="presentation"></span>
+ </label>
+ <div>
+ <h2 id="custom-recent-title" className="title">
+ <label
+ htmlFor="highlights-toggle"
+ data-l10n-id="newtab-custom-recent-title"
+ ></label>
+ </h2>
+
+ <p
+ id="custom-recent-subtitle"
+ className="subtitle"
+ data-l10n-id="newtab-custom-recent-subtitle"
+ />
+ </div>
+ </div>
+
+ <span className="divider" role="separator"></span>
+
+ <div>
+ <button
+ id="settings-link"
+ className="external-link"
+ onClick={openPreferences}
+ data-l10n-id="newtab-custom-settings"
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx
new file mode 100644
index 0000000000..2b16c7fd39
--- /dev/null
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { BackgroundsSection } from "content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection";
+import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection";
+import { connect } from "react-redux";
+import React from "react";
+import { CSSTransition } from "react-transition-group";
+
+export class _CustomizeMenu extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.onEntered = this.onEntered.bind(this);
+ this.onExited = this.onExited.bind(this);
+ }
+
+ onEntered() {
+ if (this.closeButton) {
+ this.closeButton.focus();
+ }
+ }
+
+ onExited() {
+ if (this.openButton) {
+ this.openButton.focus();
+ }
+ }
+
+ render() {
+ return (
+ <span>
+ <CSSTransition
+ timeout={300}
+ classNames="personalize-animate"
+ in={!this.props.showing}
+ appear={true}
+ >
+ <button
+ className="icon icon-settings personalize-button"
+ onClick={() => this.props.onOpen()}
+ data-l10n-id="newtab-personalize-icon-label"
+ ref={c => (this.openButton = c)}
+ />
+ </CSSTransition>
+ <CSSTransition
+ timeout={250}
+ classNames="customize-animate"
+ in={this.props.showing}
+ onEntered={this.onEntered}
+ onExited={this.onExited}
+ appear={true}
+ >
+ <div
+ className="customize-menu"
+ role="dialog"
+ data-l10n-id="newtab-personalize-dialog-label"
+ >
+ <button
+ onClick={() => this.props.onClose()}
+ className="close-button"
+ data-l10n-id="newtab-custom-close-button"
+ ref={c => (this.closeButton = c)}
+ />
+ <BackgroundsSection />
+ <ContentSection
+ openPreferences={this.props.openPreferences}
+ setPref={this.props.setPref}
+ enabledSections={this.props.enabledSections}
+ pocketRegion={this.props.pocketRegion}
+ mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites}
+ mayHaveSponsoredStories={
+ this.props.DiscoveryStream.config.show_spocs
+ }
+ mayHaveRecentSaves={this.props.DiscoveryStream.recentSavesEnabled}
+ dispatch={this.props.dispatch}
+ />
+ </div>
+ </CSSTransition>
+ </span>
+ );
+ }
+}
+
+export const CustomizeMenu = connect(state => ({
+ DiscoveryStream: state.DiscoveryStream,
+}))(_CustomizeMenu);
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
new file mode 100644
index 0000000000..98046c2326
--- /dev/null
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
@@ -0,0 +1,311 @@
+@media (max-height: 701px) {
+ .personalize-button {
+ position: absolute;
+ top: 16px;
+ inset-inline-end: 16px;
+ }
+}
+
+@media (min-height: 700px) {
+ .personalize-button {
+ position: fixed;
+ top: 16px;
+ inset-inline-end: 16px;
+ z-index: 1000;
+ }
+}
+
+.personalize-button {
+ border: 0;
+ border-radius: 4px;
+ background-color: transparent;
+ padding: 15px;
+
+ &:hover {
+ background-color: var(--newtab-element-hover-color);
+ }
+
+ &:active {
+ background-color: var(--newtab-element-active-color);
+ }
+
+ &:focus-visible {
+ @include ds-focus;
+ }
+
+ &.personalize-animate-exit-done {
+ visibility: hidden;
+ }
+}
+
+.customize-menu {
+ color: var(--newtab-text-primary-color);
+ background-color: var(--newtab-background-color-secondary);
+ width: 432px;
+ height: 100%;
+ position: fixed;
+ inset-block: 0;
+ inset-inline-end: 0;
+ z-index: 1001;
+ padding: 16px;
+ overflow: auto;
+ transform: translateX(435px);
+ visibility: hidden;
+ cursor: default;
+
+ @media (prefers-reduced-motion: no-preference) {
+ transition: transform 250ms $customize-menu-slide-bezier, visibility 250ms;
+ }
+
+ @media (forced-colors: active) {
+ border-inline-start: solid 1px;
+ }
+
+ &:dir(rtl) {
+ transform: translateX(-435px);
+ }
+
+ &.customize-animate-enter-done,
+ &.customize-animate-enter-active {
+ box-shadow: $shadow-large;
+ visibility: visible;
+ transform: translateX(0);
+ }
+
+ &.customize-animate-exit-active {
+ box-shadow: $shadow-large;
+ }
+
+ .close-button {
+ margin-inline-start: auto;
+ margin-bottom: 28px;
+ white-space: nowrap;
+ display: block;
+ background-color: var(--newtab-element-secondary-color);
+ padding: 8px 10px;
+ border: $customize-menu-border-tint;
+ border-radius: 4px;
+ color: var(--newtab-text-primary-color);
+ font-size: 13px;
+ font-weight: 600;
+ }
+
+ .close-button:hover {
+ background-color: var(--newtab-element-secondary-hover-color);
+ }
+
+ .close-button:hover:active {
+ background-color: var(--newtab-element-secondary-active-color);
+ }
+}
+
+.grid-skip {
+ display: contents;
+}
+
+.home-section {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: repeat(4, auto);
+ grid-row-gap: 32px;
+ padding: 0 16px;
+
+ .section {
+ display: grid;
+ grid-template-rows: auto;
+ grid-template-columns: auto 26px;
+
+ & > div {
+ grid-area: 1;
+ }
+
+ .title {
+ grid-column: 1 / -1;
+ margin: 0;
+ font-weight: 600;
+ font-size: 16px;
+ margin-bottom: 10px;
+ }
+
+ .subtitle {
+ margin: 0;
+ font-size: 14px;
+ }
+
+ .sponsored {
+ font-size: 14px;
+ margin-inline-start: 5px;
+ }
+
+ .check-wrapper {
+ position: relative;
+ }
+
+ .sponsored-checkbox {
+ margin-inline-start: 2px;
+ width: 16px;
+ height: 16px;
+ vertical-align: middle;
+ border: $customize-menu-border-tint;
+ box-sizing: border-box;
+ border-radius: 4px;
+ appearance: none;
+ background-color: var(--newtab-element-secondary-color);
+ }
+
+ .sponsored-checkbox:checked {
+ -moz-context-properties: fill;
+ fill: var(--newtab-primary-element-text-color);
+ background: url('chrome://global/skin/icons/check.svg') center no-repeat;
+ background-color: var(--newtab-primary-action-background);
+
+ @media (forced-colors: active) {
+ fill: $black;
+ }
+ }
+
+ .sponsored-checkbox:active + .checkmark {
+ fill: var(--newtab-element-secondary-color);
+ }
+
+ .selector {
+ color: var(--newtab-text-primary-color);
+ width: 118px;
+ display: block;
+ border: 1px solid var(--newtab-border-color);
+ border-radius: 4px;
+ appearance: none;
+ padding-block: 7px;
+ padding-inline: 10px 13px;
+ margin-inline-start: 2px;
+ margin-bottom: 2px;
+ -moz-context-properties: fill;
+ fill: var(--newtab-text-primary-color);
+ background: url('chrome://global/skin/icons/arrow-down-12.svg') right no-repeat;
+ background-size: 8px;
+ background-origin: content-box;
+ background-color: var(--newtab-background-color-secondary);
+
+ &:dir(rtl) {
+ background-position-x: left;
+ }
+ }
+
+ .switch {
+ position: relative;
+ display: inline-block;
+ width: 26px;
+ height: 16px;
+ grid-column: 2;
+ margin-top: 2px;
+ }
+
+ .switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ .slider {
+ position: absolute;
+ inset: 0;
+ transition: transform 250ms;
+ border-radius: 13px;
+ border: $customize-menu-border-tint;
+ background-color: var(--newtab-element-secondary-color);
+
+ &::before {
+ position: absolute;
+ content: '';
+ height: 8px;
+ width: 8px;
+ inset-inline-start: 3px;
+ bottom: 3px;
+ background-color: var(--newtab-primary-element-text-color);
+ transition: transform 250ms;
+ border-radius: 50%;
+ outline: $customize-menu-border-tint;
+ }
+ }
+
+ .switch input:focus-visible + .slider {
+ outline: 0;
+ box-shadow: $shadow-focus;
+ }
+
+ .switch input:not(:checked):focus-visible + .slider {
+ border: 1px solid var(--newtab-primary-action-background);
+ }
+
+ input:checked + .slider {
+ background-color: var(--newtab-primary-action-background);
+ }
+
+ input:checked + .slider::before {
+ transform: translateX(10px);
+ }
+
+ input:checked + .slider:dir(rtl)::before {
+ transform: translateX(-10px);
+ }
+
+ .more-info-top-wrapper,
+ .more-info-pocket-wrapper {
+ margin-inline-start: -2px;
+ overflow: hidden;
+
+ .more-information {
+ padding-top: 12px;
+ position: relative;
+ transition: margin-top 250ms $customize-menu-expand-bezier;
+ }
+ }
+
+ .more-info-top-wrapper {
+ .check-wrapper {
+ margin-top: 10px;
+ }
+ }
+ }
+
+ .divider {
+ border-top: 1px var(--newtab-border-color) solid;
+ margin: 0 -16px;
+ }
+
+ .external-link {
+ font-size: 14px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ -moz-context-properties: fill;
+ fill: var(--newtab-text-primary-color);
+ background: url('chrome://global/skin/icons/settings.svg') left no-repeat;
+ background-size: 16px;
+ padding-inline-start: 21px;
+ margin-bottom: 20px;
+ text-decoration: underline;
+
+ @media (forced-colors: active) {
+ padding: 8px 10px;
+ padding-inline-start: 21px;
+ }
+
+ &:dir(rtl) {
+ background-position-x: right;
+ }
+ }
+
+ .external-link:hover {
+ text-decoration: none;
+ }
+}
+
+.home-section .section .sponsored-checkbox:focus-visible,
+.selector:focus-visible,
+.external-link:focus-visible,
+.close-button:focus-visible {
+ border: 1px solid var(--newtab-primary-action-background);
+ outline: 0;
+ box-shadow: $shadow-focus;
+}