Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Widget Editor] Fixed sidebar inserter #25681

Merged
merged 5 commits into from
Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 62 additions & 41 deletions packages/edit-widgets/src/components/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,96 @@
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { ToolbarItem } from '@wordpress/components';
import { __, _x } from '@wordpress/i18n';
import { Button, ToolbarItem } from '@wordpress/components';
import {
BlockNavigationDropdown,
BlockToolbar,
Inserter,
NavigableToolbar,
} from '@wordpress/block-editor';
import { PinnedItems } from '@wordpress/interface';
import { useViewportMatch } from '@wordpress/compose';
import { plus } from '@wordpress/icons';
import { useRef } from '@wordpress/element';

/**
* Internal dependencies
*/
import SaveButton from '../save-button';
import useLastSelectedRootId from '../../hooks/use-last-selected-root-id';
import UndoButton from './undo-redo/undo';
import RedoButton from './undo-redo/redo';

const inserterToggleProps = { isPrimary: true };
import useLastSelectedRootId from '../../hooks/use-last-selected-root-id';

function Header() {
const inserterButton = useRef();
const isLargeViewport = useViewportMatch( 'medium' );
const rootClientId = useLastSelectedRootId();
const isLastSelectedWidgetAreaOpen = useSelect(
( select ) =>
select( 'core/edit-widgets' ).getIsWidgetAreaOpen( rootClientId ),
[ rootClientId ]
);
const { setIsWidgetAreaOpen } = useDispatch( 'core/edit-widgets' );
const isInserterOpened = useSelect( ( select ) =>
select( 'core/edit-widgets' ).isInserterOpened()
);
const { setIsWidgetAreaOpen, setIsInserterOpened } = useDispatch(
'core/edit-widgets'
);
const { selectBlock } = useDispatch( 'core/block-editor' );

function handleInserterOpen( isOpen ) {
if ( isOpen && ! isLastSelectedWidgetAreaOpen ) {
// Select the last selected block if hasn't already.
selectBlock( rootClientId );
// Open the last selected widget area when opening the inserter.
setIsWidgetAreaOpen( rootClientId, isOpen );
const rootClientId = useLastSelectedRootId();
const handleClick = () => {
if ( isInserterOpened ) {
// Focusing the inserter button closes the inserter popover
inserterButton.current.focus();
} else {
if ( ! isLastSelectedWidgetAreaOpen ) {
// Select the last selected block if hasn't already.
selectBlock( rootClientId );
// Open the last selected widget area when opening the inserter.
setIsWidgetAreaOpen( rootClientId, true );
}
// The DOM updates resulting from selectBlock() and setIsInserterOpened() calls are applied the
// same tick and pretty much in a random order. The inserter is closed if any other part of the
// app receives focus. If selectBlock() happens to take effect after setIsInserterOpened() then
// the inserter is visible for a brief moment and then gets auto-closed due to focus moving to
// the selected block.
window.requestAnimationFrame( () => setIsInserterOpened( true ) );
}
}
};

return (
<>
<div className="edit-widgets-header">
<NavigableToolbar
className="edit-widgets-header-toolbar"
aria-label={ __( 'Document tools' ) }
>
<ToolbarItem>
{ ( toolbarItemProps ) => (
<Inserter
position="bottom right"
showInserterHelpPanel
toggleProps={ {
...inserterToggleProps,
...toolbarItemProps,
} }
rootClientId={ rootClientId }
onToggle={ handleInserterOpen }
/>
) }
</ToolbarItem>
<ToolbarItem as={ UndoButton } />
<ToolbarItem as={ RedoButton } />
<ToolbarItem as={ BlockNavigationDropdown } />
</NavigableToolbar>
<h1 className="edit-widgets-header__title">
{ __( 'Block Areas' ) }
</h1>
<div className="edit-widgets-header__navigable-toolbar-wrapper">
<h1 className="edit-widgets-header__title">
{ __( 'Widgets' ) }
</h1>
<NavigableToolbar
className="edit-widgets-header-toolbar"
aria-label={ __( 'Document tools' ) }
>
<ToolbarItem
ref={ inserterButton }
as={ Button }
className="edit-widgets-header-toolbar__inserter-toggle"
isPrimary
isPressed={ isInserterOpened }
onMouseDown={ ( event ) => {
event.preventDefault();
} }
onClick={ handleClick }
icon={ plus }
/* translators: button label text should, if possible, be under 16
characters. */
label={ _x(
'Add block',
'Generic label for block inserter button'
) }
/>
<ToolbarItem as={ UndoButton } />
<ToolbarItem as={ RedoButton } />
<ToolbarItem as={ BlockNavigationDropdown } />
</NavigableToolbar>
</div>
<div className="edit-widgets-header__actions">
<SaveButton />
<PinnedItems.Slot scope="core/edit-widgets" />
Expand Down
55 changes: 52 additions & 3 deletions packages/edit-widgets/src/components/header/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
justify-content: space-between;
height: $header-height;
padding: 0 $grid-unit-20;
overflow: auto;

@include break-small {
overflow: visible;
}
}

.edit-widgets-header__navigable-toolbar-wrapper {
display: flex;
align-items: center;
justify-content: center;
}

.edit-widgets-header-toolbar {
Expand All @@ -27,9 +38,9 @@
}

.edit-widgets-header__title {
font-size: 16px;
padding: 0 20px;
margin: 0;
font-size: 20px;
padding: 0;
margin: 0 20px 0 0;
}

.edit-widgets-header__actions {
Expand All @@ -40,3 +51,41 @@
border-bottom: 1px solid $gray-300;
border-top: 1px solid $gray-300;
}

.edit-widgets-header-toolbar {
// The Toolbar component adds different styles to buttons, so we reset them
// here to the original button styles
> .components-button.has-icon,
> .components-dropdown > .components-button.has-icon {
height: $button-size;
min-width: $button-size;
padding: 6px;

&.is-pressed {
background: $gray-900;
}

&:focus:not(:disabled) {
box-shadow: 0 0 0 $border-width-focus var(--wp-admin-theme-color), inset 0 0 0 $border-width $white;
outline: 1px solid transparent;
}

&::before {
display: none;
}
}
}

.edit-widgets-header-toolbar__inserter-toggle.edit-widgets-header-toolbar__inserter-toggle {
padding-left: $grid-unit;
padding-right: $grid-unit;

@include break-small {
padding-left: $grid-unit-15;
padding-right: $grid-unit-15;
}

&::after {
content: none;
}
}
71 changes: 64 additions & 7 deletions packages/edit-widgets/src/components/layout/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/**
* WordPress dependencies
*/
import { Popover } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { Popover, Button } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { close } from '@wordpress/icons';
import { __experimentalLibrary as Library } from '@wordpress/block-editor';
import { useEffect } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { InterfaceSkeleton, ComplementaryArea } from '@wordpress/interface';
import { PluginArea } from '@wordpress/plugins';

Expand All @@ -13,20 +17,73 @@ import WidgetAreasBlockEditorProvider from '../widget-areas-block-editor-provide
import Header from '../header';
import Sidebar from '../sidebar';
import WidgetAreasBlockEditorContent from '../widget-areas-block-editor-content';
import PopoverWrapper from './popover-wrapper';
import useLastSelectedRootId from '../../hooks/use-last-selected-root-id';

function Layout( { blockEditorSettings } ) {
const hasSidebarEnabled = useSelect(
( select ) =>
!! select( 'core/interface' ).getActiveComplementaryArea(
'core/edit-widgets'
)
const isMobileViewport = useViewportMatch( 'medium', '<' );
const isHugeViewport = useViewportMatch( 'huge', '>=' );
const { setIsInserterOpened, closeGeneralSidebar } = useDispatch(
'core/edit-widgets'
);
const rootClientId = useLastSelectedRootId();

const { hasSidebarEnabled, isInserterOpened } = useSelect( ( select ) => ( {
hasSidebarEnabled: !! select(
'core/interface'
).getActiveComplementaryArea( 'core/edit-widgets' ),
isInserterOpened: !! select( 'core/edit-widgets' ).isInserterOpened(),
} ) );

// Inserter and Sidebars are mutually exclusive
useEffect( () => {
if ( hasSidebarEnabled && ! isHugeViewport ) {
setIsInserterOpened( false );
}
}, [ hasSidebarEnabled, isHugeViewport ] );

useEffect( () => {
if ( isInserterOpened && ! isHugeViewport ) {
closeGeneralSidebar();
}
}, [ isInserterOpened, isHugeViewport ] );

return (
<WidgetAreasBlockEditorProvider
blockEditorSettings={ blockEditorSettings }
>
<InterfaceSkeleton
header={ <Header /> }
leftSidebar={
isInserterOpened && (
<PopoverWrapper
className="edit-widgets-layout__inserter-panel-popover-wrapper"
onClose={ () => setIsInserterOpened( false ) }
>
<div className="edit-widgets-layout__inserter-panel">
<div className="edit-widgets-layout__inserter-panel-header">
<Button
icon={ close }
onClick={ () =>
setIsInserterOpened( false )
}
/>
</div>
<div className="edit-widgets-layout__inserter-panel-content">
<Library
showInserterHelpPanel
onSelect={ () => {
if ( isMobileViewport ) {
setIsInserterOpened( false );
}
} }
rootClientId={ rootClientId }
/>
</div>
</div>
</PopoverWrapper>
)
}
sidebar={
hasSidebarEnabled && (
<ComplementaryArea.Slot scope="core/edit-widgets" />
Expand Down
57 changes: 57 additions & 0 deletions packages/edit-widgets/src/components/layout/popover-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* WordPress dependencies
*/
import {
withConstrainedTabbing,
withFocusReturn,
withFocusOutside,
} from '@wordpress/components';
import { Component } from '@wordpress/element';
import { ESCAPE } from '@wordpress/keycodes';

function stopPropagation( event ) {
event.stopPropagation();
}

const DetectOutside = withFocusOutside(
class extends Component {
handleFocusOutside( event ) {
this.props.onFocusOutside( event );
}

render() {
return this.props.children;
}
}
);

const FocusManaged = withConstrainedTabbing(
withFocusReturn( ( { children } ) => children )
);

export default function PopoverWrapper( { onClose, children, className } ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very generic component and I think ideally this becomes a set of hooks or a single hook as it's more about behavior and less about UI. (that said, it's separate from this PR)

// Event handlers
const maybeClose = ( event ) => {
// Close on escape
if ( event.keyCode === ESCAPE && onClose ) {
event.stopPropagation();
onClose();
}
};

// Disable reason: this stops certain events from propagating outside of the component.
// - onMouseDown is disabled as this can cause interactions with other DOM elements
/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div
className={ className }
onKeyDown={ maybeClose }
onMouseDown={ stopPropagation }
>
<DetectOutside onFocusOutside={ onClose }>
<FocusManaged>{ children }</FocusManaged>
</DetectOutside>
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
Loading