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

Add TabPanel to document overview replacing fake tabs #50199

Merged
merged 17 commits into from
May 30, 2023
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
30 changes: 18 additions & 12 deletions packages/components/src/tab-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
* External dependencies
*/
import classnames from 'classnames';
import type { ForwardedRef } from 'react';

/**
* WordPress dependencies
*/
import {
forwardRef,
useState,
useEffect,
useLayoutEffect,
Expand Down Expand Up @@ -76,16 +78,19 @@ const TabButton = ( {
* );
* ```
*/
export function TabPanel( {
className,
children,
tabs,
selectOnMove = true,
initialTabName,
orientation = 'horizontal',
activeClass = 'is-active',
onSelect,
}: WordPressComponentProps< TabPanelProps, 'div', false > ) {
const UnforwardedTabPanel = (
{
className,
children,
tabs,
selectOnMove = true,
initialTabName,
orientation = 'horizontal',
activeClass = 'is-active',
onSelect,
}: WordPressComponentProps< TabPanelProps, 'div', false >,
ref: ForwardedRef< any >
) => {
const instanceId = useInstanceId( TabPanel, 'tab-panel' );
const [ selected, setSelected ] = useState< string >();

Expand Down Expand Up @@ -151,7 +156,7 @@ export function TabPanel( {
}, [ tabs, selectedTab?.disabled, handleTabSelection ] );

return (
<div className={ className }>
<div className={ className } ref={ ref }>
<NavigableMenu
role="tablist"
orientation={ orientation }
Expand Down Expand Up @@ -196,6 +201,7 @@ export function TabPanel( {
) }
</div>
);
}
};

export const TabPanel = forwardRef( UnforwardedTabPanel );
export default TabPanel;
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { __experimentalListView as ListView } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { Button, TabPanel } from '@wordpress/components';
import {
useFocusOnMount,
useFocusReturn,
useMergeRefs,
} from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
import { focus } from '@wordpress/dom';
import { useRef, useState } from '@wordpress/element';
import { useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { useShortcut } from '@wordpress/keyboard-shortcuts';
Expand All @@ -41,41 +36,41 @@ export default function ListViewSidebar() {
}
}

const [ tab, setTab ] = useState( 'list-view' );

// This ref refers to the sidebar as a whole.
const sidebarRef = useRef();
// This ref refers to the list view tab button.
const listViewTabRef = useRef();
// This ref refers to the outline tab button.
const outlineTabRef = useRef();
// This ref refers to the tab panel.
const tabPanelRef = useRef();
// This ref refers to the list view application area.
const listViewRef = useRef();

const listViewContainerRef = useMergeRefs( [
contentFocusReturnRef,
focusOnMountRef,
listViewRef,
] );

/*
* Callback function to handle list view or outline focus.
*
* @param {string} currentTab The current tab. Either list view or outline.
*
* @return void
*/
function handleSidebarFocus( currentTab ) {
// List view tab is selected.
if ( currentTab === 'list-view' ) {
// Either focus the list view or the list view tab button. Must have a fallback because the list view does not render when there are no blocks.
const listViewApplicationFocus = focus.tabbable.find(
listViewRef.current
)[ 0 ];
const listViewFocusArea = sidebarRef.current.contains(
listViewApplicationFocus
)
? listViewApplicationFocus
: listViewTabRef.current;
listViewFocusArea.focus();
// Outline tab is selected.
} else {
outlineTabRef.current.focus();
function handleSidebarFocus() {
alexstine marked this conversation as resolved.
Show resolved Hide resolved
// Either focus the list view or the tab panel. Must have a fallback because the list view does not render when there are no blocks.
const listViewApplicationFocus = focus.tabbable.find(
listViewRef.current
)[ 0 ];
const listViewFocusArea = sidebarRef.current.contains(
listViewApplicationFocus
)
? listViewApplicationFocus
: focus.tabbable.find( tabPanelRef.current )[ 0 ];
// Do not focus when focus is already there.
if (
sidebarRef.current.ownerDocument.activeElement === listViewFocusArea
) {
return;
}
listViewFocusArea.focus();
}

// This only fires when the sidebar is open because of the conditional rendering. It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed.
Expand All @@ -89,10 +84,21 @@ export default function ListViewSidebar() {
setIsListViewOpened( false );
// If the list view or outline does not have focus, focus should be moved to it.
} else {
handleSidebarFocus( tab );
handleSidebarFocus();
}
} );

function renderTabContent( tabName ) {
if ( tabName === 'list-view' ) {
return (
<div className="edit-post-editor__list-view-panel-content">
<ListView />
</div>
);
}
return <ListViewOutline />;
}

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
Expand All @@ -109,54 +115,32 @@ export default function ListViewSidebar() {
label={ __( 'Close' ) }
onClick={ () => setIsListViewOpened( false ) }
/>
<ul>
<li>
<Button
ref={ listViewTabRef }
onClick={ () => {
setTab( 'list-view' );
} }
className={ classnames(
'edit-post-sidebar__panel-tab',
{ 'is-active': tab === 'list-view' }
) }
aria-current={ tab === 'list-view' }
>
{ __( 'List View' ) }
</Button>
</li>
<li>
<Button
ref={ outlineTabRef }
onClick={ () => {
setTab( 'outline' );
} }
className={ classnames(
'edit-post-sidebar__panel-tab',
{ 'is-active': tab === 'outline' }
) }
aria-current={ tab === 'outline' }
>
{ __( 'Outline' ) }
</Button>
</li>
</ul>
</div>
<div
ref={ useMergeRefs( [
contentFocusReturnRef,
focusOnMountRef,
listViewRef,
] ) }
className="edit-post-editor__list-view-container"
<TabPanel
ref={ tabPanelRef }
selectOnMove={ false }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Manual activation due to focus handling with the list view.

tabs={ [
{
name: 'list-view',
title: 'List View',
className: 'edit-post-sidebar__panel-tab',
},
{
name: 'outline',
title: 'Outline',
className: 'edit-post-sidebar__panel-tab',
},
] }
>
{ tab === 'list-view' && (
<div className="edit-post-editor__list-view-panel-content">
<ListView />
{ ( tab ) => (
<div
className="edit-post-editor__list-view-container"
ref={ listViewContainerRef }
>
{ renderTabContent( tab.name ) }
</div>
) }
{ tab === 'outline' && <ListViewOutline /> }
</div>
</TabPanel>
</div>
);
}
8 changes: 4 additions & 4 deletions test/e2e/specs/editor/various/list-view.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,8 @@ test.describe( 'List View', () => {

// Focus the list view close button and make sure the shortcut will
// close the list view. This is to catch a bug where elements could be
// out of range of the sidebar region. Must shift+tab 3 times to reach
// close button before tabs.
await pageUtils.pressKeys( 'shift+Tab' );
// out of range of the sidebar region. Must shift+tab 2 times to reach
// close button before tab panel.
await pageUtils.pressKeys( 'shift+Tab' );
await pageUtils.pressKeys( 'shift+Tab' );
await expect(
Expand All @@ -488,7 +487,8 @@ test.describe( 'List View', () => {
// Focus the outline tab and select it. This test ensures the outline
// tab receives similar focus events based on the shortcut.
await pageUtils.pressKeys( 'shift+Tab' );
const outlineButton = editor.canvas.getByRole( 'button', {
await page.keyboard.press( 'ArrowRight' );
const outlineButton = editor.canvas.getByRole( 'tab', {
name: 'Outline',
} );
await expect( outlineButton ).toBeFocused();
Expand Down