Skip to content

Commit

Permalink
Add customize widgets inserter (#29549)
Browse files Browse the repository at this point in the history
Co-authored-by: Robert Anderson <[email protected]>
  • Loading branch information
kevin940726 and noisysocks authored Mar 26, 2021
1 parent f1d9a0b commit 53d1ebb
Show file tree
Hide file tree
Showing 13 changed files with 361 additions and 22 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/customize-widgets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/data": "file:../data",
"@wordpress/dom": "file:../dom",
"@wordpress/element": "file:../element",
"@wordpress/i18n": "file:../i18n",
"@wordpress/icons": "file:../icons",
"@wordpress/keycodes": "file:../keycodes",
"classnames": "^2.2.6",
"lodash": "^4.17.19"
},
Expand Down
48 changes: 48 additions & 0 deletions packages/customize-widgets/src/components/header/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* WordPress dependencies
*/
import { createPortal } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
import { Button, ToolbarItem } from '@wordpress/components';
import { NavigableToolbar } from '@wordpress/block-editor';
import { plus } from '@wordpress/icons';

/**
* Internal dependencies
*/
import Inserter from '../inserter';

function Header( { inserter, isInserterOpened, setIsInserterOpened } ) {
return (
<>
<div className="customize-widgets-header">
<NavigableToolbar
className="customize-widgets-header-toolbar"
aria-label={ __( 'Document tools' ) }
>
<ToolbarItem
as={ Button }
className="customize-widgets-header-toolbar__inserter-toggle"
isPressed={ isInserterOpened }
isPrimary
icon={ plus }
label={ _x(
'Add block',
'Generic label for block inserter button'
) }
onClick={ () => {
setIsInserterOpened( ( isOpen ) => ! isOpen );
} }
/>
</NavigableToolbar>
</div>

{ createPortal(
<Inserter setIsOpened={ setIsInserterOpened } />,
inserter.contentContainer[ 0 ]
) }
</>
);
}

export default Header;
21 changes: 21 additions & 0 deletions packages/customize-widgets/src/components/header/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.customize-widgets-header-toolbar {
display: flex;
border: none;

.customize-widgets-header-toolbar__inserter-toggle.components-button.has-icon {
border-radius: 2px;
color: $white;
padding: 0;
min-width: $grid-unit-30;
height: $grid-unit-30;
margin-left: auto;

&::before {
content: none;
}

&.is-pressed {
background: $gray-900;
}
}
}
45 changes: 45 additions & 0 deletions packages/customize-widgets/src/components/inserter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { __experimentalLibrary as Library } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { closeSmall } from '@wordpress/icons';

function Inserter( { setIsOpened } ) {
const inserterTitleId = useInstanceId(
Inserter,
'customize-widget-layout__inserter-panel-title'
);

return (
<div
className="customize-widgets-layout__inserter-panel"
aria-labelledby={ inserterTitleId }
>
<div className="customize-widgets-layout__inserter-panel-header">
<h2
id={ inserterTitleId }
className="customize-widgets-layout__inserter-panel-header-title"
>
{ __( 'Add a block' ) }
</h2>
<Button
className="customize-widgets-layout__inserter-panel-header-close-button"
icon={ closeSmall }
onClick={ () => setIsOpened( false ) }
aria-label={ __( 'Close inserter' ) }
/>
</div>
<div className="customize-widgets-layout__inserter-panel-content">
<Library
showInserterHelpPanel
onSelect={ () => setIsOpened( false ) }
/>
</div>
</div>
);
}

export default Inserter;
40 changes: 40 additions & 0 deletions packages/customize-widgets/src/components/inserter/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#customize-sidebar-outer-content {
// To override the default style of width: 100%,
// so that the inspector won't be cropped.
width: auto;
min-width: 100%;
}

#customize-outer-theme-controls #sub-accordion-section-widgets-inserter {
padding: 0;

.customize-section-description-container {
display: none;
}
}

.customize-widgets-layout__inserter-panel {
background: $white;
}

.customize-widgets-layout__inserter-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $grid-unit-20;
// Match the "wp-full-overlay-header" height in core, plus the 1px border.
height: 46px;
box-sizing: border-box;
border-bottom: 1px solid $gray-300;

.customize-widgets-layout__inserter-panel-header-title {
margin: 0;
}
}

.block-editor-inserter__quick-inserter {
.block-editor-inserter__panel-content {
// To fix quick inserter doesn't have background.
background: $white;
}
}
33 changes: 33 additions & 0 deletions packages/customize-widgets/src/components/inserter/use-inserter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* WordPress dependencies
*/
import { useState, useEffect, useCallback } from '@wordpress/element';

