From 1103f7ba9f20fada5af22cb6d86bd26e75defea6 Mon Sep 17 00:00:00 2001 From: Daniel Richards <daniel.richards@automattic.com> Date: Wed, 19 May 2021 14:59:44 +0800 Subject: [PATCH] Add more menu to customize widgets (#31970) * Add basic more menu framework * Hook up top toolbar and contain caret inside blocks preferences to editor settings * Introduce keyboard shortcut modal * Fix modal styles * Add shortcut for modal itself * Tidy up bottom margin when top toolbar is active * Toolbar adjustments * Update package lock --- package-lock.json | 1 + packages/base-styles/_z-index.scss | 1 + packages/customize-widgets/package.json | 1 + .../src/components/header/index.js | 25 ++- .../src/components/header/style.scss | 15 +- .../keyboard-shortcut-help-modal/config.js | 27 ++++ .../dynamic-shortcut.js | 40 +++++ .../keyboard-shortcut-help-modal/index.js | 153 ++++++++++++++++++ .../keyboard-shortcut-help-modal/shortcut.js | 70 ++++++++ .../keyboard-shortcut-help-modal/style.scss | 66 ++++++++ .../components/more-menu/feature-toggle.js | 56 +++++++ .../src/components/more-menu/index.js | 130 +++++++++++++++ .../src/components/more-menu/style.scss | 35 ++++ .../components/sidebar-block-editor/index.js | 34 +++- .../customize-widgets/src/store/actions.js | 17 ++ .../customize-widgets/src/store/constants.js | 4 + .../customize-widgets/src/store/defaults.js | 6 + packages/customize-widgets/src/store/index.js | 39 +++++ .../customize-widgets/src/store/reducer.js | 54 +++++++ .../customize-widgets/src/store/selectors.js | 20 +++ packages/customize-widgets/src/style.scss | 7 + 21 files changed, 786 insertions(+), 15 deletions(-) create mode 100644 packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js create mode 100644 packages/customize-widgets/src/components/keyboard-shortcut-help-modal/dynamic-shortcut.js create mode 100644 packages/customize-widgets/src/components/keyboard-shortcut-help-modal/index.js create mode 100644 packages/customize-widgets/src/components/keyboard-shortcut-help-modal/shortcut.js create mode 100644 packages/customize-widgets/src/components/keyboard-shortcut-help-modal/style.scss create mode 100644 packages/customize-widgets/src/components/more-menu/feature-toggle.js create mode 100644 packages/customize-widgets/src/components/more-menu/index.js create mode 100644 packages/customize-widgets/src/components/more-menu/style.scss create mode 100644 packages/customize-widgets/src/store/actions.js create mode 100644 packages/customize-widgets/src/store/constants.js create mode 100644 packages/customize-widgets/src/store/defaults.js create mode 100644 packages/customize-widgets/src/store/index.js create mode 100644 packages/customize-widgets/src/store/reducer.js create mode 100644 packages/customize-widgets/src/store/selectors.js diff --git a/package-lock.json b/package-lock.json index fb39fee02ed88b..f1d4ce702a8c7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13493,6 +13493,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", + "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/widgets": "file:packages/widgets", diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 8131aa86dfdf02..fd5fc778cab6b3 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -141,6 +141,7 @@ $z-layers: ( ".components-popover.block-editor-inserter__popover": 99999, ".components-popover.table-of-contents__popover": 99998, ".components-popover.block-editor-block-navigation__popover": 99998, + ".components-popover.customize-widgets-more-menu__content": 99998, ".components-popover.edit-post-more-menu__content": 99998, ".components-popover.edit-site-more-menu__content": 99998, ".components-popover.edit-widgets-more-menu__content": 99998, diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 3324ab6c992102..63b977fd8006cb 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -39,6 +39,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/widgets": "file:../widgets", diff --git a/packages/customize-widgets/src/components/header/index.js b/packages/customize-widgets/src/components/header/index.js index 11b5353a474ef5..3a2eb883f6798a 100644 --- a/packages/customize-widgets/src/components/header/index.js +++ b/packages/customize-widgets/src/components/header/index.js @@ -1,9 +1,14 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { createPortal } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; -import { Button, ToolbarItem } from '@wordpress/components'; +import { ToolbarButton } from '@wordpress/components'; import { NavigableToolbar } from '@wordpress/block-editor'; import { plus } from '@wordpress/icons'; @@ -11,17 +16,26 @@ import { plus } from '@wordpress/icons'; * Internal dependencies */ import Inserter from '../inserter'; +import MoreMenu from '../more-menu'; -function Header( { inserter, isInserterOpened, setIsInserterOpened } ) { +function Header( { + inserter, + isInserterOpened, + setIsInserterOpened, + isFixedToolbarActive, +} ) { return ( <> - <div className="customize-widgets-header"> + <div + className={ classnames( 'customize-widgets-header', { + 'is-fixed-toolbar-active': isFixedToolbarActive, + } ) } + > <NavigableToolbar className="customize-widgets-header-toolbar" aria-label={ __( 'Document tools' ) } > - <ToolbarItem - as={ Button } + <ToolbarButton className="customize-widgets-header-toolbar__inserter-toggle" isPressed={ isInserterOpened } isPrimary @@ -34,6 +48,7 @@ function Header( { inserter, isInserterOpened, setIsInserterOpened } ) { setIsInserterOpened( ( isOpen ) => ! isOpen ); } } /> + <MoreMenu /> </NavigableToolbar> </div> diff --git a/packages/customize-widgets/src/components/header/style.scss b/packages/customize-widgets/src/components/header/style.scss index ae829441cb6777..106631cd742f36 100644 --- a/packages/customize-widgets/src/components/header/style.scss +++ b/packages/customize-widgets/src/components/header/style.scss @@ -1,13 +1,20 @@ .customize-widgets-header { @include break-medium() { - // The mobile fixed block toolbar should be snug under the header. + // Make space for the floating toolbar. margin-bottom: $grid-unit-60 + $default-block-margin; } + &.is-fixed-toolbar-active { + // Top toolbar mode toolbar should be right under the header. + margin-bottom: 0; + } + + display: flex; + justify-content: flex-end; + // Offset the customizer's sidebar padding. - // Provide enough bottom margin to ensure the floating block toolbar isn't overlapped. + // Zero bottom margin so that the fixed toolbar is right under the header. margin: -15px ( -$grid-unit-15 ) ( 0 ) ( -$grid-unit-15 ); - padding: $grid-unit-15; // Match the customizer grey background. background: #f0f0f1; @@ -26,7 +33,7 @@ padding: 0; min-width: $grid-unit-30; height: $grid-unit-30; - margin-left: auto; + margin: $grid-unit-15 0 $grid-unit-15; &::before { content: none; diff --git a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js new file mode 100644 index 00000000000000..7b420cabfebb2a --- /dev/null +++ b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +export const textFormattingShortcuts = [ + { + keyCombination: { modifier: 'primary', character: 'b' }, + description: __( 'Make the selected text bold.' ), + }, + { + keyCombination: { modifier: 'primary', character: 'i' }, + description: __( 'Make the selected text italic.' ), + }, + { + keyCombination: { modifier: 'primary', character: 'k' }, + description: __( 'Convert the selected text into a link.' ), + }, + { + keyCombination: { modifier: 'primaryShift', character: 'k' }, + description: __( 'Remove a link.' ), + }, + { + keyCombination: { modifier: 'primary', character: 'u' }, + description: __( 'Underline the selected text.' ), + }, +]; diff --git a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/dynamic-shortcut.js b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/dynamic-shortcut.js new file mode 100644 index 00000000000000..fe97fba37e14ac --- /dev/null +++ b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/dynamic-shortcut.js @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import Shortcut from './shortcut'; + +function DynamicShortcut( { name } ) { + const { keyCombination, description, aliases } = useSelect( ( select ) => { + const { + getShortcutKeyCombination, + getShortcutDescription, + getShortcutAliases, + } = select( keyboardShortcutsStore ); + + return { + keyCombination: getShortcutKeyCombination( name ), + aliases: getShortcutAliases( name ), + description: getShortcutDescription( name ), + }; + } ); + + if ( ! keyCombination ) { + return null; + } + + return ( + <Shortcut + keyCombination={ keyCombination } + description={ description } + aliases={ aliases } + /> + ); +} + +export default DynamicShortcut; diff --git a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/index.js b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/index.js new file mode 100644 index 00000000000000..e600474a933cb8 --- /dev/null +++ b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/index.js @@ -0,0 +1,153 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { isString } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Modal } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + useShortcut, + store as keyboardShortcutsStore, +} from '@wordpress/keyboard-shortcuts'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { textFormattingShortcuts } from './config'; +import Shortcut from './shortcut'; +import DynamicShortcut from './dynamic-shortcut'; + +const ShortcutList = ( { shortcuts } ) => ( + /* + * Disable reason: The `list` ARIA role is redundant but + * Safari+VoiceOver won't announce the list otherwise. + */ + /* eslint-disable jsx-a11y/no-redundant-roles */ + <ul + className="customize-widgets-keyboard-shortcut-help-modal__shortcut-list" + role="list" + > + { shortcuts.map( ( shortcut, index ) => ( + <li + className="customize-widgets-keyboard-shortcut-help-modal__shortcut" + key={ index } + > + { isString( shortcut ) ? ( + <DynamicShortcut name={ shortcut } /> + ) : ( + <Shortcut { ...shortcut } /> + ) } + </li> + ) ) } + </ul> + /* eslint-enable jsx-a11y/no-redundant-roles */ +); + +const ShortcutSection = ( { title, shortcuts, className } ) => ( + <section + className={ classnames( + 'customize-widgets-keyboard-shortcut-help-modal__section', + className + ) } + > + { !! title && ( + <h2 className="customize-widgets-keyboard-shortcut-help-modal__section-title"> + { title } + </h2> + ) } + <ShortcutList shortcuts={ shortcuts } /> + </section> +); + +const ShortcutCategorySection = ( { + title, + categoryName, + additionalShortcuts = [], +} ) => { + const categoryShortcuts = useSelect( + ( select ) => { + return select( keyboardShortcutsStore ).getCategoryShortcuts( + categoryName + ); + }, + [ categoryName ] + ); + + return ( + <ShortcutSection + title={ title } + shortcuts={ categoryShortcuts.concat( additionalShortcuts ) } + /> + ); +}; + +export default function KeyboardShortcutHelpModal( { + isModalActive, + toggleModal, +} ) { + const { registerShortcut } = useDispatch( keyboardShortcutsStore ); + registerShortcut( { + name: 'core/customize-widgets/keyboard-shortcuts', + category: 'main', + description: __( 'Display these keyboard shortcuts.' ), + keyCombination: { + modifier: 'access', + character: 'h', + }, + } ); + + useShortcut( 'core/customize-widgets/keyboard-shortcuts', toggleModal, { + bindGlobal: true, + } ); + + if ( ! isModalActive ) { + return null; + } + + return ( + <Modal + className="customize-widgets-keyboard-shortcut-help-modal" + title={ __( 'Keyboard shortcuts' ) } + closeLabel={ __( 'Close' ) } + onRequestClose={ toggleModal } + > + <ShortcutSection + className="customize-widgets-keyboard-shortcut-help-modal__main-shortcuts" + shortcuts={ [ 'core/customize-widgets/keyboard-shortcuts' ] } + /> + <ShortcutCategorySection + title={ __( 'Global shortcuts' ) } + categoryName="global" + /> + + <ShortcutCategorySection + title={ __( 'Selection shortcuts' ) } + categoryName="selection" + /> + + <ShortcutCategorySection + title={ __( 'Block shortcuts' ) } + categoryName="block" + additionalShortcuts={ [ + { + keyCombination: { character: '/' }, + description: __( + 'Change the block type after adding a new paragraph.' + ), + /* translators: The forward-slash character. e.g. '/'. */ + ariaLabel: __( 'Forward-slash' ), + }, + ] } + /> + <ShortcutSection + title={ __( 'Text formatting' ) } + shortcuts={ textFormattingShortcuts } + /> + </Modal> + ); +} diff --git a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/shortcut.js b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/shortcut.js new file mode 100644 index 00000000000000..c25bdbb20e6725 --- /dev/null +++ b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/shortcut.js @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import { castArray } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Fragment } from '@wordpress/element'; +import { displayShortcutList, shortcutAriaLabel } from '@wordpress/keycodes'; + +function KeyCombination( { keyCombination, forceAriaLabel } ) { + const shortcut = keyCombination.modifier + ? displayShortcutList[ keyCombination.modifier ]( + keyCombination.character + ) + : keyCombination.character; + const ariaLabel = keyCombination.modifier + ? shortcutAriaLabel[ keyCombination.modifier ]( + keyCombination.character + ) + : keyCombination.character; + + return ( + <kbd + className="customize-widgets-keyboard-shortcut-help-modal__shortcut-key-combination" + aria-label={ forceAriaLabel || ariaLabel } + > + { castArray( shortcut ).map( ( character, index ) => { + if ( character === '+' ) { + return <Fragment key={ index }>{ character }</Fragment>; + } + + return ( + <kbd + key={ index } + className="customize-widgets-keyboard-shortcut-help-modal__shortcut-key" + > + { character } + </kbd> + ); + } ) } + </kbd> + ); +} + +function Shortcut( { description, keyCombination, aliases = [], ariaLabel } ) { + return ( + <> + <div className="customize-widgets-keyboard-shortcut-help-modal__shortcut-description"> + { description } + </div> + <div className="customize-widgets-keyboard-shortcut-help-modal__shortcut-term"> + <KeyCombination + keyCombination={ keyCombination } + forceAriaLabel={ ariaLabel } + /> + { aliases.map( ( alias, index ) => ( + <KeyCombination + keyCombination={ alias } + forceAriaLabel={ ariaLabel } + key={ index } + /> + ) ) } + </div> + </> + ); +} + +export default Shortcut; diff --git a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/style.scss b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/style.scss new file mode 100644 index 00000000000000..507935a561d433 --- /dev/null +++ b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/style.scss @@ -0,0 +1,66 @@ +.customize-widgets-keyboard-shortcut-help-modal { + &__section { + margin: 0 0 2rem 0; + } + + &__main-shortcuts .customize-widgets-keyboard-shortcut-help-modal__shortcut-list { + // Push the shortcut to be flush with top modal header. + margin-top: -$grid-unit-30 -$border-width; + } + + &__section-title { + font-size: 0.9rem; + font-weight: 600; + } + + &__shortcut { + display: flex; + align-items: baseline; + padding: 0.6rem 0; + border-top: 1px solid $gray-300; + margin-bottom: 0; + + &:last-child { + border-bottom: 1px solid $gray-300; + } + + &:empty { + display: none; + } + } + + &__shortcut-term { + font-weight: 600; + margin: 0 0 0 1rem; + text-align: right; + } + + &__shortcut-description { + flex: 1; + margin: 0; + + // IE 11 flex item fix - ensure the item does not collapse. + flex-basis: auto; + } + + &__shortcut-key-combination { + display: block; + background: none; + margin: 0; + padding: 0; + + & + .customize-widgets-keyboard-shortcut-help-modal__shortcut-key-combination { + margin-top: 10px; + } + } + + &__shortcut-key { + padding: 0.25rem 0.5rem; + border-radius: 8%; + margin: 0 0.2rem 0 0.2rem; + + &:last-child { + margin: 0 0 0 0.2rem; + } + } +} diff --git a/packages/customize-widgets/src/components/more-menu/feature-toggle.js b/packages/customize-widgets/src/components/more-menu/feature-toggle.js new file mode 100644 index 00000000000000..6235a0171814a0 --- /dev/null +++ b/packages/customize-widgets/src/components/more-menu/feature-toggle.js @@ -0,0 +1,56 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { check } from '@wordpress/icons'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import { store as customizeWidgetsStore } from '../../store'; + +export default function FeatureToggle( { + label, + info, + messageActivated, + messageDeactivated, + shortcut, + feature, +} ) { + const isActive = useSelect( + ( select ) => + select( customizeWidgetsStore ).__unstableIsFeatureActive( + feature + ), + [ feature ] + ); + const { __unstableToggleFeature: toggleFeature } = useDispatch( + customizeWidgetsStore + ); + const speakMessage = () => { + if ( isActive ) { + speak( messageDeactivated || __( 'Feature deactivated' ) ); + } else { + speak( messageActivated || __( 'Feature activated' ) ); + } + }; + + return ( + <MenuItem + icon={ isActive && check } + isSelected={ isActive } + onClick={ () => { + toggleFeature( feature ); + speakMessage(); + } } + role="menuitemcheckbox" + info={ info } + shortcut={ shortcut } + > + { label } + </MenuItem> + ); +} diff --git a/packages/customize-widgets/src/components/more-menu/index.js b/packages/customize-widgets/src/components/more-menu/index.js new file mode 100644 index 00000000000000..ebbb673bbd22fe --- /dev/null +++ b/packages/customize-widgets/src/components/more-menu/index.js @@ -0,0 +1,130 @@ +/** + * WordPress dependencies + */ +import { + ToolbarDropdownMenu, + MenuGroup, + MenuItem, + VisuallyHidden, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; +import { external, moreVertical } from '@wordpress/icons'; +import { displayShortcut } from '@wordpress/keycodes'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import FeatureToggle from './feature-toggle'; +import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal'; + +const POPOVER_PROPS = { + className: 'customize-widgets-more-menu__content', + position: 'bottom left', +}; +const TOGGLE_PROPS = { + tooltipPosition: 'bottom', +}; + +export default function MoreMenu() { + const [ + isKeyboardShortcutsModalActive, + setIsKeyboardShortcutsModalVisible, + ] = useState( false ); + const toggleKeyboardShortcutsModal = () => + setIsKeyboardShortcutsModalVisible( ! isKeyboardShortcutsModalActive ); + + useShortcut( + 'core/customize-widgets/keyboard-shortcuts', + toggleKeyboardShortcutsModal, + { + bindGlobal: true, + } + ); + + return ( + <> + <ToolbarDropdownMenu + className="customize-widgets-more-menu" + icon={ moreVertical } + /* translators: button label text should, if possible, be under 16 characters. */ + label={ __( 'Options' ) } + popoverProps={ POPOVER_PROPS } + toggleProps={ TOGGLE_PROPS } + > + { () => ( + <> + <MenuGroup label={ _x( 'View', 'noun' ) }> + <FeatureToggle + feature="fixedToolbar" + label={ __( 'Top toolbar' ) } + info={ __( + 'Access all block and document tools in a single place' + ) } + messageActivated={ __( + 'Top toolbar activated' + ) } + messageDeactivated={ __( + 'Top toolbar deactivated' + ) } + /> + </MenuGroup> + <MenuGroup label={ __( 'Tools' ) }> + <MenuItem + onClick={ () => { + setIsKeyboardShortcutsModalVisible( true ); + } } + shortcut={ displayShortcut.access( 'h' ) } + > + { __( 'Keyboard shortcuts' ) } + </MenuItem> + <FeatureToggle + feature="welcomeGuide" + label={ __( 'Welcome Guide' ) } + /> + <MenuItem + role="menuitem" + icon={ external } + href={ __( + 'https://wordpress.org/support/article/wordpress-editor/' + ) } + target="_blank" + rel="noopener noreferrer" + > + { __( 'Help' ) } + <VisuallyHidden as="span"> + { + /* translators: accessibility text */ + __( '(opens in a new tab)' ) + } + </VisuallyHidden> + </MenuItem> + </MenuGroup> + <MenuGroup> + <FeatureToggle + feature="keepCaretInsideBlock" + label={ __( + 'Contain text cursor inside block' + ) } + info={ __( + 'Aids screen readers by stopping text caret from leaving blocks.' + ) } + messageActivated={ __( + 'Contain text cursor inside block activated' + ) } + messageDeactivated={ __( + 'Contain text cursor inside block deactivated' + ) } + /> + </MenuGroup> + </> + ) } + </ToolbarDropdownMenu> + <KeyboardShortcutHelpModal + isModalActive={ isKeyboardShortcutsModalActive } + toggleModal={ toggleKeyboardShortcutsModal } + /> + </> + ); +} diff --git a/packages/customize-widgets/src/components/more-menu/style.scss b/packages/customize-widgets/src/components/more-menu/style.scss new file mode 100644 index 00000000000000..e38068f7ec7470 --- /dev/null +++ b/packages/customize-widgets/src/components/more-menu/style.scss @@ -0,0 +1,35 @@ +.customize-widgets-more-menu { + margin-left: -4px; + + // the padding and margin of the more menu is intentionally non-standard + .components-button { + width: auto; + padding: 0 2px; + } + + @include break-small() { + margin-left: 0; + + .components-button { + padding: 0 4px; + } + } +} + +.customize-widgets-more-menu__content .components-popover__content { + min-width: 280px; + + // Let the menu scale to fit items. + @include break-mobile() { + width: auto; + max-width: $break-mobile; + } + + .components-dropdown-menu__menu { + padding: 0; + } +} + +.components-popover.customize-widgets-more-menu__content { + z-index: z-index(".components-popover.customize-widgets-more-menu__content"); +} diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js index c20f57a486db4b..bbca644880a949 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/index.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -28,6 +28,7 @@ import BlockInspectorButton from '../block-inspector-button'; import Header from '../header'; import useInserter from '../inserter/use-inserter'; import SidebarEditorProvider from './sidebar-editor-provider'; +import { store as customizeWidgetsStore } from '../../store'; export default function SidebarBlockEditor( { blockEditorSettings, @@ -36,11 +37,24 @@ export default function SidebarBlockEditor( { inspector, } ) { const [ isInserterOpened, setIsInserterOpened ] = useInserter( inserter ); - const hasUploadPermissions = useSelect( - ( select ) => - defaultTo( select( coreStore ).canUser( 'create', 'media' ), true ), - [] - ); + const { + hasUploadPermissions, + isFixedToolbarActive, + keepCaretInsideBlock, + } = useSelect( ( select ) => { + return { + hasUploadPermissions: defaultTo( + select( coreStore ).canUser( 'create', 'media' ), + true + ), + isFixedToolbarActive: select( + customizeWidgetsStore + ).__unstableIsFeatureActive( 'fixedToolbar' ), + keepCaretInsideBlock: select( + customizeWidgetsStore + ).__unstableIsFeatureActive( 'keepCaretInsideBlock' ), + }; + }, [] ); const settings = useMemo( () => { let mediaUploadBlockEditor; if ( hasUploadPermissions ) { @@ -57,8 +71,15 @@ export default function SidebarBlockEditor( { ...blockEditorSettings, __experimentalSetIsInserterOpened: setIsInserterOpened, mediaUpload: mediaUploadBlockEditor, + hasFixedToolbar: isFixedToolbarActive, + keepCaretInsideBlock, }; - }, [ hasUploadPermissions, blockEditorSettings ] ); + }, [ + hasUploadPermissions, + blockEditorSettings, + isFixedToolbarActive, + keepCaretInsideBlock, + ] ); return ( <> @@ -68,6 +89,7 @@ export default function SidebarBlockEditor( { <BlockEditorKeyboardShortcuts /> <Header + isFixedToolbarActive={ isFixedToolbarActive } inserter={ inserter } isInserterOpened={ isInserterOpened } setIsInserterOpened={ setIsInserterOpened } diff --git a/packages/customize-widgets/src/store/actions.js b/packages/customize-widgets/src/store/actions.js new file mode 100644 index 00000000000000..b156e1e3bbeadb --- /dev/null +++ b/packages/customize-widgets/src/store/actions.js @@ -0,0 +1,17 @@ +/** + * Returns an action object used to toggle a feature flag. + * + * This function is unstable, as it is mostly copied from the edit-post + * package. Editor features and preferences have a lot of scope for + * being generalized and refactored. + * + * @param {string} feature Feature name. + * + * @return {Object} Action object. + */ +export function __unstableToggleFeature( feature ) { + return { + type: 'TOGGLE_FEATURE', + feature, + }; +} diff --git a/packages/customize-widgets/src/store/constants.js b/packages/customize-widgets/src/store/constants.js new file mode 100644 index 00000000000000..82ee0d383d698a --- /dev/null +++ b/packages/customize-widgets/src/store/constants.js @@ -0,0 +1,4 @@ +/** + * Module Constants + */ +export const STORE_NAME = 'core/customize-widgets'; diff --git a/packages/customize-widgets/src/store/defaults.js b/packages/customize-widgets/src/store/defaults.js new file mode 100644 index 00000000000000..61227800b91f72 --- /dev/null +++ b/packages/customize-widgets/src/store/defaults.js @@ -0,0 +1,6 @@ +export const PREFERENCES_DEFAULTS = { + features: { + fixedToolbar: false, + welcomeGuide: true, + }, +}; diff --git a/packages/customize-widgets/src/store/index.js b/packages/customize-widgets/src/store/index.js new file mode 100644 index 00000000000000..d50209f4b95994 --- /dev/null +++ b/packages/customize-widgets/src/store/index.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { createReduxStore, registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import { STORE_NAME } from './constants'; + +/** + * Block editor data store configuration. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#registerStore + * + * @type {Object} + */ +const storeConfig = { + reducer, + selectors, + actions, + persist: [ 'preferences' ], +}; + +/** + * Store definition for the edit widgets namespace. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore + * + * @type {Object} + */ +export const store = createReduxStore( STORE_NAME, storeConfig ); + +// Once we build a more generic persistence plugin that works across types of stores +// we'd be able to replace this with a register call. +registerStore( STORE_NAME, storeConfig ); diff --git a/packages/customize-widgets/src/store/reducer.js b/packages/customize-widgets/src/store/reducer.js new file mode 100644 index 00000000000000..b3087affe11540 --- /dev/null +++ b/packages/customize-widgets/src/store/reducer.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { flow } from 'lodash'; + +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { PREFERENCES_DEFAULTS } from './defaults'; + +/** + * Higher-order reducer creator which provides the given initial state for the + * original reducer. + * + * @param {*} initialState Initial state to provide to reducer. + * + * @return {Function} Higher-order reducer. + */ +const createWithInitialState = ( initialState ) => ( reducer ) => { + return ( state = initialState, action ) => reducer( state, action ); +}; + +/** + * Reducer returning the user preferences. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export const preferences = flow( [ + combineReducers, + createWithInitialState( PREFERENCES_DEFAULTS ), +] )( { + features( state, action ) { + if ( action.type === 'TOGGLE_FEATURE' ) { + return { + ...state, + [ action.feature ]: ! state[ action.feature ], + }; + } + + return state; + }, +} ); + +export default combineReducers( { + preferences, +} ); diff --git a/packages/customize-widgets/src/store/selectors.js b/packages/customize-widgets/src/store/selectors.js new file mode 100644 index 00000000000000..cc546ec6e5a7e8 --- /dev/null +++ b/packages/customize-widgets/src/store/selectors.js @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Returns whether the given feature is enabled or not. + * + * This function is unstable, as it is mostly copied from the edit-post + * package. Editor features and preferences have a lot of scope for + * being generalized and refactored. + * + * @param {Object} state Global application state. + * @param {string} feature Feature slug. + * + * @return {boolean} Is active. + */ +export function __unstableIsFeatureActive( state, feature ) { + return get( state.preferences.features, [ feature ], false ); +} diff --git a/packages/customize-widgets/src/style.scss b/packages/customize-widgets/src/style.scss index c75380757e8c10..a96c7ec0558933 100644 --- a/packages/customize-widgets/src/style.scss +++ b/packages/customize-widgets/src/style.scss @@ -2,4 +2,11 @@ @import "./components/block-inspector-button/style.scss"; @import "./components/header/style.scss"; @import "./components/inserter/style.scss"; +@import "./components/keyboard-shortcut-help-modal/style.scss"; +@import "./components/more-menu/style.scss"; @import "./controls/style.scss"; + +// Modals need a higher z-index in the customizer. +.components-modal__screen-overlay { + z-index: 999999; +}