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

[Try] Customizable toolbar contents #23613

Merged
merged 9 commits into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 2 additions & 4 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/block-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@wordpress/token-list": "file:../token-list",
"@wordpress/url": "file:../url",
"@wordpress/viewport": "file:../viewport",
"@wordpress/warning": "file:../warning",
"@wordpress/wordcount": "file:../wordcount",
"classnames": "^2.2.5",
"css-mediaquery": "^0.1.2",
Expand All @@ -60,6 +61,7 @@
"memize": "^1.1.0",
"react-autosize-textarea": "^3.0.2",
"react-spring": "^8.0.19",
"react-transition-group": "^2.9.0",
"redux-multi": "^0.1.12",
"refx": "^3.0.0",
"rememo": "^3.0.0",
Expand Down
17 changes: 13 additions & 4 deletions packages/block-editor/src/components/block-controls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,24 @@ import useDisplayBlockControls from '../use-display-block-controls';

const { Fill, Slot } = createSlotFill( 'BlockControls' );

function BlockControlsSlot( props ) {
function BlockControlsSlot( { __experimentalIsExpanded = false, ...props } ) {
const accessibleToolbarState = useContext( ToolbarContext );
return <Slot { ...props } fillProps={ accessibleToolbarState } />;
return (
<Slot
name={ buildSlotName( __experimentalIsExpanded ) }
{ ...props }
fillProps={ accessibleToolbarState }
/>
);
}

function BlockControlsFill( { controls, children } ) {
function BlockControlsFill( { controls, __experimentalIsExpanded, children } ) {
if ( ! useDisplayBlockControls() ) {
return null;
}

return (
<Fill>
<Fill name={ buildSlotName( __experimentalIsExpanded ) }>
{ ( fillProps ) => {
// Children passed to BlockControlsFill will not have access to any
// React Context whose Provider is part of the BlockControlsSlot tree.
Expand All @@ -48,6 +54,9 @@ function BlockControlsFill( { controls, children } ) {
);
}

const buildSlotName = ( isExpanded ) =>
`BlockControls${ isExpanded ? '-expanded' : '' }`;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why you had to use a different slot name though?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was suggesting to use the exact same slot and fills but have some logic on the wrapper components to avoid rendering if there is already an expanded fill on the slot or something like that. Would that be possible?

Copy link
Contributor Author

@adamziel adamziel Jul 2, 2020

Choose a reason for hiding this comment

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

How else would I make sure that expanded fills don't land inside the regular slot OR keep the non-expanded ones from replacing the entire content?

Copy link
Contributor Author

@adamziel adamziel Jul 2, 2020

Choose a reason for hiding this comment

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

I tried inspecting the list of fills and looking at the isExpanded prop, but then I realized it's just reinventing the slot name.

Copy link
Contributor Author

@adamziel adamziel Jul 2, 2020

Choose a reason for hiding this comment

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

Something's wrong with GitHub, I only saw your second comment a few minutes after posting mine.

I was suggesting to use the exact same slot and fills but have some logic on the wrapper components to avoid rendering if there is already an expanded fill on the slot or something like that. Would that be possible?

I agree this would be the perfect solution. At the moment accessing fills outside of the slot is not possible and some refactoring would be required first so I went for the "wrapping slot" approach. Let me see what would it take to make it happen, it seems like virtually bubbling slots have a hook so maybe it wouldn't be that hard after all?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to trying to get rid of the buildSlotName, as it implies there are countless options, when in fact we have two

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I'm not sure either whether it's better or not, the current implementation is also decent. It's just that it feels more conceptually correct. (shouldn't block this PR though)

Copy link
Contributor Author

@adamziel adamziel Jul 3, 2020

Choose a reason for hiding this comment

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

A small refactoring of bubbles-virtually/fill would make it possible to detect the presence of expanded fills outside of slot:

Before:

export default function Fill( { name, children } ) {
const slot = useSlot( name );
const ref = useRef( { rerender: useForceUpdate() } );

After:

export default function Fill( { name, children, refProps } ) {
	const slot = useSlot( name );
	const ref = useRef( { rerender: useForceUpdate(), ...refProps } );

With that in place, the following would be possible:

	const { fills } = useSlot( 'BlockControls' );
	const expandedFills = fills.filter( ( ref ) => ref.current?.isExpanded  );

So far so good - so we can use different logic for both cases. The problem is rendering only expanded or non-expanded fills here:

	<BlockControls.Slot
		bubblesVirtually
		className="block-editor-block-toolbar__slot"
	/>

We don't have a granular control over fills at the slot level - bubblesVirtually fill renders itself using portals. So either:

  • All BlockControls fills would have to keep track of all other BlockControl fills and handle that internally (sounds awful), OR
  • Expanded and non-expanded fills would have to create portals with different target element, for example based on slot name

So we're back to square one in regard to using slots and fills names. There would be a small win here in that we could get rid of the wrapping slot and make it a sibling of the default content instead, but it would still be a separate slot.


const BlockControls = BlockControlsFill;

BlockControls.Slot = BlockControlsSlot;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* External dependencies
*/
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { throttle } from 'lodash';

/**
* WordPress dependencies
*/
import { useRef, useState, useEffect, useCallback } from '@wordpress/element';
import warning from '@wordpress/warning';

/**
* Internal dependencies
*/
import BlockControls from '../block-controls';

export default function CustomizableBlockToolbarContent( {
children,
className,
} ) {
return (
<BlockControls.Slot __experimentalIsExpanded>
{ ( fills ) => {
return (
<CustomizableBlockToolbarContentChildren
className={ className }
fills={ fills }
>
{ children }
</CustomizableBlockToolbarContentChildren>
);
} }
</BlockControls.Slot>
);
}

function CustomizableBlockToolbarContentChildren( {
fills,
className = '',
children,
} ) {
const containerRef = useRef();
const fillsRef = useRef();
const toolbarRef = useRef();
const [ dimensions, setDimensions ] = useState( {} );

const fillsPropRef = useRef();
fillsPropRef.current = fills;
const resize = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think maybe we should name this to resizeToolbarChild or something as it too generic right now.

throttle( ( elem ) => {
if ( ! elem ) {
elem = fillsPropRef.current.length
? fillsRef.current
: toolbarRef.current;
}
if ( ! elem ) {
return;
}
elem.style.position = 'absolute';
elem.style.width = 'auto';
const css = window.getComputedStyle( elem, null );
Copy link
Contributor

Choose a reason for hiding this comment

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

Like the resize comment above css and elem should point to what they are when resize is used. The reason is that this function is only used in one place and it's specific to what we do here so reading it should lend value to that aspect.

setDimensions( {
width: css.getPropertyValue( 'width' ),
height: css.getPropertyValue( 'height' ),
} );
elem.style.position = '';
elem.style.width = '';
}, 100 ),
[]
);

useEffect( () => {
// Create an observer instance linked to the callback function
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is describing what the next line does, we can remove it

const observer = new window.MutationObserver( function (
mutationsList
) {
const hasChildList = mutationsList.find(
( { type } ) => type === 'childList'
);
if ( hasChildList ) {
resize();
}
} );

// Start observing the target node for configured mutations
Copy link
Contributor

Choose a reason for hiding this comment

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

same, this comment is describing what the next line does, we can remove it

observer.observe( containerRef.current, {
childList: true,
subtree: true,
} );

return () => observer.disconnect();
}, [] );

useEffect( () => {
if ( fills.length > 1 ) {
warning(
`${ fills.length } <BlockControls isExpanded> slots were registered but only one may be displayed.`
);
}
}, [ fills.length ] );

return (
<div
className="block-editor-block-toolbar-width-container"
ref={ containerRef }
style={ dimensions }
>
<TransitionGroup>
{ fills.length ? (
<CSSTransition
key="fills"
timeout={ 300 }
classNames="block-editor-block-toolbar-content"
>
<div className={ className } ref={ fillsRef }>
{ fills[ 0 ] }
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit pick, maybe make a const displayFill = fills[ 0 ] higher in the code and then use it here?

</div>
</CSSTransition>
) : (
<CSSTransition
key="default"
timeout={ 300 }
classNames="block-editor-block-toolbar-content"
>
<div className={ className } ref={ toolbarRef }>
{ children }
</div>
</CSSTransition>
) }
</TransitionGroup>
</div>
);
}
14 changes: 11 additions & 3 deletions packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import BlockFormatControls from '../block-format-controls';
import BlockSettingsMenu from '../block-settings-menu';
import BlockDraggable from '../block-draggable';
import { useShowMoversGestures, useToggleBlockHighlight } from './utils';
import CustomizableBlockToolbarContent from './customizable-content';

export default function BlockToolbar( { hideDragHandle } ) {
export default function BlockToolbar( {
hideDragHandle,
__experimentalCustomizableContent = false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Considering what it does __experimentalCustomizableContent could be named __experimentalExpandedControl

} ) {
const {
blockClientIds,
blockClientId,
Expand Down Expand Up @@ -100,8 +104,12 @@ export default function BlockToolbar( { hideDragHandle } ) {
shouldShowMovers && 'is-showing-movers'
);

const Wrapper = __experimentalCustomizableContent
? CustomizableBlockToolbarContent
: 'div';

return (
<div className={ classes }>
<Wrapper className={ classes }>
<div
className="block-editor-block-toolbar__mover-switcher-container"
ref={ nodeRef }
Expand Down Expand Up @@ -152,6 +160,6 @@ export default function BlockToolbar( { hideDragHandle } ) {
</>
) }
<BlockSettingsMenu clientIds={ blockClientIds } />
</div>
</Wrapper>
);
}
28 changes: 28 additions & 0 deletions packages/block-editor/src/components/block-toolbar/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,31 @@
display: block;
}
}

.block-editor-block-toolbar-width-container {
position: relative;
overflow: hidden;
transition: width 300ms;
}

.block-editor-block-toolbar-content-enter {
position: absolute;
top: 0;
left: 0;
width: auto;
opacity: 0;
}
.block-editor-block-toolbar-content-enter-active {
position: absolute;
opacity: 1;
transition: opacity 300ms;
}
.block-editor-block-toolbar-content-exit {
width: auto;
opacity: 1;
pointer-events: none;
}
.block-editor-block-toolbar-content-exit-active {
opacity: 0;
transition: opacity 300ms;
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ export default function BlockEditorArea( {
aria-label={ __( 'Block tools' ) }
>
{ hasSelectedBlock && ! isRootBlockSelected && (
<BlockToolbar hideDragHandle />
<BlockToolbar
hideDragHandle
__experimentalCustomizableContent
/>
) }
</NavigableToolbar>
<Popover.Slot name="block-toolbar" />
Expand Down