Skip to content

Commit

Permalink
Add tabName prop to make TabPanel a controlled component
Browse files Browse the repository at this point in the history
  • Loading branch information
madhusudhand committed Feb 1, 2023
1 parent 2718f97 commit 9064064
Show file tree
Hide file tree
Showing 5 changed files with 450 additions and 296 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@

- `TabPanel`: support manual tab activation ([#46004](https://github.com/WordPress/gutenberg/pull/46004)).
- `TabPanel`: support disabled prop for tab buttons ([#46471](https://github.com/WordPress/gutenberg/pull/46471)).
- `TabPanel`: add selectedTabName prop for tab panel ([#46704](https://github.com/WordPress/gutenberg/pull/46704)).
- `BaseControl`: Add `useBaseControlProps` hook to help generate id-releated props ([#46170](https://github.com/WordPress/gutenberg/pull/46170)).

### Bug Fix
Expand Down
8 changes: 8 additions & 0 deletions packages/components/src/tab-panel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ The name of the tab to be selected upon mounting of component. If this prop is n
- Required: No
- Default: none

#### selectedTabName

The name of the tab to be selected.

- Type: `String`
- Required: No
- Default: none

#### selectOnMove

When `true`, the tab will be selected when receiving focus (automatic tab activation). When `false`, the tab will be selected only when clicked (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) for more info.
Expand Down
79 changes: 28 additions & 51 deletions packages/components/src/tab-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useState, useEffect, useCallback } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
import { useCallback, useEffect } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -16,6 +16,7 @@ import { NavigableMenu } from '../navigable-container';
import Button from '../button';
import type { TabButtonProps, TabPanelProps } from './types';
import type { WordPressComponentProps } from '../ui/context';
import { useControlledValue } from '../utils';

const TabButton = ( {
tabId,
Expand Down Expand Up @@ -77,20 +78,28 @@ export function TabPanel( {
tabs,
selectOnMove = true,
initialTabName,
tabName: tabNameProp,
orientation = 'horizontal',
activeClass = 'is-active',
onSelect,
}: WordPressComponentProps< TabPanelProps, 'div', false > ) {
const instanceId = useInstanceId( TabPanel, 'tab-panel' );
const [ selected, setSelected ] = useState< string >();

const handleTabSelection = useCallback(
( tabKey: string ) => {
setSelected( tabKey );
onSelect?.( tabKey );
},
[ onSelect ]
const getTab = useCallback(
( name: string | undefined ) =>
tabs.find( ( tab ) => tab.name === name && ! tab.disabled ),
[ tabs ]
);
const firstEnabledTab = tabs.find( ( { disabled } ) => ! disabled );

const [ tabName, setTabName ] = useControlledValue( {
defaultValue: ( initialTabName
? getTab( initialTabName )
: firstEnabledTab
)?.name,
value: getTab( tabNameProp )?.name,
onChange: onSelect,
} );

// Simulate a click on the newly focused tab, which causes the component
// to show the `tab-panel` associated with the clicked tab.
Expand All @@ -100,50 +109,18 @@ export function TabPanel( {
) => {
child.click();
};
const selectedTab = tabs.find( ( { name } ) => name === selected );
const selectedTab = getTab( tabName );
const selectedId = `${ instanceId }-${ selectedTab?.name ?? 'none' }`;

// Handle selecting the initial tab.
useEffect( () => {
// If there's a selected tab, don't override it.
if ( selectedTab ) {
return;
}

const initialTab = tabs.find( ( tab ) => tab.name === initialTabName );

// Wait for the denoted initial tab to be declared before making a
// selection. This ensures that if a tab is declared lazily it can
// still receive initial selection.
if ( initialTabName && ! initialTab ) {
return;
}

if ( initialTab && ! initialTab.disabled ) {
// Select the initial tab if it's not disabled.
handleTabSelection( initialTab.name );
} else {
// Fallback to the first enabled tab when the initial is disabled.
const firstEnabledTab = tabs.find( ( tab ) => ! tab.disabled );
if ( firstEnabledTab ) handleTabSelection( firstEnabledTab.name );
}
}, [ tabs, selectedTab, initialTabName, handleTabSelection ] );

// Handle the currently selected tab becoming disabled.
useEffect( () => {
// This effect only runs when the selected tab is defined and becomes disabled.
if ( ! selectedTab?.disabled ) {
return;
}

const firstEnabledTab = tabs.find( ( tab ) => ! tab.disabled );

// If the currently selected tab becomes disabled, select the first enabled tab.
// (if there is one).
if ( firstEnabledTab ) {
handleTabSelection( firstEnabledTab.name );
// handle the case of selected tab is removed from tabs
if ( ! selectedTab ) {
const fallbackTab = initialTabName
? getTab( initialTabName )
: firstEnabledTab;
setTabName( fallbackTab?.name );
}
}, [ tabs, selectedTab?.disabled, handleTabSelection ] );
}, [ getTab, selectedTab, initialTabName, setTabName, firstEnabledTab ] );

return (
<div className={ className }>
Expand All @@ -161,14 +138,14 @@ export function TabPanel( {
'components-tab-panel__tabs-item',
tab.className,
{
[ activeClass ]: tab.name === selected,
[ activeClass ]: tab.name === selectedTab?.name,
}
) }
tabId={ `${ instanceId }-${ tab.name }` }
aria-controls={ `${ instanceId }-${ tab.name }-view` }
selected={ tab.name === selected }
selected={ tab.name === selectedTab?.name }
key={ tab.name }
onClick={ () => handleTabSelection( tab.name ) }
onClick={ () => setTabName( tab.name ) }
disabled={ tab.disabled }
label={ tab.icon && tab.title }
icon={ tab.icon }
Expand Down
Loading

0 comments on commit 9064064

Please sign in to comment.