diff --git a/packages/react-core/src/components/Tabs/Tab.tsx b/packages/react-core/src/components/Tabs/Tab.tsx index bca65327011..03378263f16 100644 --- a/packages/react-core/src/components/Tabs/Tab.tsx +++ b/packages/react-core/src/components/Tabs/Tab.tsx @@ -5,6 +5,8 @@ import { TabButton } from './TabButton'; import { TabsContext } from './TabsContext'; import { css } from '@patternfly/react-styles'; import { Tooltip } from '../Tooltip'; +import { Button } from '../Button'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export interface TabProps extends Omit, 'title'>, OUIAProps { /** content rendered inside the Tab content area. */ @@ -33,6 +35,10 @@ export interface TabProps extends Omit; /** Optional Tooltip rendered to a Tab. Should be with appropriate props for proper rendering. */ tooltip?: React.ReactElement; + /** @beta Aria-label for the close button added by passing the onClose property to Tabs. */ + closeButtonAriaLabel?: string; + /** @beta Flag indicating the close button should be disabled */ + isCloseDisabled?: boolean; } const TabBase: React.FunctionComponent = ({ @@ -49,6 +55,8 @@ const TabBase: React.FunctionComponent = ({ href, innerRef, tooltip, + closeButtonAriaLabel, + isCloseDisabled = false, ...props }: TabProps) => { const preventedEvents = inoperableEvents.reduce( @@ -60,7 +68,9 @@ const TabBase: React.FunctionComponent = ({ }), {} ); - const { mountOnEnter, localActiveKey, unmountOnExit, uniqueId, handleTabClick } = React.useContext(TabsContext); + const { mountOnEnter, localActiveKey, unmountOnExit, uniqueId, handleTabClick, handleTabClose } = React.useContext( + TabsContext + ); let ariaControls = tabContentId ? `${tabContentId}` : `pf-tab-section-${eventKey}-${childId || uniqueId}`; if ((mountOnEnter || unmountOnExit) && eventKey !== localActiveKey) { ariaControls = undefined; @@ -102,10 +112,30 @@ const TabBase: React.FunctionComponent = ({ return (
  • {tooltip ? {tabButton} : tabButton} + {handleTabClose !== undefined && ( + + + + )}
  • ); }; diff --git a/packages/react-core/src/components/Tabs/Tabs.tsx b/packages/react-core/src/components/Tabs/Tabs.tsx index 6428f31e67f..be758cb79e1 100644 --- a/packages/react-core/src/components/Tabs/Tabs.tsx +++ b/packages/react-core/src/components/Tabs/Tabs.tsx @@ -5,6 +5,7 @@ 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 } from '../../helpers/util'; import { TabContent } from './TabContent'; import { TabProps } from './Tab'; @@ -31,6 +32,12 @@ export interface TabsProps extends Omit, eventKey: number | string) => void; + /** @beta Callback to handle tab closing */ + onClose?: (event: React.MouseEvent, eventKey: number | string) => void; + /** @beta Callback for the add button. Passing this property inserts the add button */ + onAdd?: () => void; + /** @beta Aria-label for the add button */ + addButtonAriaLabel?: string; /** Uniquely identifies the tabs */ id?: string; /** Enables the filled tab list layout */ @@ -273,13 +280,21 @@ export class Tabs extends React.Component { } componentDidUpdate(prevProps: TabsProps) { - const { activeKey, mountOnEnter } = this.props; + const { activeKey, mountOnEnter, children } = this.props; const { shownKeys } = 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(); + } } render() { @@ -311,7 +326,10 @@ export class Tabs extends React.Component { defaultIsExpanded, toggleText, toggleAriaLabel, + addButtonAriaLabel, onToggle, + onClose, + onAdd, ...props } = this.props; const { @@ -348,7 +366,8 @@ export class Tabs extends React.Component { unmountOnExit, localActiveKey, uniqueId, - handleTabClick: (...args) => this.handleTabClick(...args) + handleTabClick: (...args) => this.handleTabClick(...args), + handleTabClose: onClose }} > { > + {onAdd !== undefined && ( + + + + )} {filteredChildren .filter( diff --git a/packages/react-core/src/components/Tabs/TabsContext.ts b/packages/react-core/src/components/Tabs/TabsContext.ts index 8f289cddfda..55673df02b8 100644 --- a/packages/react-core/src/components/Tabs/TabsContext.ts +++ b/packages/react-core/src/components/Tabs/TabsContext.ts @@ -11,6 +11,11 @@ export interface TabsContextProps { eventKey: number | string, tabContentRef: React.RefObject ) => void; + handleTabClose: ( + event: React.MouseEvent, + eventKey: number | string, + tabContentRef?: React.RefObject + ) => void; } export const TabsContext = React.createContext({ @@ -19,7 +24,8 @@ export const TabsContext = React.createContext({ unmountOnExit: false, localActiveKey: '', uniqueId: '', - handleTabClick: () => null + handleTabClick: () => null, + handleTabClose: undefined }); export const TabsContextProvider = TabsContext.Provider; diff --git a/packages/react-core/src/components/Tabs/__tests__/Tabs.test.tsx b/packages/react-core/src/components/Tabs/__tests__/Tabs.test.tsx index a12c8c386de..79c711477f7 100644 --- a/packages/react-core/src/components/Tabs/__tests__/Tabs.test.tsx +++ b/packages/react-core/src/components/Tabs/__tests__/Tabs.test.tsx @@ -38,6 +38,28 @@ test('should render simple tabs', () => { expect(asFragment()).toMatchSnapshot(); }); +test('should render closeable tabs', () => { + const view = render( + + "Tab item 1"} closeButtonAriaLabel="close-label"> + Tab 1 section + + + ); + expect(screen.getByLabelText('close-label')).toBeTruthy(); +}); + +test('should render add button', () => { + const view = render( + + "Tab item 1"} closeButtonAriaLabel="close-label"> + Tab 1 section + + + ); + expect(screen.getByLabelText('add-label')).toBeTruthy(); +}); + test('should render uncontrolled tabs', () => { const { asFragment } = render( diff --git a/packages/react-core/src/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap b/packages/react-core/src/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap index d0624bc6696..9b4d1b69951 100644 --- a/packages/react-core/src/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +++ b/packages/react-core/src/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap @@ -5,7 +5,7 @@ exports[`should render accessible tabs 1`] = `