diff --git a/.eslintrc.js b/.eslintrc.js
index 127f563e7..357020498 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -90,7 +90,7 @@ module.exports = {
'react/prop-types': 'off',
'react-hooks/exhaustive-deps': 'error', // Checks effect dependencies
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
- 'react-compiler/react-compiler': ['warn', {__unstable_donotuse_reportAllBailouts: true}], // Set to error once existing warnings are fixed
+ 'react-compiler/react-compiler': ['error', {__unstable_donotuse_reportAllBailouts: true}],
'react/no-unescaped-entities': 'off',
'no-restricted-imports': [
'error',
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 44f3a78f4..e46a99e75 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -38,7 +38,7 @@ jobs:
- run: pnpm install
- name: Register Problem Matcher for ESLint that handles -f compact and shows warnings and errors inline on PRs
run: echo "::add-matcher::.github/eslint-compact.json"
- - run: "pnpm lint -f compact --rule 'no-warning-comments: [off]' --max-warnings 33"
+ - run: "pnpm lint -f compact --rule 'no-warning-comments: [off]' --max-warnings 0"
test:
needs: [build]
diff --git a/package.config.ts b/package.config.ts
index cfc254862..cd657229e 100644
--- a/package.config.ts
+++ b/package.config.ts
@@ -15,5 +15,23 @@ export default defineConfig({
},
tsconfig: 'tsconfig.dist.json',
babel: {reactCompiler: true},
- reactCompilerOptions: {target: '18'},
+ reactCompilerOptions: {
+ target: '18',
+ logger: {
+ logEvent(filename, event) {
+ /* eslint-disable no-console */
+ if (event.kind === 'CompileError') {
+ console.group(`[${filename}] ${event.kind}`)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const {reason, description, severity, loc, suggestions} = event.detail as any
+
+ console.error(`[${severity}] ${reason}`)
+ console.log(`${filename}:${loc.start?.line}:${loc.start?.column} ${description}`)
+ console.log(suggestions)
+
+ console.groupEnd()
+ }
+ },
+ },
+ },
})
diff --git a/package.json b/package.json
index 6a827f964..ba48b5145 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@sanity/ui",
- "version": "2.8.23",
+ "version": "2.9.0-canary.6",
"keywords": [
"sanity",
"ui",
diff --git a/src/core/components/breadcrumbs/breadcrumbs.tsx b/src/core/components/breadcrumbs/breadcrumbs.tsx
index 4a184e444..44cd34f15 100644
--- a/src/core/components/breadcrumbs/breadcrumbs.tsx
+++ b/src/core/components/breadcrumbs/breadcrumbs.tsx
@@ -1,13 +1,4 @@
-import {
- Children,
- forwardRef,
- Fragment,
- isValidElement,
- useCallback,
- useMemo,
- useRef,
- useState,
-} from 'react'
+import {Children, forwardRef, Fragment, isValidElement, useCallback, useRef, useState} from 'react'
import {useArrayProp, useClickOutsideEvent} from '../../hooks'
import {Box, Popover, Stack, Text} from '../../primitives'
import {ExpandButton, Root} from './breadcrumbs.styles'
@@ -39,46 +30,43 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
useClickOutsideEvent(collapse, () => [expandElementRef.current, popoverElementRef.current])
- const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])
+ const rawItems = Children.toArray(children).filter(isValidElement)
- const items = useMemo(() => {
- const len = rawItems.length
+ let items = rawItems
+ const len = rawItems.length
- if (maxLength && len > maxLength) {
- const beforeLength = Math.ceil(maxLength / 2)
- const afterLength = Math.floor(maxLength / 2)
+ if (maxLength && rawItems.length > maxLength) {
+ const beforeLength = Math.ceil(maxLength / 2)
+ const afterLength = Math.floor(maxLength / 2)
- return [
- ...rawItems.slice(0, beforeLength - 1),
-
- {rawItems.slice(beforeLength - 1, len - afterLength)}
-
- }
- key="button"
- open={open}
- placement="top"
- portal
- ref={popoverElementRef}
- >
-
- ,
- ...rawItems.slice(len - afterLength),
- ]
- }
-
- return rawItems
- }, [collapse, expand, maxLength, open, rawItems, space])
+ items = [
+ ...rawItems.slice(0, beforeLength - 1),
+
+ {rawItems.slice(beforeLength - 1, len - afterLength)}
+
+ }
+ key="button"
+ open={open}
+ placement="top"
+ portal
+ ref={popoverElementRef}
+ >
+
+ ,
+ ...rawItems.slice(len - afterLength),
+ ]
+ }
return (
diff --git a/src/core/components/menu/useMenuController.ts b/src/core/components/menu/useMenuController.ts
index dd4abbbcb..92d446030 100644
--- a/src/core/components/menu/useMenuController.ts
+++ b/src/core/components/menu/useMenuController.ts
@@ -36,31 +36,28 @@ export function useMenuController(props: {
activeIndexRef.current = nextActiveIndex
}, [])
- const mount = useCallback(
- (element: HTMLElement | null, selected?: boolean): (() => void) => {
- if (!element) return () => undefined
+ const mount = (element: HTMLElement | null, selected?: boolean): (() => void) => {
+ if (!element) return () => undefined
- if (elementsRef.current.indexOf(element) === -1) {
- elementsRef.current.push(element)
- _sortElements(rootElementRef.current, elementsRef.current)
- }
+ if (elementsRef.current.indexOf(element) === -1) {
+ elementsRef.current.push(element)
+ _sortElements(rootElementRef.current, elementsRef.current)
+ }
- if (selected) {
- const selectedIndex = elementsRef.current.indexOf(element)
+ if (selected) {
+ const selectedIndex = elementsRef.current.indexOf(element)
- setActiveIndex(selectedIndex)
- }
+ setActiveIndex(selectedIndex)
+ }
- return () => {
- const idx = elementsRef.current.indexOf(element)
+ return () => {
+ const idx = elementsRef.current.indexOf(element)
- if (idx > -1) {
- elementsRef.current.splice(idx, 1)
- }
+ if (idx > -1) {
+ elementsRef.current.splice(idx, 1)
}
- },
- [rootElementRef, setActiveIndex],
- )
+ }
+ }
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
@@ -170,7 +167,7 @@ export function useMenuController(props: {
[setActiveIndex],
)
- const handleItemMouseLeave = useCallback(() => {
+ const handleItemMouseLeave = () => {
// Set the active index to -2 to deactivate all menu items
// when the user moves the mouse away from the menu item.
// We avoid using -1 because it would focus the first menu item,
@@ -178,7 +175,7 @@ export function useMenuController(props: {
// between two menu items or a menu divider.
setActiveIndex(-2)
rootElementRef.current?.focus()
- }, [rootElementRef, setActiveIndex])
+ }
// Set focus on the currently active element
useEffect(() => {
diff --git a/src/core/components/tree/treeItem.tsx b/src/core/components/tree/treeItem.tsx
index a80635071..571e58c09 100644
--- a/src/core/components/tree/treeItem.tsx
+++ b/src/core/components/tree/treeItem.tsx
@@ -1,6 +1,6 @@
import {ToggleArrowRightIcon} from '@sanity/icons'
import {ThemeFontWeightKey} from '@sanity/ui/theme'
-import {memo, useCallback, useEffect, useId, useMemo, useRef} from 'react'
+import {memo, useCallback, useEffect, useId, useMemo, useRef, useState} from 'react'
import {styled} from 'styled-components'
import {Box, BoxProps, Flex, Text} from '../../primitives'
import {
@@ -64,18 +64,21 @@ export const TreeItem = memo(function TreeItem(
weight,
...restProps
} = props
- const rootRef = useRef(null)
+ const [rootElement, setRootElement] = useState(null)
const treeitemRef = useRef(null)
const tree = useTree()
const {path, registerItem, setExpanded, setFocusedElement} = tree
const _id = useId()
const id = idProp || _id
- const itemPath = useMemo(() => path.concat([id || '']), [id, path])
- const itemKey = itemPath.join('/')
+ const [itemPath, itemKey] = useMemo(() => {
+ const result = path.concat([id || ''])
+
+ return [result, result.join('/')]
+ }, [id, path])
const itemState = tree.state[itemKey]
- const focused = tree.focusedElement === rootRef.current
+ const focused = tree.focusedElement === rootElement
const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
- const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
+ const tabIndex = tree.focusedElement && tree.focusedElement === rootElement ? 0 : -1
const contextValue = useMemo(
() => ({...tree, level: tree.level + 1, path: itemPath}),
[itemPath, tree],
@@ -94,28 +97,28 @@ export const TreeItem = memo(function TreeItem(
) {
event.stopPropagation()
setExpanded(itemKey, !expanded)
- setFocusedElement(rootRef.current)
+ setFocusedElement(rootElement)
}
},
- [expanded, itemKey, onClick, setExpanded, setFocusedElement],
+ [expanded, itemKey, onClick, rootElement, setExpanded, setFocusedElement],
)
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (focused && event.key === 'Enter') {
- const el = treeitemRef.current || rootRef.current
+ const el = treeitemRef.current || rootElement
el?.click()
}
},
- [focused],
+ [focused, rootElement],
)
useEffect(() => {
- if (!rootRef.current) return
+ if (!rootElement) return
- return registerItem(rootRef.current, itemPath.join('/'), expanded, selected)
- }, [expanded, itemPath, registerItem, selected])
+ return registerItem(rootElement, itemPath.join('/'), expanded, selected)
+ }, [expanded, itemPath, registerItem, rootElement, selected])
const content = (
@@ -154,7 +157,7 @@ export const TreeItem = memo(function TreeItem(
data-ui="TreeItem"
{...restProps}
onClick={handleClick}
- ref={rootRef}
+ ref={setRootElement}
role="none"
>
diff --git a/src/core/hooks/useMatchMedia.ts b/src/core/hooks/useMatchMedia.ts
index c155698ef..f13926c98 100644
--- a/src/core/hooks/useMatchMedia.ts
+++ b/src/core/hooks/useMatchMedia.ts
@@ -11,34 +11,24 @@ export function useMatchMedia(
mediaQueryString: `(${string})`,
getServerSnapshot?: () => boolean,
): boolean {
+ /**
+ * `subscribe` and `getSnapshot` are only called on the client and both need access to the same `matchMedia` instance
+ * we don't want to eagerly instantiate it to ensure it's only created when actually used
+ */
+ const cachedMatchMedia = useMemo(
+ () => (typeof window === 'undefined' ? null : window.matchMedia(mediaQueryString)),
+ [mediaQueryString],
+ )
const {subscribe, getSnapshot} = useMemo(() => {
- /**
- * `subscribe` and `getSnapshot` are only called on the client and both need access to the same `matchMedia` instance
- * we don't want to eagerly instantiate it to ensure it's only created when actually used
- */
- let MEDIA_QUERY_CACHE: MediaQueryList | undefined
-
- const getMatchMedia = (): MediaQueryList => {
- if (!MEDIA_QUERY_CACHE) {
- // As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
- // the `window` global is available and we're in a browser environment
- MEDIA_QUERY_CACHE = window.matchMedia(mediaQueryString)
- }
-
- return MEDIA_QUERY_CACHE
- }
-
return {
subscribe: (onStoreChange: () => void): (() => void) => {
- const matchMedia = getMatchMedia()
-
- matchMedia.addEventListener('change', onStoreChange)
+ cachedMatchMedia!.addEventListener('change', onStoreChange)
- return () => matchMedia.removeEventListener('change', onStoreChange)
+ return () => cachedMatchMedia!.removeEventListener('change', onStoreChange)
},
- getSnapshot: () => getMatchMedia().matches,
+ getSnapshot: () => cachedMatchMedia!.matches,
}
- }, [mediaQueryString])
+ }, [cachedMatchMedia])
useDebugValue(mediaQueryString)
diff --git a/src/core/primitives/popover/popover.tsx b/src/core/primitives/popover/popover.tsx
index 64bbbc65d..4484caaf6 100644
--- a/src/core/primitives/popover/popover.tsx
+++ b/src/core/primitives/popover/popover.tsx
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
- Middleware,
RootBoundary,
arrow,
autoUpdate,
@@ -21,8 +20,10 @@ import {
useCallback,
useEffect,
useImperativeHandle,
+ useLayoutEffect,
useMemo,
useRef,
+ useState,
} from 'react'
import {useArrayProp, useElementSize, useMediaIndex, usePrefersReducedMotion} from '../../hooks'
import {origin} from '../../middleware/origin'
@@ -126,15 +127,14 @@ export const Popover = memo(
__unstable_margins: margins = DEFAULT_POPOVER_MARGINS,
animate: _animate = false,
arrow: arrowProp = false,
- boundaryElement = boundaryElementContext.element,
+ boundaryElement: _boundaryElement,
children: childProp,
constrainSize = false,
content,
disabled,
- fallbackPlacements = props.fallbackPlacements ??
- DEFAULT_FALLBACK_PLACEMENTS[props.placement ?? 'bottom'],
+ fallbackPlacements: _fallbackPlacements,
matchReferenceWidth,
- floatingBoundary = props.boundaryElement ?? boundaryElementContext.element,
+ floatingBoundary: _floatingBoundary,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onActivate,
open,
@@ -144,16 +144,23 @@ export const Popover = memo(
portal,
preventOverflow = true,
radius: radiusProp = 3,
- referenceBoundary = props.boundaryElement ?? boundaryElementContext.element,
+ referenceBoundary: _referenceBoundary,
referenceElement,
scheme,
shadow: shadowProp = 3,
tone = 'inherit',
width: widthProp = 'auto',
- zOffset: zOffsetProp = layer.popover.zOffset,
+ zOffset: _zOffsetProp,
updateRef,
...restProps
} = props
+ const boundaryElement = _boundaryElement ?? boundaryElementContext.element
+ const fallbackPlacements =
+ _fallbackPlacements ?? DEFAULT_FALLBACK_PLACEMENTS[props.placement ?? 'bottom']
+ const floatingBoundary = _floatingBoundary ?? boundaryElement ?? boundaryElementContext.element
+ const referenceBoundary =
+ _referenceBoundary ?? boundaryElement ?? boundaryElementContext.element
+ const zOffsetProp = _zOffsetProp ?? layer.popover.zOffset
const prefersReducedMotion = usePrefersReducedMotion()
const animate = prefersReducedMotion ? false : _animate
const boundarySize = useElementSize(boundaryElement)?.border
@@ -163,7 +170,7 @@ export const Popover = memo(
const widthArrayProp = useArrayProp(widthProp)
const zOffset = useArrayProp(zOffsetProp)
const ref = useRef(null)
- const arrowRef = useRef(null)
+ const [arrowElement, setArrowElement] = useState(null)
const rootBoundary: RootBoundary = 'viewport'
useImperativeHandle(
@@ -174,94 +181,70 @@ export const Popover = memo(
const mediaIndex = useMediaIndex()
const boundaryWidth = constrainSize || preventOverflow ? boundarySize?.width : undefined
- // Update width when
- // - media index changes
- // - `width` property changes
- const width = calcCurrentWidth({
- container,
- mediaIndex,
- width: widthArrayProp,
- })
- const widthRef = useRef(width)
-
- useEffect(() => {
- widthRef.current = width
- }, [width])
-
- // Update max width when
- // - boundary width changes
- // - `width` property changes
- const maxWidth = calcMaxWidth({boundaryWidth, currentWidth: width})
- const maxWidthRef = useRef(maxWidth)
-
- useEffect(() => {
- maxWidthRef.current = maxWidth
- }, [maxWidth])
-
- // Keep track of reference element width (see `size` middleware below)
- const referenceWidthRef = useRef()
-
// Force apply width & max width to floating element
- useEffect(() => {
+ useLayoutEffect(() => {
const floatingElement = ref.current
- if (!open || !floatingElement) return
+ // If constrainSize or matchReferenceWidth is true, then the styles are set by the `size` middleware
+ if (!open || !floatingElement || constrainSize || matchReferenceWidth) return
- const referenceWidth = referenceWidthRef.current
+ const currentWidth = calcCurrentWidth({
+ container,
+ mediaIndex,
+ width: widthArrayProp,
+ })
+ const maxWidth = calcMaxWidth({boundaryWidth, currentWidth})
- if (matchReferenceWidth) {
- if (referenceWidth !== undefined) {
- floatingElement.style.width = `${referenceWidth}px`
- }
- } else if (width !== undefined) {
- floatingElement.style.width = `${width}px`
+ if (currentWidth !== undefined) {
+ floatingElement.style.width = `${currentWidth}px`
}
if (typeof maxWidth === 'number') {
floatingElement.style.maxWidth = `${maxWidth}px`
}
- }, [width, matchReferenceWidth, maxWidth, open])
-
- const middleware = useMemo(() => {
- const ret: Middleware[] = []
+ }, [
+ boundaryWidth,
+ constrainSize,
+ container,
+ matchReferenceWidth,
+ mediaIndex,
+ open,
+ widthArrayProp,
+ ])
- // Flip the floating element when leaving the boundary box
- if (constrainSize || preventOverflow) {
- ret.push(
+ const {x, y, middlewareData, placement, refs, strategy, update} = useFloating({
+ middleware: [
+ // Flip the floating element when leaving the boundary box
+ (constrainSize || preventOverflow) &&
flip({
boundary: floatingBoundary || undefined,
fallbackPlacements,
padding: DEFAULT_POPOVER_PADDING,
rootBoundary,
}),
- )
- }
-
- // Define distance between reference and floating element
- ret.push(offset({mainAxis: DEFAULT_POPOVER_DISTANCE}))
-
- // Track sizes
- if (constrainSize || matchReferenceWidth) {
- ret.push(
+ // Define distance between reference and floating element
+ offset({mainAxis: DEFAULT_POPOVER_DISTANCE}),
+ // Track sizes
+ (constrainSize || matchReferenceWidth) &&
size({
apply({availableWidth, availableHeight, elements, referenceWidth}) {
- // not fresh, so use refs
-
- referenceWidthRef.current = referenceWidth
-
- const _currentWidth = widthRef.current
- const _maxWidth = maxWidthRef.current
+ const currentWidth = calcCurrentWidth({
+ container,
+ mediaIndex,
+ width: widthArrayProp,
+ })
+ const maxWidth = calcMaxWidth({boundaryWidth, currentWidth})
if (matchReferenceWidth) {
elements.floating.style.width = `${referenceWidth}px`
- } else if (_currentWidth !== undefined) {
- elements.floating.style.width = `${_currentWidth}px`
+ } else if (currentWidth !== undefined) {
+ elements.floating.style.width = `${currentWidth}px`
}
if (constrainSize) {
elements.floating.style.maxWidth = `${Math.min(
availableWidth,
- _maxWidth ?? Infinity,
+ maxWidth ?? Infinity,
)}px`
elements.floating.style.maxHeight = `${availableHeight}px`
@@ -273,59 +256,28 @@ export const Popover = memo(
matchReferenceWidth,
padding: DEFAULT_POPOVER_PADDING,
}),
- )
- }
-
- // Shift the popover so its sits within the boundary element
- if (preventOverflow) {
- ret.push(
+ // Shift the popover so its sits within the boundary element
+ preventOverflow &&
shift({
boundary: floatingBoundary || undefined,
rootBoundary,
padding: DEFAULT_POPOVER_PADDING,
}),
- )
- }
-
- // Place arrow
- if (arrowProp) {
- ret.push(
+ // Place arrow
+ arrowProp &&
arrow({
- element: arrowRef,
+ element: arrowElement,
padding: DEFAULT_POPOVER_PADDING,
}),
- )
- }
-
- // Determine the origin to scale from.
- // Must be placed after `@sanity/ui/size` and `shift` middleware.
- if (animate) {
- ret.push(origin)
- }
-
- ret.push(
+ // Determine the origin to scale from.
+ // Must be placed after `@sanity/ui/size` and `shift` middleware.
+ animate && origin,
hide({
boundary: referenceBoundary || undefined,
padding: DEFAULT_POPOVER_PADDING,
strategy: 'referenceHidden',
}),
- )
-
- return ret
- }, [
- animate,
- arrowProp,
- constrainSize,
- fallbackPlacements,
- floatingBoundary,
- margins,
- matchReferenceWidth,
- preventOverflow,
- referenceBoundary,
- ])
-
- const {x, y, middlewareData, placement, refs, strategy, update} = useFloating({
- middleware,
+ ],
placement: placementProp,
whileElementsMounted: autoUpdate,
})
@@ -338,10 +290,6 @@ export const Popover = memo(
const originX = middlewareData['@sanity/ui/origin']?.originX
const originY = middlewareData['@sanity/ui/origin']?.originY
- const setArrow = useCallback((arrowEl: HTMLDivElement | null) => {
- arrowRef.current = arrowEl
- }, [])
-
const setFloating = useCallback(
(node: HTMLDivElement | null) => {
ref.current = node
@@ -350,36 +298,24 @@ export const Popover = memo(
[refs],
)
+ const [childElement, setChildElement] = useState(null)
const setReference = useCallback(
(node: HTMLElement | null) => {
refs.setReference(node)
-
- const childRef = getElementRef(childProp as any)
-
- if (typeof childRef === 'function') {
- childRef(node)
- } else if (childRef) {
- childRef.current = node
- }
+ setChildElement(node)
},
- [childProp, refs],
+ [refs],
)
+ useImperativeHandle(getElementRef(childProp as any), () => childElement, [childElement])
+
const child = useMemo(() => {
if (!childProp || referenceElement) return null
return cloneElement(childProp, {ref: setReference})
}, [childProp, referenceElement, setReference])
- useEffect(() => {
- if (updateRef) {
- if (typeof updateRef === 'function') {
- updateRef(update)
- } else if (updateRef) {
- updateRef.current = update
- }
- }
- }, [update, updateRef])
+ useImperativeHandle(updateRef, () => update, [update])
useEffect(() => {
if (child) return
@@ -397,7 +333,7 @@ export const Popover = memo(
__unstable_margins={margins}
animate={animate}
arrow={arrowProp}
- arrowRef={setArrow}
+ arrowRef={setArrowElement}
arrowX={arrowX}
arrowY={arrowY}
hidden={referenceHidden}
@@ -412,7 +348,6 @@ export const Popover = memo(
originY={originY}
strategy={strategy}
tone={tone}
- width={matchReferenceWidth ? referenceWidthRef.current : width}
x={x}
y={y}
>
@@ -447,7 +382,8 @@ Popover.displayName = 'Memo(ForwardRef(Popover))'
// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning.
-function getElementRef(element: React.ReactElement) {
+function getElementRef(element?: React.ReactElement) {
+ if (!element) return null
// React <=18 in DEV
let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get
let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning
diff --git a/src/core/primitives/popover/popoverCard.tsx b/src/core/primitives/popover/popoverCard.tsx
index fb1290028..6a6dcfa2e 100644
--- a/src/core/primitives/popover/popoverCard.tsx
+++ b/src/core/primitives/popover/popoverCard.tsx
@@ -51,7 +51,6 @@ export const PopoverCard = memo(
shadow?: number | number[]
strategy: Strategy
tone: CardTone
- width: number | undefined
x: number | null
y: number | null
} & Omit, 'as' | 'height' | 'width'>,
@@ -76,7 +75,6 @@ export const PopoverCard = memo(
strategy,
style,
tone,
- width,
x: xProp,
y: yProp,
...restProps
@@ -101,12 +99,11 @@ export const PopoverCard = memo(
originY,
position: strategy,
top: y,
- width,
zIndex,
willChange: animate ? 'transform' : undefined,
...style,
}),
- [animate, originX, originY, strategy, style, width, x, y, zIndex],
+ [animate, originX, originY, strategy, style, x, y, zIndex],
)
const arrowStyle: CSSProperties = useMemo(
diff --git a/src/core/primitives/tooltip/tooltip.tsx b/src/core/primitives/tooltip/tooltip.tsx
index 7f3a916f0..1c3b82208 100644
--- a/src/core/primitives/tooltip/tooltip.tsx
+++ b/src/core/primitives/tooltip/tooltip.tsx
@@ -97,28 +97,31 @@ export const Tooltip = forwardRef(function Tooltip(
const {
animate: _animate = false,
arrow: arrowProp = false,
- boundaryElement = boundaryElementContext?.element,
+ boundaryElement: _boundaryElement,
children: childProp,
content,
disabled,
- fallbackPlacements: fallbackPlacementsProp = props.fallbackPlacements ??
- DEFAULT_FALLBACK_PLACEMENTS[props.placement ?? 'bottom'],
+ fallbackPlacements: _fallbackPlacementsProp,
padding = 2,
placement: placementProp = 'bottom',
portal: portalProp,
radius = 2,
scheme,
shadow = 2,
- zOffset = layer.tooltip.zOffset,
+ zOffset: _zOffset,
delay,
...restProps
} = props
+ const boundaryElement = _boundaryElement ?? boundaryElementContext?.element
+ const fallbackPlacementsProp =
+ _fallbackPlacementsProp ?? DEFAULT_FALLBACK_PLACEMENTS[props.placement ?? 'bottom']
+ const zOffset = _zOffset ?? layer.tooltip.zOffset
const prefersReducedMotion = usePrefersReducedMotion()
const animate = prefersReducedMotion ? false : _animate
const fallbackPlacements = useArrayProp(fallbackPlacementsProp)
const ref = useRef(null)
const [referenceElement, setReferenceElement] = useState(null)
- const arrowRef = useRef(null)
+ const [arrowElement, setArrowElement] = useState(null)
const rootBoundary: RootBoundary = 'viewport'
const [tooltipMaxWidth, setTooltipMaxWidth] = useState(0)
@@ -155,7 +158,7 @@ export const Tooltip = forwardRef(function Tooltip(
// Place arrow
if (arrowProp) {
- ret.push(arrow({element: arrowRef, padding: DEFAULT_TOOLTIP_PADDING}))
+ ret.push(arrow({element: arrowElement, padding: DEFAULT_TOOLTIP_PADDING}))
}
// Determine the origin to scale from.
@@ -165,7 +168,7 @@ export const Tooltip = forwardRef(function Tooltip(
}
return ret
- }, [animate, arrowProp, boundaryElement, fallbackPlacements])
+ }, [animate, arrowElement, arrowProp, boundaryElement, fallbackPlacements])
const {floatingStyles, placement, middlewareData, refs, update} = useFloating({
middleware,
@@ -223,42 +226,42 @@ export const Tooltip = forwardRef(function Tooltip(
handleIsOpenChange(false)
childProp?.props?.onBlur?.(e)
},
- [childProp?.props, handleIsOpenChange],
+ [childProp, handleIsOpenChange],
)
const handleClick = useCallback(
(e: MouseEvent) => {
handleIsOpenChange(false, true)
childProp?.props.onClick?.(e)
},
- [childProp?.props, handleIsOpenChange],
+ [childProp, handleIsOpenChange],
)
const handleContextMenu = useCallback(
(e: MouseEvent) => {
handleIsOpenChange(false, true)
childProp?.props.onContextMenu?.(e)
},
- [childProp?.props, handleIsOpenChange],
+ [childProp, handleIsOpenChange],
)
const handleFocus = useCallback(
(e: FocusEvent) => {
handleIsOpenChange(true)
childProp?.props?.onFocus?.(e)
},
- [childProp?.props, handleIsOpenChange],
+ [childProp, handleIsOpenChange],
)
const handleMouseEnter = useCallback(
(e: MouseEvent) => {
handleIsOpenChange(true)
childProp?.props?.onMouseEnter?.(e)
},
- [childProp?.props, handleIsOpenChange],
+ [childProp, handleIsOpenChange],
)
const handleMouseLeave = useCallback(
(e: MouseEvent) => {
handleIsOpenChange(false)
childProp?.props?.onMouseLeave?.(e)
},
- [childProp?.props, handleIsOpenChange],
+ [childProp, handleIsOpenChange],
)
// Handle closing the tooltip when the mouse leaves the referenceElement
@@ -309,7 +312,7 @@ export const Tooltip = forwardRef(function Tooltip(
const setArrow = useCallback(
(arrowEl: HTMLDivElement | null) => {
- arrowRef.current = arrowEl
+ setArrowElement(arrowEl)
update()
},
[update],
@@ -324,9 +327,9 @@ export const Tooltip = forwardRef(function Tooltip(
)
const childRef = useRef(null)
+ const [childElement, setChildElement] = useState(null)
- // Merge refs so that any ref we are overriding is called as well
- useImperativeHandle((childProp as any)?.ref, () => childRef.current)
+ useImperativeHandle(getElementRef(childProp as any), () => childElement, [childElement])
const child = useMemo(() => {
if (!childProp) return null
@@ -338,7 +341,7 @@ export const Tooltip = forwardRef(function Tooltip(
onMouseLeave: handleMouseLeave,
onClick: handleClick,
onContextMenu: handleContextMenu,
- ref: childRef,
+ ref: setChildElement,
})
}, [
childProp,
@@ -462,3 +465,30 @@ function useCloseOnMouseLeave({
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [onMouseMove, showTooltip])
}
+
+// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`
+// After React 19 accessing `element.ref` does the opposite.
+// https://github.com/facebook/react/pull/28348
+//
+// Access the ref using the method that doesn't yield a warning.
+function getElementRef(element?: React.ReactElement) {
+ if (!element) return null
+ // React <=18 in DEV
+ let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get
+ let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning
+
+ if (mayWarn) {
+ return (element as any).ref
+ }
+
+ // React 19 in DEV
+ getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get
+ mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning
+
+ if (mayWarn) {
+ return element.props.ref
+ }
+
+ // Not DEV
+ return element.props.ref || (element as any).ref
+}
diff --git a/src/core/utils/portal/portalProvider.tsx b/src/core/utils/portal/portalProvider.tsx
index 41918f7d2..3da51f198 100644
--- a/src/core/utils/portal/portalProvider.tsx
+++ b/src/core/utils/portal/portalProvider.tsx
@@ -1,4 +1,4 @@
-import {useMemo, useRef, useSyncExternalStore} from 'react'
+import {useMemo, useState, useSyncExternalStore} from 'react'
import {PortalContext} from './portalContext'
import {PortalContextValue} from './types'
@@ -51,13 +51,13 @@ const emptySubscribe = () => () => {}
* equality comparison (eg by identity), and only goes one level deep.
*/
function useUnique(value: ValueType): ValueType {
- const valueRef = useRef(value)
+ const [cachedValue, setCachedValue] = useState(value)
- if (!_isEqual(valueRef.current, value)) {
- valueRef.current = value
+ if (!_isEqual(cachedValue, value)) {
+ setCachedValue(value)
}
- return valueRef.current
+ return cachedValue
}
function _isEqual(objA: Comparable, objB: Comparable): boolean {