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

TabPanel: Add prop to allow disabling of a tab button #46471

Merged
merged 12 commits into from
Jan 2, 2023
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### New Feature

- `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)).
- `BaseControl`: Add `useBaseControlProps` hook to help generate id-releated props ([#46170](https://github.com/WordPress/gutenberg/pull/46170)).

### Bug Fix
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/tab-panel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ An array of objects containing the following properties:
- `title`:`(string)` Defines the translated text for the tab.
- `className`:`(string)` Optional. Defines the class to put on the tab.
- `icon`:`(ReactNode)` Optional. When set, displays the icon in place of the tab title. The title is then rendered as an aria-label and tooltip.
- `disabled`:`(boolean)` Optional. Determines if the tab should be disabled or selectable.

> > **Note:** Other fields may be added to the object and accessed from the child function if desired.

Expand Down
24 changes: 19 additions & 5 deletions packages/components/src/tab-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type { WordPressComponentProps } from '../ui/context';

const TabButton = ( {
tabId,
onClick,
children,
selected,
...rest
Expand All @@ -30,7 +29,7 @@ const TabButton = ( {
tabIndex={ selected ? null : -1 }
aria-selected={ selected }
id={ tabId }
onClick={ onClick }
__experimentalIsFocusable
{ ...rest }
>
{ children }
Expand Down Expand Up @@ -106,10 +105,24 @@ export function TabPanel( {
const selectedId = `${ instanceId }-${ selectedTab?.name ?? 'none' }`;

useEffect( () => {
if ( ! selectedTab?.name && tabs.length > 0 ) {
handleTabSelection( initialTabName || tabs[ 0 ].name );
const firstEnabledTab = tabs.find( ( tab ) => ! tab.disabled );
const initialTab = tabs.find( ( tab ) => tab.name === initialTabName );
if ( ! selectedTab?.name && firstEnabledTab ) {
handleTabSelection(
initialTab && ! initialTab.disabled
? initialTab.name
: firstEnabledTab.name
);
} else if ( selectedTab?.disabled && firstEnabledTab ) {
handleTabSelection( firstEnabledTab.name );
}
}, [ tabs, selectedTab?.name, initialTabName, handleTabSelection ] );
}, [
tabs,
selectedTab?.name,
selectedTab?.disabled,
initialTabName,
handleTabSelection,
] );

return (
<div className={ className }>
Expand All @@ -135,6 +148,7 @@ export function TabPanel( {
selected={ tab.name === selected }
key={ tab.name }
onClick={ () => handleTabSelection( tab.name ) }
disabled={ tab.disabled }
label={ tab.icon && tab.title }
icon={ tab.icon }
showTooltip={ !! tab.icon }
Expand Down
20 changes: 20 additions & 0 deletions packages/components/src/tab-panel/stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,23 @@ Default.args = {
},
],
};

export const DisabledTab = Template.bind( {} );
DisabledTab.args = {
children: ( tab ) => <p>Selected tab: { tab.title }</p>,
tabs: [
{
name: 'tab1',
title: 'Tab 1',
disabled: true,
},
{
name: 'tab2',
title: 'Tab 2',
},
{
name: 'tab3',
title: 'Tab 3',
},
],
};
89 changes: 89 additions & 0 deletions packages/components/src/tab-panel/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,95 @@ describe( 'TabPanel', () => {
expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
} );

it( 'should disable the tab when `disabled` is true', async () => {
const user = setupUser();
const mockOnSelect = jest.fn();

render(
<TabPanel
tabs={ [
...TABS,
{
name: 'delta',
title: 'Delta',
className: 'delta-class',
disabled: true,
},
] }
children={ () => undefined }
onSelect={ mockOnSelect }
/>
);

expect( screen.getByRole( 'tab', { name: 'Delta' } ) ).toHaveAttribute(
'aria-disabled',
'true'
);

// onSelect gets called on the initial render.
expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );

// onSelect should not be called since the disabled tab is highlighted, but not selected.
await user.keyboard( '[ArrowLeft]' );
expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
} );

it( 'should select the first enabled tab when the inital tab is disabled', () => {
const mockOnSelect = jest.fn();

render(
<TabPanel
tabs={ [
{
name: 'alpha',
title: 'Alpha',
className: 'alpha-class',
disabled: true,
},
{
name: 'beta',
title: 'Beta',
className: 'beta-class',
},
] }
initialTabName="alpha"
children={ () => undefined }
onSelect={ mockOnSelect }
/>
);

expect( getSelectedTab() ).toHaveTextContent( 'Beta' );
} );

it( 'should select the first enabled tab when the currently selected becomes disabled', () => {
const mockOnSelect = jest.fn();

const { rerender } = render(
<TabPanel
tabs={ TABS }
children={ () => undefined }
onSelect={ mockOnSelect }
/>
);

expect( getSelectedTab() ).toHaveTextContent( 'Alpha' );

rerender(
<TabPanel
tabs={ TABS.map( ( tab ) => {
if ( tab.name === 'alpha' ) {
return { ...tab, disabled: true };
}
return tab;
} ) }
children={ () => undefined }
onSelect={ mockOnSelect }
/>
);

expect( getSelectedTab() ).toHaveTextContent( 'Beta' );
} );

describe( 'fallbacks when new tab list invalidates current selection', () => {
it( 'should select `initialTabName` if defined', async () => {
const user = setupUser();
Expand Down
14 changes: 10 additions & 4 deletions packages/components/src/tab-panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ReactNode } from 'react';
*/
import type { IconType } from '../icon';

type Tab = {
type Tab< IconProps = unknown > = {
/**
* The key of the tab.
*/
Expand All @@ -21,18 +21,24 @@ type Tab = {
* The class name to apply to the tab button.
*/
className?: string;
/**
* The icon used for the tab button.
*/
icon?: IconType< IconProps >;
/**
* Determines if the tab button should be disabled.
*/
disabled?: boolean;
} & Record< any, any >;

export type TabButtonProps< IconProps = unknown > = {
children: ReactNode;
className?: string;
icon?: IconType< IconProps >;
label?: string;
onClick: ( event: MouseEvent ) => void;
selected: boolean;
showTooltip?: boolean;
tabId: string;
};
} & Pick< Tab< IconProps >, 'className' | 'icon' | 'disabled' >;

export type TabPanelProps = {
/**
Expand Down