diff options
Diffstat (limited to 'browser/components/newtab/content-src/components/CustomizeMenu')
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; +} |