modules_Tabs_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { CrossFade, useId, useStateWithDep } from '@gravityforms/react-utils';
import { spacerClasses } from '@gravityforms/utils';
import Button from '../../elements/Button';
import StatusIndicator from '../../elements/StatusIndicator';
import { ARROW_LEFT, ARROW_RIGHT, HOME, END } from '../../utils/keymap';

const { forwardRef, useState } = React;

/**
 * @module Tabs
 * @description A tabs component.
 *
 * @since 4.7.2
 *
 * @param {object}                     props                  The component props.
 * @param {number}                     props.activeTab        The active tab index.
 * @param {boolean}                    props.automatic        Whether to automatically switch tabs using arrow keys.
 * @param {boolean}                    props.controlled       Whether the component is controlled.
 * @param {object}                     props.customAttributes Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses    Custom classes for the component.
 * @param {string}                     props.iconPrefix       The icon prefix for the component.
 * @param {string}                     props.id               The id for the component.
 * @param {string|number|Array|object} props.spacing          The spacing for the component, as a string, number, array, or object.
 * @param {Array}                      props.tabs             An array of objects for the tabs content.
 * @param {string}                     props.tabWidth         The tab width for the component, one of `auto` or `even`.
 *
 * @return {JSX.Element} The tabs component.
 *
 * @example
 * import Tabs from '@gravityforms/components/react/admin/modules/Tabs';
 *
 * return (
 *     <Tabs
 *         automatic={ true }
 *         tabs={ [
 *             {
 *                 label: 'Tab 1',
 *                 panel: 'Tab 1 Content',
 *             },
 *             {
 *                 icon: 'ellipsis',
 *                 label: 'Tab 2',
 *                 panel: 'Tab 2 Content',
 *             },
 *             {
 *                 label: 'Tab 3',
 *                 status: '2',
 *                 panel: 'Tab 3 Content',
 *             },
 *             {
 *                 icon: 'trash',
 *                 label: 'Tab 4',
 *                 status: '97',
 *                 panel: 'Tab 4 Content',
 *             }
 *         ] }
 *     />
 * );
 *
 */
const Tabs = forwardRef( ( {
	activeTab: defaultActiveTab = 0,
	automatic = true,
	controlled = false,
	customAttributes = {},
	customClasses = [],
	iconPrefix = 'gravity-component-icon',
	id: defaultId = '',
	spacing = '',
	tabs = [],
	tabWidth = 'auto',
}, ref ) => {
	const id = useId( defaultId );
	const [ stateActiveTab, setStateActiveTab ] = useState( defaultActiveTab );
	const [ controlActiveTab, setControlActiveTab ] = useStateWithDep( defaultActiveTab );
	const activeTab = controlled ? controlActiveTab : stateActiveTab;
	const lastTab = tabs.length - 1;

	/**
	 * @function getTablist
	 * @description Get the tablist.
	 *
	 * @since 4.7.2
	 *
	 * @return {JSX.Element} The tablist.
	 */
	const getTablist = () => {
		const tablistProps = {
			className: 'gform-tabs__tablist',
			role: 'tablist',
		};

		return (
			<div { ...tablistProps }>
				{ tabs.map( ( tab, index ) => {
					const tabId = `${ id }-tab-${ index }`;
					const tabPanelId = `${ id }-tabpanel-${ index }`;
					const isActive = activeTab === index;
					const tabProps = {
						customAttributes: {
							'aria-controls': tabPanelId,
							'aria-selected': isActive ? 'true' : 'false',
							id: tabId,
							onKeyDown: ( event ) => {
								if ( ! [ ARROW_LEFT, ARROW_RIGHT, HOME, END ].includes( event.key ) ) {
									return;
								}

								let nextIndex;
								switch ( event.key ) {
									case ARROW_LEFT:
										nextIndex = index === 0 ? lastTab : index - 1;
										break;
									case ARROW_RIGHT:
										nextIndex = index === lastTab ? 0 : index + 1;
										break;
									case HOME:
										nextIndex = 0;
										break;
									case END:
										nextIndex = lastTab;
										break;
									default:
										break;
								}

								document.getElementById( `${ id }-tab-${ nextIndex }` ).focus();
								if ( activeTab === nextIndex ) {
									return;
								}
								if ( automatic ) {
									setStateActiveTab( nextIndex );
									setControlActiveTab( nextIndex );
								}
							},
							role: 'tab',
							tabIndex: ! isActive ? '-1' : undefined,
						},
						customClasses: classnames( {
							'gform-tabs__tab': true,
							'gform-tabs__tab--active': isActive,
						} ),
						onClick: () => {
							if ( activeTab === index ) {
								return;
							}
							setStateActiveTab( index );
							// setControlActiveTab( index );
						},
						label: tab.label,
						size: 'size-height-m',
						type: 'simplified',
					};

					if ( tab.icon ) {
						tabProps.icon = tab.icon;
						tabProps.iconPrefix = iconPrefix;
						tabProps.iconPosition = 'leading';
					}

					if ( tab.status ) {
						const statusProps = {
							hasDot: false,
							label: tab.status,
							size: 'sm',
							status: 'gray',
						};
						tabProps.children = <StatusIndicator { ...statusProps } />;
					}

					return <Button key={ tabId } { ...tabProps } />;
				} ) }
			</div>
		);
	};

	/**
	 * @function getTabPanels
	 * @description Get the tab panels.
	 *
	 * @since 4.7.2
	 *
	 * @return {JSX.Element} The tab panels.
	 */
	const getTabPanels = () => {
		const tabPanelsProps = {
			activeIndex: activeTab,
			customClasses: [ 'gform-tabs__tabpanels' ],
			duration: 150,
			id: `${ id }-tabpanels`,
		};
		return (
			<CrossFade { ...tabPanelsProps }>
				{ tabs.map( ( tab, index ) => {
					const tabId = `${ id }-tab-${ index }`;
					const tabPanelId = `${ id }-tabpanel-${ index }`;
					const tabPanelProps = {
						'aria-labelledby': tabId,
						className: 'gform-tabs__tabpanel',
						id: tabPanelId,
						role: 'tabpanel',
						tabIndex: '0',
					};

					return (
						<div key={ tabPanelId } { ...tabPanelProps }>
							{ tab.panel }
						</div>
					);
				} ) }
			</CrossFade>
		);
	};

	const componentProps = {
		className: classnames( {
			'gform-tabs': true,
			'gform-tabs--automatic': automatic,
			[ `gform-tabs--tab-width-${ tabWidth }` ]: tabWidth,
			...spacerClasses( spacing ),
		}, customClasses ),
		...customAttributes,
	};

	return (
		<div { ...componentProps } ref={ ref }>
			{ getTablist() }
			{ getTabPanels() }
		</div>
	);
} );

Tabs.propTypes = {
	activeTab: PropTypes.number,
	automatic: PropTypes.bool,
	controlled: PropTypes.bool,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	iconPrefix: PropTypes.string,
	id: PropTypes.string,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	tabs: PropTypes.array,
	tabWidth: PropTypes.oneOf( [ 'auto', 'even' ] ),
};

export default Tabs;