diff --git a/packages/e2e-tests/specs/editor/various/preview.test.js b/packages/e2e-tests/specs/editor/various/preview.test.js index ba169b19ae7688..417b1aee7e0ed8 100644 --- a/packages/e2e-tests/specs/editor/various/preview.test.js +++ b/packages/e2e-tests/specs/editor/various/preview.test.js @@ -54,12 +54,12 @@ async function waitForPreviewNavigation( previewPage ) { * @param {boolean} shouldBeChecked If true, turns the option on. If false, off. */ async function toggleCustomFieldsOption( shouldBeChecked ) { - const baseXPath = '//*[contains(@class, "edit-post-preferences-modal")]'; - const paneslXPath = `${ baseXPath }//button[contains(text(), "Panels")]`; + const baseXPath = '//*[contains(@class, "interface-preferences-modal")]'; + const panelsXPath = `${ 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 page.waitForXPath( panelsXPath ); + const [ tabHandle ] = await page.$x( panelsXPath ); await tabHandle.click(); await page.waitForXPath( checkboxXPath ); @@ -83,7 +83,7 @@ async function toggleCustomFieldsOption( shouldBeChecked ) { return; } - await clickOnCloseModalButton( '.edit-post-preferences-modal' ); + await clickOnCloseModalButton( '.interface-preferences-modal' ); } describe( 'Preview', () => { diff --git a/packages/edit-post/src/components/preferences-modal/block-preferences.js b/packages/edit-post/src/components/preferences-modal/block-preferences.js new file mode 100644 index 00000000000000..1519c041cdd38a --- /dev/null +++ b/packages/edit-post/src/components/preferences-modal/block-preferences.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + PreferencesModalSection, + PreferencesModalFeatureToggle, +} from '@wordpress/interface'; + +/** + * Internal dependencies + */ +import BlockManager from '../block-manager'; + +export default function BlockPreferences() { + return ( + <> + + + + + + + + + ); +} diff --git a/packages/edit-post/src/components/preferences-modal/general-preferences.js b/packages/edit-post/src/components/preferences-modal/general-preferences.js new file mode 100644 index 00000000000000..32a12a695e9393 --- /dev/null +++ b/packages/edit-post/src/components/preferences-modal/general-preferences.js @@ -0,0 +1,103 @@ +/** + * WordPress dependencies + */ +import { useViewportMatch } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; +import { + PreferencesModalSection, + PreferencesModalFeatureToggle, +} from '@wordpress/interface'; + +/** + * Internal dependencies + */ +import { EnablePublishSidebarOption } from './options'; +import { store as editPostStore } from '../../store'; + +export default function GeneralPreferences() { + const isLargeViewport = useViewportMatch( 'medium' ); + const showBlockBreadcrumbsOption = useSelect( + ( select ) => { + 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 ] + ); + + return ( + <> + { isLargeViewport && ( + + + + ) } + + + + + + + { showBlockBreadcrumbsOption && ( + + ) } + + + ); +} diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index 3a536a74140bde..1a9f60429c08cf 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -1,346 +1,56 @@ -/** - * External dependencies - */ -import { get } from 'lodash'; - /** * WordPress dependencies */ -import { - __experimentalNavigation as Navigation, - __experimentalNavigationMenu as NavigationMenu, - __experimentalNavigationItem as NavigationItem, - Modal, - TabPanel, -} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -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'; +import { useMemo } from '@wordpress/element'; +import { PreferencesModal, PreferencesModalTabs } from '@wordpress/interface'; /** * Internal dependencies */ -import Section from './section'; -import { - EnablePluginDocumentSettingPanelOption, - EnablePublishSidebarOption, - EnablePanelOption, - EnableFeature, -} from './options'; -import MetaBoxesSection from './meta-boxes-section'; import { store as editPostStore } from '../../store'; -import BlockManager from '../block-manager'; +import GeneralPreferences from './general-preferences'; +import BlockPreferences from './block-preferences'; +import PanelPreferences from './panel-preferences'; const MODAL_NAME = 'edit-post/preferences'; -const PREFERENCES_MENU = 'preferences-menu'; -export default function PreferencesModal() { - const isLargeViewport = useViewportMatch( 'medium' ); +export default function PostEditorPreferencesModal() { 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 ) => { - 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 isModalActive = useSelect( + ( select ) => select( editPostStore ).isModalActive( MODAL_NAME ), + [] ); - const sections = useMemo( + + const tabs = useMemo( () => [ { name: 'general', - tabLabel: __( 'General' ), - content: ( - <> - { isLargeViewport && ( -
- -
- ) } - -
- - - - - { showBlockBreadcrumbsOption && ( - - ) } -
- - ), + title: __( 'General' ), + content: , }, { name: 'blocks', - tabLabel: __( 'Blocks' ), - content: ( - <> -
- - -
-
- -
- - ), + title: __( 'Blocks' ), + content: , }, { name: 'panels', - tabLabel: __( 'Panels' ), - content: ( - <> -
- - { isViewable && ( - - ) } - { isViewable && ( - - ) } - ( - - ) } - /> - - - - - - - - - - - - -
-
- -
- - ), + title: __( 'Panels' ), + content: , }, ], - [ 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 ( - - { modalContent } - + + + ); } diff --git a/packages/edit-post/src/components/preferences-modal/meta-boxes-section.js b/packages/edit-post/src/components/preferences-modal/meta-boxes-section.js deleted file mode 100644 index eded51abbbfb59..00000000000000 --- a/packages/edit-post/src/components/preferences-modal/meta-boxes-section.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * External dependencies - */ -import { filter, map } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { withSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import Section from './section'; -import { EnableCustomFieldsOption, EnablePanelOption } from './options'; -import { store as editPostStore } from '../../store'; - -export function MetaBoxesSection( { - areCustomFieldsRegistered, - metaBoxes, - ...sectionProps -} ) { - // The 'Custom Fields' meta box is a special case that we handle separately. - const thirdPartyMetaBoxes = filter( - metaBoxes, - ( { id } ) => id !== 'postcustom' - ); - - if ( ! areCustomFieldsRegistered && thirdPartyMetaBoxes.length === 0 ) { - return null; - } - - return ( -
- { areCustomFieldsRegistered && ( - - ) } - { map( thirdPartyMetaBoxes, ( { id, title } ) => ( - - ) ) } -
- ); -} - -export default withSelect( ( select ) => { - const { getEditorSettings } = select( editorStore ); - const { getAllMetaBoxes } = select( editPostStore ); - - return { - // This setting should not live in the block editor's store. - areCustomFieldsRegistered: - getEditorSettings().enableCustomFields !== undefined, - metaBoxes: getAllMetaBoxes(), - }; -} )( MetaBoxesSection ); diff --git a/packages/edit-post/src/components/preferences-modal/options/enable-custom-fields.js b/packages/edit-post/src/components/preferences-modal/options/enable-custom-fields.js index 6ab452f228a882..26c2a8ee555b41 100644 --- a/packages/edit-post/src/components/preferences-modal/options/enable-custom-fields.js +++ b/packages/edit-post/src/components/preferences-modal/options/enable-custom-fields.js @@ -6,11 +6,7 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import BaseOption from './base'; +import { PreferencesModalToggle } from '@wordpress/interface'; export function CustomFieldsConfirmation( { willEnable } ) { const [ isReloading, setIsReloading ] = useState( false ); @@ -46,7 +42,7 @@ export function EnableCustomFieldsOption( { label, areCustomFieldsEnabled } ) { const [ isChecked, setIsChecked ] = useState( areCustomFieldsEnabled ); return ( - ) } - + ); } diff --git a/packages/edit-post/src/components/preferences-modal/options/enable-feature.js b/packages/edit-post/src/components/preferences-modal/options/enable-feature.js index 315dd2ed656e50..d0e1447f3c25b5 100644 --- a/packages/edit-post/src/components/preferences-modal/options/enable-feature.js +++ b/packages/edit-post/src/components/preferences-modal/options/enable-feature.js @@ -3,11 +3,11 @@ */ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; +import { PreferencesModalToggle } from '@wordpress/interface'; /** * Internal dependencies */ -import BaseOption from './base'; import { store as editPostStore } from '../../../store'; export default compose( @@ -20,4 +20,4 @@ export default compose( withDispatch( ( dispatch, { featureName } ) => ( { onChange: () => dispatch( editPostStore ).toggleFeature( featureName ), } ) ) -)( BaseOption ); +)( PreferencesModalToggle ); diff --git a/packages/edit-post/src/components/preferences-modal/options/enable-panel.js b/packages/edit-post/src/components/preferences-modal/options/enable-panel.js index 0a25b4ad4d86a7..68e2474109d66e 100644 --- a/packages/edit-post/src/components/preferences-modal/options/enable-panel.js +++ b/packages/edit-post/src/components/preferences-modal/options/enable-panel.js @@ -3,11 +3,11 @@ */ import { compose, ifCondition } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; +import { PreferencesModalToggle } from '@wordpress/interface'; /** * Internal dependencies */ -import BaseOption from './base'; import { store as editPostStore } from '../../../store'; export default compose( @@ -25,4 +25,4 @@ export default compose( onChange: () => dispatch( editPostStore ).toggleEditorPanelEnabled( panelName ), } ) ) -)( BaseOption ); +)( PreferencesModalToggle ); diff --git a/packages/edit-post/src/components/preferences-modal/options/enable-publish-sidebar.js b/packages/edit-post/src/components/preferences-modal/options/enable-publish-sidebar.js index 23287e3705817d..19ba0a58534fa0 100644 --- a/packages/edit-post/src/components/preferences-modal/options/enable-publish-sidebar.js +++ b/packages/edit-post/src/components/preferences-modal/options/enable-publish-sidebar.js @@ -5,11 +5,7 @@ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; import { ifViewportMatches } from '@wordpress/viewport'; import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import BaseOption from './base'; +import { PreferencesModalToggle } from '@wordpress/interface'; export default compose( withSelect( ( select ) => ( { @@ -27,4 +23,4 @@ export default compose( // In < medium viewports we override this option and always show the publish sidebar. // See the edit-post's header component for the specific logic. ifViewportMatches( 'medium' ) -)( BaseOption ); +)( PreferencesModalToggle ); diff --git a/packages/edit-post/src/components/preferences-modal/options/test/__snapshots__/enable-custom-fields.js.snap b/packages/edit-post/src/components/preferences-modal/options/test/__snapshots__/enable-custom-fields.js.snap index a6e01408f2af75..a0f4a5bf150510 100644 --- a/packages/edit-post/src/components/preferences-modal/options/test/__snapshots__/enable-custom-fields.js.snap +++ b/packages/edit-post/src/components/preferences-modal/options/test/__snapshots__/enable-custom-fields.js.snap @@ -15,7 +15,7 @@ exports[`EnableCustomFieldsOption renders a checked checkbox and a confirmation }
{ it( 'renders a checked checkbox when custom fields are enabled', () => { @@ -37,7 +37,9 @@ describe( 'EnableCustomFieldsOption', () => { ); act( () => { - renderer.root.findByType( BaseOption ).props.onChange( false ); + renderer.root + .findByType( PreferencesModalToggle ) + .props.onChange( false ); } ); expect( renderer ).toMatchSnapshot(); } ); @@ -47,7 +49,9 @@ describe( 'EnableCustomFieldsOption', () => { ); act( () => { - renderer.root.findByType( BaseOption ).props.onChange( true ); + renderer.root + .findByType( PreferencesModalToggle ) + .props.onChange( true ); } ); expect( renderer ).toMatchSnapshot(); } ); diff --git a/packages/edit-post/src/components/preferences-modal/panel-preferences.js b/packages/edit-post/src/components/preferences-modal/panel-preferences.js new file mode 100644 index 00000000000000..4dc685c844680f --- /dev/null +++ b/packages/edit-post/src/components/preferences-modal/panel-preferences.js @@ -0,0 +1,138 @@ +/** + * External dependencies + */ +import { get, filter, map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { + PostTaxonomies, + PostExcerptCheck, + PageAttributesCheck, + PostFeaturedImageCheck, + PostTypeSupportCheck, + store as editorStore, +} from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; +import { PreferencesModalSection } from '@wordpress/interface'; + +/** + * Internal dependencies + */ +import { + EnableCustomFieldsOption, + EnablePluginDocumentSettingPanelOption, + EnablePanelOption, +} from './options'; +import { store as editPostStore } from '../../store'; + +export function MetaBoxesSection() { + const { areCustomFieldsRegistered, metaBoxes } = useSelect( ( select ) => { + const { getEditorSettings } = select( editorStore ); + const { getAllMetaBoxes } = select( editPostStore ); + + return { + // This setting should not live in the block editor's store. + areCustomFieldsRegistered: + getEditorSettings().enableCustomFields !== undefined, + metaBoxes: getAllMetaBoxes(), + }; + } ); + // The 'Custom Fields' meta box is a special case that we handle separately. + const thirdPartyMetaBoxes = filter( + metaBoxes, + ( { id } ) => id !== 'postcustom' + ); + + if ( ! areCustomFieldsRegistered && thirdPartyMetaBoxes.length === 0 ) { + return null; + } + + return ( + + { areCustomFieldsRegistered && ( + + ) } + { map( thirdPartyMetaBoxes, ( { id, title } ) => ( + + ) ) } + + ); +} + +export default function PanelPreferences() { + const isViewable = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + const { getPostType } = select( coreStore ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); + return get( postType, [ 'viewable' ], false ); + }, [] ); + + return ( + <> + + + { isViewable && ( + + ) } + { isViewable && ( + + ) } + ( + + ) } + /> + + + + + + + + + + + + + + + + ); +} diff --git a/packages/edit-post/src/components/preferences-modal/section.js b/packages/edit-post/src/components/preferences-modal/section.js deleted file mode 100644 index 6e8ad792878c85..00000000000000 --- a/packages/edit-post/src/components/preferences-modal/section.js +++ /dev/null @@ -1,15 +0,0 @@ -const Section = ( { description, title, children } ) => ( -
-

- { title } -

- { description && ( -

- { description } -

- ) } - { children } -
-); - -export default Section; diff --git a/packages/edit-post/src/components/preferences-modal/style.scss b/packages/edit-post/src/components/preferences-modal/style.scss index 71ac420cd2aba3..dc6a08cccc5125 100644 --- a/packages/edit-post/src/components/preferences-modal/style.scss +++ b/packages/edit-post/src/components/preferences-modal/style.scss @@ -1,137 +1,7 @@ -$vertical-tabs-width: 160px; - .edit-post-preferences-modal { - // To keep modal dimensions consistent as subsections are navigated, width - // and height are used instead of max-(width/height). - @include break-small() { - width: calc(100% - #{ $grid-unit-20 * 2 }); - height: calc(100% - #{ $header-height * 2 }); - } - @include break-medium() { - width: $break-medium - $grid-unit-20 * 2; - } - @include break-large() { - height: 70%; - } - - // Clears spacing to flush fit the navigation component to the modal edges. - @media (max-width: #{ ($break-medium - 1) }) { - .components-modal__content { - padding: 0; - - &::before { - content: none; - } - } - } - - .components-navigation { - background-color: $white; - padding: 0; - max-height: 100%; - overflow-y: auto; - - > * { - // Matches spacing cleared from the modal content element. - padding: $grid-unit-30 $grid-unit-40; - } - - .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; - // Aligns button text instead of button box. - margin: 0 #{-$grid-unit-20}; - width: calc(#{$grid-unit-40} + 100%); - &: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 { - .components-tab-panel__tabs { - position: absolute; - top: $header-height + $grid-unit-30; - // Aligns button text instead of button box. - left: $grid-unit-20; - width: $vertical-tabs-width; - .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 { - padding-left: $grid-unit-30; - margin-left: $vertical-tabs-width; - } - } - - &__section { - margin: 0 0 2.5rem 0; - - &:last-child { - margin: 0; - } - } - - &__section-title { - font-size: 0.9rem; - font-weight: 600; - } - - &__option { - .components-base-control { - .components-base-control__field { - align-items: center; - display: flex; - margin-bottom: 0; - - & > label { - flex-grow: 1; - padding: 0.6rem 0 0.6rem 10px; - } - } + .components-navigation__menu { + .edit-post-preferences-modal__custom-fields-confirmation-button { + width: auto; } } @@ -146,17 +16,4 @@ $vertical-tabs-width: 160px; max-width: 300px; } } - - .components-base-control__help { - margin: -$grid-unit-10 0 $grid-unit-10 58px; - font-size: $helptext-font-size; - font-style: normal; - color: $gray-700; - } - .edit-post-preferences-modal__section-description { - margin: -$grid-unit-10 0 $grid-unit-10 0; - 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 deleted file mode 100644 index 5e96f24c36fef3..00000000000000 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,192 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -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/__snapshots__/meta-boxes-section.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/panel-preferences.js.snap similarity index 53% rename from packages/edit-post/src/components/preferences-modal/test/__snapshots__/meta-boxes-section.js.snap rename to packages/edit-post/src/components/preferences-modal/test/__snapshots__/panel-preferences.js.snap index 18cd57ad1252d8..13440f4342d77e 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/meta-boxes-section.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/panel-preferences.js.snap @@ -1,48 +1,51 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MetaBoxesSection renders a Custom Fields option 1`] = ` -
-
+ `; exports[`MetaBoxesSection renders a Custom Fields option and meta box options 1`] = ` -
- - -
+ `; exports[`MetaBoxesSection renders meta box options 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 deleted file mode 100644 index 918ca19ee2b761..00000000000000 --- a/packages/edit-post/src/components/preferences-modal/test/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -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', () => { - 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', () => { - useSelect.mockImplementation( () => ( { isModalActive: false } ) ); - const wrapper = shallow( ); - expect( wrapper.isEmptyRender() ).toBe( true ); - } ); -} ); diff --git a/packages/edit-post/src/components/preferences-modal/test/meta-boxes-section.js b/packages/edit-post/src/components/preferences-modal/test/meta-boxes-section.js deleted file mode 100644 index b00d2fdffb512c..00000000000000 --- a/packages/edit-post/src/components/preferences-modal/test/meta-boxes-section.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import { MetaBoxesSection } from '../meta-boxes-section'; - -describe( 'MetaBoxesSection', () => { - it( 'does not render if there are no options', () => { - const wrapper = shallow( - - ); - expect( wrapper.isEmptyRender() ).toBe( true ); - } ); - - it( 'renders a Custom Fields option', () => { - const wrapper = shallow( - - ); - expect( wrapper ).toMatchSnapshot(); - } ); - - it( 'renders meta box options', () => { - const wrapper = shallow( - - ); - expect( wrapper ).toMatchSnapshot(); - } ); - - it( 'renders a Custom Fields option and meta box options', () => { - const wrapper = shallow( - - ); - expect( wrapper ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/edit-post/src/components/preferences-modal/test/panel-preferences.js b/packages/edit-post/src/components/preferences-modal/test/panel-preferences.js new file mode 100644 index 00000000000000..b12f7c01c3147a --- /dev/null +++ b/packages/edit-post/src/components/preferences-modal/test/panel-preferences.js @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { MetaBoxesSection } from '../panel-preferences'; + +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + +describe( 'MetaBoxesSection', () => { + it( 'does not render if there are no options', () => { + useSelect.mockImplementation( () => ( { + areCustomFieldsRegistered: false, + metaBoxes: [ + { id: 'postcustom', title: 'This should not render' }, + ], + } ) ); + + const wrapper = shallow( ); + expect( wrapper.isEmptyRender() ).toBe( true ); + } ); + + it( 'renders a Custom Fields option', () => { + useSelect.mockImplementation( () => ( { + areCustomFieldsRegistered: true, + metaBoxes: [ + { id: 'postcustom', title: 'This should not render' }, + ], + } ) ); + + const wrapper = shallow( ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders meta box options', () => { + useSelect.mockImplementation( () => ( { + areCustomFieldsRegistered: false, + metaBoxes: [ + { id: 'postcustom', title: 'This should not render' }, + { id: 'test1', title: 'Meta Box 1' }, + { id: 'test2', title: 'Meta Box 2' }, + ], + } ) ); + + const wrapper = shallow( ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders a Custom Fields option and meta box options', () => { + useSelect.mockImplementation( () => ( { + areCustomFieldsRegistered: true, + metaBoxes: [ + { id: 'postcustom', title: 'This should not render' }, + { id: 'test1', title: 'Meta Box 1' }, + { id: 'test2', title: 'Meta Box 2' }, + ], + } ) ); + + const wrapper = shallow( ); + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index 63fe0452e89735..a0ea27bd7a5871 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Change + +- The `toggleFeature` action and the `isFeatureActive` selector are now deprecated and will be removed in a future release. Implementers should migrate to the `toggleFeature` action and `isFeatureActive` selector in the `@wordpress/interface` package. + ## 3.0.0 (2021-07-29) ### Breaking Change diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index fbc70cbd974d22..8f98797f86e184 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -5,6 +5,7 @@ ### New Feature - Add support for editor 'feature' preferences. Adds an `isFeatureActive` selector, a `toggleFeature` action, a `MoreMenuDropdown` component, and a `MoreMenuFeatureToggle` component. ([#33774](https://github.com/WordPress/gutenberg/pull/33774)). +- Add support for building preferences modals. Add the `PreferencesModal`, `PreferencesModalTabs`, `PreferencesModalSection`, `PreferencesModalToggle` and `PreferencesModalFeatureToggle` components ([#34195](https://github.com/WordPress/gutenberg/pull/34195)). ## 4.0.0 (2021-07-29) diff --git a/packages/interface/src/components/index.js b/packages/interface/src/components/index.js index 27b41f4ecb999e..690dc3186fb7f5 100644 --- a/packages/interface/src/components/index.js +++ b/packages/interface/src/components/index.js @@ -1,8 +1,13 @@ +export { default as ActionItem } from './action-item'; export { default as ComplementaryArea } from './complementary-area'; export { default as ComplementaryAreaMoreMenuItem } from './complementary-area-more-menu-item'; export { default as FullscreenMode } from './fullscreen-mode'; export { default as InterfaceSkeleton } from './interface-skeleton'; -export { default as PinnedItems } from './pinned-items'; export { default as MoreMenuDropdown } from './more-menu-dropdown'; export { default as MoreMenuFeatureToggle } from './more-menu-feature-toggle'; -export { default as ActionItem } from './action-item'; +export { default as PinnedItems } from './pinned-items'; +export { default as PreferencesModal } from './preferences-modal'; +export { default as PreferencesModalTabs } from './preferences-modal/tabs'; +export { default as PreferencesModalSection } from './preferences-modal/section'; +export { default as PreferencesModalToggle } from './preferences-modal-toggle'; +export { default as PreferencesModalFeatureToggle } from './preferences-modal-toggle/feature-toggle'; diff --git a/packages/interface/src/components/preferences-modal-toggle/README.md b/packages/interface/src/components/preferences-modal-toggle/README.md new file mode 100644 index 00000000000000..263c67e2d895de --- /dev/null +++ b/packages/interface/src/components/preferences-modal-toggle/README.md @@ -0,0 +1,70 @@ +# PreferencesModalToggle components + +`PreferencesModalToggle` and `PreferencesModalFeatureToggle` can be used to implement toggles in the preferences modal (see the `PreferencesModal` component in this package). + +# PreferencesModalToggle + +`PreferencesModalToggle` can be used to implement a toggle with a custom behavior. It should be implemented as a controlled component with the `isChecked` and `onChange` props used to control the value of the toggle. + +## Props + +### isChecked + +The value for the toggle. + +- Type: `boolean` +- Required: Yes + +### onChange + +A function that is triggered when the control is toggled by the user. The function receives the boolean value of the control as an argument. Use this prop to update the value of the `isChecked` state that is used to control the component. + +- Type: `function` +- Required: Yes + +### label + +The visible label of the toggle. + +- Type: `String` +- Required: Yes + +### help + +Additional visible text that can be used to provide the user with more information about the toggle. + +- Type: `String` +- Required: No + +# PreferencesModalFeatureToggle + +`PreferencesModalFeatureToggle` is similar to `PreferencesModalToggle`, but should not be implemented as a controlled component. Instead, pass `scope` and `feature` props to the component at render time, and this component will connect to the `interface` package's store and modify a 'feature' value. + +### scope + +The 'scope' of the feature. This is usually a namespaced string that represents the name of the editor (e.g. 'core/edit-post'), and often matches the name of the store for the editor. + +- Type: `String` +- Required: Yes + +### feature + +The name of the feature to toggle (e.g. 'fixedToolbar'). + +- Type: `String` +- Required: Yes + + +### label + +The visible label of the toggle. + +- Type: `String` +- Required: Yes + +### help + +Additional visible text that can be used to provide the user with more information about the toggle. + +- Type: `String` +- Required: No diff --git a/packages/interface/src/components/preferences-modal-toggle/feature-toggle.js b/packages/interface/src/components/preferences-modal-toggle/feature-toggle.js new file mode 100644 index 00000000000000..50473bcfda6c3a --- /dev/null +++ b/packages/interface/src/components/preferences-modal-toggle/feature-toggle.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import PreferencesModalToggle from '../preferences-modal-toggle'; +import { store as interfaceStore } from '../../store'; + +export default function PreferencesModalFeatureToggle( { + scope, + feature, + ...props +} ) { + const isChecked = useSelect( + ( select ) => + select( interfaceStore ).isFeatureActive( scope, feature ), + [ scope, feature ] + ); + const { toggleFeature } = useDispatch( interfaceStore ); + + return ( + { + toggleFeature( scope, feature ); + } } + /> + ); +} diff --git a/packages/edit-post/src/components/preferences-modal/options/base.js b/packages/interface/src/components/preferences-modal-toggle/index.js similarity index 60% rename from packages/edit-post/src/components/preferences-modal/options/base.js rename to packages/interface/src/components/preferences-modal-toggle/index.js index b1edfc18a6de18..ba06356796ba84 100644 --- a/packages/edit-post/src/components/preferences-modal/options/base.js +++ b/packages/interface/src/components/preferences-modal-toggle/index.js @@ -3,9 +3,15 @@ */ import { ToggleControl } from '@wordpress/components'; -function BaseOption( { help, label, isChecked, onChange, children } ) { +export default function PreferencesModalToggle( { + help, + label, + isChecked, + onChange, + children, +} ) { return ( -
+
); } - -export default BaseOption; diff --git a/packages/interface/src/components/preferences-modal-toggle/style.scss b/packages/interface/src/components/preferences-modal-toggle/style.scss new file mode 100644 index 00000000000000..345be6b71409b0 --- /dev/null +++ b/packages/interface/src/components/preferences-modal-toggle/style.scss @@ -0,0 +1,15 @@ + +.interface-preferences-modal-toggle { + .components-base-control { + .components-base-control__field { + align-items: center; + display: flex; + margin-bottom: 0; + + & > label { + flex-grow: 1; + padding: 0.6rem 0 0.6rem 10px; + } + } + } +} diff --git a/packages/interface/src/components/preferences-modal/README.md b/packages/interface/src/components/preferences-modal/README.md new file mode 100644 index 00000000000000..286a33bbd91dd6 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/README.md @@ -0,0 +1,136 @@ +# PreferencesModal components + +`PreferencesModal`, `PreferencesModalTabs` and `PreferencesModalSection` are a family of components that greatly simplify building a WordPress editor preferences modal. + +`PreferencesModal` is a basic modal component that implements `Modal` from the `@wordpress/components` package. + +`PreferenceModalTabs` should be used to implement the responsive tab based menu structure within the preferences modal. It implements a `TabPanel` from `@wordpress/components` for desktop viewports, and a `Navigation` menu for smaller viewports. + +`PreferenceModalSection` represents a particular section within a tab that groups settings. + +See also the `PreferencesModalToggle` and `PreferencesModalFeatureToggle` components in this package that can be used to implement individual preference toggles. + +## Example + +```jsx +function MyPreferencesModal( { closeModal }) { + // ... + + const tabs = useMemo( + () => [ + { + name: 'general', + title: __( 'General' ), + content: ( + + + + + ), + }, + { + name: 'panels', + title: __( 'Panels' ), + content: ( + + + + ), + } + ], + [] + ); + + return ( + + + + ); +} +``` + +# PreferencesModal + +`PreferencesModal` implements `Modal` from the `@wordpress/components` package and accepts the same props. + +## Props + +### onRequestClose + +A function that is called when the user has indicated they want to close the modal. For example when clicking the close button, pressing Escape, or clicking outside of the modal. + +- Type: `function` +- Required: Yes + +### title + +The title of the modal. Defaults to the internationalized string 'Preferences'. + +- Type: `String` +- Required: No + +### closeLabel + +The label of the close button in the modal. Defaults tot he internationalized string 'Close'. + +- Type: `String` +- Required: No + +# PreferencesModalTabs + +## Props + +### tabs + +A flat array of tabs. Each item in the array is an object with three properties: + +- `name` - A unique name for the tab. This value is used internally by the `PreferencesModalTabs` to track which tab is currently visible. +- `title` - The visible title of the tab. +- `content` - A react element representing the content of the tab. + +- Type: `array` +- Required: Yes + +# PreferencesModalSection + +## Props + +### title + +The title of the section. + +- Type: `String` +- Required: Yes + +### description + +A description of the section. + +- Type: `String` +- Required: No diff --git a/packages/interface/src/components/preferences-modal/index.js b/packages/interface/src/components/preferences-modal/index.js new file mode 100644 index 00000000000000..87d0de8f394c38 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { Modal } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +export default function PreferencesModal( { + title = __( 'Preferences' ), + closeLabel = __( 'Close' ), + ...props +} ) { + return ( + + ); +} diff --git a/packages/interface/src/components/preferences-modal/section.js b/packages/interface/src/components/preferences-modal/section.js new file mode 100644 index 00000000000000..3567f15a862337 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/section.js @@ -0,0 +1,19 @@ +export default function PreferencesModalSection( { + description, + title, + children, +} ) { + return ( +
+

+ { title } +

+ { description && ( +

+ { description } +

+ ) } + { children } +
+ ); +} diff --git a/packages/interface/src/components/preferences-modal/style.scss b/packages/interface/src/components/preferences-modal/style.scss new file mode 100644 index 00000000000000..d4f8fec5bf4f4f --- /dev/null +++ b/packages/interface/src/components/preferences-modal/style.scss @@ -0,0 +1,151 @@ +$vertical-tabs-width: 160px; + +// Modal styles. +.interface-preferences-modal { + // To keep modal dimensions consistent as subsections are navigated, width + // and height are used instead of max-(width/height). + @include break-small() { + width: calc(100% - #{ $grid-unit-20 * 2 }); + height: calc(100% - #{ $header-height * 2 }); + } + @include break-medium() { + width: $break-medium - $grid-unit-20 * 2; + } + @include break-large() { + height: 70%; + } + + // Clears spacing to flush fit the navigation component to the modal edges. + @media (max-width: #{ ($break-medium - 1) }) { + .components-modal__content { + padding: 0; + + &::before { + content: none; + } + } + } + + // TODO - find out why this is needed. + .components-base-control__help { + margin: -$grid-unit-10 0 $grid-unit-10 58px; + font-size: $helptext-font-size; + font-style: normal; + color: $gray-700; + } +} + +// Tab styles. +.interface-preferences-modal__tab-panel { + .components-tab-panel__tabs { + position: absolute; + top: $header-height + $grid-unit-30; + + // Aligns button text instead of button box. + left: $grid-unit-20; + width: $vertical-tabs-width; + + .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 { + padding-left: $grid-unit-30; + margin-left: $vertical-tabs-width; + } +} + +// Needs specificity, so double up the class name. +.interface-preferences-modal__navigation.components-navigation { + background-color: $white; + padding: 0; + max-height: 100%; + overflow-y: auto; + + > * { + // Matches spacing cleared from the modal content element. + padding: $grid-unit-30 $grid-unit-40; + } +} + +// Needs specificity, so double up the class name. +.interface-preferences-modal__navigation-menu.components-navigation__menu { + margin: 0; + color: $gray-900; + + .components-navigation__menu-title-heading { + color: inherit; + border-bottom: 1px solid $gray-300; + padding-left: 0; + padding-right: 0; + } + + // Needs specificity, so double up the class name. + .components-navigation__back-button.components-button { + color: inherit; + padding-left: 0; + + &:hover { + color: var(--wp-admin-theme-color); + } + } +} + +// Needs specificity, so double up the class name. +.interface-preferences-modal__navigation-item.components-navigation__item { + color: $gray-900; + + & > button { + color: inherit; + padding: 3px $grid-unit-20; + height: $grid-unit-60; + + // Aligns button text instead of button box. + margin: 0 #{-$grid-unit-20}; + width: calc(#{$grid-unit-40} + 100%); + + &:focus { + background: $gray-100; + font-weight: 500; + } + + &:hover { + color: var(--wp-admin-theme-color); + } + } + + .components-toggle-control__label { + color: inherit; + } +} + +// Section styles. +.interface-preferences-modal__section { + margin: 0 0 2.5rem 0; + + &:last-child { + margin: 0; + } +} + +.interface-preferences-modal__section-title { + font-size: 0.9rem; + font-weight: 600; +} + +.interface-preferences-modal__section-description { + margin: -$grid-unit-10 0 $grid-unit-10 0; + font-size: $helptext-font-size; + font-style: normal; + color: $gray-700; +} diff --git a/packages/interface/src/components/preferences-modal/tabs.js b/packages/interface/src/components/preferences-modal/tabs.js new file mode 100644 index 00000000000000..a9848b39fedd93 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/tabs.js @@ -0,0 +1,84 @@ +/** + * WordPress dependencies + */ +import { + __experimentalNavigation as Navigation, + __experimentalNavigationMenu as NavigationMenu, + __experimentalNavigationItem as NavigationItem, + TabPanel, +} from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; +import { useCallback, useState } from '@wordpress/element'; + +const TOP_LEVEL_MENU_NAME = 'interface/preferences-modal-top-level-menu'; + +export default function PreferencesModalTabs( { tabs } ) { + const isTabbedLayout = useViewportMatch( 'medium' ); + const [ activeTabName, setActiveTabName ] = useState(); + + // Tab panel requires a callback child, so make one. + const getCurrentTabContent = useCallback( + () => + tabs.find( ( tab ) => tab.name === activeTabName )?.content || + tabs[ 0 ].content, + [ tabs, activeTabName ] + ); + + // On big screens, do a tabbed layout. + if ( isTabbedLayout ) { + // If the 'top level menu' is selected in the navigation layout, treat + // it as an unselected tab. + const hasUnselectedTab = + ! activeTabName || activeTabName === TOP_LEVEL_MENU_NAME; + + return ( + + { getCurrentTabContent } + + ); + } + + // On little screens, do a navigation layout. + return ( + + + { tabs.map( ( tab ) => ( + + ) ) } + + { tabs.map( ( tab ) => ( + + + { tab.content } + + + ) ) } + + ); +} diff --git a/packages/interface/src/style.scss b/packages/interface/src/style.scss index e6950de411156a..77664aeead7896 100644 --- a/packages/interface/src/style.scss +++ b/packages/interface/src/style.scss @@ -4,3 +4,5 @@ @import "./components/interface-skeleton/style.scss"; @import "./components/more-menu-dropdown/style.scss"; @import "./components/pinned-items/style.scss"; +@import "./components/preferences-modal/style.scss"; +@import "./components/preferences-modal-toggle/style.scss";