diff --git a/packages/components/src/tab-panel/index.js b/packages/components/src/tab-panel/index.js index fa867139c0339..c5423975e5ec1 100644 --- a/packages/components/src/tab-panel/index.js +++ b/packages/components/src/tab-panel/index.js @@ -90,6 +90,7 @@ export default function TabPanel( { { selectedTab && (
# **toggleScreenOption** +# **togglePreferencesOption** -Toggles the screen option with the given label. +Toggles a preference option with the given tab label and the option label. _Parameters_ -- _label_ `string`: The label of the screen option, e.g. 'Show Tips'. -- _shouldBeChecked_ `?boolean`: If true, turns the option on. If false, off. If undefined, the option will be toggled. +- _tabLabel_ `string`: The preferences tab label to click. +- _optionLabel_ `string`: The option label to search the button for. +- _shouldBeChecked_ `[boolean]`: If true, turns the option on. If false, off. If not provided, the option will be toggled. # **transformBlockTo** diff --git a/packages/e2e-test-utils/src/disable-pre-publish-checks.js b/packages/e2e-test-utils/src/disable-pre-publish-checks.js index b912fcd0aab52..5ced16d0f6a91 100644 --- a/packages/e2e-test-utils/src/disable-pre-publish-checks.js +++ b/packages/e2e-test-utils/src/disable-pre-publish-checks.js @@ -1,13 +1,17 @@ /** * Internal dependencies */ -import { toggleScreenOption } from './toggle-screen-option'; +import { togglePreferencesOption } from './toggle-preferences-option'; import { toggleMoreMenu } from './toggle-more-menu'; /** * Disables Pre-publish checks. */ export async function disablePrePublishChecks() { - await toggleScreenOption( 'Include pre-publish checklist', false ); + await togglePreferencesOption( + 'General', + 'Include pre-publish checklist', + false + ); await toggleMoreMenu(); } diff --git a/packages/e2e-test-utils/src/enable-pre-publish-checks.js b/packages/e2e-test-utils/src/enable-pre-publish-checks.js index 53582987c4af8..3dd1b7b13246c 100644 --- a/packages/e2e-test-utils/src/enable-pre-publish-checks.js +++ b/packages/e2e-test-utils/src/enable-pre-publish-checks.js @@ -1,13 +1,17 @@ /** * Internal dependencies */ -import { toggleScreenOption } from './toggle-screen-option'; +import { togglePreferencesOption } from './toggle-preferences-option'; import { toggleMoreMenu } from './toggle-more-menu'; /** * Enables Pre-publish checks. */ export async function enablePrePublishChecks() { - await toggleScreenOption( 'Include pre-publish checklist', true ); + await togglePreferencesOption( + 'General', + 'Include pre-publish checklist', + true + ); await toggleMoreMenu(); } diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index 06899b154c983..20fd36167a5b7 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -70,7 +70,7 @@ export { switchUserToTest } from './switch-user-to-test'; export { isThemeInstalled } from './theme-installed'; export { toggleMoreMenu } from './toggle-more-menu'; export { toggleOfflineMode, isOfflineMode } from './offline-mode'; -export { toggleScreenOption } from './toggle-screen-option'; +export { togglePreferencesOption } from './toggle-preferences-option'; export { transformBlockTo } from './transform-block-to'; export { uninstallPlugin } from './uninstall-plugin'; export { visitAdminPage } from './visit-admin-page'; diff --git a/packages/e2e-test-utils/src/toggle-preferences-option.js b/packages/e2e-test-utils/src/toggle-preferences-option.js new file mode 100644 index 0000000000000..03472c1314391 --- /dev/null +++ b/packages/e2e-test-utils/src/toggle-preferences-option.js @@ -0,0 +1,36 @@ +/** + * Internal dependencies + */ +import { clickOnCloseModalButton } from './click-on-close-modal-button'; +import { clickOnMoreMenuItem } from './click-on-more-menu-item'; + +/** + * Toggles a preference option with the given tab label and the option label. + * + * @param {string} tabLabel The preferences tab label to click. + * @param {string} optionLabel The option label to search the button for. + * @param {boolean} [shouldBeChecked] If true, turns the option on. If false, off. If not provided, the option will be toggled. + */ +export async function togglePreferencesOption( + tabLabel, + optionLabel, + shouldBeChecked +) { + await clickOnMoreMenuItem( 'Preferences' ); + const [ tabHandle ] = await page.$x( + `//button[contains(text(), "${ tabLabel }")]` + ); + await tabHandle.click(); + const [ optionHandle ] = await page.$x( + `//label[contains(text(), "${ optionLabel }")]` + ); + const isChecked = await page.evaluate( + ( element ) => element.control.checked, + optionHandle + ); + if ( isChecked !== shouldBeChecked ) { + await optionHandle.click(); + } + + await clickOnCloseModalButton(); +} diff --git a/packages/e2e-test-utils/src/toggle-screen-option.js b/packages/e2e-test-utils/src/toggle-screen-option.js deleted file mode 100644 index a7478fa4a15ec..0000000000000 --- a/packages/e2e-test-utils/src/toggle-screen-option.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Internal dependencies - */ -import { clickOnCloseModalButton } from './click-on-close-modal-button'; -import { clickOnMoreMenuItem } from './click-on-more-menu-item'; - -/** - * Toggles the screen option with the given label. - * - * @param {string} label The label of the screen option, e.g. 'Show Tips'. - * @param {?boolean} shouldBeChecked If true, turns the option on. If false, off. If - * undefined, the option will be toggled. - */ -export async function toggleScreenOption( label, shouldBeChecked = undefined ) { - await clickOnMoreMenuItem( 'Preferences' ); - const [ handle ] = await page.$x( - `//label[contains(text(), "${ label }")]` - ); - - const isChecked = await page.evaluate( - ( element ) => element.control.checked, - handle - ); - if ( isChecked !== shouldBeChecked ) { - await handle.click(); - } - - await clickOnCloseModalButton(); -} diff --git a/packages/e2e-tests/specs/editor/plugins/block-variations.js b/packages/e2e-tests/specs/editor/plugins/block-variations.js index 1e3b48e656f31..a1dc6ec263bbc 100644 --- a/packages/e2e-tests/specs/editor/plugins/block-variations.js +++ b/packages/e2e-tests/specs/editor/plugins/block-variations.js @@ -9,7 +9,7 @@ import { searchForBlock, pressKeyWithModifier, openDocumentSettingsSidebar, - toggleScreenOption, + togglePreferencesOption, toggleMoreMenu, } from '@wordpress/e2e-test-utils'; @@ -110,12 +110,20 @@ describe( 'Block variations', () => { // @see @wordpres/block-editor/src/components/use-block-display-information (`useBlockDisplayInformation` hook). describe( 'testing block display information with matching variations', () => { beforeEach( async () => { - await toggleScreenOption( 'Display block breadcrumbs', true ); + await togglePreferencesOption( + 'General', + 'Display block breadcrumbs', + true + ); await toggleMoreMenu(); } ); afterEach( async () => { - await toggleScreenOption( 'Display block breadcrumbs', false ); + await togglePreferencesOption( + 'General', + 'Display block breadcrumbs', + false + ); await toggleMoreMenu(); } ); diff --git a/packages/e2e-tests/specs/editor/various/preview.test.js b/packages/e2e-tests/specs/editor/various/preview.test.js index d0996211a512c..f58490d5745e1 100644 --- a/packages/e2e-tests/specs/editor/various/preview.test.js +++ b/packages/e2e-tests/specs/editor/various/preview.test.js @@ -46,16 +46,21 @@ async function waitForPreviewNavigation( previewPage ) { /** * Enables or disables the custom fields option. * - * Note that this is implemented separately from the `toggleScreenOptions` + * Note that this is implemented separately from the `togglePreferencesOption` * utility, since the custom fields option triggers a page reload and requires * extra async logic to wait for navigation to complete. * * @param {boolean} shouldBeChecked If true, turns the option on. If false, off. */ async function toggleCustomFieldsOption( shouldBeChecked ) { - const checkboxXPath = - '//*[contains(@class, "edit-post-preferences-modal")]//label[contains(text(), "Custom fields")]'; + const baseXPath = '//*[contains(@class, "edit-post-preferences-modal")]'; + const paneslXPath = `${ baseXPath }//button[contains(text(), "Panels")]`; + const checkboxXPath = `${ baseXPath }//label[contains(text(), "Custom fields")]`; await clickOnMoreMenuItem( 'Preferences' ); + await page.waitForXPath( paneslXPath ); + const [ tabHandle ] = await page.$x( paneslXPath ); + await tabHandle.click(); + await page.waitForXPath( checkboxXPath ); const [ checkboxHandle ] = await page.$x( checkboxXPath ); diff --git a/packages/edit-post/src/components/header/preferences-menu-item/index.js b/packages/edit-post/src/components/header/preferences-menu-item/index.js index 163ef900f362e..bc747ca0afde8 100644 --- a/packages/edit-post/src/components/header/preferences-menu-item/index.js +++ b/packages/edit-post/src/components/header/preferences-menu-item/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { withDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { MenuItem } from '@wordpress/components'; @@ -10,7 +10,8 @@ import { MenuItem } from '@wordpress/components'; */ import { store as editPostStore } from '../../../store'; -export function PreferencesMenuItem( { openModal } ) { +export default function PreferencesMenuItem() { + const { openModal } = useDispatch( editPostStore ); return ( { @@ -21,11 +22,3 @@ export function PreferencesMenuItem( { openModal } ) { ); } - -export default withDispatch( ( dispatch ) => { - const { openModal } = dispatch( editPostStore ); - - return { - openModal, - }; -} )( PreferencesMenuItem ); diff --git a/packages/edit-post/src/components/manage-blocks-modal/manager.js b/packages/edit-post/src/components/manage-blocks-modal/manager.js index b6df0ee16e41d..475b641271608 100644 --- a/packages/edit-post/src/components/manage-blocks-modal/manager.js +++ b/packages/edit-post/src/components/manage-blocks-modal/manager.js @@ -27,9 +27,9 @@ function BlockManager( { isMatchingSearchTerm, numberOfHiddenBlocks, } ) { - // Filtering occurs here (as opposed to `withSelect`) to avoid wasted - // wasted renders by consequence of `Array#filter` producing a new - // value reference on each call. + // Filtering occurs here (as opposed to `withSelect`) to avoid + // wasted renders by consequence of `Array#filter` producing + // a new value reference on each call. blockTypes = blockTypes.filter( ( blockType ) => hasBlockSupport( blockType, 'inserter', true ) && diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index fde173f47c07c..7be365d1c2992 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -6,17 +6,26 @@ import { get } from 'lodash'; /** * WordPress dependencies */ -import { Modal } from '@wordpress/components'; +import { + __experimentalNavigation as Navigation, + __experimentalNavigationMenu as NavigationMenu, + __experimentalNavigationItem as NavigationItem, + Modal, + TabPanel, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { withSelect, withDispatch, useSelect } from '@wordpress/data'; -import { compose, useViewportMatch } from '@wordpress/compose'; +import { useViewportMatch } from '@wordpress/compose'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useMemo, useCallback, useState } from '@wordpress/element'; import { PostTaxonomies, PostExcerptCheck, PageAttributesCheck, PostFeaturedImageCheck, PostTypeSupportCheck, + store as editorStore, } from '@wordpress/editor'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -32,28 +41,278 @@ import MetaBoxesSection from './meta-boxes-section'; import { store as editPostStore } from '../../store'; const MODAL_NAME = 'edit-post/preferences'; +const PREFERENCES_MENU = 'preferences-menu'; -export function PreferencesModal( { isModalActive, isViewable, closeModal } ) { - const isMobileViewport = useViewportMatch( 'medium', '<' ); - const { mode, isRichEditingEnabled, hasReducedUI } = useSelect( +export default function PreferencesModal() { + const isLargeViewport = useViewportMatch( 'medium' ); + const { closeModal } = useDispatch( editPostStore ); + const { isModalActive, isViewable } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + const { getPostType } = select( coreStore ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); + return { + isModalActive: select( editPostStore ).isModalActive( MODAL_NAME ), + isViewable: get( postType, [ 'viewable' ], false ), + }; + }, [] ); + const showBlockBreadcrumbsOption = useSelect( ( select ) => { - return { - mode: select( editPostStore ).getEditorMode(), - isRichEditingEnabled: select( - 'core/editor' - ).getEditorSettings().richEditingEnabled, - hasReducedUI: select( editPostStore ).isFeatureActive( - 'reducedUI' - ), - }; + const { getEditorSettings } = select( editorStore ); + const { getEditorMode, isFeatureActive } = select( editPostStore ); + const mode = getEditorMode(); + const isRichEditingEnabled = getEditorSettings().richEditingEnabled; + const hasReducedUI = isFeatureActive( 'reducedUI' ); + return ( + ! hasReducedUI && + isLargeViewport && + isRichEditingEnabled && + mode === 'visual' + ); }, - [] + [ isLargeViewport ] + ); + const sections = useMemo( + () => [ + { + name: 'general', + tabLabel: __( 'General' ), + content: ( + <> + { isLargeViewport && ( +
+ +
+ ) } + +
+ + + { showBlockBreadcrumbsOption && ( + + ) } +
+ + ), + }, + { + name: 'appearance', + tabLabel: __( 'Appearance' ), + content: ( +
+ + +
+ ), + }, + { + name: 'blocks', + tabLabel: __( 'Blocks' ), + content: ( +
+ + +
+ ), + }, + { + name: 'panels', + tabLabel: __( 'Panels' ), + content: ( + <> +
+ + { isViewable && ( + + ) } + ( + + ) } + /> + + + + + + + + + + + + +
+
+ +
+ + ), + }, + ], + [ isViewable, isLargeViewport, showBlockBreadcrumbsOption ] ); + // This is also used to sync the two different rendered components + // between small and large viewports. + const [ activeMenu, setActiveMenu ] = useState( PREFERENCES_MENU ); + /** + * Create helper objects from `sections` for easier data handling. + * `tabs` is used for creating the `TabPanel` and `sectionsContentMap` + * is used for easier access to active tab's content. + */ + const { tabs, sectionsContentMap } = useMemo( + () => + sections.reduce( + ( accumulator, { name, tabLabel: title, content } ) => { + accumulator.tabs.push( { name, title } ); + accumulator.sectionsContentMap[ name ] = content; + return accumulator; + }, + { tabs: [], sectionsContentMap: {} } + ), + [ sections ] + ); + const getCurrentTab = useCallback( + ( tab ) => sectionsContentMap[ tab.name ] || null, + [ sectionsContentMap ] + ); if ( ! isModalActive ) { return null; } - + let modalContent; + // We render different components based on the viewport size. + if ( isLargeViewport ) { + modalContent = ( + + { getCurrentTab } + + ); + } else { + modalContent = ( + + + { tabs.map( ( tab ) => { + return ( + + ); + } ) } + + { sections.map( ( section ) => { + return ( + + { section.content } + + ); + } ) } + + ); + } return ( -
- - -
-
- -
-
- - - - - { ! hasReducedUI && - ! isMobileViewport && - isRichEditingEnabled && - mode === 'visual' && ( - - ) } -
-
- - { isViewable && ( - - ) } - ( - - ) } - /> - - - - - - - - - - - - -
- + { modalContent }
); } - -export default compose( - withSelect( ( select ) => { - const { getEditedPostAttribute } = select( 'core/editor' ); - const { getPostType } = select( 'core' ); - const postType = getPostType( getEditedPostAttribute( 'type' ) ); - - return { - isModalActive: select( editPostStore ).isModalActive( MODAL_NAME ), - isViewable: get( postType, [ 'viewable' ], false ), - }; - } ), - withDispatch( ( dispatch ) => { - return { - closeModal: () => dispatch( editPostStore ).closeModal(), - }; - } ) -)( PreferencesModal ); diff --git a/packages/edit-post/src/components/preferences-modal/options/base.js b/packages/edit-post/src/components/preferences-modal/options/base.js index 10b3c87e76ad0..b1edfc18a6de1 100644 --- a/packages/edit-post/src/components/preferences-modal/options/base.js +++ b/packages/edit-post/src/components/preferences-modal/options/base.js @@ -1,12 +1,12 @@ /** * WordPress dependencies */ -import { CheckboxControl } from '@wordpress/components'; +import { ToggleControl } from '@wordpress/components'; function BaseOption( { help, label, isChecked, onChange, children } ) { return (
-
- - - + +
@@ -90,40 +81,31 @@ exports[`EnableCustomFieldsOption renders a checked checkbox when custom fields className="edit-post-preferences-modal__option" >
- - - + +
@@ -148,26 +130,31 @@ exports[`EnableCustomFieldsOption renders an unchecked checkbox and a confirmati className="edit-post-preferences-modal__option" >
+ +
@@ -206,26 +193,31 @@ exports[`EnableCustomFieldsOption renders an unchecked checkbox when custom fiel className="edit-post-preferences-modal__option" >
+ +
diff --git a/packages/edit-post/src/components/preferences-modal/style.scss b/packages/edit-post/src/components/preferences-modal/style.scss index f746ccd197989..da668997f2d1c 100644 --- a/packages/edit-post/src/components/preferences-modal/style.scss +++ b/packages/edit-post/src/components/preferences-modal/style.scss @@ -1,4 +1,86 @@ .edit-post-preferences-modal { + min-width: 360px; + + // Better use the space on mobile. + width: 100%; + @include break-medium() { + width: auto; + } + + @include break-small() { + height: calc(100% - #{$header-height} - #{$header-height}); + } + + .components-navigation { + background-color: $white; + margin: -#{$grid-unit-10}; + padding: $grid-unit-10; + .components-navigation__menu { + margin: 0; + color: $gray-900; + + .components-navigation__item { + color: $gray-900; // The inheritance of some items is quite strong, so we have to duplicate this one. + + & > button { + color: inherit; + padding: 3px $grid-unit-20; + height: $grid-unit-60; + &:focus { + background: $gray-100; + font-weight: 500; + } + &:hover { + color: var(--wp-admin-theme-color); + } + } + .components-toggle-control__label { + color: inherit; + } + } + .components-navigation__menu-title-heading { + color: inherit; + border-bottom: 1px solid $gray-300; + padding-left: 0; + padding-right: 0; + } + .components-navigation__back-button { + color: inherit; + padding-left: 0; + &:hover { + color: var(--wp-admin-theme-color); + } + } + .edit-post-preferences-modal__custom-fields-confirmation-button { + width: auto; + } + } + } + + .edit-post-preferences__tabs { + display: flex; + flex-direction: row; + .components-tab-panel__tabs { + width: 160px; + .components-tab-panel__tabs-item { + border-radius: $radius-block-ui; + font-weight: 400; + &.is-active { + background: $gray-100; + box-shadow: none; + font-weight: 500; + } + &:focus:not(:disabled) { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + } + .components-tab-panel__tab-content { + width: 500px; + padding-left: $grid-unit-30; + } + } + &__section { margin: 0 0 2.5rem 0; } @@ -9,16 +91,17 @@ } &__option { + .components-base-control { + .components-base-control__field { + align-items: center; + display: flex; + margin-bottom: 0; - .components-base-control__field { - align-items: center; - display: flex; - margin: 0; - } - - .components-checkbox-control__label { - flex-grow: 1; - padding: 0.6rem 0 0.6rem 10px; + & > label { + flex-grow: 1; + padding: 0.6rem 0 0.6rem 10px; + } + } } } @@ -35,7 +118,7 @@ } .components-base-control__help { - margin: -$grid-unit-10 0 $grid-unit-10 ( $grid-unit-50 + 2px ); + margin: -$grid-unit-10 0 $grid-unit-10 58px; font-size: $helptext-font-size; font-style: normal; color: $gray-700; diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap index 7a33119c0eded..62231d8d5c8ea 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap @@ -1,105 +1,206 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PreferencesModal should match snapshot when the modal is active 1`] = ` +exports[`PreferencesModal should match snapshot when the modal is active large viewports 1`] = ` -
- - -
-
- -
-
- - - - - -
-
+ + +`; + +exports[`PreferencesModal should match snapshot when the modal is active small viewports 1`] = ` + + - - - - + - - - - - - - - - - -
- + + + +
+ + + +
+
+
+ + +
+ + +
+
+
+ + +
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + +
+
+ +
+
+
+
`; diff --git a/packages/edit-post/src/components/preferences-modal/test/index.js b/packages/edit-post/src/components/preferences-modal/test/index.js index 7bdba43c200a3..918ca19ee2b76 100644 --- a/packages/edit-post/src/components/preferences-modal/test/index.js +++ b/packages/edit-post/src/components/preferences-modal/test/index.js @@ -3,19 +3,40 @@ */ import { shallow } from 'enzyme'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useViewportMatch } from '@wordpress/compose'; + /** * Internal dependencies */ -import { PreferencesModal } from '../'; +import PreferencesModal from '../'; + +// This allows us to tweak the returned value on each test +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); +jest.mock( '@wordpress/compose/src/hooks/use-viewport-match', () => jest.fn() ); describe( 'PreferencesModal', () => { - it( 'should match snapshot when the modal is active', () => { - const wrapper = shallow( ); - expect( wrapper ).toMatchSnapshot(); + describe( 'should match snapshot when the modal is active', () => { + it( 'large viewports', () => { + useSelect.mockImplementation( () => ( { isModalActive: true } ) ); + useViewportMatch.mockImplementation( () => true ); + const wrapper = shallow( ); + expect( wrapper ).toMatchSnapshot(); + } ); + it( 'small viewports', () => { + useSelect.mockImplementation( () => ( { isModalActive: true } ) ); + useViewportMatch.mockImplementation( () => false ); + const wrapper = shallow( ); + expect( wrapper ).toMatchSnapshot(); + } ); } ); it( 'should not render when the modal is not active', () => { - const wrapper = shallow( ); + useSelect.mockImplementation( () => ( { isModalActive: false } ) ); + const wrapper = shallow( ); expect( wrapper.isEmptyRender() ).toBe( true ); } ); } );