From 5b688c27b1c15df69d11c456690976f4d32f7674 Mon Sep 17 00:00:00 2001 From: Tarun Vashishtha <65185652+tarunvashishth@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:38:44 +0000 Subject: [PATCH 1/2] fix(Label): add ref forwarding Wrap Label in forwardRef so that refs passed to the component are attached to its root element (span, or button for overflow/add variants). Closes patternfly/patternfly-react#12651 --- .../react-core/src/components/Label/Label.tsx | 9 ++++++-- .../components/Label/__tests__/Label.test.tsx | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/react-core/src/components/Label/Label.tsx b/packages/react-core/src/components/Label/Label.tsx index 8fd0f643188..103d713029d 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'; @@ -80,6 +80,8 @@ export interface LabelProps extends React.HTMLProps { content: React.ReactNode; componentRef: any; }) => React.ReactNode; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } const colorStyles = { @@ -102,7 +104,7 @@ const statusIcons = { custom: }; -export const Label: React.FunctionComponent = ({ +const LabelBase: React.FunctionComponent = ({ children, className = '', color = 'grey', @@ -125,6 +127,7 @@ export const Label: React.FunctionComponent = ({ closeBtnProps, href, render, + innerRef, ...props }: LabelProps) => { const [isEditableActive, setIsEditableActive] = useState(false); @@ -343,6 +346,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 f5991a4cddd..2701b2e692a 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'; @@ -236,4 +237,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' })); + }); }); From 4b8ae976c5ce6a2417d112e3124c8906a8b3809a Mon Sep 17 00:00:00 2001 From: Tarun Vashishtha <65185652+tarunvashishth@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:38:44 +0000 Subject: [PATCH 2/2] fix(Drawer): add ref forwarding to Drawer and its sub-components Wrap Drawer, DrawerActions, DrawerCloseButton, DrawerContent, DrawerContentBody, DrawerHead, DrawerMain, DrawerPanelBody, DrawerPanelContent, DrawerPanelDescription and DrawerSection in forwardRef. Components that already hold an internal ref to their root element (Drawer, DrawerContent, DrawerPanelContent) expose it through useImperativeHandle so internal behavior such as resizing keeps working. Closes patternfly/patternfly-react#12650 --- .../src/components/Drawer/Drawer.tsx | 11 +++- .../src/components/Drawer/DrawerActions.tsx | 11 +++- .../components/Drawer/DrawerCloseButton.tsx | 11 +++- .../src/components/Drawer/DrawerContent.tsx | 11 +++- .../components/Drawer/DrawerContentBody.tsx | 11 +++- .../src/components/Drawer/DrawerHead.tsx | 11 +++- .../src/components/Drawer/DrawerMain.tsx | 11 +++- .../src/components/Drawer/DrawerPanelBody.tsx | 15 ++++- .../components/Drawer/DrawerPanelContent.tsx | 11 +++- .../Drawer/DrawerPanelDescription.tsx | 13 +++- .../src/components/Drawer/DrawerSection.tsx | 10 ++- .../Drawer/__tests__/Drawer.test.tsx | 63 ++++++++++++++++++- 12 files changed, 167 insertions(+), 22 deletions(-) diff --git a/packages/react-core/src/components/Drawer/Drawer.tsx b/packages/react-core/src/components/Drawer/Drawer.tsx index 3470426a6b8..65e0336d6c9 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'; @@ -25,6 +25,8 @@ export interface DrawerProps extends React.HTMLProps { 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; } export interface DrawerContextProps { @@ -47,7 +49,7 @@ export const DrawerContext = createContext>({ isInline: false }); -export const Drawer: React.FunctionComponent = ({ +const DrawerBase: React.FunctionComponent = ({ className = '', children, isExpanded = false, @@ -56,10 +58,12 @@ export const Drawer: React.FunctionComponent = ({ isStatic = false, position = 'end', onExpand = () => {}, + innerRef, ...props }: DrawerProps) => { const drawerRef = useRef(undefined); const drawerContentRef = useRef(undefined); + useImperativeHandle(innerRef, () => drawerRef.current); return ( @@ -82,4 +86,7 @@ export const Drawer: React.FunctionComponent = ({ ); }; +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 14639244ac3..ad09629a6ac 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 } from '../Button'; @@ -10,16 +11,22 @@ export interface DrawerCloseButtonProps extends React.HTMLProps onClose?: () => void; /** Accessible label for the drawer close button */ 'aria-label'?: string; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerCloseButton: React.FunctionComponent = ({ +const DrawerCloseButtonBase: React.FunctionComponent = ({ className = '', onClose = () => undefined as any, 'aria-label': ariaLabel = 'Close drawer panel', + innerRef, ...props }: DrawerCloseButtonProps) => ( -
+
); +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 a489316842d..99d06b4fb90 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 7adbead74a9..b030be125ea 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'; @@ -60,11 +60,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, @@ -81,10 +83,12 @@ export const DrawerPanelContent: React.FunctionComponent { 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 } = useContext(DrawerContext); const hidden = isStatic ? false : !isExpanded; @@ -433,4 +437,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 011f2dde0b8..a4cd477efa6 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'; @@ -9,12 +10,15 @@ export interface DrawerSectionProps extends React.HTMLProps { children?: React.ReactNode; /** Color variant of the background of the drawer Section */ colorVariant?: DrawerColorVariant | 'no-background' | 'default' | 'secondary'; + /** @hide Forwarded ref */ + innerRef?: React.Ref; } -export const DrawerSection: React.FunctionComponent = ({ +const DrawerSectionBase: React.FunctionComponent = ({ className = '', children, colorVariant = DrawerColorVariant.default, + innerRef, ...props }: DrawerSectionProps) => (
= ({ 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 14a0f2f6295..760c622aee2 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'; @@ -187,3 +190,61 @@ 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')); +});