diff --git a/packages/react/MIGRATION.md b/packages/react/MIGRATION.md index 005ae1055..2f3d7a491 100644 --- a/packages/react/MIGRATION.md +++ b/packages/react/MIGRATION.md @@ -31,7 +31,7 @@ npm install --save-dev @types/react@^19 @types/react-dom@^19 ``` Follow the [official React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for the changes -it requires in your own code. The components and hooks of the library keep their current API. +it requires in your own code. ## 0.16.0 → 0.17.0 diff --git a/packages/react/src/components/Alert/Alert.tsx b/packages/react/src/components/Alert/Alert.tsx index 39c5dc384..8e4ce6476 100644 --- a/packages/react/src/components/Alert/Alert.tsx +++ b/packages/react/src/components/Alert/Alert.tsx @@ -1,5 +1,7 @@ 'use client'; +import { RefAttributes } from 'react'; + import { AlertProps } from './Alert.types'; import clsx from 'clsx'; @@ -17,7 +19,7 @@ const defaultIconMapping = { /** * An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task. */ -export const Alert = (inProps: AlertProps) => { +export const Alert = ({ ref, ...inProps }: AlertProps & RefAttributes) => { const { className, children, @@ -38,6 +40,7 @@ export const Alert = (inProps: AlertProps) => { return (
{ +export const AlertActions = ({ ref, ...inProps }: AlertActionsProps & RefAttributes) => { const { className, children, style } = useDefaultProps({ props: inProps, name: 'ESAlertActions', }); return ( -
+
{children}
); diff --git a/packages/react/src/components/Alert/AlertClose/AlertClose.tsx b/packages/react/src/components/Alert/AlertClose/AlertClose.tsx index 5d1e82957..e63fa4dcb 100644 --- a/packages/react/src/components/Alert/AlertClose/AlertClose.tsx +++ b/packages/react/src/components/Alert/AlertClose/AlertClose.tsx @@ -1,5 +1,7 @@ 'use client'; +import { RefAttributes } from 'react'; + import { AlertCloseProps } from './AlertClose.types'; import clsx from 'clsx'; @@ -11,7 +13,7 @@ import { Button } from '../../Button'; /** * @see `Alert` */ -export const AlertClose = (inProps: AlertCloseProps) => { +export const AlertClose = ({ ref, ...inProps }: AlertCloseProps & RefAttributes) => { const { className, style, @@ -27,6 +29,7 @@ export const AlertClose = (inProps: AlertCloseProps) => { return (
- - {editable && !editing && ( -
- {iconEdit} -
- )} - - {editing && ( -
- - -
- )}
- ); - }) -); + + {editable && !editing && ( +
{iconEdit}
+ )} + + {editing && ( +
+ + +
+ )} + + ); +}); diff --git a/packages/react/src/components/Table/TableFoot/TableFoot.tsx b/packages/react/src/components/Table/TableFoot/TableFoot.tsx index c2834c947..7bcabac7b 100644 --- a/packages/react/src/components/Table/TableFoot/TableFoot.tsx +++ b/packages/react/src/components/Table/TableFoot/TableFoot.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef, memo, useRef, useState } from 'react'; +import { memo, RefAttributes, useRef, useState } from 'react'; import { TableFootProps } from './TableFoot.types'; @@ -12,45 +12,46 @@ import { useDefaultProps } from '../../../theming'; /** * @see `Table` */ -export const TableFoot = memo( - forwardRef(function TableFoot(inProps, inRef) { - const { children, className, sticky, style } = useDefaultProps({ - props: inProps, - name: 'ESTableFoot', - }); - - const [isStuck, setStuck] = useState(false); - - const ref = useRef(null); - - const rootRef = useForkRef(ref, inRef); - - useIntersectionObserver( - ref, - (entries) => { - setStuck(entries[0].intersectionRatio < 1); - }, - { threshold: [1], rootMargin: `0px 0px -${(sticky || 0) + 1}px` } - ); - - return ( -
- {children} -
- ); - }) -); +export const TableFoot = memo(function TableFoot({ + ref: inRef, + ...inProps +}: TableFootProps & RefAttributes) { + const { children, className, sticky, style } = useDefaultProps({ + props: inProps, + name: 'ESTableFoot', + }); + + const [isStuck, setStuck] = useState(false); + + const ref = useRef(null); + + const rootRef = useForkRef(ref, inRef); + + useIntersectionObserver( + ref, + (entries) => { + setStuck(entries[0].intersectionRatio < 1); + }, + { threshold: [1], rootMargin: `0px 0px -${(sticky || 0) + 1}px` } + ); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/react/src/components/Table/TableHead/TableHead.tsx b/packages/react/src/components/Table/TableHead/TableHead.tsx index 0fae36251..0b759137c 100644 --- a/packages/react/src/components/Table/TableHead/TableHead.tsx +++ b/packages/react/src/components/Table/TableHead/TableHead.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef, memo, useMemo, useState } from 'react'; +import { memo, RefAttributes, useMemo, useState } from 'react'; import { TableHeadProps } from './TableHead.types'; @@ -17,58 +17,59 @@ const TABLE_CELL_CONTEXT_VALUE = { variant: 'head' as const }; /** * @see `Table` */ -export const TableHead = memo( - forwardRef(function TableHead(inProps, inRef) { - const { - children, - className, - sticky, - style, - rowDividers = true, - colDividers = false, - } = useDefaultProps({ - props: inProps, - name: 'ESTableHead', - }); +export const TableHead = memo(function TableHead({ + ref: inRef, + ...inProps +}: TableHeadProps & RefAttributes) { + const { + children, + className, + sticky, + style, + rowDividers = true, + colDividers = false, + } = useDefaultProps({ + props: inProps, + name: 'ESTableHead', + }); - const [isStuck, setStuck] = useState(false); + const [isStuck, setStuck] = useState(false); - const { ref, setRef } = useTableHeadContext(); - const rootRef = useForkRef(setRef, inRef); + const { ref, setRef } = useTableHeadContext(); + const rootRef = useForkRef(setRef, inRef); - useIntersectionObserver( - { current: ref }, - (entries) => { - setStuck(entries[0].intersectionRatio < 1); - }, - { threshold: [1], rootMargin: `-${(sticky || 0) + 1}px 0px 0px` } - ); + useIntersectionObserver( + { current: ref }, + (entries) => { + setStuck(entries[0].intersectionRatio < 1); + }, + { threshold: [1], rootMargin: `-${(sticky || 0) + 1}px 0px 0px` } + ); - const value = useMemo(() => { - return { ...TABLE_CELL_CONTEXT_VALUE, rowDividers, colDividers }; - }, [rowDividers, colDividers]); + const value = useMemo(() => { + return { ...TABLE_CELL_CONTEXT_VALUE, rowDividers, colDividers }; + }, [rowDividers, colDividers]); - return ( - -
-
{children}
-
-
- ); - }) -); + return ( + +
+
{children}
+
+
+ ); +}); diff --git a/packages/react/src/components/Table/TableItem/TableItem.tsx b/packages/react/src/components/Table/TableItem/TableItem.tsx index 72db2118e..203870a8d 100644 --- a/packages/react/src/components/Table/TableItem/TableItem.tsx +++ b/packages/react/src/components/Table/TableItem/TableItem.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef } from 'react'; +import { RefAttributes } from 'react'; import { TableItemProps } from './TableItem.types'; @@ -11,7 +11,7 @@ import { useDefaultProps } from '../../../theming'; /** * @see `Table` */ -export const TableItem = forwardRef(function TableItem(inProps, ref) { +export const TableItem = ({ ref, ...inProps }: TableItemProps & RefAttributes) => { const { className, style, avatar, primary, secondary } = useDefaultProps({ props: inProps, name: 'ESTableItem', @@ -26,4 +26,4 @@ export const TableItem = forwardRef(function Tab ); -}); +}; diff --git a/packages/react/src/components/Table/TableRow/TableRow.tsx b/packages/react/src/components/Table/TableRow/TableRow.tsx index 7c4ddb273..305855fa0 100644 --- a/packages/react/src/components/Table/TableRow/TableRow.tsx +++ b/packages/react/src/components/Table/TableRow/TableRow.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef, memo } from 'react'; +import { memo } from 'react'; import { TableRowProps, TableRowTypeMap } from './TableRow.types'; @@ -13,43 +13,36 @@ import { useTableContext } from '../Table.context'; /** * @see `Table` */ -export const TableRow = memo( - forwardRef(function TableRow(inProps: TableRowProps, ref) { - const { - component: Component = 'div', - children, - className, - style, - selected, - hover, - ...props - } = useDefaultProps({ - props: inProps, - name: 'ESTableRow', - }); +export const TableRow = memo(function TableRow({ ref, ...inProps }: TableRowProps) { + const { + component: Component = 'div', + children, + className, + style, + selected, + hover, + ...props + } = useDefaultProps({ + props: inProps, + name: 'ESTableRow', + }); - const { columns } = useTableContext(); + const { columns } = useTableContext(); - return ( -
+ - - {children} - -
- ); - }) -) as OverridableComponent; + {children} + + + ); +}) as OverridableComponent; diff --git a/packages/react/src/components/Table/TableScrollbar/TableScrollbar.tsx b/packages/react/src/components/Table/TableScrollbar/TableScrollbar.tsx index 0ed561b46..60d6c0072 100644 --- a/packages/react/src/components/Table/TableScrollbar/TableScrollbar.tsx +++ b/packages/react/src/components/Table/TableScrollbar/TableScrollbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef, memo } from 'react'; +import { memo, RefAttributes } from 'react'; import { TableScrollbarProps } from './TableScrollbar.types'; @@ -14,21 +14,22 @@ import { useDefaultProps } from '../../../theming'; /** * @see `Table` */ -export const TableScrollbar = memo( - forwardRef(function TableScrollbar(inProps, inRef) { - const { className, style } = useDefaultProps({ - props: inProps, - name: 'ESTableScrollbar', - }); - - const { width, setRef } = useTableScrollbarContext(); - - const rootRef = useForkRef(setRef, inRef); - - return ( -
-
-
- ); - }) -); +export const TableScrollbar = memo(function TableScrollbar({ + ref: inRef, + ...inProps +}: TableScrollbarProps & RefAttributes) { + const { className, style } = useDefaultProps({ + props: inProps, + name: 'ESTableScrollbar', + }); + + const { width, setRef } = useTableScrollbarContext(); + + const rootRef = useForkRef(setRef, inRef); + + return ( +
+
+
+ ); +}); diff --git a/packages/react/src/components/Table/TableText/TableText.tsx b/packages/react/src/components/Table/TableText/TableText.tsx index f42640fc1..55497bdd3 100644 --- a/packages/react/src/components/Table/TableText/TableText.tsx +++ b/packages/react/src/components/Table/TableText/TableText.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef, memo, MutableRefObject } from 'react'; +import { memo, RefAttributes, RefObject } from 'react'; import { TableTextProps } from './TableText.types'; @@ -12,52 +12,46 @@ import { TooltipEllipsis } from '../../TooltipEllipsis'; /** * @see `Table` */ -export const TableText = memo( - forwardRef(function TableText(inProps, ref) { - const { - children, - className, - style, - tooltip = true, - TooltipProps, - } = useDefaultProps({ - props: inProps, - name: 'ESTableText', - }); - - if (tooltip) { - return ( - - {({ ref }) => ( -
} - className={clsx('es-table-text', className)} - style={style} - > - {children} -
- )} -
- ); - } +export const TableText = memo(function TableText({ ref, ...inProps }: TableTextProps & RefAttributes) { + const { + children, + className, + style, + tooltip = true, + TooltipProps, + } = useDefaultProps({ + props: inProps, + name: 'ESTableText', + }); + if (tooltip) { return ( -
- {children} -
+ + {({ ref }) => ( +
} className={clsx('es-table-text', className)} style={style}> + {children} +
+ )} +
); - }) -); + } + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/react/src/components/Table/useTableResize.ts b/packages/react/src/components/Table/useTableResize.ts index 43140c4a0..e9ea7db0c 100644 --- a/packages/react/src/components/Table/useTableResize.ts +++ b/packages/react/src/components/Table/useTableResize.ts @@ -1,6 +1,6 @@ 'use client'; -import { Dispatch, MutableRefObject, SetStateAction } from 'react'; +import { Dispatch, RefObject, SetStateAction } from 'react'; /** * The hook that manages resizing of the table columns. @@ -11,8 +11,8 @@ import { Dispatch, MutableRefObject, SetStateAction } from 'react'; * @returns The `onResize` and `onResizeCommit` functions taking column index and colSpan and returning callbacks for the corresponding `TableCell`. */ export const useTableResize = ( - ref: MutableRefObject, - rowRef: MutableRefObject, + ref: RefObject, + rowRef: RefObject, columns: string[], setColumns: Dispatch> ) => { diff --git a/packages/react/src/components/Table/useTableStickyOffset.ts b/packages/react/src/components/Table/useTableStickyOffset.ts index 726e9914d..36523a49d 100644 --- a/packages/react/src/components/Table/useTableStickyOffset.ts +++ b/packages/react/src/components/Table/useTableStickyOffset.ts @@ -1,6 +1,6 @@ 'use client'; -import { MutableRefObject, useEffect } from 'react'; +import { RefObject, useEffect } from 'react'; const getStickyOffset = (element: HTMLElement | null, pin: 'left' | 'right') => { let width = 0; @@ -38,7 +38,7 @@ const getStickyOffset = (element: HTMLElement | null, pin: 'left' | 'right') => * @internal * The hook that sets left and right offsets for sticky (pinned) cells. */ -export const useTableStickyOffset = (ref: MutableRefObject) => { +export const useTableStickyOffset = (ref: RefObject) => { useEffect(() => { if (ref.current) { const resizeObserver = new ResizeObserver(() => { diff --git a/packages/react/src/components/Tabs/Tab/Tab.tsx b/packages/react/src/components/Tabs/Tab/Tab.tsx index 803aaf7e2..50ebc4604 100644 --- a/packages/react/src/components/Tabs/Tab/Tab.tsx +++ b/packages/react/src/components/Tabs/Tab/Tab.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FocusEvent, forwardRef, MouseEvent } from 'react'; +import { FocusEvent, MouseEvent, RefAttributes } from 'react'; import { TabProps } from './Tab.types'; @@ -12,7 +12,7 @@ import { ButtonBase } from '../../ButtonBase'; /** * @see `Tabs` */ -export const Tab = forwardRef(function Tab(inProps: TabProps, ref) { +export const Tab = ({ ref, ...inProps }: TabProps & RefAttributes) => { const { className, disabled = false, @@ -77,4 +77,4 @@ export const Tab = forwardRef(function Tab(inProps: {indicator} ); -}); +}; diff --git a/packages/react/src/components/Tabs/Tab/Tab.types.ts b/packages/react/src/components/Tabs/Tab/Tab.types.ts index f2b64e4ee..88b971753 100644 --- a/packages/react/src/components/Tabs/Tab/Tab.types.ts +++ b/packages/react/src/components/Tabs/Tab/Tab.types.ts @@ -11,11 +11,6 @@ export interface TabProps { * @default false */ disabled?: boolean; - /** - * If `true`, the keyboard focus ripple is disabled. - * @default true - */ - disableFocusRipple?: boolean; /** * The label element. */ diff --git a/packages/react/src/components/Tabs/TabScrollButton/TabScrollButton.tsx b/packages/react/src/components/Tabs/TabScrollButton/TabScrollButton.tsx index 82f65b049..02a01c314 100644 --- a/packages/react/src/components/Tabs/TabScrollButton/TabScrollButton.tsx +++ b/packages/react/src/components/Tabs/TabScrollButton/TabScrollButton.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef } from 'react'; +import { RefAttributes } from 'react'; import { TabScrollButtonProps } from './TabScrollButton.types'; @@ -14,10 +14,7 @@ import { Divider } from '../../Divider'; /** * @see `Tabs` */ -export const TabScrollButton = forwardRef(function TabScrollButton( - inProps: TabScrollButtonProps, - ref -) { +export const TabScrollButton = ({ ref, ...inProps }: TabScrollButtonProps & RefAttributes) => { const { className, direction, @@ -66,4 +63,4 @@ export const TabScrollButton = forwardRef( )} ); -}); +}; diff --git a/packages/react/src/components/Tabs/Tabs.tsx b/packages/react/src/components/Tabs/Tabs.tsx index 8ff7768cc..66f36ad49 100644 --- a/packages/react/src/components/Tabs/Tabs.tsx +++ b/packages/react/src/components/Tabs/Tabs.tsx @@ -4,11 +4,11 @@ import { Children, cloneElement, CSSProperties, - forwardRef, isValidElement, JSX, KeyboardEvent, ReactElement, + RefAttributes, useCallback, useEffect, useImperativeHandle, @@ -151,7 +151,7 @@ let warnedOnceTabPresent = false; /** * Tabs organize content across different screens and views, and let the user navigate between them. */ -export const Tabs = forwardRef(function Tabs(inProps: TabsProps, ref) { +export const Tabs = ({ ref, ...inProps }: TabsProps & RefAttributes) => { const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, @@ -738,4 +738,4 @@ export const Tabs = forwardRef(function Tabs(inProps: {conditionalElements.scrollButtonEnd} ); -}); +}; diff --git a/packages/react/src/components/Tag/Tag.tsx b/packages/react/src/components/Tag/Tag.tsx index a38ebc657..727191a25 100644 --- a/packages/react/src/components/Tag/Tag.tsx +++ b/packages/react/src/components/Tag/Tag.tsx @@ -1,5 +1,7 @@ 'use client'; +import { RefAttributes } from 'react'; + import { TagProps } from './Tag.types'; import clsx from 'clsx'; @@ -10,7 +12,7 @@ import { ButtonBase } from '../ButtonBase'; /** * The Tag component is used to categorize content. */ -export const Tag = (inProps: TagProps) => { +export const Tag = ({ ref, ...inProps }: TagProps & RefAttributes) => { const { children, className, @@ -30,6 +32,7 @@ export const Tag = (inProps: TagProps) => { return ( (function TextField(inProps: TextFieldProps, ref) { +export const TextField = ({ ref, ...inProps }: TextFieldProps & RefAttributes) => { const { className, style, @@ -67,4 +67,4 @@ export const TextField = forwardRef(function Tex {!!helperText && {helperText}} ); -}); +}; diff --git a/packages/react/src/components/TextFieldGroup/TextFieldGroup.tsx b/packages/react/src/components/TextFieldGroup/TextFieldGroup.tsx index 1a6c9b615..e4b9ba33f 100644 --- a/packages/react/src/components/TextFieldGroup/TextFieldGroup.tsx +++ b/packages/react/src/components/TextFieldGroup/TextFieldGroup.tsx @@ -1,5 +1,7 @@ 'use client'; +import { RefAttributes } from 'react'; + import { TextFieldGroupProps } from './TextFieldGroup.types'; import clsx from 'clsx'; @@ -9,7 +11,7 @@ import { useDefaultProps } from '../../theming'; /** * This component allows to group multiple text fields for related information. */ -export const TextFieldGroup = (inProps: TextFieldGroupProps) => { +export const TextFieldGroup = ({ ref, ...inProps }: TextFieldGroupProps & RefAttributes) => { const { children, className, @@ -22,6 +24,7 @@ export const TextFieldGroup = (inProps: TextFieldGroupProps) => { return (
diff --git a/packages/react/src/components/Tooltip/Tooltip.tsx b/packages/react/src/components/Tooltip/Tooltip.tsx index 440bbeb89..f3b981e38 100644 --- a/packages/react/src/components/Tooltip/Tooltip.tsx +++ b/packages/react/src/components/Tooltip/Tooltip.tsx @@ -3,9 +3,9 @@ import { cloneElement, FocusEvent, - forwardRef, isValidElement, MouseEvent, + RefAttributes, SyntheticEvent, TouchEvent, useEffect, @@ -71,7 +71,7 @@ const defaultArrowIconMapping = { /** * Tooltips display informative text when the user hovers over, focuses on, or taps an element. */ -export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { +export const Tooltip = ({ ref, ...inProps }: TooltipProps & RefAttributes) => { const { arrow = true, arrowSize = '6', @@ -121,9 +121,10 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { const [openState, setOpenState] = useControlled(false, openProp); - let open = openState; + // There is no point in displaying an empty tooltip. + const open = openState && (!!title || title === 0); - const { current: isControlled } = useRef(openProp !== undefined); + const [isControlled] = useState(openProp !== undefined); useEffect(() => { if (process.env.NODE_ENV === 'production') { @@ -168,7 +169,7 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { }; }, [stopTouchInteraction]); - const handleOpen = (event: MouseEvent | FocusEvent | TouchEvent) => { + const handleOpen = useEvent((event: MouseEvent | FocusEvent | TouchEvent) => { hystersisTimer.clear(); setHystersisOpen(true); @@ -180,7 +181,7 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { if (onOpen && !open) { onOpen(event); } - }; + }); const handleClose = useEvent((event: MouseEvent | FocusEvent | TouchEvent | KeyboardEvent) => { hystersisTimer.start(800 + leaveDelay, () => { @@ -198,7 +199,7 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { }); }); - const handleMouseOver = (event: MouseEvent | FocusEvent | TouchEvent) => { + const handleMouseOver = useEvent((event: MouseEvent | FocusEvent | TouchEvent) => { if (ignoreNonTouchEvents.current && event.type !== 'touchstart') { return; } @@ -220,15 +221,15 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { } else { handleOpen(event); } - }; + }); - const handleMouseLeave = (event: MouseEvent | FocusEvent) => { + const handleMouseLeave = useEvent((event: MouseEvent | FocusEvent) => { enterTimer.clear(); leaveTimer.start(leaveDelay, () => { handleClose(event); }); - }; + }); const { isFocusVisibleRef, @@ -240,16 +241,16 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { // We just need to re-render the Tooltip if the focus-visible state changes. const [, setChildIsFocusVisible] = useState(false); - const handleBlur = (event: FocusEvent) => { + const handleBlur = useEvent((event: FocusEvent) => { handleBlurVisible(event); if (isFocusVisibleRef.current === false) { setChildIsFocusVisible(false); handleMouseLeave(event); } - }; + }); - const handleFocus = (event: FocusEvent) => { + const handleFocus = useEvent((event: FocusEvent) => { // Workaround for https://github.com/facebook/react/issues/7769 // The autoFocus of React might trigger the event before the componentDidMount. // We need to account for this eventuality. @@ -263,9 +264,9 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { setChildIsFocusVisible(true); handleMouseOver(event); } - }; + }); - const detectTouchStart = (event: TouchEvent) => { + const detectTouchStart = useEvent((event: TouchEvent) => { ignoreNonTouchEvents.current = true; const childrenProps = children.props; @@ -273,9 +274,9 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { if (childrenProps.onTouchStart) { childrenProps.onTouchStart(event); } - }; + }); - const handleTouchStart = (event: TouchEvent) => { + const handleTouchStart = useEvent((event: TouchEvent) => { detectTouchStart(event); leaveTimer.clear(); closeTimer.clear(); @@ -289,9 +290,9 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { setBodyUserSelect(prevUserSelect.current); handleMouseOver(event); }); - }; + }); - const handleTouchEnd = (event: TouchEvent) => { + const handleTouchEnd = useEvent((event: TouchEvent) => { if (children.props.onTouchEnd) { children.props.onTouchEnd(event); } @@ -301,7 +302,7 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { leaveTimer.start(leaveTouchDelay, () => { handleClose(event); }); - }; + }); useEffect(() => { if (!open) { @@ -327,14 +328,9 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { const handleRef = useForkRef(getReactElementRef(children), focusVisibleRef, setChildNode, ref); - // There is no point in displaying an empty tooltip. - if (!title && title !== 0) { - open = false; - } - const popperRef = useRef(null); - const handleMouseMove = (event: MouseEvent) => { + const handleMouseMove = useEvent((event: MouseEvent) => { const childrenProps = children.props; if (childrenProps.onMouseMove) { @@ -346,7 +342,7 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { if (popperRef.current) { popperRef.current.update(); } - }; + }); const nameOrDescProps: { title?: string | null; @@ -441,6 +437,7 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { const middleware = useMemo(() => { const items: Middleware[] = [ + // eslint-disable-next-line react-hooks/refs -- the refs are read when the middleware runs, never during render offset(({ placement: currentPlacement, rects }) => { const padding = 8; const arrowOffsetX = 14; @@ -535,6 +532,8 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { className: clsx( 'es-tooltip__tooltip', arrow && 'es-tooltip__tooltip--arrow', + // The flag is read during render on purpose: a change of it must not re-render the tooltip on its own. + // eslint-disable-next-line react-hooks/refs ignoreNonTouchEvents.current && 'es-tooltip__tooltip--touch', placement && `es-tooltip__tooltip--placement--${placement}`, color && `es-tooltip__tooltip--color--${color}`, @@ -607,4 +606,4 @@ export const Tooltip = forwardRef(function Tooltip(inProps: TooltipProps, ref) { ); -}); +}; diff --git a/packages/react/src/components/TooltipEllipsis/TooltipEllipsis.types.ts b/packages/react/src/components/TooltipEllipsis/TooltipEllipsis.types.ts index a28240460..24ec15113 100644 --- a/packages/react/src/components/TooltipEllipsis/TooltipEllipsis.types.ts +++ b/packages/react/src/components/TooltipEllipsis/TooltipEllipsis.types.ts @@ -1,10 +1,7 @@ -import { MutableRefObject, ReactElement } from 'react'; +import { ReactElement, RefObject } from 'react'; import { TooltipProps } from '../Tooltip'; export type TooltipEllipsisProps = Omit & { - children: (props: { - ref: MutableRefObject; - childrenRef: MutableRefObject; - }) => ReactElement; + children: (props: { ref: RefObject; childrenRef: RefObject }) => ReactElement; }; diff --git a/packages/react/src/components/TouchRipple/TouchRipple.tsx b/packages/react/src/components/TouchRipple/TouchRipple.tsx index 719db49e0..cc4246296 100644 --- a/packages/react/src/components/TouchRipple/TouchRipple.tsx +++ b/packages/react/src/components/TouchRipple/TouchRipple.tsx @@ -1,6 +1,6 @@ 'use client'; -import { forwardRef } from 'react'; +import { RefAttributes } from 'react'; import { TouchRippleProps } from './TouchRipple.types'; @@ -11,11 +11,11 @@ import { useDefaultProps } from '../../theming'; /** * The touch ripple for ButtonBase component. */ -export const TouchRipple = forwardRef(function TouchRipple(inProps, ref) { +export const TouchRipple = ({ ref, ...inProps }: TouchRippleProps & RefAttributes) => { const { className, style } = useDefaultProps({ props: inProps, name: 'ESTouchRipple', }); return
; -}); +}; diff --git a/packages/react/src/components/Tour/Tour.tsx b/packages/react/src/components/Tour/Tour.tsx index 0d859becb..9274f0c5f 100644 --- a/packages/react/src/components/Tour/Tour.tsx +++ b/packages/react/src/components/Tour/Tour.tsx @@ -1,6 +1,6 @@ 'use client'; -import { CSSProperties, forwardRef, useEffect, useMemo, useRef, useState } from 'react'; +import { CSSProperties, RefAttributes, useEffect, useMemo, useRef, useState } from 'react'; import { TourContentProps, TourDirection, TourProps } from './Tour.types'; @@ -47,7 +47,7 @@ const toClientRect = (rect: TourRect) => ({ /** * The Tour walks the user through the interface, highlighting one element at a time and showing a card next to it. */ -export const Tour = forwardRef(function Tour(inProps, ref) { +export const Tour = ({ ref, ...inProps }: TourProps & RefAttributes) => { const { steps, open, @@ -163,6 +163,8 @@ export const Tour = forwardRef(function Tour(inProps, controllerRef.current?.abort(); controllerRef.current = null; pendingRef.current = null; + // The tour has to drop the resolved step as soon as it closes, so the reset cannot wait for an event. + // eslint-disable-next-line react-hooks/set-state-in-effect setActive(null); setLoading(false); return; @@ -352,6 +354,8 @@ export const Tour = forwardRef(function Tour(inProps,
)} {isTargetPresent && + // The anchor reads the spotlight ref when floating-ui measures it, never during render. + // eslint-disable-next-line react-hooks/refs (anchorEl ? ( (function Tour(inProps, ); -}); +}; diff --git a/packages/react/src/components/Zoom/Zoom.tsx b/packages/react/src/components/Zoom/Zoom.tsx index bd2df6317..0d6ba7e53 100644 --- a/packages/react/src/components/Zoom/Zoom.tsx +++ b/packages/react/src/components/Zoom/Zoom.tsx @@ -1,6 +1,6 @@ 'use client'; -import { cloneElement, CSSProperties, forwardRef, useRef } from 'react'; +import { cloneElement, CSSProperties, RefAttributes, useRef } from 'react'; import { Transition, TransitionStatus } from 'react-transition-group'; import { ZoomProps } from './Zoom.types'; @@ -8,7 +8,7 @@ import { ZoomProps } from './Zoom.types'; import { useForkRef } from '../../hooks'; import { useDefaultProps } from '../../theming'; import { createTransition, duration } from '../../transitions'; -import { createTransitionCallbacks } from '../../transitions/transitionCallbacks'; +import { useTransitionCallbacks } from '../../transitions/useTransitionCallbacks'; import { getTransitionProps, reflow } from '../../transitions/utils'; import { getReactElementRef } from '../../utils'; @@ -29,7 +29,7 @@ const styles: Partial> = { /** * The Zoom transition scales a single child element in and out. */ -export const Zoom = forwardRef(function Zoom(inProps, ref) { +export const Zoom = ({ ref, ...inProps }: ZoomProps & RefAttributes) => { const { addEndListener, appear = true, @@ -53,7 +53,7 @@ export const Zoom = forwardRef(function Zoom(inProps, ref) { const nodeRef = useRef(null); const handleRef = useForkRef(nodeRef, getReactElementRef(children), ref); - const callbacks = createTransitionCallbacks(nodeRef); + const callbacks = useTransitionCallbacks(nodeRef); const handleEnter = callbacks.enter((node, isAppearing) => { // So the animation always starts from the start. @@ -117,4 +117,4 @@ export const Zoom = forwardRef(function Zoom(inProps, ref) { }} ); -}); +}; diff --git a/packages/react/src/hooks/useForkRef/useForkRef.ts b/packages/react/src/hooks/useForkRef/useForkRef.ts index 671087f37..121af1dbd 100644 --- a/packages/react/src/hooks/useForkRef/useForkRef.ts +++ b/packages/react/src/hooks/useForkRef/useForkRef.ts @@ -3,7 +3,7 @@ import { Ref, RefCallback, useMemo } from 'react'; const setRef = ( - ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, + ref: React.RefObject | ((instance: T | null) => void) | null | undefined, value: T | null ) => { if (typeof ref === 'function') { diff --git a/packages/react/src/hooks/useIsFocusVisible/useIsFocusVisible.ts b/packages/react/src/hooks/useIsFocusVisible/useIsFocusVisible.ts index 0cf096b23..725bd9804 100644 --- a/packages/react/src/hooks/useIsFocusVisible/useIsFocusVisible.ts +++ b/packages/react/src/hooks/useIsFocusVisible/useIsFocusVisible.ts @@ -2,7 +2,7 @@ // Based on https://github.com/WICG/focus-visible/blob/v4.1.5/src/focus-visible.js -import { FocusEvent, MutableRefObject, RefCallback, useCallback, useRef } from 'react'; +import { FocusEvent, RefCallback, RefObject, useCallback, useRef } from 'react'; import { Timeout } from '../../utils'; @@ -106,7 +106,7 @@ const isEventFocusVisible = (event: FocusEvent): boolean => { }; export interface UseIsFocusVisibleResult { - isFocusVisibleRef: MutableRefObject; + isFocusVisibleRef: RefObject; onBlur: (event: FocusEvent) => boolean; onFocus: (event: FocusEvent) => boolean; ref: RefCallback; diff --git a/packages/react/src/hooks/useMenuAim/useMenuAim.ts b/packages/react/src/hooks/useMenuAim/useMenuAim.ts index 7827f17d9..65a03c568 100644 --- a/packages/react/src/hooks/useMenuAim/useMenuAim.ts +++ b/packages/react/src/hooks/useMenuAim/useMenuAim.ts @@ -1,6 +1,6 @@ 'use client'; -import { MutableRefObject, useRef } from 'react'; +import { RefObject, useRef } from 'react'; const MOUSE_LOCS_TRACKED = 3; const DELAY = 300; @@ -14,7 +14,7 @@ const DELAY = 300; * @returns The onMouseMove, onMouseOver and onMouseLeave handlers. */ export const useMenuAim = ( - ref: MutableRefObject, + ref: RefObject, position: string, isActive: boolean, callback: (target: HTMLElement) => void diff --git a/packages/react/src/hooks/useSticky/useSticky.ts b/packages/react/src/hooks/useSticky/useSticky.ts index 45a348404..30f36239d 100644 --- a/packages/react/src/hooks/useSticky/useSticky.ts +++ b/packages/react/src/hooks/useSticky/useSticky.ts @@ -1,6 +1,6 @@ 'use client'; -import { MutableRefObject, useEffect, useLayoutEffect } from 'react'; +import { RefObject, useEffect, useLayoutEffect } from 'react'; import { useLatest } from '../useLatest'; import { useResizeObserver } from '../useResizeObserver'; @@ -18,7 +18,7 @@ const setTransform = (element: HTMLElement, transform: string) => { * @param options.relativeTo The scrolling ancestor for the sticky element. */ export const useSticky = ( - ref: MutableRefObject, + ref: RefObject, options: { top?: number; bottom?: number; relativeTo?: HTMLElement | null } ) => { const update = () => { diff --git a/packages/react/src/transitions/transitionCallbacks.ts b/packages/react/src/transitions/useTransitionCallbacks.ts similarity index 87% rename from packages/react/src/transitions/transitionCallbacks.ts rename to packages/react/src/transitions/useTransitionCallbacks.ts index 843a53bb1..dcb51a866 100644 --- a/packages/react/src/transitions/transitionCallbacks.ts +++ b/packages/react/src/transitions/useTransitionCallbacks.ts @@ -8,7 +8,7 @@ export type ESExitCallback = (node: HTMLElement) => void; * Adapts the public transition callbacks, which receive the animated node, to the signature `react-transition-group` * uses when a `nodeRef` is provided. */ -export const createTransitionCallbacks = (nodeRef: RefObject) => ({ +export const useTransitionCallbacks = (nodeRef: RefObject) => ({ enter: (callback?: ESEnterCallback) => (isAppearing: boolean) => { if (callback && nodeRef.current) { callback(nodeRef.current, isAppearing); diff --git a/packages/react/src/utils/setRef.ts b/packages/react/src/utils/setRef.ts index d759078f7..87acac60d 100644 --- a/packages/react/src/utils/setRef.ts +++ b/packages/react/src/utils/setRef.ts @@ -1,4 +1,4 @@ -import { MutableRefObject } from 'react'; +import { RefObject } from 'react'; /** * Assigns a value to a ref, no matter whether it is a callback or an object ref. Falsy refs are a no-op. @@ -7,7 +7,7 @@ import { MutableRefObject } from 'react'; * changes. */ export const setRef = ( - ref: MutableRefObject | ((instance: T | null) => void) | null | undefined, + ref: RefObject | ((instance: T | null) => void) | null | undefined, value: T | null ): void => { if (typeof ref === 'function') {