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

Focus control in widgets customizer #31308

Merged
merged 12 commits into from
May 4, 2021
8 changes: 5 additions & 3 deletions lib/widgets-customize.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ function gutenberg_widgets_customize_register( $manager ) {
$manager,
"sidebars_widgets[$sidebar_id]",
array(
'section' => "sidebar-widgets-$sidebar_id",
'settings' => "sidebars_widgets[$sidebar_id]",
'sidebar_id' => $sidebar_id,
'section' => "sidebar-widgets-$sidebar_id",
'settings' => "sidebars_widgets[$sidebar_id]",
'sidebar_id' => $sidebar_id,
'label' => $sidebar['name'],
'description' => $sidebar['description'],
)
)
);
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* WordPress dependencies
*/
import { useState, useEffect, createPortal } from '@wordpress/element';

/**
* Internal dependencies
*/
import SidebarBlockEditor from '../sidebar-block-editor';
import FocusControl from '../focus-control';
import SidebarControls from '../sidebar-controls';

export default function CustomizeWidgets( {
api,
sidebarControls,
blockEditorSettings,
} ) {
const [ activeSidebarControl, setActiveSidebarControl ] = useState( null );

useEffect( () => {
const unsubscribers = sidebarControls.map( ( sidebarControl ) =>
sidebarControl.subscribe( ( expanded ) => {
if ( expanded ) {
setActiveSidebarControl( sidebarControl );
}
} )
);

return () => {
unsubscribers.forEach( ( unsubscriber ) => unsubscriber() );
};
}, [ sidebarControls ] );

const activeSidebar =
activeSidebarControl &&
createPortal(
<SidebarBlockEditor
key={ activeSidebarControl.id }
blockEditorSettings={ blockEditorSettings }
sidebar={ activeSidebarControl.sidebarAdapter }
inserter={ activeSidebarControl.inserter }
inspector={ activeSidebarControl.inspector }
/>,
activeSidebarControl.container[ 0 ]
);

return (
<SidebarControls
sidebarControls={ sidebarControls }
activeSidebarControl={ activeSidebarControl }
>
<FocusControl api={ api } sidebarControls={ sidebarControls }>
{ activeSidebar }
</FocusControl>
</SidebarControls>
);
}
85 changes: 85 additions & 0 deletions packages/customize-widgets/src/components/focus-control/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* WordPress dependencies
*/
import {
createContext,
useState,
useEffect,
useContext,
useCallback,
useMemo,
} from '@wordpress/element';

/**
* Internal dependencies
*/
import { settingIdToWidgetId } from '../../utils';

const FocusControlContext = createContext();

export default function FocusControl( { api, sidebarControls, children } ) {
const [ focusedWidgetIdRef, setFocusedWidgetIdRef ] = useState( {
current: null,
} );

const focusWidget = useCallback(
( widgetId ) => {
for ( const sidebarControl of sidebarControls ) {
const widgets = sidebarControl.setting.get();

if ( widgets.includes( widgetId ) ) {
sidebarControl.sectionInstance.expand( {
// Schedule it after the complete callback so that
// it won't be overridden by the "Back" button focus.
completeCallback() {
// Create a "ref-like" object every time to ensure
// the same widget id can also triggers the focus control.
setFocusedWidgetIdRef( { current: widgetId } );
},
} );

break;
}
}
},
[ sidebarControls ]
);

useEffect( () => {
function handleFocus( settingId ) {
const widgetId = settingIdToWidgetId( settingId );

focusWidget( widgetId );
}

function handleReady() {
api.previewer.preview.bind(
'focus-control-for-setting',
handleFocus
);
}

api.previewer.bind( 'ready', handleReady );

return () => {
api.previewer.unbind( 'ready', handleReady );
api.previewer.preview.unbind(
'focus-control-for-setting',
handleFocus
);
};
}, [ api, focusWidget ] );

const context = useMemo( () => [ focusedWidgetIdRef, focusWidget ], [
focusedWidgetIdRef,
focusWidget,
] );

return (
<FocusControlContext.Provider value={ context }>
{ children }
</FocusControlContext.Provider>
);
}

