From a37f05e856d154dfdcccbd354a30d71ebe0badff Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 25 Apr 2022 11:08:11 -0400 Subject: [PATCH 01/12] feat(Tabs): allow dynamic close/add --- .../react-core/src/components/Tabs/Tab.tsx | 30 ++++++++++++- .../react-core/src/components/Tabs/Tabs.tsx | 25 ++++++++++- .../src/components/Tabs/TabsContext.ts | 8 +++- .../components/Tabs/__tests__/Tabs.test.tsx | 22 ++++++++++ .../__snapshots__/Tabs.test.tsx.snap | 24 +++++------ .../src/components/Tabs/examples/Tabs.md | 7 +++ .../components/Tabs/examples/TabsDynamic.tsx | 43 +++++++++++++++++++ 7 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 packages/react-core/src/components/Tabs/examples/TabsDynamic.tsx diff --git a/packages/react-core/src/components/Tabs/Tab.tsx b/packages/react-core/src/components/Tabs/Tab.tsx index bca65327011..af0ce3fd7a0 100644 --- a/packages/react-core/src/components/Tabs/Tab.tsx +++ b/packages/react-core/src/components/Tabs/Tab.tsx @@ -1,10 +1,12 @@ 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 { OUIAProps } from '../../helpers'; import { TabButton } from './TabButton'; import { TabsContext } from './TabsContext'; import { css } from '@patternfly/react-styles'; import { Tooltip } from '../Tooltip'; +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,8 @@ export interface TabProps extends Omit; /** Optional Tooltip rendered to a Tab. Should be with appropriate props for proper rendering. */ tooltip?: React.ReactElement; + /** Aria-label for the close button added by passing the onClose property to Tabs. */ + closeAriaLabel?: string; } const TabBase: React.FunctionComponent = ({ @@ -49,6 +53,7 @@ const TabBase: React.FunctionComponent = ({ href, innerRef, tooltip, + closeAriaLabel, ...props }: TabProps) => { const preventedEvents = inoperableEvents.reduce( @@ -60,7 +65,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 +109,29 @@ 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..35a50df0c10 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; + /** Callback to handle tab closing */ + onClose?: (event: React.MouseEvent, eventKey: number | string) => void; + /** Callback for the add button. Passing this property inserts the add button */ + onAdd?: () => void; + /** Aria-label for the add button */ + addAriaLabel?: string; /** Uniquely identifies the tabs */ id?: string; /** Enables the filled tab list layout */ @@ -311,7 +318,10 @@ export class Tabs extends React.Component { defaultIsExpanded, toggleText, toggleAriaLabel, + addAriaLabel, onToggle, + onClose, + onAdd, ...props } = this.props; const { @@ -348,7 +358,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..bf7509ecb09 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"} closeAriaLabel="close-label"> + Tab 1 section + + + ); + expect(screen.getByLabelText('close-label')).toBeTruthy(); +}); + +test('should render add button', () => { + const view = render( + + "Tab item 1"} closeAriaLabel="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..26a532eb4ba 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`] = `