All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.components.Tabs.Tabs.tsx Maven / Gradle / Ivy

Go to download

This library provides a set of common React components for use with the PatternFly reference implementation.

The newest version!
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/Tabs/tabs';
import buttonStyles from '@patternfly/react-styles/css/components/Button/button';
import { css } from '@patternfly/react-styles';
import { PickOptional } from '../../helpers/typeUtils';
import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
import { getUniqueId, isElementInView, formatBreakpointMods, getLanguageDirection } from '../../helpers/util';
import { TabContent } from './TabContent';
import { TabProps } from './Tab';
import { TabsContextProvider } from './TabsContext';
import { OverflowTab } from './OverflowTab';
import { Button } from '../Button';
import { getOUIAProps, OUIAProps, getDefaultOUIAId, canUseDOM } from '../../helpers';
import { GenerateId } from '../../helpers/GenerateId/GenerateId';

export enum TabsComponent {
  div = 'div',
  nav = 'nav'
}

export interface HorizontalOverflowObject {
  /** Flag which shows the count of overflowing tabs when enabled */
  showTabCount?: boolean;
  /** The text which displays when an overflowing tab isn't selected */
  defaultTitleText?: string;
  /** The aria label applied to the button which toggles the tab overflow menu */
  toggleAriaLabel?: string;
}

type TabElement = React.ReactElement>;
type TabsChild = TabElement | boolean | null | undefined;

export interface TabsProps extends Omit, 'onSelect'>, OUIAProps {
  /** Content rendered inside the tabs component. Only `Tab` components or expressions resulting in a falsy value are allowed here. */
  children: TabsChild | TabsChild[];
  /** Additional classes added to the tabs */
  className?: string;
  /** Tabs background color variant */
  variant?: 'default' | 'light300';
  /** The index of the active tab */
  activeKey?: number | string;
  /** The index of the default active tab. Set this for uncontrolled Tabs */
  defaultActiveKey?: number | string;
  /** Callback to handle tab selection */
  onSelect?: (event: React.MouseEvent, eventKey: number | string) => void;
  /** Callback to handle tab closing and adds a basic close button to all tabs. This is overridden by the tab actions property. */
  onClose?: (event: React.MouseEvent, eventKey: number | string) => void;
  /** Callback for the add button. Passing this property inserts the add button */
  onAdd?: (event: React.MouseEvent) => void;
  /** Aria-label for the add button */
  addButtonAriaLabel?: string;
  /** Uniquely identifies the tabs */
  id?: string;
  /** Enables the filled tab list layout */
  isFilled?: boolean;
  /** Enables secondary tab styling */
  isSecondary?: boolean;
  /** Enables box styling to the tab component */
  isBox?: boolean;
  /** Enables vertical tab styling */
  isVertical?: boolean;
  /** Disables border bottom tab styling on tabs. Defaults to false. To remove the bottom border, set this prop to true. */
  hasNoBorderBottom?: boolean;
  /** @deprecated Please use backScrollAriaLabel. Aria-label for the left scroll button */
  leftScrollAriaLabel?: string;
  /** @deprecated Please use forwardScrollAriaLabel. Aria-label for the right scroll button */
  rightScrollAriaLabel?: string;
  /** Aria-label for the back scroll button */
  backScrollAriaLabel?: string;
  /** Aria-label for the forward scroll button */
  forwardScrollAriaLabel?: string;
  /** Determines what tag is used around the tabs. Use "nav" to define the tabs inside a navigation region */
  component?: 'div' | 'nav';
  /** Provides an accessible label for the tabs. Labels should be unique for each set of tabs that are present on a page. When component is set to nav, this prop should be defined to differentiate the tabs from other navigation regions on the page. */
  'aria-label'?: string;
  /** Waits until the first "enter" transition to mount tab children (add them to the DOM) */
  mountOnEnter?: boolean;
  /** Unmounts tab children (removes them from the DOM) when they are no longer visible */
  unmountOnExit?: boolean;
  /** Flag indicates that the tabs should use page insets. */
  usePageInsets?: boolean;
  /** Insets at various breakpoints. */
  inset?: {
    default?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl';
    sm?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl';
    md?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl';
    lg?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl';
    xl?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl';
    '2xl'?: 'insetNone' | 'insetSm' | 'insetMd' | 'insetLg' | 'insetXl' | 'inset2xl';
  };
  /** Enable expandable vertical tabs at various breakpoints. (isVertical should be set to true for this to work) */
  expandable?: {
    default?: 'expandable' | 'nonExpandable';
    sm?: 'expandable' | 'nonExpandable';
    md?: 'expandable' | 'nonExpandable';
    lg?: 'expandable' | 'nonExpandable';
    xl?: 'expandable' | 'nonExpandable';
    '2xl'?: 'expandable' | 'nonExpandable';
  };
  /** Flag to indicate if the vertical tabs are expanded */
  isExpanded?: boolean;
  /** Flag indicating the default expanded state for uncontrolled expand/collapse of */
  defaultIsExpanded?: boolean;
  /** Text that appears in the expandable toggle */
  toggleText?: string;
  /** Aria-label for the expandable toggle */
  toggleAriaLabel?: string;
  /** Callback function to toggle the expandable tabs. */
  onToggle?: (event: React.MouseEvent, isExpanded: boolean) => void;
  /** Flag which places overflowing tabs into a menu triggered by the last tab. Additionally an object can be passed with custom settings for the overflow tab. */
  isOverflowHorizontal?: boolean | HorizontalOverflowObject;
  /** Value to overwrite the randomly generated data-ouia-component-id.*/
  ouiaId?: number | string;
  /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */
  ouiaSafe?: boolean;
}

