diff options
Diffstat (limited to 'browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx')
-rw-r--r-- | browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx | 620 |
1 files changed, 620 insertions, 0 deletions
diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx new file mode 100644 index 0000000000..ffe64f05f1 --- /dev/null +++ b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx @@ -0,0 +1,620 @@ +/* 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, { useEffect, useState } from "react"; +import { Localized } from "./MSLocalized"; +import { AboutWelcomeUtils } from "../lib/aboutwelcome-utils.mjs"; +import { MobileDownloads } from "./MobileDownloads"; +import { MultiSelect } from "./MultiSelect"; +import { Themes } from "./Themes"; +import { + SecondaryCTA, + StepsIndicator, + ProgressBar, +} from "./MultiStageAboutWelcome"; +import { LanguageSwitcher } from "./LanguageSwitcher"; +import { CTAParagraph } from "./CTAParagraph"; +import { HeroImage } from "./HeroImage"; +import { OnboardingVideo } from "./OnboardingVideo"; +import { AdditionalCTA } from "./AdditionalCTA"; +import { EmbeddedMigrationWizard } from "./EmbeddedMigrationWizard"; +import { AddonsPicker } from "./AddonsPicker"; +import { LinkParagraph } from "./LinkParagraph"; + +export const MultiStageProtonScreen = props => { + const { autoAdvance, handleAction, order } = props; + useEffect(() => { + if (autoAdvance) { + const timer = setTimeout(() => { + handleAction({ + currentTarget: { + value: autoAdvance, + }, + name: "AUTO_ADVANCE", + }); + }, 20000); + return () => clearTimeout(timer); + } + return () => {}; + }, [autoAdvance, handleAction, order]); + + return ( + <ProtonScreen + content={props.content} + id={props.id} + order={props.order} + activeTheme={props.activeTheme} + screenMultiSelects={props.screenMultiSelects} + setScreenMultiSelects={props.setScreenMultiSelects} + activeMultiSelect={props.activeMultiSelect} + setActiveMultiSelect={props.setActiveMultiSelect} + totalNumberOfScreens={props.totalNumberOfScreens} + handleAction={props.handleAction} + isFirstScreen={props.isFirstScreen} + isLastScreen={props.isLastScreen} + isSingleScreen={props.isSingleScreen} + previousOrder={props.previousOrder} + autoAdvance={props.autoAdvance} + isRtamo={props.isRtamo} + addonName={props.addonName} + isTheme={props.isTheme} + iconURL={props.iconURL} + messageId={props.messageId} + negotiatedLanguage={props.negotiatedLanguage} + langPackInstallPhase={props.langPackInstallPhase} + forceHideStepsIndicator={props.forceHideStepsIndicator} + ariaRole={props.ariaRole} + aboveButtonStepsIndicator={props.aboveButtonStepsIndicator} + /> + ); +}; + +export const ProtonScreenActionButtons = props => { + const { content, addonName, activeMultiSelect } = props; + const defaultValue = content.checkbox?.defaultValue; + + const [isChecked, setIsChecked] = useState(defaultValue || false); + const buttonRef = React.useRef(null); + + const shouldFocusButton = content?.primary_button?.should_focus_button; + + useEffect(() => { + if (shouldFocusButton) { + buttonRef.current?.focus(); + } + }, [shouldFocusButton]); + + if ( + !content.primary_button && + !content.secondary_button && + !content.additional_button + ) { + return null; + } + + // If we have a multi-select screen, we want to disable the primary button + // until the user has selected at least one item. + const isPrimaryDisabled = primaryDisabledValue => + primaryDisabledValue === "hasActiveMultiSelect" + ? !(activeMultiSelect?.length > 0) + : primaryDisabledValue; + + return ( + <div + className={`action-buttons ${ + content.additional_button ? "additional-cta-container" : "" + }`} + flow={content.additional_button?.flow} + alignment={content.additional_button?.alignment} + > + <Localized text={content.primary_button?.label}> + <button + ref={buttonRef} + className={`${content.primary_button?.style ?? "primary"}${ + content.primary_button?.has_arrow_icon ? " arrow-icon" : "" + }`} + // Whether or not the checkbox is checked determines which action + // should be handled. By setting value here, we indicate to + // this.handleAction() where in the content tree it should take + // the action to execute from. + value={isChecked ? "checkbox" : "primary_button"} + disabled={isPrimaryDisabled(content.primary_button?.disabled)} + onClick={props.handleAction} + data-l10n-args={ + addonName + ? JSON.stringify({ + "addon-name": addonName, + }) + : "" + } + /> + </Localized> + {content.additional_button ? ( + <AdditionalCTA content={content} handleAction={props.handleAction} /> + ) : null} + {content.checkbox ? ( + <div className="checkbox-container"> + <input + type="checkbox" + id="action-checkbox" + checked={isChecked} + onChange={() => { + setIsChecked(!isChecked); + }} + ></input> + <Localized text={content.checkbox.label}> + <label htmlFor="action-checkbox"></label> + </Localized> + </div> + ) : null} + {content.secondary_button ? ( + <SecondaryCTA + content={content} + handleAction={props.handleAction} + activeMultiSelect={activeMultiSelect} + /> + ) : null} + </div> + ); +}; + +export class ProtonScreen extends React.PureComponent { + componentDidMount() { + this.mainContentHeader.focus(); + } + + getScreenClassName( + isFirstScreen, + isLastScreen, + includeNoodles, + isVideoOnboarding, + isAddonsPicker + ) { + const screenClass = `screen-${this.props.order % 2 !== 0 ? 1 : 2}`; + + if (isVideoOnboarding) { + return "with-video"; + } + + if (isAddonsPicker) { + return "addons-picker"; + } + + return `${isFirstScreen ? `dialog-initial` : ``} ${ + isLastScreen ? `dialog-last` : `` + } ${includeNoodles ? `with-noodles` : ``} ${screenClass}`; + } + + renderTitle({ title, title_logo }) { + if (title_logo) { + const { alignment, ...rest } = title_logo; + return ( + <div + className="inline-icon-container" + alignment={alignment ?? "center"} + > + {this.renderPicture({ ...rest })} + <Localized text={title}> + <h1 id="mainContentHeader" /> + </Localized> + </div> + ); + } + return ( + <Localized text={title}> + <h1 id="mainContentHeader" /> + </Localized> + ); + } + + renderPicture({ + imageURL = "chrome://branding/content/about-logo.svg", + darkModeImageURL, + reducedMotionImageURL, + darkModeReducedMotionImageURL, + alt = "", + width, + height, + marginBlock, + marginInline, + className = "logo-container", + }) { + function getLoadingStrategy() { + for (let url of [ + imageURL, + darkModeImageURL, + reducedMotionImageURL, + darkModeReducedMotionImageURL, + ]) { + if (AboutWelcomeUtils.getLoadingStrategyFor(url) === "lazy") { + return "lazy"; + } + } + return "eager"; + } + + return ( + <picture className={className} style={{ marginInline, marginBlock }}> + {darkModeReducedMotionImageURL ? ( + <source + srcSet={darkModeReducedMotionImageURL} + media="(prefers-color-scheme: dark) and (prefers-reduced-motion: reduce)" + /> + ) : null} + {darkModeImageURL ? ( + <source + srcSet={darkModeImageURL} + media="(prefers-color-scheme: dark)" + /> + ) : null} + {reducedMotionImageURL ? ( + <source + srcSet={reducedMotionImageURL} + media="(prefers-reduced-motion: reduce)" + /> + ) : null} + <Localized text={alt}> + <div className="sr-only logo-alt" /> + </Localized> + <img + className="brand-logo" + style={{ height, width }} + src={imageURL} + alt="" + loading={getLoadingStrategy()} + role={alt ? null : "presentation"} + /> + </picture> + ); + } + + renderContentTiles() { + const { content } = this.props; + return ( + <React.Fragment> + {content.tiles && + content.tiles.type === "addons-picker" && + content.tiles.data ? ( + <AddonsPicker + content={content} + message_id={this.props.messageId} + handleAction={this.props.handleAction} + /> + ) : null} + {content.tiles && + content.tiles.type === "theme" && + content.tiles.data ? ( + <Themes + content={content} + activeTheme={this.props.activeTheme} + handleAction={this.props.handleAction} + /> + ) : null} + {content.tiles && + content.tiles.type === "mobile_downloads" && + content.tiles.data ? ( + <MobileDownloads + data={content.tiles.data} + handleAction={this.props.handleAction} + /> + ) : null} + {content.tiles && + content.tiles.type === "multiselect" && + content.tiles.data ? ( + <MultiSelect + content={content} + screenMultiSelects={this.props.screenMultiSelects} + setScreenMultiSelects={this.props.setScreenMultiSelects} + activeMultiSelect={this.props.activeMultiSelect} + setActiveMultiSelect={this.props.setActiveMultiSelect} + /> + ) : null} + {content.tiles && content.tiles.type === "migration-wizard" ? ( + <EmbeddedMigrationWizard handleAction={this.props.handleAction} /> + ) : null} + </React.Fragment> + ); + } + + renderNoodles() { + return ( + <React.Fragment> + <div className={"noodle orange-L"} /> + <div className={"noodle purple-C"} /> + <div className={"noodle solid-L"} /> + <div className={"noodle outline-L"} /> + <div className={"noodle yellow-circle"} /> + </React.Fragment> + ); + } + + renderLanguageSwitcher() { + return this.props.content.languageSwitcher ? ( + <LanguageSwitcher + content={this.props.content} + handleAction={this.props.handleAction} + negotiatedLanguage={this.props.negotiatedLanguage} + langPackInstallPhase={this.props.langPackInstallPhase} + messageId={this.props.messageId} + /> + ) : null; + } + + renderDismissButton() { + const { size, marginBlock, marginInline, label } = + this.props.content.dismiss_button; + return ( + <button + className="dismiss-button" + onClick={this.props.handleAction} + value="dismiss_button" + data-l10n-id={label?.string_id || "spotlight-dialog-close-button"} + button-size={size} + style={{ marginBlock, marginInline }} + ></button> + ); + } + + renderStepsIndicator() { + const currentStep = (this.props.order ?? 0) + 1; + const previousStep = (this.props.previousOrder ?? -1) + 1; + const { content, totalNumberOfScreens: total } = this.props; + return ( + <div + id="steps" + className={`steps${content.progress_bar ? " progress-bar" : ""}`} + data-l10n-id={ + content.steps_indicator?.string_id || + "onboarding-welcome-steps-indicator-label" + } + data-l10n-args={JSON.stringify({ + current: currentStep, + total: total ?? 0, + })} + data-l10n-attrs="aria-label" + role="progressbar" + aria-valuenow={currentStep} + aria-valuemin={1} + aria-valuemax={total} + > + {content.progress_bar ? ( + <ProgressBar + step={currentStep} + previousStep={previousStep} + totalNumberOfScreens={total} + /> + ) : ( + <StepsIndicator + order={this.props.order} + totalNumberOfScreens={total} + /> + )} + </div> + ); + } + + renderSecondarySection(content) { + return ( + <div + className={`section-secondary ${ + content.hide_secondary_section ? "with-secondary-section-hidden" : "" + }`} + style={ + content.background + ? { + background: content.background, + "--mr-secondary-background-position-y": + content.split_narrow_bkg_position, + } + : {} + } + > + <Localized text={content.image_alt_text}> + <div className="sr-only image-alt" role="img" /> + </Localized> + {content.hero_image ? ( + <HeroImage url={content.hero_image.url} /> + ) : ( + <React.Fragment> + <div className="message-text"> + <div className="spacer-top" /> + <Localized text={content.hero_text}> + <h1 /> + </Localized> + <div className="spacer-bottom" /> + </div> + </React.Fragment> + )} + </div> + ); + } + + renderOrderedContent(content) { + const elements = []; + for (const item of content) { + switch (item.type) { + case "text": + elements.push( + <LinkParagraph + text_content={item} + handleAction={this.props.handleAction} + /> + ); + break; + case "image": + elements.push( + this.renderPicture({ + imageURL: item.url, + darkModeImageURL: item.darkModeImageURL, + height: item.height, + width: item.width, + alt: item.alt_text, + marginInline: item.marginInline, + className: "inline-image", + }) + ); + } + } + return <>{elements}</>; + } + + render() { + const { + autoAdvance, + content, + isRtamo, + isTheme, + isFirstScreen, + isLastScreen, + isSingleScreen, + forceHideStepsIndicator, + ariaRole, + aboveButtonStepsIndicator, + } = this.props; + const includeNoodles = content.has_noodles; + // The default screen position is "center" + const isCenterPosition = content.position === "center" || !content.position; + const hideStepsIndicator = + autoAdvance || + content?.video_container || + isSingleScreen || + forceHideStepsIndicator; + const textColorClass = content.text_color + ? `${content.text_color}-text` + : ""; + // Assign proton screen style 'screen-1' or 'screen-2' to centered screens + // by checking if screen order is even or odd. + const screenClassName = isCenterPosition + ? this.getScreenClassName( + isFirstScreen, + isLastScreen, + includeNoodles, + content?.video_container, + content.tiles?.type === "addons-picker" + ) + : ""; + const isEmbeddedMigration = content.tiles?.type === "migration-wizard"; + + return ( + <main + className={`screen ${this.props.id || ""} + ${screenClassName} ${textColorClass}`} + role={ariaRole ?? "alertdialog"} + layout={content.layout} + pos={content.position || "center"} + tabIndex="-1" + aria-labelledby="mainContentHeader" + ref={input => { + this.mainContentHeader = input; + }} + > + {isCenterPosition ? null : this.renderSecondarySection(content)} + <div + className={`section-main ${ + isEmbeddedMigration ? "embedded-migration" : "" + }`} + hide-secondary-section={ + content.hide_secondary_section + ? String(content.hide_secondary_section) + : null + } + role="document" + > + {content.secondary_button_top ? ( + <SecondaryCTA + content={content} + handleAction={this.props.handleAction} + position="top" + /> + ) : null} + {includeNoodles ? this.renderNoodles() : null} + {content.dismiss_button ? this.renderDismissButton() : null} + <div + className={`main-content ${hideStepsIndicator ? "no-steps" : ""}`} + style={{ + background: + content.background && isCenterPosition + ? content.background + : null, + width: + content.width && content.position !== "split" + ? content.width + : null, + }} + > + {content.logo ? this.renderPicture(content.logo) : null} + + {isRtamo ? ( + <div className="rtamo-icon"> + <img + className={`${isTheme ? "rtamo-theme-icon" : "brand-logo"}`} + src={this.props.iconURL} + loading={AboutWelcomeUtils.getLoadingStrategyFor( + this.props.iconURL + )} + alt="" + role="presentation" + /> + </div> + ) : null} + + <div className="main-content-inner"> + <div className={`welcome-text ${content.title_style || ""}`}> + {content.title ? this.renderTitle(content) : null} + + {content.subtitle ? ( + <Localized text={content.subtitle}> + <h2 + data-l10n-args={JSON.stringify({ + "addon-name": this.props.addonName, + ...this.props.appAndSystemLocaleInfo?.displayNames, + })} + aria-flowto={ + this.props.messageId?.includes("FEATURE_TOUR") + ? "steps" + : "" + } + /> + </Localized> + ) : null} + {content.cta_paragraph ? ( + <CTAParagraph + content={content.cta_paragraph} + handleAction={this.props.handleAction} + /> + ) : null} + </div> + {content.video_container ? ( + <OnboardingVideo + content={content.video_container} + handleAction={this.props.handleAction} + /> + ) : null} + {content.above_button_content + ? this.renderOrderedContent(content.above_button_content) + : null} + {this.renderContentTiles()} + {this.renderLanguageSwitcher()} + {!hideStepsIndicator && aboveButtonStepsIndicator + ? this.renderStepsIndicator() + : null} + <ProtonScreenActionButtons + content={content} + addonName={this.props.addonName} + handleAction={this.props.handleAction} + activeMultiSelect={this.props.activeMultiSelect} + /> + </div> + {!hideStepsIndicator && !aboveButtonStepsIndicator + ? this.renderStepsIndicator() + : null} + </div> + </div> + <Localized text={content.info_text}> + <span className="info-text" /> + </Localized> + </main> + ); + } +} |