export default function useInserter( inserter ) {
const [ isInserterOpened, setIsInserterOpened ] = useState(
() => inserter.isOpen
);

useEffect( () => {
return inserter.subscribe( setIsInserterOpened );
}, [ inserter ] );

return [
isInserterOpened,
useCallback(
( updater ) => {
let isOpen = updater;
if ( typeof updater === 'function' ) {
isOpen = updater( inserter.isOpen );
}

if ( isOpen ) {
inserter.open();
} else {
inserter.close();
}
},
[ inserter ]
),
];
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { useReducer, createPortal } from '@wordpress/element';
import { useReducer, createPortal, useMemo } from '@wordpress/element';
import {
BlockEditorProvider,
BlockList,
Expand All @@ -21,7 +21,9 @@ import {
* Internal dependencies
*/
import Inspector, { BlockInspectorButton } from '../inspector';
import Header from '../header';
import useSidebarBlockEditor from './use-sidebar-block-editor';
import useInserter from '../inserter/use-inserter';

const inspectorOpenStateReducer = ( state, action ) => {
switch ( action ) {
Expand All @@ -45,7 +47,7 @@ const inspectorOpenStateReducer = ( state, action ) => {
}
};

export default function SidebarBlockEditor( { sidebar } ) {
export default function SidebarBlockEditor( { sidebar, inserter } ) {
const [ blocks, onInput, onChange ] = useSidebarBlockEditor( sidebar );
const [
{ open: isInspectorOpened, busy: isInspectorAnimating },
Expand All @@ -54,6 +56,13 @@ export default function SidebarBlockEditor( { sidebar } ) {
const parentContainer = document.getElementById(
'customize-theme-controls'
);
const [ isInserterOpened, setIsInserterOpened ] = useInserter( inserter );
const settings = useMemo(
() => ( {
__experimentalSetIsInserterOpened: setIsInserterOpened,
} ),
[]
);

return (
<>
Expand All @@ -65,10 +74,17 @@ export default function SidebarBlockEditor( { sidebar } ) {
value={ blocks }
onInput={ onInput }
onChange={ onChange }
settings={ settings }
useSubRegistry={ false }
>
<BlockEditorKeyboardShortcuts />

<Header
inserter={ inserter }
isInserterOpened={ isInserterOpened }
setIsInserterOpened={ setIsInserterOpened }
/>

<BlockSelectionClearer>
<WritingFlow>
<ObserveTyping>
Expand Down
118 changes: 118 additions & 0 deletions packages/customize-widgets/src/controls/inserter-outer-section.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* WordPress dependencies
*/
import { ESCAPE } from '@wordpress/keycodes';
import { focus } from '@wordpress/dom';

const {
wp: { customize },
} = window;

const OuterSection = customize.OuterSection;
// Override the OuterSection class to handle multiple outer sections.
// It closes all the other outer sections whenever one is opened.
// The result is that at most one outer section can be opened at the same time.
customize.OuterSection = class extends OuterSection {
onChangeExpanded( expanded, args ) {
if ( expanded ) {
customize.section.each( ( section ) => {
if (
section.params.type === 'outer' &&
section.id !== this.id
) {
if ( section.expanded() ) {
section.collapse();
}
}
} );
}

return super.onChangeExpanded( expanded, args );
}
};
// Handle constructor so that "params.type" can be correctly pointed to "outer".
customize.sectionConstructor.outer = customize.OuterSection;

class InserterOuterSection extends customize.OuterSection {
constructor( ...args ) {
super( ...args );

// This is necessary since we're creating a new class which is not identical to the original OuterSection.
// @See https://github.com/WordPress/wordpress-develop/blob/42b05c397c50d9dc244083eff52991413909d4bd/src/js/_enqueues/wp/customize/controls.js#L1427-L1436
this.params.type = 'outer';

this.activeElementBeforeExpanded = null;

const ownerWindow = this.contentContainer[ 0 ].ownerDocument
.defaultView;

// Handle closing the inserter when pressing the Escape key.
ownerWindow.addEventListener(
'keydown',
( event ) => {
if (
this.isOpen &&
( event.keyCode === ESCAPE || event.code === 'Escape' )
) {
event.stopPropagation();

this.close();
}
},
// Use capture mode to make this run before other event listeners.
true
);
}
get isOpen() {
return this.expanded();
}
subscribe( handler ) {
this.expanded.bind( handler );
return () => this.expanded.unbind( handler );
}
open() {
if ( ! this.isOpen ) {
const contentContainer = this.contentContainer[ 0 ];
this.activeElementBeforeExpanded =
contentContainer.ownerDocument.activeElement;

this.expand( {
completeCallback() {
// We have to do this in a "completeCallback" or else the elements will not yet be visible/tabbable.
// The first one should be the close button,
// we want to skip it and choose the second one instead, which is the search box.
const searchBox = focus.tabbable.find(
contentContainer
)[ 1 ];
if ( searchBox ) {
searchBox.focus();
}
},
} );
}
}
close() {
if ( this.isOpen ) {
const contentContainer = this.contentContainer[ 0 ];
const activeElement = contentContainer.ownerDocument.activeElement;

this.collapse( {
completeCallback() {
// Return back the focus when closing the inserter.
// Only do this if the active element which triggers the action is inside the inserter,
// (the close button for instance). In that case the focus will be lost.
// Otherwise, we don't hijack the focus when the user is focusing on other elements
// (like the quick inserter).
if ( contentContainer.contains( activeElement ) ) {
// Return back the focus when closing the inserter.
if ( this.activeElementBeforeExpanded ) {
this.activeElementBeforeExpanded.focus();
}
}
},
} );
}
}
}

export default InserterOuterSection;
Loading

0 comments on commit 53d1ebb

Please sign in to comment.