const variantStyle = {
  default: '',
  light300: styles.modifiers.colorSchemeLight_300
};

interface TabsState {
  /** Used to signal if the scroll buttons should be used  */
  enableScrollButtons: boolean;
  /** Used to control if the scroll buttons should be shown to the user via the pf-m-scrollable class */
  showScrollButtons: boolean;
  /** Used to control if the scroll buttons should be rendered. Rendering must occur before the scroll buttons are
   * shown and rendering must be stopped after they stop being shown to preserve CSS transitions.
   */
  renderScrollButtons: boolean;
  disableBackScrollButton: boolean;
  disableForwardScrollButton: boolean;
  shownKeys: (string | number)[];
  uncontrolledActiveKey: number | string;
  uncontrolledIsExpandedLocal: boolean;
  ouiaStateId: string;
  overflowingTabCount: number;
}

class Tabs extends React.Component {
  static displayName = 'Tabs';
  tabList = React.createRef();
  leftScrollButtonRef = React.createRef();
  private direction = 'ltr';
  constructor(props: TabsProps) {
    super(props);
    this.state = {
      enableScrollButtons: false,
      showScrollButtons: false,
      renderScrollButtons: false,
      disableBackScrollButton: true,
      disableForwardScrollButton: true,
      shownKeys: this.props.defaultActiveKey !== undefined ? [this.props.defaultActiveKey] : [this.props.activeKey], // only for mountOnEnter case
      uncontrolledActiveKey: this.props.defaultActiveKey,
      uncontrolledIsExpandedLocal: this.props.defaultIsExpanded,
      ouiaStateId: getDefaultOUIAId(Tabs.displayName),
      overflowingTabCount: 0
    };

    if (this.props.isVertical && this.props.expandable !== undefined) {
      if (!this.props.toggleAriaLabel && !this.props.toggleText) {
        // eslint-disable-next-line no-console
        console.error(
          'Tabs:',
          'toggleAriaLabel or the toggleText prop is required to make the toggle button accessible'
        );
      }
    }
  }

  scrollTimeout: NodeJS.Timeout = null;

  static defaultProps: PickOptional = {
    activeKey: 0,
    onSelect: () => undefined as any,
    isFilled: false,
    isSecondary: false,
    isVertical: false,
    isBox: false,
    hasNoBorderBottom: false,
    leftScrollAriaLabel: 'Scroll left',
    backScrollAriaLabel: 'Scroll back',
    rightScrollAriaLabel: 'Scroll right',
    forwardScrollAriaLabel: 'Scroll forward',
    component: TabsComponent.div,
    mountOnEnter: false,
    unmountOnExit: false,
    ouiaSafe: true,
    variant: 'default',
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    onToggle: (_event: React.MouseEvent, _isExpanded: boolean): void => undefined
  };

  handleTabClick(
    event: React.MouseEvent,
    eventKey: number | string,
    tabContentRef: React.RefObject
  ) {
    const { shownKeys } = this.state;
    const { onSelect, defaultActiveKey } = this.props;
    // if defaultActiveKey Tabs are uncontrolled, set new active key internally
    if (defaultActiveKey !== undefined) {
      this.setState({
        uncontrolledActiveKey: eventKey
      });
    } else {
      onSelect(event, eventKey);
    }

    // process any tab content sections outside of the component
    if (tabContentRef) {
      React.Children.toArray(this.props.children)
        .filter((child): child is TabElement => React.isValidElement(child))
        .filter(({ props }) => props.tabContentRef && props.tabContentRef.current)
        .forEach((child) => (child.props.tabContentRef.current.hidden = true));
      // most recently selected tabContent
      if (tabContentRef.current) {
        tabContentRef.current.hidden = false;
      }
    }
    if (this.props.mountOnEnter) {
      this.setState({
        shownKeys: shownKeys.concat(eventKey)
      });
    }
  }

