diff --git a/packages/react-core/src/components/Drawer/Drawer.tsx b/packages/react-core/src/components/Drawer/Drawer.tsx index 0d11c7271c1..09bd902d826 100644 --- a/packages/react-core/src/components/Drawer/Drawer.tsx +++ b/packages/react-core/src/components/Drawer/Drawer.tsx @@ -1,4 +1,4 @@ -import { createContext, useRef } from 'react'; +import { createContext, forwardRef, useImperativeHandle, useRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; import { useOUIAProps, OUIAProps } from '../../helpers'; @@ -31,6 +31,8 @@ export interface DrawerProps extends React.HTMLProps, OUIAProps position?: 'start' | 'end' | 'bottom' | 'left' | 'right'; /** Callback when drawer panel is expanded after waiting 250ms for animation to complete. */ onExpand?: (event: KeyboardEvent | React.MouseEvent | React.TransitionEvent) => void; + /** @hide Forwarded ref */ + innerRef?: React.Ref; /** 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. */ @@ -59,7 +61,7 @@ export const DrawerContext = createContext>({ isViewport: false }); -export const Drawer: React.FunctionComponent = ({ +const DrawerBase: React.FunctionComponent = ({ className = '', children, isExpanded = false, @@ -69,6 +71,7 @@ export const Drawer: React.FunctionComponent = ({ isStatic = false, position = 'end', onExpand = () => {}, + innerRef, ouiaId, ouiaSafe = true, ...props @@ -76,6 +79,7 @@ export const Drawer: React.FunctionComponent = ({ const ouiaProps = useOUIAProps(Drawer.displayName, ouiaId, ouiaSafe); const drawerRef = useRef(undefined); const drawerContentRef = useRef(undefined); + useImperativeHandle(innerRef, () => drawerRef.current); return ( = ({ ); }; +export const Drawer = forwardRef((props: DrawerProps, ref: React.Ref) => ( + +)); Drawer.displayName = 'Drawer'; diff --git a/packages/react-core/src/components/Drawer/DrawerActions.tsx b/packages/react-core/src/components/Drawer/DrawerActions.tsx index 97d4367861c..94b168eb76e 100644 --- a/packages/react-core/src/components/Drawer/DrawerActions.tsx +++ b/packages/react-core/src/components/Drawer/DrawerActions.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; @@ -6,15 +7,21 @@ export interface DrawerActionsProps extends React.HTMLProps { className?: string; /** Actions to be rendered in the panel head. */ children?: React.ReactNode; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerActions: React.FunctionComponent = ({ +const DrawerActionsBase: React.FunctionComponent = ({ className = '', children, + innerRef, ...props }: DrawerActionsProps) => ( -
+
{children}
); +export const DrawerActions = forwardRef((props: DrawerActionsProps, ref: React.Ref) => ( + +)); DrawerActions.displayName = 'DrawerActions'; diff --git a/packages/react-core/src/components/Drawer/DrawerCloseButton.tsx b/packages/react-core/src/components/Drawer/DrawerCloseButton.tsx index 73eb1ec5ee1..f531f071187 100644 --- a/packages/react-core/src/components/Drawer/DrawerCloseButton.tsx +++ b/packages/react-core/src/components/Drawer/DrawerCloseButton.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; import { Button, ButtonProps } from '../Button'; @@ -10,22 +11,25 @@ export interface DrawerCloseButtonProps extends React.HTMLProps onClose?: () => void; /** Accessible label for the drawer close button */ 'aria-label'?: string; + /** @hide Forwarded ref */ + innerRef?: React.Ref; /** Additional properties spread to the close button */ buttonProps?: Omit; } -export const DrawerCloseButton: React.FunctionComponent = ({ +const DrawerCloseButtonBase: React.FunctionComponent = ({ className = '', onClose = () => undefined as any, 'aria-label': ariaLabel = 'Close drawer panel', buttonProps, + innerRef, ...props }: DrawerCloseButtonProps) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { onClick: _onClick, ...restButtonProps } = (buttonProps ?? {}) as ButtonProps; return ( -
+
); }; +export const DrawerCloseButton = forwardRef((props: DrawerCloseButtonProps, ref: React.Ref) => ( + +)); DrawerCloseButton.displayName = 'DrawerCloseButton'; diff --git a/packages/react-core/src/components/Drawer/DrawerContent.tsx b/packages/react-core/src/components/Drawer/DrawerContent.tsx index 50ddc5d488e..7cc2cf05ac8 100644 --- a/packages/react-core/src/components/Drawer/DrawerContent.tsx +++ b/packages/react-core/src/components/Drawer/DrawerContent.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { forwardRef, useContext, useImperativeHandle } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; import { DrawerMain } from './DrawerMain'; @@ -19,16 +19,20 @@ export interface DrawerContentProps extends React.HTMLProps { panelContent: React.ReactNode; /** Color variant of the background of the drawer panel */ colorVariant?: DrawerContentColorVariant | 'default' | 'primary' | 'secondary'; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerContent: React.FunctionComponent = ({ +const DrawerContentBase: React.FunctionComponent = ({ className, children, panelContent, colorVariant = DrawerContentColorVariant.default, + innerRef, ...props }: DrawerContentProps) => { const { drawerContentRef } = useContext(DrawerContext); + useImperativeHandle(innerRef, () => drawerContentRef.current); return ( @@ -48,4 +52,7 @@ export const DrawerContent: React.FunctionComponent = ({ ); }; +export const DrawerContent = forwardRef((props: DrawerContentProps, ref: React.Ref) => ( + +)); DrawerContent.displayName = 'DrawerContent'; diff --git a/packages/react-core/src/components/Drawer/DrawerContentBody.tsx b/packages/react-core/src/components/Drawer/DrawerContentBody.tsx index a522457b4a2..c0a94354ce7 100644 --- a/packages/react-core/src/components/Drawer/DrawerContentBody.tsx +++ b/packages/react-core/src/components/Drawer/DrawerContentBody.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; @@ -8,16 +9,22 @@ export interface DrawerContentBodyProps extends React.HTMLProps children?: React.ReactNode; /** Indicates if there should be padding around the drawer content body */ hasPadding?: boolean; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerContentBody: React.FunctionComponent = ({ +const DrawerContentBodyBase: React.FunctionComponent = ({ className = '', children, hasPadding = false, + innerRef, ...props }: DrawerContentBodyProps) => ( -
+
{children}
); +export const DrawerContentBody = forwardRef((props: DrawerContentBodyProps, ref: React.Ref) => ( + +)); DrawerContentBody.displayName = 'DrawerContentBody'; diff --git a/packages/react-core/src/components/Drawer/DrawerHead.tsx b/packages/react-core/src/components/Drawer/DrawerHead.tsx index 703531699ea..33719be34a5 100644 --- a/packages/react-core/src/components/Drawer/DrawerHead.tsx +++ b/packages/react-core/src/components/Drawer/DrawerHead.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; @@ -6,15 +7,21 @@ export interface DrawerHeadProps extends React.HTMLProps { className?: string; /** Content to be rendered in the drawer head */ children?: React.ReactNode; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerHead: React.FunctionComponent = ({ +const DrawerHeadBase: React.FunctionComponent = ({ className = '', children, + innerRef, ...props }: DrawerHeadProps) => ( -
+
{children}
); +export const DrawerHead = forwardRef((props: DrawerHeadProps, ref: React.Ref) => ( + +)); DrawerHead.displayName = 'DrawerHead'; diff --git a/packages/react-core/src/components/Drawer/DrawerMain.tsx b/packages/react-core/src/components/Drawer/DrawerMain.tsx index 1cda6baddab..38760f34e45 100644 --- a/packages/react-core/src/components/Drawer/DrawerMain.tsx +++ b/packages/react-core/src/components/Drawer/DrawerMain.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; @@ -6,15 +7,21 @@ export interface DrawerMainProps extends React.HTMLProps { className?: string; /** Content to be rendered in the drawer main wrapper*/ children?: React.ReactNode; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerMain: React.FunctionComponent = ({ +const DrawerMainBase: React.FunctionComponent = ({ className = '', children, + innerRef, ...props }: DrawerMainProps) => ( -
+
{children}
); +export const DrawerMain = forwardRef((props: DrawerMainProps, ref: React.Ref) => ( + +)); DrawerMain.displayName = 'DrawerMain'; diff --git a/packages/react-core/src/components/Drawer/DrawerPanelBody.tsx b/packages/react-core/src/components/Drawer/DrawerPanelBody.tsx index 9b6ca99e73e..ee4dc201eca 100644 --- a/packages/react-core/src/components/Drawer/DrawerPanelBody.tsx +++ b/packages/react-core/src/components/Drawer/DrawerPanelBody.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; @@ -8,16 +9,26 @@ export interface DrawerPanelBodyProps extends React.HTMLProps { children?: React.ReactNode; /** Indicates if there should be no padding around the drawer panel body */ hasNoPadding?: boolean; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerPanelBody: React.FunctionComponent = ({ +const DrawerPanelBodyBase: React.FunctionComponent = ({ className = '', children, hasNoPadding = false, + innerRef, ...props }: DrawerPanelBodyProps) => ( -
+
{children}
); +export const DrawerPanelBody = forwardRef((props: DrawerPanelBodyProps, ref: React.Ref) => ( + +)); DrawerPanelBody.displayName = 'DrawerPanelBody'; diff --git a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx index 14b8c4eb056..99a976ea269 100644 --- a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx +++ b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx @@ -1,4 +1,4 @@ -import { Fragment, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { forwardRef, Fragment, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; import { DrawerColorVariant, DrawerContext } from './Drawer'; @@ -70,11 +70,13 @@ export interface DrawerPanelContentProps extends Omit; } let isResizing: boolean = null; let newSize: number = 0; -export const DrawerPanelContent: React.FunctionComponent = ({ +const DrawerPanelContentBase: React.FunctionComponent = ({ className, id, children, @@ -94,11 +96,13 @@ export const DrawerPanelContent: React.FunctionComponent { const panelId = useSSRSafeId('pf-drawer-panel-'); const panel = useRef(undefined); const splitterRef = useRef(undefined); + useImperativeHandle(innerRef, () => panel.current); const [separatorValue, setSeparatorValue] = useState(0); const { position, isExpanded, isStatic, onExpand, drawerRef, drawerContentRef, isInline, isViewport } = useContext(DrawerContext); @@ -443,4 +447,7 @@ export const DrawerPanelContent: React.FunctionComponent ); }; +export const DrawerPanelContent = forwardRef((props: DrawerPanelContentProps, ref: React.Ref) => ( + +)); DrawerPanelContent.displayName = 'DrawerPanelContent'; diff --git a/packages/react-core/src/components/Drawer/DrawerPanelDescription.tsx b/packages/react-core/src/components/Drawer/DrawerPanelDescription.tsx index 7d044777f8d..1fbca07f970 100644 --- a/packages/react-core/src/components/Drawer/DrawerPanelDescription.tsx +++ b/packages/react-core/src/components/Drawer/DrawerPanelDescription.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; @@ -8,15 +9,23 @@ export interface DrawerPanelDescriptionProps extends React.HTMLProps; } -export const DrawerPanelDescription: React.FunctionComponent = ({ +const DrawerPanelDescriptionBase: React.FunctionComponent = ({ className, children, + innerRef, ...props }: DrawerPanelDescriptionProps) => ( -
+
{children}
); +export const DrawerPanelDescription = forwardRef( + (props: DrawerPanelDescriptionProps, ref: React.Ref) => ( + + ) +); DrawerPanelDescription.displayName = 'DrawerPanelDescription'; diff --git a/packages/react-core/src/components/Drawer/DrawerSection.tsx b/packages/react-core/src/components/Drawer/DrawerSection.tsx index 941fa3fa862..fd39e5c0d66 100644 --- a/packages/react-core/src/components/Drawer/DrawerSection.tsx +++ b/packages/react-core/src/components/Drawer/DrawerSection.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; import { css } from '@patternfly/react-styles'; import { DrawerColorVariant } from './Drawer'; @@ -12,14 +13,17 @@ export interface DrawerSectionProps extends React.HTMLProps { * The `no-background` value is deprecated; use the `isPlain` prop instead. */ colorVariant?: DrawerColorVariant | 'no-background' | 'default' | 'secondary'; + /** @hide Forwarded ref */ + innerRef?: React.Ref; /** @beta Flag indicating that the drawer section should use plain styles. */ isPlain?: boolean; } -export const DrawerSection: React.FunctionComponent = ({ +const DrawerSectionBase: React.FunctionComponent = ({ className = '', children, colorVariant = DrawerColorVariant.default, + innerRef, isPlain = false, ...props }: DrawerSectionProps) => ( @@ -31,9 +35,13 @@ export const DrawerSection: React.FunctionComponent = ({ colorVariant === DrawerColorVariant.secondary && styles.modifiers.secondary, className )} + ref={innerRef} {...props} > {children}
); +export const DrawerSection = forwardRef((props: DrawerSectionProps, ref: React.Ref) => ( + +)); DrawerSection.displayName = 'DrawerSection'; diff --git a/packages/react-core/src/components/Drawer/__tests__/Drawer.test.tsx b/packages/react-core/src/components/Drawer/__tests__/Drawer.test.tsx index 60c9c5977c8..283b81c362f 100644 --- a/packages/react-core/src/components/Drawer/__tests__/Drawer.test.tsx +++ b/packages/react-core/src/components/Drawer/__tests__/Drawer.test.tsx @@ -1,4 +1,4 @@ -import { createContext } from 'react'; +import { createContext, createRef } from 'react'; import { Drawer, DrawerActions, @@ -8,8 +8,11 @@ import { DrawerHead, DrawerPanelBody, DrawerPanelContent, + DrawerPanelDescription, + DrawerSection, DrawerColorVariant } from '../'; +import { DrawerMain } from '../DrawerMain'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { KeyTypes } from '../../../helpers'; @@ -188,6 +191,64 @@ test(`Renders with ${styles.modifiers.pill} class when specified`, () => { expect(screen.getByTestId('drawer')).toHaveClass(styles.modifiers.pill); }); +test('Drawer and its sub-components forward refs to their underlying div elements', () => { + const drawerRef = createRef(); + const sectionRef = createRef(); + const contentRef = createRef(); + const contentBodyRef = createRef(); + const panelContentRef = createRef(); + const headRef = createRef(); + const actionsRef = createRef(); + const closeButtonRef = createRef(); + const panelDescriptionRef = createRef(); + const panelBodyRef = createRef(); + const mainRef = createRef(); + + const panelContent = ( + + + + + + + + Description + + + Panel body + + + ); + + render( + <> + + + Section + + + + Content body + + + + + + ); + + expect(drawerRef.current).toBe(screen.getByTestId('drawer')); + expect(sectionRef.current).toBe(screen.getByTestId('section')); + expect(contentRef.current).toBe(screen.getByTestId('content')); + expect(contentBodyRef.current).toBe(screen.getByTestId('content-body')); + expect(panelContentRef.current).toBe(screen.getByTestId('panel-content')); + expect(headRef.current).toBe(screen.getByTestId('head')); + expect(actionsRef.current).toBe(screen.getByTestId('actions')); + expect(closeButtonRef.current).toBe(screen.getByTestId('close-button')); + expect(panelDescriptionRef.current).toBe(screen.getByTestId('panel-description')); + expect(panelBodyRef.current).toBe(screen.getByTestId('panel-body')); + expect(mainRef.current).toBe(screen.getByTestId('main')); +}); + test('Renders with custom ouiaId', () => { render( diff --git a/packages/react-core/src/components/Label/Label.tsx b/packages/react-core/src/components/Label/Label.tsx index 295f8794fd0..31337688f91 100644 --- a/packages/react-core/src/components/Label/Label.tsx +++ b/packages/react-core/src/components/Label/Label.tsx @@ -1,4 +1,4 @@ -import { createRef, Fragment, useEffect, useRef, useState } from 'react'; +import { createRef, forwardRef, Fragment, useEffect, useRef, useState } from 'react'; import styles from '@patternfly/react-styles/css/components/Label/label'; import labelGrpStyles from '@patternfly/react-styles/css/components/Label/label-group'; import { Button } from '../Button'; @@ -102,6 +102,8 @@ export interface LabelProps extends React.HTMLProps { content: React.ReactNode; componentRef: any; }) => React.ReactNode; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } const colorStyles: Record = { @@ -124,7 +126,7 @@ const statusIcons: Record = { [LabelStatus.custom]: }; -export const Label: React.FunctionComponent = ({ +const LabelBase: React.FunctionComponent = ({ children, className = '', color = LabelColor.grey, @@ -147,6 +149,7 @@ export const Label: React.FunctionComponent = ({ closeBtnProps, href, render, + innerRef, ...props }: LabelProps) => { const [isEditableActive, setIsEditableActive] = useState(false); @@ -364,6 +367,7 @@ export const Label: React.FunctionComponent = ({ return ( = ({ ); }; +export const Label = forwardRef((props: LabelProps, ref: React.Ref) => ); Label.displayName = 'Label'; diff --git a/packages/react-core/src/components/Label/__tests__/Label.test.tsx b/packages/react-core/src/components/Label/__tests__/Label.test.tsx index fa587679fac..1cea48e6169 100644 --- a/packages/react-core/src/components/Label/__tests__/Label.test.tsx +++ b/packages/react-core/src/components/Label/__tests__/Label.test.tsx @@ -1,3 +1,4 @@ +import { createRef } from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; @@ -276,4 +277,26 @@ describe('Label', () => { expect(closeButton).toBeDisabled(); }); + + test('forwards ref to the root element', () => { + const ref = createRef(); + render( + + ); + + expect(ref.current).toBe(screen.getByTestId('label')); + }); + + test('forwards ref to the root button element for overflow labels', () => { + const ref = createRef(); + render( + + ); + + expect(ref.current).toBe(screen.getByRole('button', { name: 'Overflow' })); + }); });