diff options
Diffstat (limited to 'browser/components/newtab/content-src')
18 files changed, 1106 insertions, 77 deletions
diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx index 1738f8f51a..61722fd418 100644 --- a/browser/components/newtab/content-src/components/Base/Base.jsx +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -12,6 +12,7 @@ import { CustomizeMenu } from "content-src/components/CustomizeMenu/CustomizeMen import React from "react"; import { Search } from "content-src/components/Search/Search"; import { Sections } from "content-src/components/Sections/Sections"; +import { Weather } from "content-src/components/Weather/Weather"; const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; @@ -235,7 +236,7 @@ export class BaseContent extends React.PureComponent { return ( <p className={`wallpaper-attribution`} - key={name} + key={name.string} data-l10n-id="newtab-wallpaper-attribution" data-l10n-args={JSON.stringify({ author_string: name.string, @@ -278,6 +279,15 @@ export class BaseContent extends React.PureComponent { `--newtab-wallpaper-dark`, `url(${darkWallpaper?.wallpaperUrl || ""})` ); + + // Add helper class to body if user has a wallpaper selected + if (lightWallpaper) { + global.document?.body.classList.add("hasWallpaperLight"); + } + + if (darkWallpaper) { + global.document?.body.classList.add("hasWallpaperDark"); + } } } @@ -290,6 +300,7 @@ export class BaseContent extends React.PureComponent { const activeWallpaper = prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + const weatherEnabled = prefs.showWeather; const { pocketConfig } = prefs; @@ -322,10 +333,12 @@ export class BaseContent extends React.PureComponent { showSponsoredPocketEnabled: prefs.showSponsored, showRecentSavesEnabled: prefs.showRecentSaves, topSitesRowsCount: prefs.topSitesRows, + weatherEnabled: prefs.showWeather, }; const pocketRegion = prefs["feeds.system.topstories"]; const mayHaveSponsoredStories = prefs["system.showSponsored"]; + const mayHaveWeather = prefs["system.showWeather"]; const { mayHaveSponsoredTopSites } = prefs; const outerClassName = [ @@ -358,6 +371,7 @@ export class BaseContent extends React.PureComponent { pocketRegion={pocketRegion} mayHaveSponsoredTopSites={mayHaveSponsoredTopSites} mayHaveSponsoredStories={mayHaveSponsoredStories} + mayHaveWeather={mayHaveWeather} spocMessageVariant={spocMessageVariant} showing={customizeMenuVisible} /> @@ -393,6 +407,13 @@ export class BaseContent extends React.PureComponent { <ConfirmDialog /> {wallpapersEnabled && this.renderWallpaperAttribution()} </main> + <aside> + {weatherEnabled && ( + <ErrorBoundary> + <Weather /> + </ErrorBoundary> + )} + </aside> </div> </div> ); @@ -410,4 +431,5 @@ export const Base = connect(state => ({ DiscoveryStream: state.DiscoveryStream, Search: state.Search, Wallpapers: state.Wallpapers, + Weather: state.Weather, }))(_Base); diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx index 1dd13fc965..494d506da9 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -28,7 +28,7 @@ export class ContentSection extends React.PureComponent { } onPreferenceSelect(e) { - // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS + // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS | WEATHER const { preference, eventSource } = e.target.dataset; let value; if (e.target.nodeName === "SELECT") { @@ -97,6 +97,7 @@ export class ContentSection extends React.PureComponent { pocketRegion, mayHaveSponsoredStories, mayHaveRecentSaves, + mayHaveWeather, openPreferences, spocMessageVariant, wallpapersEnabled, @@ -107,6 +108,7 @@ export class ContentSection extends React.PureComponent { topSitesEnabled, pocketEnabled, highlightsEnabled, + weatherEnabled, showSponsoredTopSitesEnabled, showSponsoredPocketEnabled, showRecentSavesEnabled, @@ -269,6 +271,22 @@ export class ContentSection extends React.PureComponent { </label> </div> + {mayHaveWeather && ( + <div id="weather-section" className="section"> + <label className="switch"> + <moz-toggle + id="weather-toggle" + pressed={weatherEnabled || null} + onToggle={this.onPreferenceSelect} + data-preference="showWeather" + data-eventSource="WEATHER" + data-l10n-id="newtab-custom-weather-toggle" + data-l10n-attrs="label, description" + /> + </label> + </div> + )} + {pocketRegion && mayHaveSponsoredStories && spocMessageVariant === "variant-c" && ( diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx index f1c723fed2..035e84af58 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx @@ -55,12 +55,14 @@ export class _CustomizeMenu extends React.PureComponent { 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)} - /> + <div className="close-button-wrapper"> + <button + onClick={() => this.props.onClose()} + className="close-button" + data-l10n-id="newtab-custom-close-button" + ref={c => (this.closeButton = c)} + /> + </div> <ContentSection openPreferences={this.props.openPreferences} setPref={this.props.setPref} @@ -71,6 +73,7 @@ export class _CustomizeMenu extends React.PureComponent { mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites} mayHaveSponsoredStories={this.props.mayHaveSponsoredStories} mayHaveRecentSaves={this.props.DiscoveryStream.recentSavesEnabled} + mayHaveWeather={this.props.mayHaveWeather} spocMessageVariant={this.props.spocMessageVariant} dispatch={this.props.dispatch} /> diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss index c20da5ce50..403a62a50f 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss +++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss @@ -47,7 +47,8 @@ inset-block: 0; inset-inline-end: 0; z-index: 1001; - padding: 16px; + padding-block: 0 var(--space-large); + padding-inline: var(--space-large); overflow: auto; transform: translateX(435px); visibility: hidden; @@ -85,9 +86,17 @@ box-shadow: $shadow-large; } + .close-button-wrapper { + position: sticky; + top: 0; + padding-block-start: var(--space-large); + background-color: var(--newtab-background-color-secondary); + z-index: 1; + } + .close-button { margin-inline-start: auto; - margin-bottom: 28px; + margin-inline-end: var(--space-large); white-space: nowrap; display: block; background-color: var(--newtab-element-secondary-color); @@ -117,7 +126,7 @@ grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); grid-row-gap: 32px; - padding: 0 16px; + padding: var(--space-large); .wallpapers-section h2 { font-size: inherit; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx index 8b9d64dfc1..79d453a7c9 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx @@ -126,8 +126,11 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { this.systemTick = this.systemTick.bind(this); this.syncRemoteSettings = this.syncRemoteSettings.bind(this); this.onStoryToggle = this.onStoryToggle.bind(this); + this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this); + this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this); this.state = { toggledStories: {}, + weatherQuery: "", }; } @@ -182,6 +185,16 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS); } + handleWeatherUpdate(e) { + this.setState({ weatherQuery: e.target.value || "" }); + } + + handleWeatherSubmit(e) { + e.preventDefault(); + const { weatherQuery } = this.state; + this.props.dispatch(ac.SetPref("weather.query", weatherQuery)); + } + renderComponent(width, component) { return ( <table> @@ -200,6 +213,46 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { ); } + renderWeatherData() { + const { suggestions } = this.props.state.Weather; + let weatherTable; + if (suggestions) { + weatherTable = ( + <div className="weather-section"> + <form onSubmit={this.handleWeatherSubmit}> + <label htmlFor="weather-query">Weather query</label> + <input + type="text" + min="3" + max="10" + id="weather-query" + onChange={this.handleWeatherUpdate} + value={this.weatherQuery} + /> + <button type="submit">Submit</button> + </form> + <table> + <tbody> + {suggestions.map(suggestion => ( + <tr className="message-item" key={suggestion.city_name}> + <td className="message-id"> + <span> + {suggestion.city_name} <br /> + </span> + </td> + <td className="message-summary"> + <pre>{JSON.stringify(suggestion, null, 2)}</pre> + </td> + </tr> + ))} + </tbody> + </table> + </div> + ); + } + return weatherTable; + } + renderFeedData(url) { const { feeds } = this.props.state.DiscoveryStream; const feed = feeds.data[url].data; @@ -376,6 +429,8 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { {this.renderSpocs()} <h3>Feeds Data</h3> {this.renderFeedsData()} + <h3>Weather Data</h3> + {this.renderWeatherData()} </div> ); } @@ -412,6 +467,7 @@ export class DiscoveryStreamAdminInner extends React.PureComponent { state={{ DiscoveryStream: this.props.DiscoveryStream, Personalization: this.props.Personalization, + Weather: this.props.Weather, }} otherPrefs={this.props.Prefs.values} dispatch={this.props.dispatch} @@ -500,4 +556,5 @@ export const DiscoveryStreamAdmin = connect(state => ({ DiscoveryStream: state.DiscoveryStream, Personalization: state.Personalization, Prefs: state.Prefs, + Weather: state.Weather, }))(_DiscoveryStreamAdmin); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss index a01227dd3d..dcad97c917 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss @@ -334,4 +334,16 @@ } } } + + .weather-section { + margin-block-end: 24px; + + form { + display: flex; + + label { + margin-inline-end: 12px; + } + } + } } diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx index b3d965530d..461d54899f 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -468,57 +468,34 @@ export class _DSCard extends React.PureComponent { dispatch={this.props.dispatch} spocMessageVariant={this.props.spocMessageVariant} /> - {saveToPocketCard && ( - <div className="card-stp-button-hover-background"> - <div className="card-stp-button-position-wrapper"> - {!this.props.flightId && stpButton()} - <DSLinkMenu - id={this.props.id} - index={this.props.pos} - dispatch={this.props.dispatch} - url={this.props.url} - title={this.props.title} - source={source} - type={this.props.type} - pocket_id={this.props.pocket_id} - shim={this.props.shim} - bookmarkGuid={this.props.bookmarkGuid} - flightId={ - !this.props.is_collection ? this.props.flightId : undefined - } - showPrivacyInfo={!!this.props.flightId} - onMenuUpdate={this.onMenuUpdate} - onMenuShow={this.onMenuShow} - saveToPocketCard={saveToPocketCard} - pocket_button_enabled={pocketButtonEnabled} - isRecentSave={isRecentSave} - /> - </div> + + <div className="card-stp-button-hover-background"> + <div className="card-stp-button-position-wrapper"> + {saveToPocketCard && <>{!this.props.flightId && stpButton()}</>} + + <DSLinkMenu + id={this.props.id} + index={this.props.pos} + dispatch={this.props.dispatch} + url={this.props.url} + title={this.props.title} + source={source} + type={this.props.type} + pocket_id={this.props.pocket_id} + shim={this.props.shim} + bookmarkGuid={this.props.bookmarkGuid} + flightId={ + !this.props.is_collection ? this.props.flightId : undefined + } + showPrivacyInfo={!!this.props.flightId} + onMenuUpdate={this.onMenuUpdate} + onMenuShow={this.onMenuShow} + saveToPocketCard={saveToPocketCard} + pocket_button_enabled={pocketButtonEnabled} + isRecentSave={isRecentSave} + /> </div> - )} - {!saveToPocketCard && ( - <DSLinkMenu - id={this.props.id} - index={this.props.pos} - dispatch={this.props.dispatch} - url={this.props.url} - title={this.props.title} - source={source} - type={this.props.type} - pocket_id={this.props.pocket_id} - shim={this.props.shim} - bookmarkGuid={this.props.bookmarkGuid} - flightId={ - !this.props.is_collection ? this.props.flightId : undefined - } - showPrivacyInfo={!!this.props.flightId} - hostRef={this.contextMenuButtonHostRef} - onMenuUpdate={this.onMenuUpdate} - onMenuShow={this.onMenuShow} - pocket_button_enabled={pocketButtonEnabled} - isRecentSave={isRecentSave} - /> - )} + </div> </article> ); } diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss index 9004e609df..e5ac19b553 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss @@ -54,12 +54,6 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); fill: $white; } - .context-menu-button { - position: static; - transition: none; - border-radius: 3px; - } - .context-menu-position-container { position: relative; } @@ -83,6 +77,10 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); padding: 6px; white-space: nowrap; color: $white; + + &:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + } } button, @@ -95,6 +93,28 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); } } + // Override note: The colors set here are intentionally static + // due to transparency issues over images. + .context-menu-button { + position: static; + transition: none; + border-radius: 3px; + background-color: var(--newtab-button-static-background); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + + &:active { + background-color: var(--newtab-button-static-active-background); + } + } + + &:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + background-color: var(--newtab-button-static-focus-background); + } + } + &.last-item { .card-stp-button-hover-background { .context-menu { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss index 54b39524d8..f726e936b9 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss @@ -37,6 +37,24 @@ a { text-decoration: none; } + + // Override note: The colors set here are intentionally static + // due to transparency issues over images. + .context-menu-button { + background-color: var(--newtab-button-static-background); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + + &:active { + background-color: var(--newtab-button-static-active-background); + } + } + + &:focus-visible { + background-color: var(--newtab-button-static-focus-background); + } + } } } } diff --git a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss index 4e4019513d..09a20c235d 100644 --- a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss +++ b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss @@ -126,12 +126,23 @@ $letter-fallback-color: $white; } } + // Necessary for when navigating by a keyboard, having the context + // menu open should display the "…" button. This style is a clone + // of the `:active` state for `.context-menu-button` + &.active { + .context-menu-button { + opacity: 1; + background-color: var(--newtab-button-active-background); + } + } + .context-menu-button { background-image: url('chrome://global/skin/icons/more.svg'); + background-color: var(--newtab-button-background); border: 0; border-radius: 4px; cursor: pointer; - fill: var(--newtab-text-primary-color); + fill: var(--newtab-button-text); -moz-context-properties: fill; height: 20px; width: 20px; @@ -141,11 +152,18 @@ $letter-fallback-color: $white; top: -20px; transition: opacity 200ms; - &:is(:active, :focus) { - outline: 0; + &:hover { + background-color: var(--newtab-button-hover-background); + + &:active { + background-color: var(--newtab-button-active-background); + } + } + + &:focus-visible { + background-color: var(--newtab-button-focus-background); + border-color: var(--newtab-button-focus-border); opacity: 1; - background-color: var(--newtab-element-hover-color); - fill: var(--newtab-primary-action-background); } } diff --git a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx index 0b51a146f5..6fcd4b3a15 100644 --- a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx +++ b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx @@ -4,6 +4,7 @@ import React from "react"; import { connect } from "react-redux"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; export class _WallpapersSection extends React.PureComponent { constructor(props) { @@ -25,6 +26,10 @@ export class _WallpapersSection extends React.PureComponent { const prefs = this.props.Prefs.values; const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + this.handleUserEvent({ + selected_wallpaper: id, + hadPreviousWallpaper: !!this.props.activeWallpaper, + }); // bug 1892095 if ( prefs["newtabWallpapers.wallpaper-dark"] === "" && @@ -50,6 +55,20 @@ export class _WallpapersSection extends React.PureComponent { handleReset() { const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + this.handleUserEvent({ + selected_wallpaper: "none", + hadPreviousWallpaper: !!this.props.activeWallpaper, + }); + } + + // Record user interaction when changing wallpaper and reseting wallpaper to default + handleUserEvent(data) { + this.props.dispatch( + ac.OnlyToMain({ + type: at.WALLPAPER_CLICK, + data, + }) + ); } render() { diff --git a/browser/components/newtab/content-src/components/Weather/Weather.jsx b/browser/components/newtab/content-src/components/Weather/Weather.jsx new file mode 100644 index 0000000000..9273f9a4bd --- /dev/null +++ b/browser/components/newtab/content-src/components/Weather/Weather.jsx @@ -0,0 +1,350 @@ +/* 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 { connect } from "react-redux"; +import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; +import React from "react"; + +const VISIBLE = "visible"; +const VISIBILITY_CHANGE_EVENT = "visibilitychange"; + +export class _Weather extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + contextMenuKeyboard: false, + showContextMenu: false, + url: "https://example.com", + impressionSeen: false, + errorSeen: false, + }; + this.setImpressionRef = element => { + this.impressionElement = element; + }; + this.setErrorRef = element => { + this.errorElement = element; + }; + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onUpdate = this.onUpdate.bind(this); + this.onProviderClick = this.onProviderClick.bind(this); + } + + componentDidMount() { + const { props } = this; + + if (!props.dispatch) { + return; + } + + if (props.document.visibilityState === VISIBLE) { + // Setup the impression observer once the page is visible. + this.setImpressionObservers(); + } else { + // We should only ever send the latest impression stats ping, so remove any + // older listeners. + if (this._onVisibilityChange) { + props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + + this._onVisibilityChange = () => { + if (props.document.visibilityState === VISIBLE) { + // Setup the impression observer once the page is visible. + this.setImpressionObservers(); + props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + }; + props.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + componentWillUnmount() { + // Remove observers on unmount + if (this.observer && this.impressionElement) { + this.observer.unobserve(this.impressionElement); + } + if (this.observer && this.errorElement) { + this.observer.unobserve(this.errorElement); + } + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + setImpressionObservers() { + if (this.impressionElement) { + this.observer = new IntersectionObserver(this.onImpression.bind(this)); + this.observer.observe(this.impressionElement); + } + if (this.errorElement) { + this.observer = new IntersectionObserver(this.onError.bind(this)); + this.observer.observe(this.errorElement); + } + } + + onImpression(entries) { + if (this.state) { + const entry = entries.find(e => e.isIntersecting); + + if (entry) { + if (this.impressionElement) { + this.observer.unobserve(this.impressionElement); + } + + this.props.dispatch( + ac.OnlyToMain({ + type: at.WEATHER_IMPRESSION, + }) + ); + + // Stop observing since element has been seen + this.setState({ + impressionSeen: true, + }); + } + } + } + + onError(entries) { + if (this.state) { + const entry = entries.find(e => e.isIntersecting); + + if (entry) { + if (this.errorElement) { + this.observer.unobserve(this.errorElement); + } + + this.props.dispatch( + ac.OnlyToMain({ + type: at.WEATHER_LOAD_ERROR, + }) + ); + + // Stop observing since element has been seen + this.setState({ + errorSeen: true, + }); + } + } + } + + openContextMenu(isKeyBoard) { + if (this.props.onUpdate) { + this.props.onUpdate(true); + } + this.setState({ + showContextMenu: true, + contextMenuKeyboard: isKeyBoard, + }); + } + + onClick(event) { + event.preventDefault(); + this.openContextMenu(false, event); + } + + onKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this.openContextMenu(true, event); + } + } + + onUpdate(showContextMenu) { + if (this.props.onUpdate) { + this.props.onUpdate(showContextMenu); + } + this.setState({ showContextMenu }); + } + + onProviderClick() { + this.props.dispatch( + ac.OnlyToMain({ + type: at.WEATHER_OPEN_PROVIDER_URL, + data: { + source: "WEATHER", + }, + }) + ); + } + + render() { + // Check if weather should be rendered + const isWeatherEnabled = this.props.Prefs.values["system.showWeather"]; + + if (!isWeatherEnabled || !this.props.Weather.initialized) { + return false; + } + + const { showContextMenu } = this.state; + + const WEATHER_SUGGESTION = this.props.Weather.suggestions?.[0]; + + const { + className, + index, + dispatch, + eventSource, + shouldSendImpressionStats, + } = this.props; + const { props } = this; + const isContextMenuOpen = this.state.activeCard === index; + + const outerClassName = [ + "weather", + className, + isContextMenuOpen && "active", + props.placeholder && "placeholder", + ] + .filter(v => v) + .join(" "); + + const showDetailedView = + this.props.Prefs.values["weather.display"] === "detailed"; + + // Note: The temperature units/display options will become secondary menu items + const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [ + ...(this.props.Prefs.values["weather.locationSearchEnabled"] + ? ["ChangeWeatherLocation"] + : []), + ...(this.props.Prefs.values["weather.temperatureUnits"] === "f" + ? ["ChangeTempUnitCelsius"] + : ["ChangeTempUnitFahrenheit"]), + ...(this.props.Prefs.values["weather.display"] === "simple" + ? ["ChangeWeatherDisplayDetailed"] + : ["ChangeWeatherDisplaySimple"]), + "HideWeather", + "OpenLearnMoreURL", + ]; + + // Only return the widget if we have data. Otherwise, show error state + if (WEATHER_SUGGESTION) { + return ( + <div ref={this.setImpressionRef} className={outerClassName}> + <div className="weatherCard"> + <a + data-l10n-id="newtab-weather-see-forecast" + data-l10n-args='{"provider": "AccuWeather"}' + href={WEATHER_SUGGESTION.forecast.url} + className="weatherInfoLink" + onClick={this.onProviderClick} + > + <div className="weatherIconCol"> + <span + className={`weatherIcon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}`} + /> + </div> + <div className="weatherText"> + <div className="weatherForecastRow"> + <span className="weatherTemperature"> + { + WEATHER_SUGGESTION.current_conditions.temperature[ + this.props.Prefs.values["weather.temperatureUnits"] + ] + } + °{this.props.Prefs.values["weather.temperatureUnits"]} + </span> + </div> + <div className="weatherCityRow"> + <span className="weatherCity"> + {WEATHER_SUGGESTION.city_name} + </span> + </div> + {showDetailedView ? ( + <div className="weatherDetailedSummaryRow"> + <div className="weatherHighLowTemps"> + {/* Low Forecasted Temperature */} + <span> + { + WEATHER_SUGGESTION.forecast.high[ + this.props.Prefs.values["weather.temperatureUnits"] + ] + } + ° + {this.props.Prefs.values["weather.temperatureUnits"]} + </span> + {/* Spacer / Bullet */} + <span>•</span> + {/* Low Forecasted Temperature */} + <span> + { + WEATHER_SUGGESTION.forecast.low[ + this.props.Prefs.values["weather.temperatureUnits"] + ] + } + ° + {this.props.Prefs.values["weather.temperatureUnits"]} + </span> + </div> + <span className="weatherTextSummary"> + {WEATHER_SUGGESTION.current_conditions.summary} + </span> + </div> + ) : null} + </div> + </a> + <div className="weatherButtonContextMenuWrapper"> + <button + aria-haspopup="true" + onKeyDown={this.onKeyDown} + onClick={this.onClick} + data-l10n-id="newtab-menu-section-tooltip" + className="weatherButtonContextMenu" + > + {showContextMenu ? ( + <LinkMenu + dispatch={dispatch} + index={index} + source={eventSource} + onUpdate={this.onUpdate} + options={WEATHER_SOURCE_CONTEXT_MENU_OPTIONS} + site={{ + url: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page", + }} + link="https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page" + shouldSendImpressionStats={shouldSendImpressionStats} + /> + ) : null} + </button> + </div> + </div> + <span + data-l10n-id="newtab-weather-sponsored" + data-l10n-args='{"provider": "AccuWeather"}' + className="weatherSponsorText" + ></span> + </div> + ); + } + + return ( + <div ref={this.setErrorRef} className={outerClassName}> + <div className="weatherNotAvailable"> + <span className="icon icon-small-spacer icon-info-critical" />{" "} + <span data-l10n-id="newtab-weather-error-not-available"></span> + </div> + </div> + ); + } +} + +export const Weather = connect(state => ({ + Weather: state.Weather, + Prefs: state.Prefs, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, +}))(_Weather); diff --git a/browser/components/newtab/content-src/components/Weather/_Weather.scss b/browser/components/newtab/content-src/components/Weather/_Weather.scss new file mode 100644 index 0000000000..0616530f98 --- /dev/null +++ b/browser/components/newtab/content-src/components/Weather/_Weather.scss @@ -0,0 +1,393 @@ +// Custom font sizing for weather widget +:root { + --newtab-weather-content-font-size: 11px; + --newtab-weather-sponsor-font-size: 8px; +} + +.weather { + font-size: var(--font-size-root); + position: absolute; + left: var(--space-xlarge); + top: var(--space-xlarge); + z-index: 1; +} + +// Unavailable / Error State +.weatherNotAvailable { + font-size: var(--newtab-weather-content-font-size); + color: var(--text-color-error); + display: flex; + align-items: center; + + .icon { + fill: var(--icon-color-critical); + -moz-context-properties: fill; + } +} + +.weatherCard { + margin-block-end: var(--space-xsmall); + display: flex; + flex-wrap: nowrap; + align-items: stretch; + border-radius: var(--border-radius-medium); + overflow: hidden; + + &:hover, &:focus-within { + ~ .weatherSponsorText { + visibility: visible; + } + } + + &:focus-within { + overflow: visible; + } + + &:hover { + box-shadow: var(--box-shadow-10); + background: var(--background-color-box); + } + + a { + color: var(--text-color); + } + +} + +.weatherSponsorText { + visibility: hidden; + font-size: var(--newtab-weather-sponsor-font-size); + color: var(--text-color-deemphasized); +} + +.weatherInfoLink, .weatherButtonContextMenuWrapper { + appearance: none; + background-color: var(--background-color-ghost); + border: 0; + padding: var(--space-small); + cursor: pointer; + + &:hover { + // TODO: Add Wallpaper Background Color Fix + background-color: var(--button-background-color-ghost-hover); + + &::after { + background-color: transparent + } + + &:active { + // TODO: Add Wallpaper Background Color Fix + background-color: var(--button-background-color-ghost-active); + } + } + + &:focus-visible { + outline: var(--focus-outline); + } + + // Contrast fix for users who have wallpapers set + .hasWallpaperDark & { + @media (prefers-color-scheme: dark) { + // TODO: Replace with token + background-color: rgba(35, 34, 43, 70%); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + } + + &:hover:active { + background-color: var(--newtab-button-static-active-background); + } + } + + @media (prefers-contrast) and (prefers-color-scheme: dark) { + background-color: var(--background-color-box); + } + } + + .hasWallpaperLight & { + @media (prefers-color-scheme: light) { + // TODO: Replace with token + background-color: rgba(255, 255, 255, 70%); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + } + + &:hover:active { + background-color: var(--newtab-button-static-active-background); + } + } + + @media (prefers-contrast) and (prefers-color-scheme: light) { + background-color: var(--background-color-box); + } + } + +} + +.weatherInfoLink { + display: flex; + gap: var(--space-medium); + padding: var(--space-small) var(--space-medium); + border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium); + text-decoration: none; + color: var(--text-color);; + min-width: 130px; + max-width: 190px; + text-overflow: ellipsis; + + @media(min-width: $break-point-medium) { + min-width: unset; + } + + &:hover ~.weatherButtonContextMenuWrapper { + &::after { + background-color: transparent + } + } + + &:focus-visible { + border-radius: var(--border-radius-medium); + + ~ .weatherButtonContextMenuWrapper { + &::after { + background-color: transparent + } + } + } +} + +.weatherButtonContextMenuWrapper { + position: relative; + cursor: pointer; + border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0; + display: flex; + align-items: stretch; + width: 50px; + padding: 0; + + &::after { + content: ''; + left: 0; + top: 10px; + height: calc(100% - 20px); + width: 1px; + background-color: var(--newtab-button-static-background); + display: block; + position: absolute; + z-index: 0; + } + + @media (prefers-color-scheme: dark) { + &::after { + background-color: var(--color-gray-70); + } + } + + &:hover { + &::after { + background-color: transparent + } + } + + &:focus-visible { + border-radius: var(--border-radius-medium); + + &::after { + background-color: transparent + } + } +} + +.weatherButtonContextMenu { + background-image: url('chrome://global/skin/icons/more.svg'); + background-repeat: no-repeat; + background-size: var(--size-item-small) auto; + background-position: center; + background-color: transparent; + cursor: pointer; + fill: var(--icon-color); + -moz-context-properties: fill; + width: 100%; + height: 100%; + border: 0; + appearance: none; + min-width: var(--size-item-large); +} + +.weatherText { + height: min-content; +} + +.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-small); +} + +.weatherForecastRow { + text-transform: uppercase; + font-weight: var(--font-weight-bold); +} + +.weatherCityRow { + color: var(--text-color-deemphasized); +} + +.weatherCity { + text-overflow: ellipsis; + font-size: var(--font-size-small); +} + +// Add additional margin if detailed summary is in view +.weatherCityRow + .weatherDetailedSummaryRow { + margin-block-start: var(--space-xsmall); +} + +.weatherDetailedSummaryRow { + font-size: var(--newtab-weather-content-font-size); + gap: var(--space-large); +} + +.weatherHighLowTemps { + display: flex; + gap: var(--space-xxsmall); + text-transform: uppercase; + word-spacing: var(--space-xxsmall); +} + +.weatherTextSummary { + text-align: center; + max-width: 90px; +} + +.weatherTemperature { + font-size: var(--font-size-large); +} + +// Weather Symbol Icons +.weatherIconCol { + width: var(--size-item-large); + height: var(--size-item-large); + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + align-self: center; +} + +.weatherIcon { + width: var(--size-item-large); + height: auto; + vertical-align: middle; + + @media (prefers-contrast) { + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; + } + + &.iconId1 { + content: url('chrome://browser/skin/weather/sunny.svg'); + // height: var(--size-item-large); + } + + &.iconId2 { + content: url('chrome://browser/skin/weather/mostly-sunny.svg'); + // height: var(--size-item-large); + } + + &:is(.iconId3, .iconId4, .iconId6) { + content: url('chrome://browser/skin/weather/partly-sunny.svg'); + // height: var(--size-item-large); + } + + &.iconId5 { + content: url('chrome://browser/skin/weather/hazy-sunshine.svg'); + // height: var(--size-item-large); + } + + &:is(.iconId7, .iconId8) { + content: url('chrome://browser/skin/weather/cloudy.svg'); + } + + &.iconId11 { + content: url('chrome://browser/skin/weather/fog.svg'); + } + + &.iconId12 { + content: url('chrome://browser/skin/weather/showers.svg'); + } + + &:is(.iconId13, .iconId14) { + content: url('chrome://browser/skin/weather/mostly-cloudy-with-showers.svg'); + // height: var(--size-item-large); + } + + &.iconId15 { + content: url('chrome://browser/skin/weather/thunderstorms.svg'); + } + + &:is(.iconId16, .iconId17) { + content: url('chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg'); + } + + &.iconId18 { + content: url('chrome://browser/skin/weather/rain.svg'); + } + + &:is(.iconId19, .iconId20, .iconId25) { + content: url('chrome://browser/skin/weather/flurries.svg'); + } + + &.iconId21 { + content: url('chrome://browser/skin/weather/partly-sunny-with-flurries.svg'); + } + + &:is(.iconId22, .iconId23) { + content: url('chrome://browser/skin/weather/snow.svg'); + } + + &:is(.iconId24, .iconId31) { + content: url('chrome://browser/skin/weather/ice.svg'); + } + + &:is(.iconId26, .iconId29) { + content: url('chrome://browser/skin/weather/freezing-rain.svg'); + } + + &.iconId30 { + content: url('chrome://browser/skin/weather/hot.svg'); + } + + &.iconId32 { + content: url('chrome://browser/skin/weather/windy.svg'); + } + + &.iconId33 { + content: url('chrome://browser/skin/weather/night-clear.svg'); + } + + &:is(.iconId34, .iconId35, .iconId36, .iconId38) { + content: url('chrome://browser/skin/weather/night-mostly-clear.svg'); + } + + &.iconId37 { + content: url('chrome://browser/skin/weather/night-hazy-moonlight.svg'); + } + + &:is(.iconId39, .iconId40) { + content: url('chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg'); + height: var(--size-item-large); + } + + &:is(.iconId41, .iconId42) { + content: url('chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg'); + } + + &:is(.iconId43, .iconId44) { + content: url('chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg'); + } +} diff --git a/browser/components/newtab/content-src/lib/link-menu-options.mjs b/browser/components/newtab/content-src/lib/link-menu-options.mjs index f10a5e34c6..23dcf8b050 100644 --- a/browser/components/newtab/content-src/lib/link-menu-options.mjs +++ b/browser/components/newtab/content-src/lib/link-menu-options.mjs @@ -306,4 +306,68 @@ export const LinkMenuOptions = { : LinkMenuOptions.EmptyItem(), OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(), + ChangeWeatherLocation: () => ({ + id: "newtab-weather-menu-change-location", + action: ac.OnlyToMain({ + type: at.CHANGE_WEATHER_LOCATION, + data: { url: "https://mozilla.org" }, + }), + }), + ChangeWeatherDisplaySimple: () => ({ + id: "newtab-weather-menu-change-weather-display-simple", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.display", + value: "simple", + }, + }), + }), + ChangeWeatherDisplayDetailed: () => ({ + id: "newtab-weather-menu-change-weather-display-detailed", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.display", + value: "detailed", + }, + }), + }), + ChangeTempUnitFahrenheit: () => ({ + id: "newtab-weather-menu-change-temperature-units-fahrenheit", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.temperatureUnits", + value: "f", + }, + }), + }), + ChangeTempUnitCelsius: () => ({ + id: "newtab-weather-menu-change-temperature-units-celsius", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.temperatureUnits", + value: "c", + }, + }), + }), + HideWeather: () => ({ + id: "newtab-weather-menu-hide-weather", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "showWeather", + value: false, + }, + }), + }), + OpenLearnMoreURL: site => ({ + id: "newtab-weather-menu-learn-more", + action: ac.OnlyToMain({ + type: at.OPEN_LINK, + data: { url: site.url }, + }), + }), }; diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index d2e66667b2..580f35416e 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -149,6 +149,7 @@ input { @import '../components/ConfirmDialog/ConfirmDialog'; @import '../components/CustomizeMenu/CustomizeMenu'; @import '../components/WallpapersSection/WallpapersSection'; +@import '../components/Weather/Weather'; @import '../components/Card/Card'; @import '../components/CollapsibleSection/CollapsibleSection'; @import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin'; diff --git a/browser/components/newtab/content-src/styles/_icons.scss b/browser/components/newtab/content-src/styles/_icons.scss index 8be97ad9ae..39879b2b44 100644 --- a/browser/components/newtab/content-src/styles/_icons.scss +++ b/browser/components/newtab/content-src/styles/_icons.scss @@ -4,7 +4,7 @@ background-size: $icon-size; -moz-context-properties: fill; display: inline-block; - color: var(--newtab-text-primary-color); + color: var(--icon-color); fill: currentColor; height: $icon-size; vertical-align: middle; @@ -70,6 +70,10 @@ background-image: url('chrome://global/skin/icons/info.svg'); } + &.icon-info-critical { + background-image: url('chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg'); + } + &.icon-help { background-image: url('chrome://global/skin/icons/help.svg'); } @@ -167,6 +171,10 @@ background-image: url('chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg'); } + &.icon-weather { + background-image: url('chrome://browser/skin/weather/sunny.svg'); + } + &.icon-highlights { background-image: url('chrome://global/skin/icons/highlights.svg'); } diff --git a/browser/components/newtab/content-src/styles/_theme.scss b/browser/components/newtab/content-src/styles/_theme.scss index 6b097ae93e..78b54f4f8e 100644 --- a/browser/components/newtab/content-src/styles/_theme.scss +++ b/browser/components/newtab/content-src/styles/_theme.scss @@ -38,6 +38,21 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15; --newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #{$black}); --newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #{$black}); + // --newtab-button-*-color is used on all new page card/top site options buttons + --newtab-button-background: var(--button-background-color); + --newtab-button-focus-background: var(--newtab-button-background); + --newtab-button-focus-border: var(--focus-outline-color); + --newtab-button-hover-background: var(--button-background-color-hover); + --newtab-button-active-background: var(--button-background-color-active); + --newtab-button-text: var(--button-text-color); + + // --newtab-button-static*-color is used on pocket cards and require a + // static color unit due to transparency issues with `color-mix` + --newtab-button-static-background: #F0F0F4; + --newtab-button-static-focus-background: var(--newtab-button-static-background); + --newtab-button-static-hover-background: #E0E0E6; + --newtab-button-static-active-background: #CFCFD8; + // --newtab-element-secondary*-color is used when an element needs to be set // off from the secondary background color. --newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent); @@ -87,6 +102,12 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15; --newtab-primary-element-text-color: #{$primary-text-color-dark}; --newtab-wordmark-color: #{$newtab-wordmark-darktheme-color}; --newtab-status-success: #{$status-dark-green}; + + // --newtab-button-static*-color is used on pocket cards and require a + // static color unit due to transparency issues with `color-mix` + --newtab-button-static-background: #2B2A33; + --newtab-button-static-hover-background: #52525E; + --newtab-button-static-active-background: #5B5B66; } } diff --git a/browser/components/newtab/content-src/styles/_variables.scss b/browser/components/newtab/content-src/styles/_variables.scss index 9fd0083841..43672c7796 100644 --- a/browser/components/newtab/content-src/styles/_variables.scss +++ b/browser/components/newtab/content-src/styles/_variables.scss @@ -157,14 +157,17 @@ $customize-menu-border-tint: 1px solid rgba(0, 0, 0, 15%); @mixin context-menu-button { .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url('chrome://global/skin/icons/more.svg'); background-position: 55%; - border: $border-primary; + border: 0; + outline: $border-primary; + outline-width: 0; border-radius: 100%; box-shadow: $context-menu-button-boxshadow; cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: $context-menu-button-size; inset-inline-end: math.div(-$context-menu-button-size, 2); opacity: 0; @@ -175,10 +178,26 @@ $customize-menu-border-tint: 1px solid rgba(0, 0, 0, 15%); transition-property: transform, opacity; width: $context-menu-button-size; - &:is(:active, :focus) { + &:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } + + &:is(:hover) { + background-color: var(--newtab-button-hover-background); + } + + &:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; + } + + &:is(:active) { + background-color: var(--newtab-button-active-background); + } + + } } |