  countOverflowingElements = (container: HTMLUListElement) => {
    const elements = Array.from(container.children);
    return elements.filter((element) => !isElementInView(container, element as HTMLElement, false)).length;
  };

  handleScrollButtons = () => {
    const { isOverflowHorizontal: isOverflowHorizontal } = this.props;
    // add debounce to the scroll event
    clearTimeout(this.scrollTimeout);
    this.scrollTimeout = setTimeout(() => {
      const container = this.tabList.current;
      let disableBackScrollButton = true;
      let disableForwardScrollButton = true;
      let enableScrollButtons = false;
      let overflowingTabCount = 0;

      if (container && !this.props.isVertical && !isOverflowHorizontal) {
        // get first element and check if it is in view
        const overflowOnLeft = !isElementInView(container, container.firstChild as HTMLElement, false);

        // get last element and check if it is in view
        const overflowOnRight = !isElementInView(container, container.lastChild as HTMLElement, false);

        enableScrollButtons = overflowOnLeft || overflowOnRight;

        disableBackScrollButton = !overflowOnLeft;
        disableForwardScrollButton = !overflowOnRight;
      }

      if (isOverflowHorizontal) {
        overflowingTabCount = this.countOverflowingElements(container);
      }

      this.setState({
        enableScrollButtons,
        disableBackScrollButton,
        disableForwardScrollButton,
        overflowingTabCount
      });
    }, 100);
  };

  scrollBack = () => {
    // find first Element that is fully in view on the left, then scroll to the element before it
    if (this.tabList.current) {
      const container = this.tabList.current;
      const childrenArr = Array.from(container.children);
      let firstElementInView: any;
      let lastElementOutOfView: any;
      let i;
      for (i = 0; i < childrenArr.length && !firstElementInView; i++) {
        if (isElementInView(container, childrenArr[i] as HTMLElement, false)) {
          firstElementInView = childrenArr[i];
          lastElementOutOfView = childrenArr[i - 1];
        }
      }
      if (lastElementOutOfView) {
        if (this.direction === 'ltr') {
          // LTR scrolls left to go back
          container.scrollLeft -= lastElementOutOfView.scrollWidth;
        } else {
          // RTL scrolls right to go back
          container.scrollLeft += lastElementOutOfView.scrollWidth;
        }
      }
    }
  };

  scrollForward = () => {
    // find last Element that is fully in view on the right, then scroll to the element after it
    if (this.tabList.current) {
      const container = this.tabList.current as any;
      const childrenArr = Array.from(container.children);
      let lastElementInView: any;
      let firstElementOutOfView: any;
      for (let i = childrenArr.length - 1; i >= 0 && !lastElementInView; i--) {
        if (isElementInView(container, childrenArr[i] as HTMLElement, false)) {
          lastElementInView = childrenArr[i];
          firstElementOutOfView = childrenArr[i + 1];
        }
      }
      if (firstElementOutOfView) {
        if (this.direction === 'ltr') {
          // LTR scrolls right to go forward
          container.scrollLeft += firstElementOutOfView.scrollWidth;
        } else {
          // RTL scrolls left to go forward
          container.scrollLeft -= firstElementOutOfView.scrollWidth;
        }
      }
    }
  };

  hideScrollButtons = () => {
    const { enableScrollButtons, renderScrollButtons, showScrollButtons } = this.state;
    if (!enableScrollButtons && !showScrollButtons && renderScrollButtons) {
      this.setState({ renderScrollButtons: false });
    }
  };

  componentDidMount() {
    if (!this.props.isVertical) {
      if (canUseDOM) {
        window.addEventListener('resize', this.handleScrollButtons, false);
      }
      this.direction = getLanguageDirection(this.tabList.current);
      // call the handle resize function to check if scroll buttons should be shown
      this.handleScrollButtons();
    }
  }

  componentWillUnmount() {
    if (!this.props.isVertical) {
      if (canUseDOM) {
        window.removeEventListener('resize', this.handleScrollButtons, false);
      }
    }
    clearTimeout(this.scrollTimeout);
    this.leftScrollButtonRef.current?.removeEventListener('transitionend', this.hideScrollButtons);
  }

