Skip to content

Commit

Permalink
Add TabPanel to document overview replacing fake tabs (WordPress#50199)
Browse files Browse the repository at this point in the history
* Add TabPanel to document overview sidebar.

* TabPanel: Changed to support ref forwarding. Added prop for attaching parent refs to wrapping content div.

* Reviewer feedback addressed.

* Fix E2E tests.

* Update styles to more closely reflect the look of the tabs in trunk

* Restore scrollbar behaviour

* Add changelog entry for components package

* Better comments.

* Fix E2E.

* Add back useState for cleaner code.

* Fix list view prop. Changelog update.

* Fix tab panel focus. Still need to use focus.tabbable.find().

* Fix changelog format.

---------

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
  • Loading branch information
2 people authored and sethrubenstein committed Jul 13, 2023
1 parent aa3b199 commit 4689dcb
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 117 deletions.
5 changes: 4 additions & 1 deletion packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@

- `FocalPointUnitControl`: Add aria-labels ([#50993](https://github.com/WordPress/gutenberg/pull/50993)).

### Enhancements

- Wrapped `TabPanel` in a `forwardRef` call ([#50199](https://github.com/WordPress/gutenberg/pull/50199)).

### Experimental

- `DropdownMenu` v2: Tweak styles ([#50967](https://github.com/WordPress/gutenberg/pull/50967)).


## 25.0.0 (2023-05-24)

### Breaking Changes
Expand Down
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
Expand Up @@ -141,7 +141,7 @@ describe( 'Navigating the block hierarchy', () => {
// Navigate to the third column in the columns block.
await pressKeyWithModifier( 'ctrlShift', '`' );
await pressKeyWithModifier( 'ctrlShift', '`' );
await pressKeyTimes( 'Tab', 4 );
await pressKeyTimes( 'Tab', 3 );
await pressKeyTimes( 'ArrowDown', 4 );
await page.waitForSelector(
'.is-highlighted[aria-label="Block: Column (3 of 3)"]'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
/**
* 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,
Expand All @@ -30,7 +25,9 @@ import ListViewOutline from './list-view-outline';
export default function ListViewSidebar() {
const { setIsListViewOpened } = useDispatch( editPostStore );

// This hook handles focus when the sidebar first renders.
const focusOnMountRef = useFocusOnMount( 'firstElement' );
// The next 2 hooks handle focus for when the sidebar closes and returning focus to the element that had focus before sidebar opened.
const headerFocusReturnRef = useFocusReturn();
const contentFocusReturnRef = useFocusReturn();

Expand All @@ -44,18 +41,24 @@ export default function ListViewSidebar() {
// Use internal state instead of a ref to make sure that the component
// re-renders when the dropZoneElement updates.
const [ dropZoneElement, setDropZoneElement ] = useState( null );

// Tracks our current tab.
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();

// Must merge the refs together so focus can be handled properly in the next function.
const listViewContainerRef = useMergeRefs( [
contentFocusReturnRef,
focusOnMountRef,
listViewRef,
setDropZoneElement,
] );

/*
* Callback function to handle list view or outline focus.
*
Expand All @@ -64,21 +67,23 @@ export default function ListViewSidebar() {
* @return void
*/
function handleSidebarFocus( currentTab ) {
// Tab panel focus.
const tabPanelFocus = focus.tabbable.find( tabPanelRef.current )[ 0 ];
// 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.
// 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
: listViewTabRef.current;
: tabPanelFocus;
listViewFocusArea.focus();
// Outline tab is selected.
} else {
outlineTabRef.current.focus();
tabPanelFocus.focus();
}
}

Expand All @@ -97,71 +102,63 @@ export default function ListViewSidebar() {
}
} );

/**
* Render tab content for a given tab name.
*
* @param {string} tabName The name of the tab to render.
*/
function renderTabContent( tabName ) {
if ( tabName === 'list-view' ) {
return (
<div className="edit-post-editor__list-view-panel-content">
<ListView dropZoneElement={ dropZoneElement } />
</div>
);
}
return <ListViewOutline />;
}

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="edit-post-editor__document-overview-panel"
onKeyDown={ closeOnEscape }
ref={ sidebarRef }
>
<div
className="edit-post-editor__document-overview-panel-header components-panel__header edit-post-sidebar__panel-tabs"
<Button
className="edit-post-editor__document-overview-panel__close-button"
ref={ headerFocusReturnRef }
icon={ closeSmall }
label={ __( 'Close' ) }
onClick={ () => setIsListViewOpened( false ) }
/>
<TabPanel
className="edit-post-editor__document-overview-panel__tab-panel"
ref={ tabPanelRef }
onSelect={ ( tabName ) => setTab( tabName ) }
selectOnMove={ false }
tabs={ [
{
name: 'list-view',
title: 'List View',
className: 'edit-post-sidebar__panel-tab',
},
{
name: 'outline',
title: 'Outline',
className: 'edit-post-sidebar__panel-tab',
},
] }
>
<Button
icon={ closeSmall }
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,
setDropZoneElement,
] ) }
className="edit-post-editor__list-view-container"
>
{ tab === 'list-view' && (
<div className="edit-post-editor__list-view-panel-content">
<ListView dropZoneElement={ dropZoneElement } />
{ ( currentTab ) => (
<div
className="edit-post-editor__list-view-container"
ref={ listViewContainerRef }
>
{ renderTabContent( currentTab.name ) }
</div>
) }
{ tab === 'outline' && <ListViewOutline /> }
</div>
</TabPanel>
</div>
);
}
59 changes: 28 additions & 31 deletions packages/edit-post/src/components/secondary-sidebar/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,29 @@
width: 350px;
}

.edit-post-sidebar__panel-tabs {
flex-direction: row-reverse;
.edit-post-editor__document-overview-panel__close-button {
position: absolute;
right: $grid-unit-10;
top: math.div($grid-unit-60 - $button-size, 2); // ( tab height - button size ) / 2
z-index: 1;
background: $white;
}

// The TabPanel style overrides in the following blocks should be removed when the new TabPanel is available.
.components-tab-panel__tabs {
border-bottom: $border-width solid $gray-300;
box-sizing: border-box;
display: flex;
width: 100%;
padding-right: $grid-unit-70;

.edit-post-sidebar__panel-tab {
width: 50%;
}
}

.components-tab-panel__tab-content {
height: calc(100% - #{$grid-unit-60});
}
}

Expand All @@ -37,34 +58,6 @@
}
}

.edit-post-editor__document-overview-panel-header {
border-bottom: $border-width solid $gray-300;
display: flex;
justify-content: space-between;
height: $grid-unit-60;
padding-left: $grid-unit-20;
padding-right: $grid-unit-05;
ul {
width: calc(100% - #{ $grid-unit-50 });
}
li {
width: 50%;
button {
width: 100%;
text-align: initial;
}
}
li:only-child {
width: 100%;
}

&.components-panel__header.edit-post-sidebar__panel-tabs {
.components-button.has-icon {
display: flex;
}
}
}

.edit-post-editor__list-view-panel-content,
.edit-post-editor__list-view-container > .document-outline,
.edit-post-editor__list-view-empty-headings {
Expand Down Expand Up @@ -118,5 +111,9 @@
.edit-post-editor__list-view-container {
display: flex;
flex-direction: column;
height: calc(100% - #{$grid-unit-60});
height: 100%;
}

.edit-post-editor__document-overview-panel__tab-panel {
height: 100%;
}
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 @@ -331,9 +331,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 @@ -354,7 +353,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

0 comments on commit 4689dcb

Please sign in to comment.