Skip to content

Commit

Permalink
address tab flow race condition
Browse files Browse the repository at this point in the history
  • Loading branch information
chad1008 committed Feb 1, 2024
1 parent 306abfb commit 6aa6f3f
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 7 deletions.
31 changes: 29 additions & 2 deletions packages/edit-site/src/components/sidebar-edit-mode/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@wordpress/components';
import { isRTL, __ } from '@wordpress/i18n';
import { drawerLeft, drawerRight } from '@wordpress/icons';
import { useCallback, useContext, useEffect } from '@wordpress/element';
import { useCallback, useContext, useEffect, useRef } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
import { store as blockEditorStore } from '@wordpress/block-editor';
Expand Down Expand Up @@ -39,11 +39,38 @@ const FillContents = ( {
isEditingPage,
supportsGlobalStyles,
} ) => {
const tabListRef = useRef( null );
// Because `DefaultSidebar` renders a `ComplementaryArea`, we
// need to forward the `Tabs` context so it can be passed through the
// underlying slot/fill.
const tabsContextValue = useContext( Tabs.Context );

// This effect addresses a race condition caused by tabbing from the last
// block in the editor into the settings sidebar. Without this effect, the
// selected tab and browser focus can become separated in an unexpected way.
useEffect( () => {
const tabsElements = Array.from(
tabListRef.current?.querySelectorAll( '[role="tab"]' ) || []
);
const selectedTabElement = tabsElements.find(
// We are purposefully using a custom `data-tab-id` attribute here
// because we don't want rely on any assumptions about `Tabs`
// component internals.
( element ) => element.getAttribute( 'data-tab-id' ) === sidebarName
);
const activeElement = selectedTabElement?.ownerDocument.activeElement;
const tabsHasFocus = tabsElements.some( ( element ) => {
return activeElement && activeElement.id === element.id;
} );
if (
tabsHasFocus &&
selectedTabElement &&
selectedTabElement.id !== activeElement?.id
) {
selectedTabElement?.focus();
}
}, [ sidebarName ] );

return (
<>
<DefaultSidebar
Expand All @@ -53,7 +80,7 @@ const FillContents = ( {
closeLabel={ __( 'Close Settings' ) }
header={
<Tabs.Context.Provider value={ tabsContextValue }>
<SettingsHeader />
<SettingsHeader ref={ tabListRef } />
</Tabs.Context.Provider>
}
headerClassName="edit-site-sidebar-edit-mode__panel-tabs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { forwardRef } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -14,18 +15,30 @@ import { unlock } from '../../../lock-unlock';

const { Tabs } = unlock( componentsPrivateApis );

const SettingsHeader = () => {
const SettingsHeader = ( _, ref ) => {
const postTypeLabel = useSelect(
( select ) => select( editorStore ).getPostTypeLabel(),
[]
);

return (
<Tabs.TabList>
<Tabs.Tab tabId={ SIDEBAR_TEMPLATE }>{ postTypeLabel }</Tabs.Tab>
<Tabs.Tab tabId={ SIDEBAR_BLOCK }>{ __( 'Block' ) }</Tabs.Tab>
<Tabs.TabList ref={ ref }>
<Tabs.Tab
tabId={ SIDEBAR_TEMPLATE }
// Used for focus management in the SettingsSidebar component.
data-tab-id={ SIDEBAR_TEMPLATE }
>
{ postTypeLabel }
</Tabs.Tab>
<Tabs.Tab
tabId={ SIDEBAR_BLOCK }
// Used for focus management in the SettingsSidebar component.
data-tab-id={ SIDEBAR_BLOCK }
>
{ __( 'Block' ) }
</Tabs.Tab>
</Tabs.TabList>
);
};

export default SettingsHeader;
export default forwardRef( SettingsHeader );

0 comments on commit 6aa6f3f

Please sign in to comment.