  componentDidUpdate(prevProps: TabsProps, prevState: TabsState) {
    const { activeKey, mountOnEnter, isOverflowHorizontal, children } = this.props;
    const { shownKeys, overflowingTabCount, enableScrollButtons } = this.state;
    if (prevProps.activeKey !== activeKey && mountOnEnter && shownKeys.indexOf(activeKey) < 0) {
      this.setState({
        shownKeys: shownKeys.concat(activeKey)
      });
    }

    if (
      prevProps.children &&
      children &&
      React.Children.toArray(prevProps.children).length !== React.Children.toArray(children).length
    ) {
      this.handleScrollButtons();
    }

    const currentOverflowingTabCount = this.countOverflowingElements(this.tabList.current);
    if (isOverflowHorizontal && currentOverflowingTabCount) {
      this.setState({ overflowingTabCount: currentOverflowingTabCount + overflowingTabCount });
    }

    if (!prevState.enableScrollButtons && enableScrollButtons) {
      this.setState({ renderScrollButtons: true });
      setTimeout(() => {
        this.leftScrollButtonRef.current?.addEventListener('transitionend', this.hideScrollButtons);
        this.setState({ showScrollButtons: true });
      }, 100);
    } else if (prevState.enableScrollButtons && !enableScrollButtons) {
      this.setState({ showScrollButtons: false });
    }

    this.direction = getLanguageDirection(this.tabList.current);
  }

  render() {
    const {
      className,
      children,
      activeKey,
      defaultActiveKey,
      id,
      isFilled,
      isSecondary,
      isVertical,
      isBox,
      hasNoBorderBottom,
      leftScrollAriaLabel,
      rightScrollAriaLabel,
      backScrollAriaLabel,
      forwardScrollAriaLabel,
      'aria-label': ariaLabel,
      component,
      ouiaId,
      ouiaSafe,
      mountOnEnter,
      unmountOnExit,
      usePageInsets,
      inset,
      variant,
      expandable,
      isExpanded,
      defaultIsExpanded,
      toggleText,
      toggleAriaLabel,
      addButtonAriaLabel,
      onToggle,
      onClose,
      onAdd,
      isOverflowHorizontal: isOverflowHorizontal,
      ...props
    } = this.props;
    const {
      showScrollButtons,
      renderScrollButtons,
      disableBackScrollButton,
      disableForwardScrollButton,
      shownKeys,
      uncontrolledActiveKey,
      uncontrolledIsExpandedLocal,
      overflowingTabCount
    } = this.state;
    const filteredChildren = React.Children.toArray(children)
      .filter((child): child is TabElement => React.isValidElement(child))
      .filter(({ props }) => !props.isHidden);

    const filteredChildrenWithoutOverflow = filteredChildren.slice(0, filteredChildren.length - overflowingTabCount);
    const filteredChildrenOverflowing = filteredChildren.slice(filteredChildren.length - overflowingTabCount);
    const overflowingTabProps = filteredChildrenOverflowing.map((child: React.ReactElement) => child.props);

    const uniqueId = id || getUniqueId();
    const Component: any = component === TabsComponent.nav ? 'nav' : 'div';
    const localActiveKey = defaultActiveKey !== undefined ? uncontrolledActiveKey : activeKey;

    const isExpandedLocal = defaultIsExpanded !== undefined ? uncontrolledIsExpandedLocal : isExpanded;
    /*  Uncontrolled expandable tabs */
    const toggleTabs = (event: React.MouseEvent, newValue: boolean) => {
      if (isExpanded === undefined) {
        this.setState({ uncontrolledIsExpandedLocal: newValue });
      } else {
        onToggle(event, newValue);
      }
    };

    const hasOverflowTab = isOverflowHorizontal && overflowingTabCount > 0;
    const overflowObjectProps = typeof isOverflowHorizontal === 'object' ? { ...isOverflowHorizontal } : {};

    return (
       this.handleTabClick(...args),
          handleTabClose: onClose
        }}
      >
        
          {expandable && isVertical && (
            
              {(randomId) => (
                
)}
)} {renderScrollButtons && ( )}
    {isOverflowHorizontal ? filteredChildrenWithoutOverflow : filteredChildren} {hasOverflowTab && }
{renderScrollButtons && ( )} {onAdd !== undefined && ( )}
{filteredChildren .filter( (child) => child.props.children && !(unmountOnExit && child.props.eventKey !== localActiveKey) && !(mountOnEnter && shownKeys.indexOf(child.props.eventKey) === -1) ) .map((child) => ( ))}
); } } export { Tabs };




© 2015 - 2024 Weber Informatics LLC | Privacy Policy