From 142dd49177eea857e28125cd2bb5fd64d23d9c39 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Mon, 22 Jun 2026 10:44:22 +0200 Subject: [PATCH] fix(ui-motion,ui-alerts): capture child DOM via elementRef in Transition to avoid React 'ref is not a prop' warning --- packages/ui-alerts/src/Alert/v1/index.tsx | 4 ++ packages/ui-alerts/src/Alert/v1/props.ts | 8 ++- packages/ui-alerts/src/Alert/v2/index.tsx | 4 ++ packages/ui-alerts/src/Alert/v2/props.ts | 8 ++- .../src/Transition/BaseTransition/index.ts | 51 ++++++++++++++----- 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/ui-alerts/src/Alert/v1/index.tsx b/packages/ui-alerts/src/Alert/v1/index.tsx index fd038baf62..6b8d8d9f0d 100644 --- a/packages/ui-alerts/src/Alert/v1/index.tsx +++ b/packages/ui-alerts/src/Alert/v1/index.tsx @@ -99,6 +99,10 @@ class Alert extends Component { handleRef = (el: Element | null) => { this.ref = el + const { elementRef } = this.props + if (typeof elementRef === 'function') { + elementRef(el) + } } handleTimeout = () => { diff --git a/packages/ui-alerts/src/Alert/v1/props.ts b/packages/ui-alerts/src/Alert/v1/props.ts index e93aefe82c..62e03e7de0 100644 --- a/packages/ui-alerts/src/Alert/v1/props.ts +++ b/packages/ui-alerts/src/Alert/v1/props.ts @@ -109,6 +109,11 @@ type AlertOwnProps = { * An icon, or function that returns an icon. Setting it will override the variant's icon. */ renderCustomIcon?: Renderable + + /** + * provides a reference to the underlying html root element + */ + elementRef?: (element: Element | null) => void } type PropKeys = keyof AlertOwnProps @@ -137,7 +142,8 @@ const allowedProps: AllowedPropKeys = [ 'transition', 'open', 'hasShadow', - 'renderCustomIcon' + 'renderCustomIcon', + 'elementRef' ] type AlertState = { diff --git a/packages/ui-alerts/src/Alert/v2/index.tsx b/packages/ui-alerts/src/Alert/v2/index.tsx index 3beb2df6ff..510de2e32b 100644 --- a/packages/ui-alerts/src/Alert/v2/index.tsx +++ b/packages/ui-alerts/src/Alert/v2/index.tsx @@ -98,6 +98,10 @@ class Alert extends Component { handleRef = (el: Element | null) => { this.ref = el + const { elementRef } = this.props + if (typeof elementRef === 'function') { + elementRef(el) + } } handleTimeout = () => { diff --git a/packages/ui-alerts/src/Alert/v2/props.ts b/packages/ui-alerts/src/Alert/v2/props.ts index 33a1966d06..8dfc5f3fe6 100644 --- a/packages/ui-alerts/src/Alert/v2/props.ts +++ b/packages/ui-alerts/src/Alert/v2/props.ts @@ -110,6 +110,11 @@ type AlertOwnProps = { * An icon, or function that returns an icon. Setting it will override the variant's icon. */ renderCustomIcon?: Renderable + + /** + * provides a reference to the underlying html root element + */ + elementRef?: (element: Element | null) => void } type PropKeys = keyof AlertOwnProps @@ -138,7 +143,8 @@ const allowedProps: AllowedPropKeys = [ 'transition', 'open', 'hasShadow', - 'renderCustomIcon' + 'renderCustomIcon', + 'elementRef' ] type AlertState = { diff --git a/packages/ui-motion/src/Transition/BaseTransition/index.ts b/packages/ui-motion/src/Transition/BaseTransition/index.ts index 741aeddf9a..01efb056a4 100644 --- a/packages/ui-motion/src/Transition/BaseTransition/index.ts +++ b/packages/ui-motion/src/Transition/BaseTransition/index.ts @@ -22,13 +22,14 @@ * SOFTWARE. */ -import { Component, ReactElement } from 'react' +import { Component, ReactElement, ReactInstance } from 'react' import { getClassList, findDOMNode } from '@instructure/ui-dom-utils' import { ensureSingleChild, safeCloneElement } from '@instructure/ui-react-utils' +import { createChainedFunction } from '@instructure/ui-utils' import { allowedProps } from './props.js' import type { @@ -334,19 +335,43 @@ class BaseTransition extends Component< } renderChildren() { - return this.props.children - ? safeCloneElement( - ensureSingleChild(this.props.children) as ReactElement, - { - 'aria-hidden': !this.props.in ? true : undefined, - ref: (el: React.ReactInstance | null) => { - const ref = (findDOMNode(el) as Element) || null - - this.handleRef(ref) - } + if (!this.props.children) { + return null + } + + const child = ensureSingleChild(this.props.children) as ReactElement + + const elementOnlyRef = (el: ReactInstance | Element | null) => { + if (el instanceof Element) { + this.handleRef(el) + } + } + + // `typeof type === 'object'` => forwardRef wrapper (withStyle-decorated InstUI components) + const refProps = + typeof child.type === 'object' + ? { + // chain so the child's own elementRef still fires instead of being overwritten + elementRef: createChainedFunction( + (child.props as { elementRef?: (el: Element | null) => void }) + ?.elementRef, + this.handleRef + ), + // fallback for forwardRef children that expose their node via `ref`, not elementRef + ref: elementOnlyRef } - ) - : null + : { + // for host el / plain class|fn: findDOMNode is the fallback + ref: (el: ReactInstance | Element | null) => + this.handleRef( + el instanceof Element ? el : (findDOMNode(el) as Element) ?? null + ) + } + + return safeCloneElement(child, { + 'aria-hidden': !this.props.in ? true : undefined, + ...refProps + }) } render() {