export const useFocusControl = () => useContext( FocusControlContext );
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* WordPress dependencies
*/
import { useRef, useEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { getWidgetIdFromBlock } from '@wordpress/widgets';

/**
* Internal dependencies
*/
import { useFocusControl } from '.';

export default function useBlocksFocusControl( blocks ) {
const { selectBlock } = useDispatch( blockEditorStore );
const [ focusedWidgetIdRef ] = useFocusControl();

const blocksRef = useRef( blocks );

useEffect( () => {
blocksRef.current = blocks;
}, [ blocks ] );

useEffect( () => {
if ( focusedWidgetIdRef.current ) {
const focusedBlock = blocksRef.current.find(
( block ) =>
getWidgetIdFromBlock( block ) === focusedWidgetIdRef.current
);

if ( focusedBlock ) {
selectBlock( focusedBlock.clientId );
// If the block is already being selected, the DOM node won't
// get focused again automatically.
// We select the DOM and focus it manually here.
const blockNode = document.querySelector(
`[data-block="${ focusedBlock.clientId }"]`
);
blockNode?.focus();
}
}
}, [ focusedWidgetIdRef, selectBlock ] );
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import { settingIdToWidgetId } from '../../utils';

const { wp } = window;

function parseWidgetId( widgetId ) {
Expand All @@ -22,31 +27,10 @@ function widgetIdToSettingId( widgetId ) {
return `widget_${ idBase }`;
}

function parseSettingId( settingId ) {
const matches = settingId.match( /^widget_(.+)(?:\[(\d+)\])$/ );
if ( matches ) {
return {
idBase: matches[ 1 ],
number: parseInt( matches[ 2 ], 10 ),
};
}

return { idBase: settingId };
}

function settingIdToWidgetId( settingId ) {
const { idBase, number } = parseSettingId( settingId );
if ( number ) {
return `${ idBase }-${ number }`;
}

return idBase;
}

export default class SidebarAdapter {
constructor( setting, allSettings ) {
constructor( setting, api ) {
this.setting = setting;
this.allSettings = allSettings;
this.api = api;

this.locked = false;
this.widgetsCache = new WeakMap();
Expand All @@ -59,32 +43,21 @@ export default class SidebarAdapter {
];
this.historyIndex = 0;

this._handleSettingChange = this._handleSettingChange.bind( this );
this._handleAllSettingsChange = this._handleAllSettingsChange.bind(
this
);
this.setting.bind( this._handleSettingChange.bind( this ) );
this.api.bind( 'change', this._handleAllSettingsChange.bind( this ) );
Comment on lines +46 to +47
Copy link
Member Author

Choose a reason for hiding this comment

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

Move to constructor so that it can still listen to the changes even when it's not active yet.


this.canUndo = this.canUndo.bind( this );
this.canRedo = this.canRedo.bind( this );
this.undo = this.undo.bind( this );
this.redo = this.redo.bind( this );
}

subscribe( callback ) {
if ( ! this.subscribers.size ) {
this.setting.bind( this._handleSettingChange );
this.allSettings.bind( 'change', this._handleAllSettingsChange );
}

this.subscribers.add( callback );
}

unsubscribe( callback ) {
this.subscribers.delete( callback );

if ( ! this.subscribers.size ) {
this.setting.unbind( this._handleSettingChange );
this.allSettings.unbind( 'change', this._handleAllSettingsChange );
}
return () => {
this.subscribers.delete( callback );
};
}

getWidgets() {
Expand Down Expand Up @@ -170,7 +143,7 @@ export default class SidebarAdapter {
: 'refresh',
previewer: this.setting.previewer,
};
const setting = this.allSettings.create(
const setting = this.api.create(
settingId,
settingId,
'',
Expand Down Expand Up @@ -203,7 +176,7 @@ export default class SidebarAdapter {
prevWidget.idBase === widget.idBase
) {
const settingId = widgetIdToSettingId( widget.id );
this.allSettings( settingId ).set( widget.instance );
this.api( settingId ).set( widget.instance );
return widget.id;
}

Expand All @@ -219,7 +192,7 @@ export default class SidebarAdapter {

const { idBase, number } = parseWidgetId( widgetId );
const settingId = widgetIdToSettingId( widgetId );
const setting = this.allSettings( settingId );
const setting = this.api( settingId );

if ( ! setting ) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BlockEditorProvider } from '@wordpress/block-editor';
* Internal dependencies
*/
import useSidebarBlockEditor from './use-sidebar-block-editor';
import useBlocksFocusControl from '../focus-control/use-blocks-focus-control';

export default function SidebarEditorProvider( {
sidebar,
Expand All @@ -15,6 +16,8 @@ export default function SidebarEditorProvider( {
} ) {
const [ blocks, onInput, onChange ] = useSidebarBlockEditor( sidebar );

useBlocksFocusControl( blocks );

return (
<BlockEditorProvider
value={ blocks }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,7 @@ import { omit, isEqual } from 'lodash';
import { serialize, parse, createBlock } from '@wordpress/blocks';
import { useState, useEffect, useCallback } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';

function addWidgetIdToBlock( block, widgetId ) {
return {
...block,
attributes: {
...( block.attributes || {} ),
__internalWidgetId: widgetId,
},
};
}

function getWidgetId( block ) {
return block.attributes.__internalWidgetId;
}
import { getWidgetIdFromBlock, addWidgetIdToBlock } from '@wordpress/widgets';

function blockToWidget( block, existingWidget = null ) {
let widget;
Expand Down Expand Up @@ -122,7 +109,7 @@ export default function useSidebarBlockEditor( sidebar ) {
);
const prevBlocksMap = new Map(
prevBlocks.map( ( block ) => [
getWidgetId( block ),
getWidgetIdFromBlock( block ),
block,
] )
);
Expand Down Expand Up @@ -157,13 +144,13 @@ export default function useSidebarBlockEditor( sidebar ) {

const prevBlocksMap = new Map(
prevBlocks.map( ( block ) => [
getWidgetId( block ),
getWidgetIdFromBlock( block ),
block,
] )
);

const nextWidgets = nextBlocks.map( ( nextBlock ) => {
const widgetId = getWidgetId( nextBlock );
const widgetId = getWidgetIdFromBlock( nextBlock );

// Update existing widgets.
if ( widgetId && prevBlocksMap.has( widgetId ) ) {
Expand Down
Loading