diff --git a/.eslintrc b/.eslintrc index acc3d4002c6..0c4eb6bfaaf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -57,7 +57,7 @@ "react/no-unused-prop-types": "error", "react/self-closing-comp": "error", "import/no-unresolved": [ "error", { - "ignore": [ "jquery", "amp-block-editor-data", "amp-setup" ] + "ignore": [ "jquery", "amp-block-editor-data", "amp-settings" ] } ], "import/order": [ "error", { "groups": [ "builtin", [ "external", "unknown" ], "internal", "parent", "sibling", "index" ] } ], "jsdoc/check-indentation": "error", diff --git a/assets/src/setup/components/amp-info/index.js b/assets/src/components/amp-info/index.js similarity index 100% rename from assets/src/setup/components/amp-info/index.js rename to assets/src/components/amp-info/index.js diff --git a/assets/src/setup/components/amp-info/style.css b/assets/src/components/amp-info/style.css similarity index 100% rename from assets/src/setup/components/amp-info/style.css rename to assets/src/components/amp-info/style.css diff --git a/assets/src/setup/components/amp-info/test/__snapshots__/index.js.snap b/assets/src/components/amp-info/test/__snapshots__/index.js.snap similarity index 100% rename from assets/src/setup/components/amp-info/test/__snapshots__/index.js.snap rename to assets/src/components/amp-info/test/__snapshots__/index.js.snap diff --git a/assets/src/setup/components/amp-info/test/index.js b/assets/src/components/amp-info/test/index.js similarity index 100% rename from assets/src/setup/components/amp-info/test/index.js rename to assets/src/components/amp-info/test/index.js diff --git a/assets/src/setup/components/amp-notice/index.js b/assets/src/components/amp-notice/index.js similarity index 96% rename from assets/src/setup/components/amp-notice/index.js rename to assets/src/components/amp-notice/index.js index 6ce4887bd7c..dc7df6a477d 100644 --- a/assets/src/setup/components/amp-notice/index.js +++ b/assets/src/components/amp-notice/index.js @@ -73,7 +73,7 @@ function getNoticeIcon( type ) { * @param {string} props.size The notice size. * @param {string} props.type The notice type. */ -export function AMPNotice( { children, className, size, type } ) { +export function AMPNotice( { children, className, size = NOTICE_SIZE_LARGE, type = NOTICE_TYPE_INFO } ) { const noticeIcon = getNoticeIcon( type ); const classNames = [ @@ -98,6 +98,6 @@ export function AMPNotice( { children, className, size, type } ) { AMPNotice.propTypes = { children: PropTypes.node, className: PropTypes.string, - size: PropTypes.oneOf( [ NOTICE_SIZE_LARGE, NOTICE_SIZE_SMALL ] ).isRequired, - type: PropTypes.oneOf( [ NOTICE_TYPE_INFO, NOTICE_TYPE_SUCCESS, NOTICE_TYPE_WARNING ] ).isRequired, + size: PropTypes.oneOf( [ NOTICE_SIZE_LARGE, NOTICE_SIZE_SMALL ] ), + type: PropTypes.oneOf( [ NOTICE_TYPE_INFO, NOTICE_TYPE_SUCCESS, NOTICE_TYPE_WARNING ] ), }; diff --git a/assets/src/setup/components/amp-notice/style.css b/assets/src/components/amp-notice/style.css similarity index 76% rename from assets/src/setup/components/amp-notice/style.css rename to assets/src/components/amp-notice/style.css index 7f909f5dfad..5efbe53b0c3 100644 --- a/assets/src/setup/components/amp-notice/style.css +++ b/assets/src/components/amp-notice/style.css @@ -5,11 +5,21 @@ line-height: 1.85; } +.amp-notice p { + font-size: 13px; +} + .amp-notice__body { flex-grow: 1; text-align: left; } +.amp-notice__body .components-panel__body-toggle, +.amp-notice__body .components-panel__body-toggle:focus:not(:disabled) { + color: var(--black); + outline: none; +} + .amp-notice--success { background-color: #ecfef1; } @@ -23,6 +33,7 @@ } .amp-notice--small { + line-height: 1.5; padding: 0.5rem 1rem 0.5rem 0.5rem; } diff --git a/assets/src/setup/components/amp-notice/test/__snapshots__/index.js.snap b/assets/src/components/amp-notice/test/__snapshots__/index.js.snap similarity index 100% rename from assets/src/setup/components/amp-notice/test/__snapshots__/index.js.snap rename to assets/src/components/amp-notice/test/__snapshots__/index.js.snap diff --git a/assets/src/setup/components/amp-notice/test/index.js b/assets/src/components/amp-notice/test/index.js similarity index 87% rename from assets/src/setup/components/amp-notice/test/index.js rename to assets/src/components/amp-notice/test/index.js index f64b29e0dbb..89de327d10a 100644 --- a/assets/src/setup/components/amp-notice/test/index.js +++ b/assets/src/components/amp-notice/test/index.js @@ -46,6 +46,17 @@ describe( 'AMPNotice', () => { } ); it( 'has correct classes', () => { + act( () => { + render( + + { 'children' } + , + container, + ); + } ); + + expect( container.querySelector( 'div' ).getAttribute( 'class' ) ).toBe( 'amp-notice amp-notice--info amp-notice--large' ); + act( () => { render( diff --git a/assets/src/components/amp-setting-toggle/index.js b/assets/src/components/amp-setting-toggle/index.js new file mode 100644 index 00000000000..400ffb28655 --- /dev/null +++ b/assets/src/components/amp-setting-toggle/index.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './style.css'; + +/** + * Styled toggle control. + * + * @param {Object} props Component props. + * @param {boolean} props.checked Whether the toggle is on. + * @param {boolean} props.disabled Whether the toggle is disabled. + * @param {Function} props.onChange Change handler. + * @param {?string} props.text Toggle text. + * @param {string} props.title Toggle title. + */ +export function AMPSettingToggle( { checked, disabled = false, onChange, text, title } ) { + return ( +
+ +

+ { title } +

+ { text && ( +

+ { text } +

) } +
+ ) } + onChange={ onChange } + /> + + ); +} +AMPSettingToggle.propTypes = { + checked: PropTypes.bool.isRequired, + disabled: PropTypes.bool, + onChange: PropTypes.func.isRequired, + text: PropTypes.string, + title: PropTypes.string.isRequired, +}; diff --git a/assets/src/components/amp-setting-toggle/style.css b/assets/src/components/amp-setting-toggle/style.css new file mode 100644 index 00000000000..30b1a138de2 --- /dev/null +++ b/assets/src/components/amp-setting-toggle/style.css @@ -0,0 +1,49 @@ +/** + * AMP setting toggle component. + */ +.amp-setting-toggle__label-text h3 { + font-weight: 700; + font-size: 1.25rem; + margin-bottom: 1rem; + margin-top: 0; +} + +.amp-setting-toggle__label-text p:last-child { + margin-bottom: 0; +} + +.amp-setting-toggle .components-toggle-control .components-base-control__field .components-toggle-control__label { + display: flex; + flex-wrap: wrap; +} + +.amp-setting-toggle__illustration { + padding-bottom: 1.5rem; + + @media screen and (min-width: 783px) { + padding: 0 1.5rem; + } +} + +.amp .amp-setting-toggle .components-form-toggle { + margin-bottom: 2.25rem; + margin-right: 2.25rem; +} + +.amp-setting-toggle .components-toggle-control .components-base-control__field { + align-items: baseline; + margin-bottom: 0; +} + +.amp-setting-toggle .components-form-toggle.is-checked .components-form-toggle__track { + background-color: var(--brand); +} + +.amp-setting-toggle .components-form-toggle__input:focus + .components-form-toggle__track { + box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px var(--brand); +} + +.amp-setting-toggle--disabled .components-form-toggle { + pointer-events: none; + opacity: 0.5; +} diff --git a/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap b/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000..20ff713c6ac --- /dev/null +++ b/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AMPSettingToggle matches snapshots 1`] = ` +
+
+
+ + + + + + + + + +
+
+
+`; + +exports[`AMPSettingToggle matches snapshots 2`] = ` +
+
+
+ + + + + + + + + +
+
+
+`; diff --git a/assets/src/components/amp-setting-toggle/test/index.js b/assets/src/components/amp-setting-toggle/test/index.js new file mode 100644 index 00000000000..2ded77a821b --- /dev/null +++ b/assets/src/components/amp-setting-toggle/test/index.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { act } from 'react-dom/test-utils'; +import { create } from 'react-test-renderer'; + +/** + * WordPress dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { AMPSettingToggle } from '../'; + +let container; + +describe( 'AMPSettingToggle', () => { + beforeEach( () => { + container = document.createElement( 'div' ); + document.body.appendChild( container ); + } ); + + afterEach( () => { + document.body.removeChild( container ); + container = null; + } ); + + it( 'matches snapshots', () => { + let wrapper = create( + null } text={ 'My text' } title={ 'My title' } />, + ); + expect( wrapper.toJSON() ).toMatchSnapshot(); + + wrapper = create( + null } title={ 'My title' } />, + ); + expect( wrapper.toJSON() ).toMatchSnapshot(); + } ); + + it( 'has correct elements and text', () => { + act( () => { + render( + null } checked={ false }> + { 'children' } + , + container, + ); + } ); + + expect( container.querySelector( 'h3' ).textContent ).toBe( 'My title' ); + expect( container.querySelector( 'p' ) ).toBeNull(); + expect( container.querySelector( 'input:checked' ) ).toBeNull(); + + act( () => { + render( + null } checked={ true } text="My text"> + { 'children' } + , + container, + ); + } ); + + expect( container.querySelector( 'h3' ).textContent ).toBe( 'My title' ); + expect( container.querySelector( 'p' ).textContent ).toBe( 'My text' ); + expect( container.querySelector( 'input:checked' ) ).not.toBeNull(); + } ); +} ); diff --git a/assets/src/setup/components/loading/index.js b/assets/src/components/loading/index.js similarity index 100% rename from assets/src/setup/components/loading/index.js rename to assets/src/components/loading/index.js diff --git a/assets/src/setup/components/loading/style.css b/assets/src/components/loading/style.css similarity index 100% rename from assets/src/setup/components/loading/style.css rename to assets/src/components/loading/style.css diff --git a/assets/src/setup/components/__mocks__/options-context-provider.js b/assets/src/components/options-context-provider/__mocks__/index.js similarity index 100% rename from assets/src/setup/components/__mocks__/options-context-provider.js rename to assets/src/components/options-context-provider/__mocks__/index.js diff --git a/assets/src/setup/components/options-context-provider.js b/assets/src/components/options-context-provider/index.js similarity index 91% rename from assets/src/setup/components/options-context-provider.js rename to assets/src/components/options-context-provider/index.js index f114a9a0a30..678ab020535 100644 --- a/assets/src/setup/components/options-context-provider.js +++ b/assets/src/components/options-context-provider/index.js @@ -12,7 +12,7 @@ import PropTypes from 'prop-types'; /** * Internal dependencies */ -import { useError } from '../utils/use-error'; +import { useError } from '../../utils/use-error'; export const Options = createContext(); @@ -68,9 +68,10 @@ export function OptionsContextProvider( { children, optionsRestEndpoint } ) { return; } - if ( fetchedOptions.wizard_completed === false ) { + if ( fetchedOptions.plugin_configured === false ) { fetchedOptions.mobile_redirect = true; fetchedOptions.reader_theme = null; + fetchedOptions.theme_support = null; } setOriginalOptions( fetchedOptions ); @@ -102,12 +103,12 @@ export function OptionsContextProvider( { children, optionsRestEndpoint } ) { // If this is the first time running the wizard and mobile_redirect is not in updates, set mobile_redirect to true. // We do this here instead of in the fetch effect to prevent the exit confirmation before the user has interacted. - if ( ! originalOptions.wizard_completed && ! ( 'mobile_redirect' in updatesToSave ) ) { + if ( ! originalOptions.plugin_configured && ! ( 'mobile_redirect' in updatesToSave ) ) { updatesToSave.mobile_redirect = originalOptions.mobile_redirect; } - if ( ! originalOptions.wizard_completed ) { - updatesToSave.wizard_completed = true; + if ( ! originalOptions.plugin_configured ) { + updatesToSave.plugin_configured = true; } // Ensure this promise lasts at least a second so that the "Saving Options" load screen is @@ -144,10 +145,10 @@ export function OptionsContextProvider( { children, optionsRestEndpoint } ) { * * @param {Object} newOptions Updated options values. */ - const updateOptions = ( newOptions ) => { + const updateOptions = useCallback( ( newOptions ) => { setUpdates( { ...updates, ...newOptions } ); setDidSaveOptions( false ); - }; + }, [ updates ] ); // Allows an item in the updates object to be removed. const unsetOption = useCallback( ( option ) => { diff --git a/assets/src/setup/components/phone/index.js b/assets/src/components/phone/index.js similarity index 100% rename from assets/src/setup/components/phone/index.js rename to assets/src/components/phone/index.js diff --git a/assets/src/setup/components/phone/style.css b/assets/src/components/phone/style.css similarity index 100% rename from assets/src/setup/components/phone/style.css rename to assets/src/components/phone/style.css diff --git a/assets/src/setup/pages/choose-reader-theme/index.js b/assets/src/components/reader-theme-selection/index.js similarity index 55% rename from assets/src/setup/pages/choose-reader-theme/index.js rename to assets/src/components/reader-theme-selection/index.js index 444896851f1..02aa4b1f74b 100644 --- a/assets/src/setup/pages/choose-reader-theme/index.js +++ b/assets/src/components/reader-theme-selection/index.js @@ -1,57 +1,55 @@ /** - * WordPress dependencies + * External dependencies */ -import { __, sprintf } from '@wordpress/i18n'; -import { useEffect, useContext, useMemo } from '@wordpress/element'; +import PropTypes from 'prop-types'; +import { AMP_QUERY_VAR, DEFAULT_AMP_QUERY_VAR, LEGACY_THEME_SLUG, AMP_QUERY_VAR_CUSTOMIZED_LATE } from 'amp-settings'; // From WP inline script. /** - * External dependencies + * WordPress dependencies */ -import { AMP_QUERY_VAR, DEFAULT_AMP_QUERY_VAR, LEGACY_THEME_SLUG, AMP_QUERY_VAR_CUSTOMIZED_LATE } from 'amp-setup'; // From WP inline script. +import { __, sprintf } from '@wordpress/i18n'; +import { useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import { Loading } from '../../components/loading'; -import { Navigation } from '../../components/navigation-context-provider'; -import { Options } from '../../components/options-context-provider'; -import { ReaderThemes } from '../../components/reader-themes-context-provider'; +import { ReaderThemes } from '../reader-themes-context-provider'; +import { Loading } from '../loading'; +import './style.css'; +import { AMPNotice } from '../amp-notice'; import { ThemeCard } from './theme-card'; /** - * Screen for choosing the Reader theme. + * Component for selecting a reader theme. + * + * @param {Object} props Component props. + * @param {boolean} props.hideCurrentlyActiveTheme Whether the currently active theme should be unselectable. */ -export function ChooseReaderTheme() { - const { canGoForward, setCanGoForward } = useContext( Navigation ); - const { editedOptions } = useContext( Options ); - const { fetchingThemes, themes } = useContext( ReaderThemes ); +export function ReaderThemeSelection( { hideCurrentlyActiveTheme = false } ) { + const { currentTheme, fetchingThemes, themes: unprocessedThemes } = useContext( ReaderThemes ); - const { reader_theme: readerTheme, theme_support: themeSupport } = editedOptions; + const { activeTheme, themes } = useMemo( () => { + let active, processedThemes; - /** - * Allow moving forward. - */ - useEffect( () => { - if ( 'reader' !== themeSupport ) { - setCanGoForward( true ); - return; + if ( hideCurrentlyActiveTheme ) { + processedThemes = ( unprocessedThemes || [] ).filter( ( theme ) => { + if ( 'active' === theme.availability ) { + active = theme; + return false; + } + return true; + } ); + } else { + active = null; + processedThemes = unprocessedThemes; } - if ( - themes && - readerTheme && - canGoForward === false && - ! AMP_QUERY_VAR_CUSTOMIZED_LATE - ? themes.map( ( { slug } ) => slug ).includes( readerTheme ) - : readerTheme === LEGACY_THEME_SLUG - ) { - setCanGoForward( true ); - } - }, [ canGoForward, setCanGoForward, readerTheme, themes, themeSupport ] ); + return { activeTheme: active, themes: processedThemes }; + }, [ hideCurrentlyActiveTheme, unprocessedThemes ] ); // Separate available themes (both installed and installable) from those that need to be installed manually. const { availableThemes, unavailableThemes } = useMemo( - () => themes.reduce( + () => ( themes || [] ).reduce( ( collections, theme ) => { if ( ( AMP_QUERY_VAR_CUSTOMIZED_LATE && theme.slug !== LEGACY_THEME_SLUG ) || theme.availability === 'non-installable' ) { collections.unavailableThemes.push( theme ); @@ -63,40 +61,46 @@ export function ChooseReaderTheme() { }, { availableThemes: [], unavailableThemes: [] }, ), - [ themes ] ); + [ themes ], + ); if ( fetchingThemes ) { - return ( - - ); - } - - if ( 'reader' !== themeSupport ) { - return ( -

- { __( 'This screen is only relevant to sites that use Reader mode. Go back if you would like to select Reader mode, or move forward to complete the setup wizard.', 'amp' ) } -

- ); + return ; } return ( -
+

{ // @todo Probably improve this text. __( 'Select the theme template for mobile visitors', 'amp' ) }

-
+ { activeTheme && hideCurrentlyActiveTheme && ( + + { + sprintf( + /* translators: placeholder is the name of a WordPress theme. */ + __( 'Your active theme ā€œ%sā€ is not available as a reader theme. If you wish to use it, Transitional mode may be the best option for you.', 'amp' ), + activeTheme.name, + ) + } + + ) } +
{ 0 < availableThemes.length && (
    - { availableThemes.map( ( theme ) => ( - - ) ) } + { availableThemes.map( ( theme ) => { + const disabled = hideCurrentlyActiveTheme && currentTheme.name === theme.name; + + return ! disabled && ( + + ); + } ) }
) } @@ -129,14 +133,18 @@ export function ChooseReaderTheme() { ) ) }
) } -
+
); } + +ReaderThemeSelection.propTypes = { + hideCurrentlyActiveTheme: PropTypes.bool, +}; diff --git a/assets/src/setup/pages/choose-reader-theme/style.css b/assets/src/components/reader-theme-selection/style.css similarity index 86% rename from assets/src/setup/pages/choose-reader-theme/style.css rename to assets/src/components/reader-theme-selection/style.css index 07af25f64e9..5f2acc470f4 100644 --- a/assets/src/setup/pages/choose-reader-theme/style.css +++ b/assets/src/components/reader-theme-selection/style.css @@ -1,4 +1,4 @@ -.choose-reader-theme > form { +.choose-reader-theme > div { margin-left: -8px; margin-right: -8px; } @@ -41,7 +41,12 @@ display: flex; } -.theme-card__label-header h2 { +.theme-card__label-header > input { + flex-shrink: 0; +} + +.theme-card__label-header h3 { + line-height: 1.3; margin-left: 5px; margin-top: 0.75em; } @@ -55,7 +60,7 @@ width: 100%; } -.theme-card__description { +p.theme-card__description { font-size: 13px; line-height: 1.666; display: -webkit-box; diff --git a/assets/src/setup/pages/choose-reader-theme/theme-card.js b/assets/src/components/reader-theme-selection/theme-card.js similarity index 77% rename from assets/src/setup/pages/choose-reader-theme/theme-card.js rename to assets/src/components/reader-theme-selection/theme-card.js index 12474e51743..bffd3436b92 100644 --- a/assets/src/setup/pages/choose-reader-theme/theme-card.js +++ b/assets/src/components/reader-theme-selection/theme-card.js @@ -4,6 +4,7 @@ */ import { useContext } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; +import { __ } from '@wordpress/i18n'; /** * External dependencies @@ -13,11 +14,9 @@ import PropTypes from 'prop-types'; /** * Internal dependencies */ -import './style.css'; -import { __ } from '@wordpress/i18n'; -import { Options } from '../../components/options-context-provider'; -import { Selectable } from '../../components/selectable'; -import { Phone } from '../../components/phone'; +import { Options } from '../options-context-provider'; +import { Selectable } from '../selectable'; +import { Phone } from '../phone'; /** * A selectable card showing a theme in a list of themes. @@ -28,16 +27,16 @@ import { Phone } from '../../components/phone'; * @param {string} props.screenshotUrl URL for screenshot of theme. * @param {string} props.slug Theme slug. * @param {string} props.name Theme name. - * @param {boolean} props.unavailable Whether the theme is not automatically installable in the current environment. + * @param {boolean} props.disabled Whether the theme is not automatically installable in the current environment. */ -export function ThemeCard( { description, homepage, screenshotUrl, slug, name, unavailable } ) { +export function ThemeCard( { description, homepage, screenshotUrl, slug, name, disabled } ) { const { editedOptions, updateOptions } = useContext( Options ); const { reader_theme: readerTheme } = editedOptions; const id = `theme-card__${ slug }`; return ( - +