diff --git a/.gitignore b/.gitignore index e94214f4846..c84316b4463 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -# Created by .ignore support plugin (hsz.mobi) node_modules *.iml yarn-error.log diff --git a/.storybook/components/DocsHeader.tsx b/.storybook/components/DocsHeader.tsx index 8600485a689..b865514aabe 100644 --- a/.storybook/components/DocsHeader.tsx +++ b/.storybook/components/DocsHeader.tsx @@ -95,8 +95,7 @@ export const InfoTable = ({ { setOpen(false); diff --git a/packages/charts/src/components/LineChart/index.tsx b/packages/charts/src/components/LineChart/index.tsx index eb58d1c647f..459a21e6867 100644 --- a/packages/charts/src/components/LineChart/index.tsx +++ b/packages/charts/src/components/LineChart/index.tsx @@ -1,7 +1,7 @@ 'use client'; import { enrichEventWithDetails, ThemingParameters, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base'; -import { forwardRef, useCallback, useRef } from 'react'; +import { forwardRef, useRef } from 'react'; import type { LineProps, YAxisProps } from 'recharts'; import { Brush, @@ -182,32 +182,29 @@ const LineChart = forwardRef((props, ref) => { const onItemLegendClick = useLegendItemClick(onLegendClick); const preventOnClickCall = useRef(0); - const onDataPointClickInternal = useCallback( - (payload, eventOrIndex) => { - if (eventOrIndex.dataKey && typeof onDataPointClick === 'function') { - preventOnClickCall.current = 2; - onDataPointClick( - enrichEventWithDetails({} as any, { - value: eventOrIndex.value, - dataKey: eventOrIndex.dataKey, - dataIndex: eventOrIndex.index, - payload: eventOrIndex.payload, - }), - ); - } else if (typeof onClick === 'function' && preventOnClickCall.current === 0) { - onClick( - enrichEventWithDetails(eventOrIndex, { - payload: payload?.activePayload?.[0]?.payload, - activePayloads: payload?.activePayload, - }), - ); - } - if (preventOnClickCall.current > 0) { - preventOnClickCall.current -= 1; - } - }, - [onDataPointClick, preventOnClickCall.current], - ); + const onDataPointClickInternal = (payload, eventOrIndex) => { + if (eventOrIndex.dataKey && typeof onDataPointClick === 'function') { + preventOnClickCall.current = 2; + onDataPointClick( + enrichEventWithDetails({} as any, { + value: eventOrIndex.value, + dataKey: eventOrIndex.dataKey, + dataIndex: eventOrIndex.index, + payload: eventOrIndex.payload, + }), + ); + } else if (typeof onClick === 'function' && preventOnClickCall.current === 0) { + onClick( + enrichEventWithDetails(eventOrIndex, { + payload: payload?.activePayload?.[0]?.payload, + activePayloads: payload?.activePayload, + }), + ); + } + if (preventOnClickCall.current > 0) { + preventOnClickCall.current -= 1; + } + }; const isBigDataSet = dataset?.length > 30; const primaryDimensionAccessor = primaryDimension?.accessor; diff --git a/packages/charts/src/components/PieChart/index.tsx b/packages/charts/src/components/PieChart/index.tsx index b7e3f130942..94e043b4e35 100644 --- a/packages/charts/src/components/PieChart/index.tsx +++ b/packages/charts/src/components/PieChart/index.tsx @@ -159,10 +159,9 @@ const PieChart = forwardRef((props, ref) => { ); }; - const tooltipValueFormatter = useCallback( - (value, name) => [measure.formatter(value), dimension.formatter(name)], - [measure.formatter, dimension.formatter], - ); + const tooltipValueFormatter = (value, name) => { + return [measure.formatter(value), dimension.formatter(name)]; + }; const onItemLegendClick = useLegendItemClick(onLegendClick, () => measure.accessor); const onClickInternal = useOnClickInternal(onClick); @@ -248,7 +247,7 @@ const PieChart = forwardRef((props, ref) => { ); }, - [showActiveSegmentDataLabel, chartConfig.activeSegment, isDonutChart], + [showActiveSegmentDataLabel, chartConfig.activeSegment, isDonutChart, chartRef, measure], ); const renderLabelLine = useCallback( @@ -258,7 +257,7 @@ const PieChart = forwardRef((props, ref) => { if (hideDataLabel || chartConfig.activeSegment === props.index) return null; return Pie.renderLabelLineItem({}, props, undefined); }, - [chartConfig.activeSegment, measure.hideDataLabel], + [chartConfig.activeSegment, measure], ); const legendWrapperStyle = useMemo(() => { diff --git a/packages/charts/src/components/TimelineChart/TimelineChartHeaders.tsx b/packages/charts/src/components/TimelineChart/TimelineChartHeaders.tsx index 4f4cdb79744..2eec8437a94 100644 --- a/packages/charts/src/components/TimelineChart/TimelineChartHeaders.tsx +++ b/packages/charts/src/components/TimelineChart/TimelineChartHeaders.tsx @@ -75,6 +75,7 @@ const TimelineChartColumnLabel = ({ const newLabelArray = columnLabels ? columnLabels : Array.from(Array(totalDuration).keys()).map((num) => `${num + start}`); + // eslint-disable-next-line react-hooks/set-state-in-effect setLabelArray(newLabelArray); } }, [isDiscrete, columnLabels, start, totalDuration]); @@ -95,7 +96,7 @@ const TimelineChartColumnLabel = ({ height: `${halfHeaderHeight}px`, lineHeight: `${halfHeaderHeight}px`, }} - > + /> {isDiscrete ? (
{ + evt.preventDefault(); + if (evt.deltaY < 0) { + // Only scale up if scaled width will not exceed MAX_BODY_WIDTH + const msrWidth = bodyRef.current.getBoundingClientRect().width; + if (msrWidth * SCALE_FACTOR < MAX_BODY_WIDTH) { + scaleExpRef.current++; + } + } else { + // Only scale down if scaled width will not be less than original + // width + if (scaleExpRef.current > 0) { + resetScroll(); + scaleExpRef.current--; + } + } + onScale(Math.pow(SCALE_FACTOR, scaleExpRef.current)); + }; + useEffect(() => { const bodyElement = bodyRef.current; bodyElement?.addEventListener('wheel', onMouseWheelEvent); @@ -81,25 +100,6 @@ const TimelineChartBody = ({ }; const hideTooltip = () => tooltipRef.current?.onLeaveItem(); - const onMouseWheelEvent = (evt: WheelEvent) => { - evt.preventDefault(); - if (evt.deltaY < 0) { - // Only scale up if scaled width will not exceed MAX_BODY_WIDTH - const msrWidth = bodyRef.current.getBoundingClientRect().width; - if (msrWidth * SCALE_FACTOR < MAX_BODY_WIDTH) { - scaleExpRef.current++; - } - } else { - // Only scale down if scaled width will not be less than original - // width - if (scaleExpRef.current > 0) { - resetScroll(); - scaleExpRef.current--; - } - } - onScale(Math.pow(SCALE_FACTOR, scaleExpRef.current)); - }; - const showArrows = () => setDisplayArrows(true); return ( @@ -121,6 +121,8 @@ const TimelineChartBody = ({ dataSet={dataset} width={width} rowHeight={rowHeight} + // todo: check side-effect + // eslint-disable-next-line react-hooks/refs bodyRect={bodyRef.current?.getBoundingClientRect()} /> diff --git a/packages/charts/src/components/TimelineChart/chartbody/TimelineChartRow.tsx b/packages/charts/src/components/TimelineChart/chartbody/TimelineChartRow.tsx index 48912dacbac..696e41e52b9 100644 --- a/packages/charts/src/components/TimelineChart/chartbody/TimelineChartRow.tsx +++ b/packages/charts/src/components/TimelineChart/chartbody/TimelineChartRow.tsx @@ -27,6 +27,8 @@ const TimelineChartRow = ({ showTooltip, hideTooltip, }: TimelineChartRowProps) => { + // todo: fix mutation + // eslint-disable-next-line react-hooks/immutability rowData.color = rowData.color ?? `var(--sapChart_OrderedColor_${(rowIndex % 12) + 1})`; return ( diff --git a/packages/charts/src/internal/ChartContainer.tsx b/packages/charts/src/internal/ChartContainer.tsx index 4dd187bdb19..1e43f99ce9f 100644 --- a/packages/charts/src/internal/ChartContainer.tsx +++ b/packages/charts/src/internal/ChartContainer.tsx @@ -1,5 +1,5 @@ -import { BusyIndicator, Label } from '@ui5/webcomponents-react'; import type { CommonProps } from '@ui5/webcomponents-react'; +import { BusyIndicator, Label } from '@ui5/webcomponents-react'; import { useStylesheet } from '@ui5/webcomponents-react-base'; import { addCustomCSSWithScoping } from '@ui5/webcomponents-react-base/internal/utils'; import { clsx } from 'clsx'; @@ -8,7 +8,6 @@ import { Component, forwardRef } from 'react'; import { ResponsiveContainer } from 'recharts'; import { classNames, styleData } from './ChartContainer.module.css.js'; -//todo: add feature request for parts or even a fix if this turns out to be a bug addCustomCSSWithScoping( 'ui5-busy-indicator', ` diff --git a/packages/cli/src/scripts/codemod/transforms/v2/main.cts b/packages/cli/src/scripts/codemod/transforms/v2/main.cts index af9dc9639f5..8e24d0f0ef8 100644 --- a/packages/cli/src/scripts/codemod/transforms/v2/main.cts +++ b/packages/cli/src/scripts/codemod/transforms/v2/main.cts @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access,import/order */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ import type { API, ASTPath, Collection, FileInfo, JSCodeshift, JSXElement } from 'jscodeshift'; - const config = require('./codemodConfig.json'); interface ComponentTransformConfig { diff --git a/packages/compat/package.json b/packages/compat/package.json index df0e855c739..b9a781e5d55 100644 --- a/packages/compat/package.json +++ b/packages/compat/package.json @@ -93,7 +93,7 @@ "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "@ui5/webcomponents-compat": "~2.18.0", + "@ui5/webcomponents-compat": "~2.18.0 || ~2.19.0", "@ui5/webcomponents-react": "~2.18.0", "react": "^18 || ^19", "react-dom": "^18 || ^19" diff --git a/packages/compat/src/components/Toolbar/OverflowPopover.tsx b/packages/compat/src/components/Toolbar/OverflowPopover.tsx index 33e503afdb2..8f2f3c72191 100644 --- a/packages/compat/src/components/Toolbar/OverflowPopover.tsx +++ b/packages/compat/src/components/Toolbar/OverflowPopover.tsx @@ -164,6 +164,7 @@ export const OverflowPopover: FC = (props: OverflowPopover return ( {overflowButton ? ( + // eslint-disable-next-line react-hooks/refs cloneElement(overflowButton, { onClick: clonedOverflowButtonClick }) ) : ( ((props, ref) => { }, [children]); const childrenWithRef = useMemo(() => { + // eslint-disable-next-line react-hooks/refs controlMetaData.current = []; let hasOnlySpacersOrSeparators = true; + // eslint-disable-next-line react-hooks/refs const enrichedChildren = flatChildren.map((item, index) => { const itemRef: RefObject = createRef(); // @ts-expect-error: if type is not defined, it's not a spacer diff --git a/packages/cypress-commands/package.json b/packages/cypress-commands/package.json index 53b1489d52d..fa15b13e8d7 100644 --- a/packages/cypress-commands/package.json +++ b/packages/cypress-commands/package.json @@ -23,8 +23,8 @@ "clean": "rimraf dist api-commands.json api-queries.json" }, "peerDependencies": { - "@ui5/webcomponents": "~2.18.0", - "@ui5/webcomponents-base": "~2.18.0", + "@ui5/webcomponents": "~2.18.0 || ~2.19.0", + "@ui5/webcomponents-base": "~2.18.0 || ~2.19.0", "cypress": "^12 || ^13 || ^14 || ^15" }, "peerDependenciesMeta": { diff --git a/packages/main/package.json b/packages/main/package.json index 840b6f9bedc..119c5512c9b 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -809,10 +809,10 @@ "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "@ui5/webcomponents": "~2.18.0", - "@ui5/webcomponents-base": "~2.18.0", - "@ui5/webcomponents-fiori": "~2.18.0", - "@ui5/webcomponents-icons": "~2.18.0", + "@ui5/webcomponents": "~2.18.0 || ~2.19.0", + "@ui5/webcomponents-base": "~2.18.0 || ~2.19.0", + "@ui5/webcomponents-fiori": "~2.18.0 || ~2.19.0", + "@ui5/webcomponents-icons": "~2.18.0 || ~2.19.0", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx index de57e4b4446..587f171af94 100644 --- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx +++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx @@ -395,10 +395,9 @@ describe('AnalyticalTable', () => { cy.findByRole('grid').should('have.attr', 'data-per-page', '15'); cy.findByText('Name-14').should('be.visible'); cy.findByText('Name-15').should('not.be.visible'); - cy.findByTitle('Drag to resize') - .trigger('mousedown') - .trigger('mousemove', { pageY: 200, force: true }) - .trigger('mouseup', { pageY: 200 }); + cy.findByTitle('Drag to resize').realMouseDown(); + cy.findByTitle('Drag to resize').realMouseMove(0, -540, { scrollBehavior: false }); + cy.get('body').realMouseUp({ position: { x: 100, y: 200 } }); cy.findByRole('grid').should('have.attr', 'data-per-page', '3'); cy.findByText('Name-2').should('be.visible'); cy.findByText('Name-3').should('not.be.visible'); @@ -3852,6 +3851,7 @@ describe('AnalyticalTable', () => { // transform data to the pattern which is accepted by the tree table // NOTES: this algorithm is less likely related to the bug, because in our reality project there is a different algorithm to generate the tree table and the bug still occurs. const data = useMemo(() => { + // eslint-disable-next-line react-hooks/refs raw.forEach((item) => { const newItem = { ...item }; rowById.current[newItem.nodeId] = { @@ -3875,7 +3875,7 @@ describe('AnalyticalTable', () => { rowById.current[newItem.parentId].subRows.push(rowById.current[newItem.nodeId]); } }); - + // eslint-disable-next-line react-hooks/refs return Object.values(rowById.current).filter((row) => !row.parentId); }, [raw]); diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx index 6fa2e23beb7..40cefa4437f 100644 --- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx +++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx @@ -254,7 +254,8 @@ export const InfiniteScrolling: Story = { const [data, setData] = useState(args.data.slice(0, 50)); const [loading, setLoading] = useState(false); const offset = useRef(50); - const onLoadMore = () => { + const onLoadMore = (e) => { + args.onLoadMore(e); setLoading(true); }; useEffect(() => { diff --git a/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx b/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx index 13e84a807a5..cd2cb639a05 100644 --- a/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx +++ b/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx @@ -176,6 +176,7 @@ export const ColumnHeader = (props: ColumnHeaderProps) => { if (!column) return null; return (
{ {hasPopover && popoverOpen && // render the popover and add the props to the table instance + // todo: remove openerRef in v3.0.0 + // eslint-disable-next-line react-hooks/refs column.render(RenderColumnTypes.Popover, { popoverProps: { id: `${id}-popover`, openerRef: columnHeaderRef, + openerId: `${id}-opener`, setOpen: setPopoverOpen, }, })} diff --git a/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx b/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx index 105a9476edc..3e382b0ff22 100644 --- a/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx +++ b/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx @@ -6,7 +6,6 @@ import type { AnalyticalTablePropTypes, ClassNames, DivWithCustomScrollProp, - ReactVirtualScrollToMethods, TableInstance, TriggerScrollState, } from '../types/index.js'; @@ -35,7 +34,6 @@ interface VirtualTableBodyProps { subRowsKey: string; scrollContainerRef?: MutableRefObject; triggerScroll?: TriggerScrollState; - scrollToRef: MutableRefObject; rowVirtualizer: Virtualizer; } @@ -54,7 +52,6 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => { classes, prepareRow, rows, - scrollToRef, isTreeTable, internalRowHeight, visibleColumns, @@ -76,12 +73,6 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => { const rowHeight = popInRowHeight !== internalRowHeight ? popInRowHeight : internalRowHeight; const lastNonEmptyRow = useRef(null); - scrollToRef.current = { - ...scrollToRef.current, - scrollToOffset: rowVirtualizer.scrollToOffset, - scrollToIndex: rowVirtualizer.scrollToIndex, - }; - useEffect(() => { if (triggerScroll && triggerScroll.direction === 'vertical') { if (triggerScroll.type === 'offset') { @@ -112,6 +103,8 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => { width: `${columnVirtualizer.getTotalSize()}px`, }} > + {/* Safe to update: lastNonEmptyRow ref holds non-render data only.*/} + {/* eslint-disable-next-line react-hooks/refs */} {rowVirtualizer.getVirtualItems().map((virtualRow, visibleRowIndex) => { const row = rows[virtualRow.index]; const rowIndexWithHeader = virtualRow.index + 1; diff --git a/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx b/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx index 0600da74c93..6b94d9928e8 100644 --- a/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx +++ b/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx @@ -48,9 +48,11 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps) useEffect(() => { if (parentRef.current) { + // Trigger one-time re-render after first render -> safe to set state here + // eslint-disable-next-line react-hooks/set-state-in-effect setIsMounted(true); } - }, [parentRef.current]); + }, [parentRef]); const dataLength = rows.length; @@ -102,16 +104,16 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps) } }, [ + handleExternalScroll, infiniteScroll, infiniteScrollThreshold, - onLoadMore, - rows, internalRowHeight, - firedInfiniteLoadEvents, - lastScrollTop, - handleExternalScroll, + isGrouped, + onLoadMore, popInRowHeight, + rows, tableBodyHeight, + visibleRows, ], ); diff --git a/packages/main/src/components/AnalyticalTable/VerticalResizer.tsx b/packages/main/src/components/AnalyticalTable/VerticalResizer.tsx index 7351759ef69..c5d97f48ead 100644 --- a/packages/main/src/components/AnalyticalTable/VerticalResizer.tsx +++ b/packages/main/src/components/AnalyticalTable/VerticalResizer.tsx @@ -9,7 +9,6 @@ interface VerticalResizerProps { analyticalTableRef: MutableRefObject; dispatch: (e: { type: string; payload?: any }) => void; extensionsHeight: number; - internalRowHeight: number; hasPopInColumns: boolean; popInRowHeight: number; rowsLength: number; @@ -18,6 +17,12 @@ interface VerticalResizerProps { classNames: ClassNames; } +interface VerticalResizerPosition { + left: number; + top: number; + width: number; +} + const isTouchEvent = (e, touchEvent) => { if (e.type === touchEvent) { return !(e.touches && e.touches.length > 1); @@ -30,7 +35,6 @@ export const VerticalResizer = (props: VerticalResizerProps) => { analyticalTableRef, dispatch, extensionsHeight, - internalRowHeight, hasPopInColumns, popInRowHeight, rowsLength, @@ -41,38 +45,37 @@ export const VerticalResizer = (props: VerticalResizerProps) => { const startY = useRef(null); const verticalResizerRef = useRef(null); - const [resizerPosition, setResizerPosition] = useState(undefined); + const [resizerPosition, setResizerPosition] = useState(undefined); const [isDragging, setIsDragging] = useState(false); const [mountTouchEvents, setMountTouchEvents] = useState(false); const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); - const handleResizeStart = useCallback( - (e) => { - e.preventDefault(); - const touchEvent = isTouchEvent(e, 'touchstart'); - startY.current = touchEvent ? Math.round(e.touches[0].pageY) : e.pageY; - setMountTouchEvents(touchEvent); - setIsDragging(true); - }, - [startY.current, setIsDragging], - ); + const handleResizeStart = useCallback((e) => { + e.preventDefault(); + const touchEvent = isTouchEvent(e, 'touchstart'); + startY.current = touchEvent ? Math.round(e.touches[0].pageY) : e.pageY; + setMountTouchEvents(touchEvent); + setIsDragging(true); + }, []); const handleMove = useCallback( - (e) => { + (e: TouchEvent | MouseEvent) => { setResizerPosition((prev) => ({ ...prev, - top: isTouchEvent(e, 'touchmove') ? Math.round(e.touches[0].pageY) : e.pageY, + top: isTouchEvent(e, 'touchmove') ? Math.round((e as TouchEvent).touches[0].pageY) : (e as MouseEvent).pageY, })); }, [setResizerPosition], ); const handleResizeEnd = useCallback( - (e) => { + (e: TouchEvent | MouseEvent) => { setIsDragging(false); const rowCount = Math.floor( (analyticalTableRef.current.clientHeight + - (isTouchEvent(e, 'touchend') ? Math.round(e.changedTouches[0].pageY) : e.pageY) - + (isTouchEvent(e, 'touchend') + ? Math.round((e as TouchEvent).changedTouches[0].pageY) + : (e as MouseEvent).pageY) - startY.current - extensionsHeight - 5) /*resizer height*/ / @@ -86,8 +89,9 @@ export const VerticalResizer = (props: VerticalResizerProps) => { payload: { visibleRows: rowCount }, }); }, - [analyticalTableRef.current?.clientHeight, startY.current, extensionsHeight, internalRowHeight, dispatch], + [analyticalTableRef, dispatch, extensionsHeight, hasPopInColumns, popInRowHeight], ); + useEffect(() => { const removeEventListeners = () => { if (mountTouchEvents) { @@ -112,22 +116,18 @@ export const VerticalResizer = (props: VerticalResizerProps) => { return () => { removeEventListeners(); }; - }, [isDragging]); + }, [handleMove, handleResizeEnd, isDragging, mountTouchEvents]); useEffect(() => { const resizerPosTop = verticalResizerRef.current?.getBoundingClientRect()?.top + window.scrollY; const resizerPosLeft = verticalResizerRef.current?.getBoundingClientRect()?.left + window.scrollX; const resizerPosWidth = verticalResizerRef.current?.getBoundingClientRect()?.width; if (!isDragging && resizerPosTop > 0) { - setResizerPosition({ left: resizerPosLeft, top: resizerPosTop, width: resizerPosWidth }); + requestAnimationFrame(() => { + setResizerPosition({ left: resizerPosLeft, top: resizerPosTop, width: resizerPosWidth }); + }); } - }, [verticalResizerRef.current?.getBoundingClientRect()?.top, isDragging]); - - useEffect(() => { - return () => { - dispatch({ type: 'WITH_POPIN', payload: false }); - }; - }, []); + }, [isDragging]); const isInitial = useRef(true); useEffect(() => { @@ -135,15 +135,7 @@ export const VerticalResizer = (props: VerticalResizerProps) => { handleOnLoadMore({ type: 'tableGrow' } as Event); } isInitial.current = false; - }, [rowsLength, visibleRows]); - - const [allowed, setAllowed] = useState(false); - useEffect(() => { - setAllowed(true); - }, []); - if (!allowed) { - return null; - } + }, [handleOnLoadMore, rowsLength, visibleRows]); return (
{ - const { setOpen, openerRef, id } = instance.popoverProps; + const { id, setOpen, openerRef, openerId } = instance.popoverProps; const { column, state, webComponentsReactProperties } = instance; const { isRtl, groupBy } = state; const { onGroup, onSort, classes: classNames } = webComponentsReactProperties; @@ -179,6 +179,10 @@ export const ColumnHeaderModal = (instance: TableInstanceWithPopoverProps) => { return ( { @@ -26,15 +26,7 @@ const getPadding = (level) => { } }; -interface ExpandableProps { - cell: Record; - row: RowType; - column: ColumnType; - visibleColumns: ColumnType[]; - webComponentsReactProperties: WCRPropertiesType; -} - -export const Expandable = (props: ExpandableProps) => { +export const Expandable = (props: CellInstance) => { const { cell, row, column, visibleColumns: columns, webComponentsReactProperties } = props; const { renderRowSubComponent, @@ -60,7 +52,6 @@ export const Expandable = (props: ExpandableProps) => { return ( <> {columnIndex === 0 && ( - // todo rowProps should be applied to the whole row, not just the cell. We should consider refactoring this. <> {row.canExpand || subComponentExpandable ? ( { onClick={rowProps.onClick} mode={IconMode.Interactive} name={row.isExpanded ? iconNavDownArrow : iconNavRightArrow} - aria-expanded={`${row.isExpanded}`} + aria-expanded={`${row.isExpanded}` as 'true' | 'false'} data-component-name="AnalyticalTableExpandIcon" className={classNames.expandableIcon} accessibleName={ diff --git a/packages/main/src/components/AnalyticalTable/defaults/Column/Grouped.tsx b/packages/main/src/components/AnalyticalTable/defaults/Column/Grouped.tsx index 215ce2aa6d1..a1a52f57743 100644 --- a/packages/main/src/components/AnalyticalTable/defaults/Column/Grouped.tsx +++ b/packages/main/src/components/AnalyticalTable/defaults/Column/Grouped.tsx @@ -4,9 +4,10 @@ import { clsx } from 'clsx'; import type { CSSProperties } from 'react'; import { TextAlign } from '../../../../enums/TextAlign.js'; import { Icon } from '../../../../webComponents/Icon/index.js'; +import type { CellInstance } from '../../types/index.js'; import { RenderColumnTypes } from '../../types/index.js'; -export const Grouped = (props) => { +export const Grouped = (props: CellInstance) => { const { cell, row, webComponentsReactProperties } = props; const { translatableTexts, classes } = webComponentsReactProperties; diff --git a/packages/main/src/components/AnalyticalTable/defaults/Column/PopIn.tsx b/packages/main/src/components/AnalyticalTable/defaults/Column/PopIn.tsx index 7cb96b5cfe2..f2de3e6b33f 100644 --- a/packages/main/src/components/AnalyticalTable/defaults/Column/PopIn.tsx +++ b/packages/main/src/components/AnalyticalTable/defaults/Column/PopIn.tsx @@ -5,10 +5,10 @@ import { FlexBoxDirection } from '../../../../enums/FlexBoxDirection.js'; import { FlexBoxWrap } from '../../../../enums/FlexBoxWrap.js'; import { Text } from '../../../../webComponents/Text/index.js'; import { FlexBox } from '../../../FlexBox/index.js'; -import type { TableInstance } from '../../types/index.js'; +import type { CellInstance } from '../../types/index.js'; import { RenderColumnTypes } from '../../types/index.js'; -export const PopIn = (instance: TableInstance) => { +export const PopIn = (instance: CellInstance) => { const { state, contentToRender, diff --git a/packages/main/src/components/AnalyticalTable/defaults/FilterComponent/index.tsx b/packages/main/src/components/AnalyticalTable/defaults/FilterComponent/index.tsx index 4949221fd08..52c54aecc40 100644 --- a/packages/main/src/components/AnalyticalTable/defaults/FilterComponent/index.tsx +++ b/packages/main/src/components/AnalyticalTable/defaults/FilterComponent/index.tsx @@ -5,12 +5,13 @@ import { Input } from '../../../../webComponents/Input/index.js'; import type { FilterProps } from '../../types/index.js'; export const DefaultFilterComponent = ({ column, accessibleNameRef }: FilterProps) => { + const { setFilter } = column; const handleInput: InputPropTypes['onInput'] = useCallback( (e) => { // Setting the filter to `undefined` removes it - column.setFilter(e.target.value || undefined); + setFilter(e.target.value || undefined); }, - [column.setFilter], + [setFilter], ); const handleKeyDown: InputPropTypes['onKeyDown'] = (e) => { diff --git a/packages/main/src/components/AnalyticalTable/hooks/useCanUseVoiceOver.ts b/packages/main/src/components/AnalyticalTable/hooks/useCanUseVoiceOver.ts index ea94b3f5be2..8c2a6a5a1c5 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useCanUseVoiceOver.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useCanUseVoiceOver.ts @@ -8,6 +8,8 @@ export function useCanUseVoiceOver() { const [canUseVoiceOver, setCanUseVoiceOver] = useState(false); useEffect(() => { + // Needs another rendering cycle to prevent hydration errors + // eslint-disable-next-line react-hooks/set-state-in-effect setCanUseVoiceOver(isIOS() || isMac()); }, []); diff --git a/packages/main/src/components/AnalyticalTable/hooks/useIsFirefox.ts b/packages/main/src/components/AnalyticalTable/hooks/useIsFirefox.ts index 05f1f5fdd41..d95f3e1e628 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useIsFirefox.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useIsFirefox.ts @@ -9,6 +9,8 @@ export function useIsFirefox() { const [isFirefox, setIsFirefox] = useState(false); useEffect(() => { + // safe here because we only update state after mount for SSR hydration + // eslint-disable-next-line react-hooks/set-state-in-effect setIsFirefox(isFirefoxFn()); }, []); diff --git a/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts b/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts index 84622d34f74..236b5a15f9b 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts @@ -68,6 +68,7 @@ const useGetTableProps = ( { instance: { webComponentsReactProperties, data, columns, state } }: { instance: TableInstance }, ) => { const { showOverlay, tableRef } = webComponentsReactProperties; + const { isRtl } = state; const currentlyFocusedCell = useRef(null); const noData = data.length === 0; @@ -97,7 +98,7 @@ const useGetTableProps = ( currentlyFocusedCell.current = null; tableRef.current.tabIndex = 0; } - }, [data, columns, showOverlay]); + }, [data, columns, showOverlay, tableRef]); const onTableFocus = useCallback( (e) => { @@ -105,7 +106,6 @@ const useGetTableProps = ( if ( dataset.emptyRowCell === 'true' || Object.prototype.hasOwnProperty.call(dataset, 'subcomponentActiveElement') || - // todo: with the new popover API of ui5wc this might not be necessary anymore dataset.componentName === 'ATHeaderPopoverList' || dataset.componentName === 'ATHeaderPopover' || dataset.componentName === 'AnalyticalTableNoDataContainer' @@ -153,12 +153,11 @@ const useGetTableProps = ( } } }, - [currentlyFocusedCell.current, tableRef.current, noData], + [noData, tableRef], ); const onKeyboardNavigation = useCallback( (e) => { - const { isRtl } = state; const isActiveItemInSubComponent = Object.prototype.hasOwnProperty.call( e.target.dataset, 'subcomponentActiveElement', @@ -284,6 +283,8 @@ const useGetTableProps = ( ); if (hasSubcomponent && !currentlyFocusedCell.current?.dataset?.subcomponent) { currentlyFocusedCell.current.tabIndex = -1; + // Happens outside of React's scope + // eslint-disable-next-line react-hooks/immutability firstChildOfParent.tabIndex = 0; firstChildOfParent.dataset.rowIndexSub = `${rowIndex}`; firstChildOfParent.dataset.columnIndexSub = `${columnIndex}`; @@ -325,7 +326,7 @@ const useGetTableProps = ( } } }, - [currentlyFocusedCell.current, tableRef.current, state?.isRtl], + [isRtl, tableRef], ); if (showOverlay) { return tableProps; diff --git a/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts b/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts index 2da0d705b75..a2fa0759cb6 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts @@ -23,6 +23,8 @@ export function useSyncScroll( return; } + // Is a React ref + // eslint-disable-next-line react-hooks/immutability scrollbar.scrollTop = content.scrollTop; const sync = (source: 'content' | 'scrollbar') => { diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx index 6eb9275bcc0..0f3b5806b60 100644 --- a/packages/main/src/components/AnalyticalTable/index.tsx +++ b/packages/main/src/components/AnalyticalTable/index.tsx @@ -12,7 +12,7 @@ import { useSyncRef, } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; -import type { CSSProperties, MutableRefObject } from 'react'; +import type { CSSProperties } from 'react'; import { forwardRef, useCallback, useEffect, useId, useMemo, useRef } from 'react'; import { useColumnOrder, @@ -389,13 +389,6 @@ const AnalyticalTable = forwardRef { if (triggerScroll && triggerScroll.direction === 'horizontal') { if (triggerScroll.type === 'offset') { @@ -414,7 +407,7 @@ const AnalyticalTable = forwardRef>).current = tableInstanceRef.current; + (tableInstance as { current: TableInstance }).current = tableInstanceRef.current; } if (typeof tableInstance === 'function') { tableInstance(tableInstanceRef.current); @@ -504,15 +497,7 @@ const AnalyticalTable = forwardRef { setGlobalFilter(globalFilterValue); @@ -534,7 +519,7 @@ const AnalyticalTable = forwardRef { dispatch({ type: 'IS_RTL', payload: { isRtl } }); @@ -555,7 +540,7 @@ const AnalyticalTable = forwardRef { if (groupBy) { @@ -567,19 +552,13 @@ const AnalyticalTable = forwardRef { - if (tableState?.interactiveRowsHavePopIn && (!tableState?.popInColumns || tableState?.popInColumns?.length === 0)) { - dispatch({ type: 'WITH_POPIN', payload: false }); - } - }, [tableState?.interactiveRowsHavePopIn, tableState?.popInColumns?.length]); + }, [dispatch, selectedRowIds]); const tableBodyHeight = useMemo(() => { if (typeof tableState.bodyHeight === 'number') { return tableState.bodyHeight; } - let rowNum; + let rowNum: number; const noDataAuto = !rows.length && visibleRowCountMode.startsWith('Auto'); if (visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows || noDataAuto) { rowNum = internalVisibleRowCount; @@ -748,6 +727,16 @@ const AnalyticalTable = forwardRef
{ - instance.dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 }); - }, [instance.dispatch]); + dispatch({ type: 'CELL_CONTENT_TAB_INDEX', payload: -1 }); + }, [dispatch]); } diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx b/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx index da9cd2e6cd5..f4e5a64df2d 100644 --- a/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx +++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx @@ -7,10 +7,10 @@ import type { ReactTableHooks, RowType, TableInstance } from '../types/index.js' type onIndeterminateChange = (e: { indeterminateRowsById: Record; - tableInstance: Record; + tableInstance: TableInstance; }) => void; -const getParentRow = (id, rowsById) => { +const getParentRow = (id: string, rowsById: TableInstance['rowsById']): [RowType, number] => { let lastDotIndex = id.lastIndexOf('.'); if (lastDotIndex === -1) { lastDotIndex = Infinity; diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts b/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts index 36e4edb43cf..377a29f6adb 100644 --- a/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts +++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts @@ -23,7 +23,7 @@ export const useManualRowSelect = (manualRowSelectedKey = 'isSelected') => { toggleRowSelected(id, original.isSelected); } }); - }, [flatRows, manualRowSelectedKey]); + }, [flatRows, toggleRowSelected]); }; const manualRowSelect = (hooks: ReactTableHooks) => { diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useOnColumnResize.ts b/packages/main/src/components/AnalyticalTable/pluginHooks/useOnColumnResize.ts index 546020183aa..989755b7171 100644 --- a/packages/main/src/components/AnalyticalTable/pluginHooks/useOnColumnResize.ts +++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useOnColumnResize.ts @@ -50,7 +50,7 @@ export const useOnColumnResize = (callback: useOnColumnResizeFunc, options?: use if (options?.liveUpdate) { return () => debouncedEvent.cancel(); } - }, [options?.liveUpdate]); + }, []); useEffect(() => { if (!options?.liveUpdate) { diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx b/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx index 12cf1061c8d..38752544c48 100644 --- a/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx +++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx @@ -5,7 +5,7 @@ import { CheckBox } from '../../../webComponents/CheckBox/index.js'; import type { ReactTableHooks, RowType, TableInstance } from '../types/index.js'; import { getBy } from '../util/index.js'; -type DisableRowSelectionType = string | ((row: Record) => boolean); +type DisableRowSelectionType = string | ((row: RowType) => boolean); const customCheckBoxStyling = { verticalAlign: 'middle', diff --git a/packages/main/src/components/AnalyticalTable/scrollbars/VerticalScrollbar.tsx b/packages/main/src/components/AnalyticalTable/scrollbars/VerticalScrollbar.tsx index c62a0a52bf2..ca0920b5f00 100644 --- a/packages/main/src/components/AnalyticalTable/scrollbars/VerticalScrollbar.tsx +++ b/packages/main/src/components/AnalyticalTable/scrollbars/VerticalScrollbar.tsx @@ -1,8 +1,9 @@ import { isChrome as isChromeFn } from '@ui5/webcomponents-react-base/Device'; import { useSyncRef } from '@ui5/webcomponents-react-base/internal/hooks'; +import { debounce } from '@ui5/webcomponents-react-base/internal/utils/debounce'; import { clsx } from 'clsx'; import type { MutableRefObject } from 'react'; -import { forwardRef, useEffect, useRef } from 'react'; +import { forwardRef, useEffect, useRef, useState } from 'react'; import { FlexBoxDirection } from '../../../enums/FlexBoxDirection.js'; import { FlexBox } from '../../FlexBox/index.js'; import type { ClassNames } from '../types/index.js'; @@ -19,11 +20,31 @@ const isChrome = isChromeFn(); export const VerticalScrollbar = forwardRef((props, ref) => { const { internalRowHeight, tableRef, tableBodyHeight, scrollContainerRef, classNames } = props; - const hasHorizontalScrollbar = tableRef?.current?.offsetWidth !== tableRef?.current?.scrollWidth; - const horizontalScrollbarSectionStyles = clsx(hasHorizontalScrollbar && classNames.bottomSection); + const [hasHorizontalScrollbar, setHasHorizontalScrollbar] = useState(false); const [componentRef, scrollbarRef] = useSyncRef(ref); const contentRef = useRef(null); + useEffect(() => { + const tableElement = tableRef?.current; + if (!tableElement) return; + + const debouncedCheckScrollbar = debounce(() => { + requestAnimationFrame(() => { + const hasScrollbar = tableElement.offsetWidth !== tableElement.scrollWidth; + setHasHorizontalScrollbar(hasScrollbar); + }); + }, 100); + + const resizeObserver = new ResizeObserver(debouncedCheckScrollbar); + resizeObserver.observe(tableElement); + debouncedCheckScrollbar(); + + return () => { + debouncedCheckScrollbar.cancel(); + resizeObserver.disconnect(); + }; + }, [tableRef]); + // Force style recalculation to fix Chrome scrollbar-color bug (track height not updating correctly) useEffect(() => { if (!isChrome) { @@ -43,7 +64,7 @@ export const VerticalScrollbar = forwardRef
-
+
); }); diff --git a/packages/main/src/components/AnalyticalTable/types/index.ts b/packages/main/src/components/AnalyticalTable/types/index.ts index 96b416779cc..39b03294319 100644 --- a/packages/main/src/components/AnalyticalTable/types/index.ts +++ b/packages/main/src/components/AnalyticalTable/types/index.ts @@ -319,13 +319,6 @@ export interface TriggerScrollState { args: [number, Omit?]; } -export interface ReactVirtualScrollToMethods { - scrollToOffset?: (offset: number, options?: ScrollToOptions) => void; - scrollToIndex?: (index: number, options?: ScrollToOptions) => void; - horizontalScrollToOffset?: (offset: number, options?: ScrollToOptions) => void; - horizontalScrollToIndex?: (index: number, options?: ScrollToOptions) => void; -} - interface PopInColumnsState { id: string; column: ColumnType; @@ -394,11 +387,17 @@ interface PopoverProps { setOpen: Dispatch>; /** * React Ref that holds the reference to the respective table header element. + * + * @deprecated Use `openerId` instead. */ openerRef: MutableRefObject; + /** + * ID of the respective table header element that opens the popover. + */ + openerId: string; } -export interface TableInstanceWithPopoverProps extends TableInstance { +export interface TableInstanceWithPopoverProps extends CellInstance { popoverProps: PopoverProps; } diff --git a/packages/main/src/components/FilterBar/FilterDialog.tsx b/packages/main/src/components/FilterBar/FilterDialog.tsx index 028a32627d3..614d91cec9d 100644 --- a/packages/main/src/components/FilterBar/FilterDialog.tsx +++ b/packages/main/src/components/FilterBar/FilterDialog.tsx @@ -6,7 +6,7 @@ import listIcon from '@ui5/webcomponents-icons/dist/list.js'; import searchIcon from '@ui5/webcomponents-icons/dist/search.js'; import { enrichEventWithDetails, useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base'; import { addCustomCSSWithScoping } from '@ui5/webcomponents-react-base/internal/utils'; -import { Children, cloneElement, useEffect, useId, useReducer, useRef, useState } from 'react'; +import { Children, cloneElement, useCallback, useEffect, useId, useReducer, useRef, useState } from 'react'; import type { ReactElement, RefObject } from 'react'; import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js'; import { FlexBoxJustifyContent } from '../../enums/FlexBoxJustifyContent.js'; @@ -200,16 +200,19 @@ export const FilterDialog = (props: FilterDialogPropTypes) => { } }; - const visibleChildren = () => - children.filter((item) => { - return !!item?.props && !item?.props?.hidden; - }); + const visibleChildren = useCallback( + () => + children.filter((item) => { + return !!item?.props && !item?.props?.hidden; + }), + [children], + ); useEffect(() => { if (children.length) { setOrderedChildren(visibleChildren()); } - }, [children]); + }, [children, visibleChildren]); const renderChildren = () => { const searchStringLower = searchString.toLowerCase(); @@ -408,6 +411,8 @@ export const FilterDialog = (props: FilterDialogPropTypes) => { setForceRequired(undefined); } + // `forceRequired` triggers async DOM update; no extra deps needed + // eslint-disable-next-line react-hooks/exhaustive-deps }, [forceRequired]); const renderGroups = () => { @@ -450,11 +455,9 @@ export const FilterDialog = (props: FilterDialogPropTypes) => { }); }; - useEffect(() => { - if (initialSelected.current === undefined && selected.length) { - initialSelected.current = selectedFilters; - } - }, [selected]); + if (initialSelected.current === undefined && selected.length) { + initialSelected.current = selectedFilters; + } return ( ((props, ref) => useEffect(() => { if (filterBarCollapsed !== undefined) { + // syncing controlled prop to state + // eslint-disable-next-line react-hooks/set-state-in-effect setShowFilters(!hideToolbar ? !filterBarCollapsed : true); } - }, [setShowFilters, hideToolbar, filterBarCollapsed]); + }, [hideToolbar, filterBarCollapsed]); useStylesheet(styleData, FilterBar.displayName); @@ -207,6 +209,8 @@ const FilterBar = forwardRef((props, ref) => } }; + const calculatedChildren = renderChildren(); + const handleFBRestore = () => { handleRestoreFilters({ source: 'filterBar', @@ -288,7 +292,7 @@ const FilterBar = forwardRef((props, ref) => useEffect(() => { const debouncedObserverFn = debounce(([area]: ResizeObserverEntry[]) => { const firstChild = area.target?.children?.[0] as HTMLDivElement; - if (firstChild && firstChild.offsetWidth !== firstChildWidth) { + if (firstChild) { setFirstChildWidth(firstChild.offsetWidth + 16 /*margin*/); } }, 100); @@ -300,14 +304,12 @@ const FilterBar = forwardRef((props, ref) => debouncedObserverFn.cancel(); filterAreaObserver.disconnect(); }; - }, [filterAreaRef.current, hideToolbar]); + }, [hideToolbar]); useEffect(() => { const debouncedObserverFn = debounce(([area]: ResizeObserverEntry[]) => { const filterWidth = resizeObserverEntryWidth(area); - if (filterWidth !== filterBarButtonsWidth) { - setFilterAreaWidth(filterWidth); - } + setFilterAreaWidth(filterWidth); }, 100); const filterAreaObserver = new ResizeObserver(debouncedObserverFn); if (hideToolbar && filterAreaRef.current) { @@ -317,14 +319,12 @@ const FilterBar = forwardRef((props, ref) => debouncedObserverFn.cancel(); filterAreaObserver.disconnect(); }; - }, [filterAreaWidth, filterAreaRef.current, hideToolbar]); + }, [hideToolbar]); useEffect(() => { const debouncedObserverFn = debounce(([buttons]: ResizeObserverEntry[]) => { const buttonsWidth = resizeObserverEntryWidth(buttons); - if (buttonsWidth !== filterBarButtonsWidth) { - setFilterBarButtonsWidth(buttonsWidth); - } + setFilterBarButtonsWidth(buttonsWidth); }, 100); const filterBarButtonsObserver = new ResizeObserver(debouncedObserverFn); if (hideToolbar && filterBarButtonsRef.current) { @@ -334,9 +334,7 @@ const FilterBar = forwardRef((props, ref) => debouncedObserverFn.cancel(); filterBarButtonsObserver.disconnect(); }; - }, [filterBarButtonsRef.current, hideToolbar, filterBarButtonsWidth]); - - const calculatedChildren = renderChildren(); + }, [hideToolbar]); // calculates the number of spacers depending on the available width inside the row const renderSpacers = () => { diff --git a/packages/main/src/components/FilterGroupItem/index.tsx b/packages/main/src/components/FilterGroupItem/index.tsx index ea9229f5fd7..f175d443a49 100644 --- a/packages/main/src/components/FilterGroupItem/index.tsx +++ b/packages/main/src/components/FilterGroupItem/index.tsx @@ -10,7 +10,7 @@ import moveDownIcon from '@ui5/webcomponents-icons/dist/navigation-down-arrow.js import moveUpIcon from '@ui5/webcomponents-icons/dist/navigation-up-arrow.js'; import { useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; -import { forwardRef, useContext, useEffect, useRef, useState } from 'react'; +import { forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { FlexBoxAlignItems } from '../../enums/FlexBoxAlignItems.js'; import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js'; import { FlexBoxJustifyContent } from '../../enums/FlexBoxJustifyContent.js'; @@ -89,11 +89,25 @@ const FilterGroupItem = forwardRef(undefined); + const initialPosition = useMemo(() => { + if (index === 0) { + return 'first'; + } + if (index === filtersCount - 1) { + return 'last'; + } + return undefined; + }, [index, filtersCount]); + + const [itemPosition, setItemPosition] = useState(initialPosition); + + useEffect(() => { + setItemPosition(initialPosition); + }, [initialPosition]); const handleFocus = (e) => { setShowBtnsOnHover(false); @@ -106,15 +120,6 @@ const FilterGroupItem = forwardRef { - if (index === 0) { - setItemPosition('first'); - } - if (index === filtersCount - 1) { - setItemPosition('last'); - } - }, [index]); - const handleReorder = (e: Parameters[0]) => { setItemPosition(undefined); onReorder({ @@ -173,7 +178,7 @@ const FilterGroupItem = forwardRef((props, ref) => { const { titleText, subtitleText, counter, type = ValueState.Negative, children, className, ...rest } = props; const [isTitleTextOverflowing, setIsTitleTextIsOverflowing] = useState(false); - const [titleTextStr, setTitleTextStr] = useState(''); const titleTextRef = useRef(null); const hasDetails = !!(children || isTitleTextOverflowing); const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); + const titleTextStr = (() => { + if (typeof titleText === 'string') { + return titleText; + } else if (isValidElement(titleText) && typeof titleText.props.children === 'string') { + return titleText.props.children; + } + return ''; + })(); useStylesheet(styleData, MessageItem.displayName); @@ -125,14 +132,6 @@ const MessageItem = forwardRef((prop }; }, [hasChildren]); - useEffect(() => { - if (typeof titleText === 'string') { - setTitleTextStr(titleText); - } else if (isValidElement(titleText) && typeof titleText.props.children === 'string') { - setTitleTextStr(titleText.props.children); - } - }, [titleText]); - return ( ((prop
- + {titleText && ( {titleText} diff --git a/packages/main/src/components/MessageView/index.tsx b/packages/main/src/components/MessageView/index.tsx index 1673500dc3f..bb09fc5838f 100644 --- a/packages/main/src/components/MessageView/index.tsx +++ b/packages/main/src/components/MessageView/index.tsx @@ -158,7 +158,7 @@ const MessageView = forwardRef((props, if (internalRef.current) { internalRef.current.navigateBack = navigateBack; } - }, [internalRef.current, navigateBack]); + }, [internalRef, navigateBack]); const handleListFilterChange: SegmentedButtonPropTypes['onSelectionChange'] = (e) => { setListFilter(e.detail.selectedItems.at(0).dataset.key as never); diff --git a/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx b/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx index ecedcfa6746..563584e69f2 100644 --- a/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx +++ b/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx @@ -39,6 +39,8 @@ export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => { }, [image, imageShapeCircle]); useEffect(() => { + // Trigger fade-in animation after mount + // eslint-disable-next-line react-hooks/set-state-in-effect setIsMounted(true); }, []); diff --git a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx index 7e01ac6b534..337c892819c 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx @@ -378,8 +378,8 @@ describe('ObjectPage', () => { cy.findByText('Test').should('be.visible'); cy.get('[ui5-tabcontainer]').findUi5TabByText('Employment').realClick(); - cy.get('[data-section-id="test"]').shouldNeverHaveAttribute('selected', 300); - cy.get('[data-section-id="personal"]').shouldNeverHaveAttribute('selected', 300); + cy.get('[data-section-id="test"]').shouldNeverHaveAttribute('selected', { observerTime: 300 }); + cy.get('[data-section-id="personal"]').shouldNeverHaveAttribute('selected', { observerTime: 300 }); cy.get(`[ui5-tab][data-index="3"]`).should('have.attr', 'selected'); cy.findByText('Employment').should('be.visible'); @@ -424,27 +424,35 @@ describe('ObjectPage', () => { cy.findByText('Test').should('be.visible'); cy.findByTestId('footer').should('be.visible'); - // Select Employment tab cy.get('[ui5-tabcontainer]').findUi5TabByText('Goals').focus(); - cy.get('[ui5-tabcontainer]').realPress('ArrowRight'); - cy.get('[ui5-tabcontainer]').realPress('ArrowRight'); - cy.get('[ui5-tabcontainer]').realPress('ArrowRight'); - cy.get('[ui5-tabcontainer]').realPress('Enter'); + cy.wait(50); + cy.realPress('ArrowRight'); + cy.focused().should('contain.text', 'Test'); + cy.wait(50); + cy.realPress('ArrowRight'); + cy.focused().should('contain.text', 'Personal'); + cy.wait(50); + cy.realPress('ArrowRight'); + cy.focused().should('contain.text', 'Employment'); + cy.wait(50); + cy.realPress('Enter'); - cy.wait(200); - //fallback click - cy.get('[ui5-tabcontainer]').findUi5TabByText('Employment').realClick(); - cy.get('[data-section-id="test"]').shouldNeverHaveAttribute('selected', 500); - cy.get('[data-section-id="personal"]').shouldNeverHaveAttribute('selected', 500); + cy.get('[data-section-id="test"]').shouldNeverHaveAttribute('selected', { observerTime: 500 }); + cy.get('[data-section-id="personal"]').shouldNeverHaveAttribute('selected', { + observerTime: 500, + delayed: 500, + }); cy.findByTestId('footer').should('be.visible'); + // smooth scrolling + observer check delay + cy.wait(1000); cy.findByText('Employment').should('be.visible'); cy.findByText('Job Information').should('be.visible'); cy.wait(200); cy.get('[ui5-tabcontainer]').findUi5TabByText('Goals').click(); - cy.get('[data-section-id="test"]').shouldNeverHaveAttribute('selected', 300); - cy.get('[data-section-id="personal"]').shouldNeverHaveAttribute('selected', 300); + cy.get('[data-section-id="test"]').shouldNeverHaveAttribute('selected', { observerTime: 300 }); + cy.get('[data-section-id="personal"]').shouldNeverHaveAttribute('selected', { observerTime: 300 }); cy.findByText('Test').should('be.visible'); cy.findByTestId('footer').should('be.visible'); diff --git a/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx b/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx index bf8e26d8e79..fc046a78df9 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx @@ -3,7 +3,6 @@ import SampleImage from '@sb/demoImages/Person.png'; import type { Meta, StoryObj } from '@storybook/react-vite'; import BarDesign from '@ui5/webcomponents/dist/types/BarDesign.js'; import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; -import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js'; import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js'; import declineIcon from '@ui5/webcomponents-icons/dist/decline.js'; import sunIcon from '@ui5/webcomponents-icons/dist/general-leave-request.js'; @@ -34,7 +33,6 @@ import { ObjectPageMode, ObjectPageSection, ObjectPageSubSection, - ObjectStatus, Text, Title, Toolbar, diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index bfd9ae0c18e..16d6b94ffca 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -131,11 +131,23 @@ const ObjectPage = forwardRef((props, ref setCurrentTabModeSection(currentSection); }, [mode, children, internalSelectedSectionId]); - const fireOnSelectedChangedEvent = (targetEvent, index, id, section) => { - if (typeof onSelectedSectionChange === 'function' && targetEvent && prevInternalSelectedSectionId.current !== id) { - onSelectedSectionChange( + const onSelectedSectionChangeRef = useRef(onSelectedSectionChange); + const onToggleHeaderAreaRef = useRef(onToggleHeaderArea); + const onScrollRef = useRef(rest.onScroll); + // Keep refs in sync with props to avoid stale closure + onSelectedSectionChangeRef.current = onSelectedSectionChange; + onToggleHeaderAreaRef.current = onToggleHeaderArea; + onScrollRef.current = rest.onScroll; + + const fireOnSelectedChangedEvent = (targetEvent, index: number | string, id: string, section) => { + if ( + typeof onSelectedSectionChangeRef.current === 'function' && + targetEvent && + prevInternalSelectedSectionId.current !== id + ) { + onSelectedSectionChangeRef.current( enrichEventWithDetails(targetEvent, { - selectedSectionIndex: parseInt(index, 10), + selectedSectionIndex: typeof index === 'number' ? index : parseInt(index, 10), selectedSectionId: id, section, }), @@ -147,8 +159,14 @@ const ObjectPage = forwardRef((props, ref useEffect(() => { return () => { debouncedOnSectionChange.cancel(); - clearTimeout(selectionScrollTimeout.current); + if (selectionScrollTimeout.current) { + // Access .current at cleanup time to clear the actual timeout, not the stale value from mount + // eslint-disable-next-line react-hooks/exhaustive-deps + clearTimeout(selectionScrollTimeout.current); + } }; + // debouncedOnSectionChange and selectionScrollTimeout are stable refs + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // observe heights of header parts @@ -165,9 +183,33 @@ const ObjectPage = forwardRef((props, ref ); const scrollPaddingBlock = `${Math.ceil(12 + topHeaderHeight + tabContainerHeaderHeight + (!headerCollapsed && headerPinned ? headerContentHeight : 0))}px ${footerArea ? 'calc(var(--_ui5wcr-BarHeight) + 1.25rem)' : 0}`; + const onToggleHeaderContentVisibility = useCallback( + (e) => { + isToggledRef.current = true; + scrollTimeout.current = performance.now() + 500; + setToggledCollapsedHeaderWasVisible(false); + if (!e.detail.visible) { + if (objectPageRef.current.scrollTop <= headerContentHeight) { + setToggledCollapsedHeaderWasVisible(true); + if (firstSectionId === internalSelectedSectionId || mode === ObjectPageMode.IconTabBar) { + objectPageRef.current.scrollTop = 0; + } + } + setHeaderCollapsedInternal(true); + setScrolledHeaderExpanded(false); + } else { + setHeaderCollapsedInternal(false); + if (objectPageRef.current.scrollTop >= headerContentHeight && objectPageRef.current.scrollTop > 0) { + setScrolledHeaderExpanded(true); + } + } + }, + [headerContentHeight, firstSectionId, internalSelectedSectionId, mode, objectPageRef], + ); + useEffect(() => { - if (typeof onToggleHeaderArea === 'function' && isToggledRef.current) { - onToggleHeaderArea(headerCollapsed !== true); + if (typeof onToggleHeaderAreaRef.current === 'function' && isToggledRef.current) { + onToggleHeaderAreaRef.current(headerCollapsed !== true); } }, [headerCollapsed]); @@ -184,7 +226,7 @@ const ObjectPage = forwardRef((props, ref }, }); } - }, [headerCollapsed]); + }, [headerCollapsed, onToggleHeaderContentVisibility, objectPageRef]); const avatar = useMemo(() => { if (!image) { @@ -210,52 +252,67 @@ const ObjectPage = forwardRef((props, ref } }, [image, imageShapeCircle]); - const scrollToSectionById = (id: string | undefined, isSubSection = false) => { - const scroll = () => { - const section = getSectionElementById(objectPageRef.current, isSubSection, id); - scrollTimeout.current = performance.now() + 500; - if (section) { - const safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight.current; - - const scrollMargin = - -1 /* reduce margin-block so that intersection observer detects correct section*/ + - safeTopHeaderHeight + - tabContainerHeaderHeight + - (headerPinned && !headerCollapsed ? headerContentHeight : 0); - section.style.scrollMarginBlockStart = scrollMargin + 'px'; - if (isSubSection) { - section.focus(); - } + const scrollToSectionById = useCallback( + (id: string | undefined, isSubSection = false) => { + const scroll = () => { + const section = getSectionElementById(objectPageRef.current, isSubSection, id); + scrollTimeout.current = performance.now() + 500; + if (section) { + const safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight.current; + + const scrollMargin = + -1 /* reduce margin-block so that intersection observer detects correct section*/ + + safeTopHeaderHeight + + tabContainerHeaderHeight + + (headerPinned && !headerCollapsed ? headerContentHeight : 0); + section.style.scrollMarginBlockStart = scrollMargin + 'px'; + if (isSubSection) { + section.focus(); + } - const sectionRect = section.getBoundingClientRect(); - const objectPageElement = objectPageRef.current; - const objectPageRect = objectPageElement.getBoundingClientRect(); + const sectionRect = section.getBoundingClientRect(); + const objectPageElement = objectPageRef.current; + const objectPageRect = objectPageElement.getBoundingClientRect(); - // Calculate the top position of the section relative to the container - objectPageElement.scrollTop = sectionRect.top - objectPageRect.top + objectPageElement.scrollTop - scrollMargin; + // Calculate the top position of the section relative to the container + objectPageElement.scrollTop = + sectionRect.top - objectPageRect.top + objectPageElement.scrollTop - scrollMargin; - section.style.scrollMarginBlockStart = ''; + section.style.scrollMarginBlockStart = ''; + } + }; + // In TabBar mode the section is only rendered when selected: delay scroll for subsection + if (mode === ObjectPageMode.IconTabBar && isSubSection) { + setTimeout(scroll, 300); + } else { + scroll(); } - }; - // In TabBar mode the section is only rendered when selected: delay scroll for subsection - if (mode === ObjectPageMode.IconTabBar && isSubSection) { - setTimeout(scroll, 300); - } else { - scroll(); - } - }; + }, + [ + mode, + topHeaderHeight, + headerPinned, + headerCollapsed, + headerContentHeight, + tabContainerHeaderHeight, + objectPageRef, + ], + ); - const scrollToSection = (sectionId?: string) => { - if (!sectionId) { - return; - } - if (firstSectionId === sectionId) { - objectPageRef.current?.scrollTo({ top: 0 }); - } else { - scrollToSectionById(sectionId); - } - isProgrammaticallyScrolled.current = false; - }; + const scrollToSection = useCallback( + (sectionId?: string) => { + if (!sectionId) { + return; + } + if (firstSectionId === sectionId) { + objectPageRef.current?.scrollTo({ top: 0 }); + } else { + scrollToSectionById(sectionId); + } + isProgrammaticallyScrolled.current = false; + }, + [firstSectionId, scrollToSectionById, objectPageRef], + ); // section was selected by clicking on the tab bar buttons const handleOnSectionSelected: HandleOnSectionSelectedType = (targetEvent, newSelectionSectionId, index, section) => { @@ -307,7 +364,7 @@ const ObjectPage = forwardRef((props, ref if (mode === ObjectPageMode.Default && isProgrammaticallyScrolled.current === true && !selectedSubSectionId) { scrollToSection(internalSelectedSectionId); } - }, [internalSelectedSectionId, mode, selectedSubSectionId]); + }, [internalSelectedSectionId, mode, selectedSubSectionId, scrollToSection]); // Scrolling for Sub Section Selection useEffect(() => { @@ -315,7 +372,7 @@ const ObjectPage = forwardRef((props, ref scrollToSectionById(selectedSubSectionId, true); isProgrammaticallyScrolled.current = false; } - }, [selectedSubSectionId, isProgrammaticallyScrolled.current, sectionSpacer]); + }, [selectedSubSectionId, sectionSpacer, scrollToSectionById]); useEffect(() => { if (headerPinnedProp !== undefined) { @@ -324,7 +381,7 @@ const ObjectPage = forwardRef((props, ref if (headerPinnedProp) { onToggleHeaderContentVisibility({ detail: { visible: true } }); } - }, [headerPinnedProp]); + }, [headerPinnedProp, onToggleHeaderContentVisibility]); const prevHeaderPinned = useRef(headerPinned); useEffect(() => { @@ -335,7 +392,7 @@ const ObjectPage = forwardRef((props, ref if (!prevHeaderPinned.current && headerPinned) { prevHeaderPinned.current = true; } - }, [headerPinned, topHeaderHeight]); + }, [headerPinned, topHeaderHeight, onToggleHeaderContentVisibility, objectPageRef]); const isInitialTabBarMode = useRef(true); useEffect(() => { @@ -376,7 +433,7 @@ const ObjectPage = forwardRef((props, ref } } isInitialTabBarMode.current = false; - }, [props.selectedSubSectionId, isMounted]); + }, [props.selectedSubSectionId, isMounted, childrenArray, debouncedOnSectionChange, mode]); const tabContainerContainerRef = useRef(null); const isHeaderPinnedAndExpanded = headerPinned && !headerCollapsed; @@ -450,27 +507,6 @@ const ObjectPage = forwardRef((props, ref objectPageRef, ]); - const onToggleHeaderContentVisibility = (e) => { - isToggledRef.current = true; - scrollTimeout.current = performance.now() + 500; - setToggledCollapsedHeaderWasVisible(false); - if (!e.detail.visible) { - if (objectPageRef.current.scrollTop <= headerContentHeight) { - setToggledCollapsedHeaderWasVisible(true); - if (firstSectionId === internalSelectedSectionId || mode === ObjectPageMode.IconTabBar) { - objectPageRef.current.scrollTop = 0; - } - } - setHeaderCollapsedInternal(true); - setScrolledHeaderExpanded(false); - } else { - setHeaderCollapsedInternal(false); - if (objectPageRef.current.scrollTop >= headerContentHeight && objectPageRef.current.scrollTop > 0) { - setScrolledHeaderExpanded(true); - } - } - }; - const { onScroll: _0, selectedSubSectionId: _1, ...propsWithoutOmitted } = rest; const visibleSectionIds = useRef>(new Set()); @@ -531,6 +567,8 @@ const ObjectPage = forwardRef((props, ref childrenArray.length, scrolledHeaderExpanded, mode, + objectPageRef, + debouncedOnSectionChange, ]); const onTitleClick = (e) => { @@ -580,8 +618,8 @@ const ObjectPage = forwardRef((props, ref } setToggledCollapsedHeaderWasVisible(false); scrollEvent.current = e; - if (typeof props.onScroll === 'function') { - props.onScroll(e); + if (typeof onScrollRef.current === 'function') { + onScrollRef.current(e); } if (selectedSubSectionId) { setSelectedSubSectionId(undefined); @@ -603,7 +641,7 @@ const ObjectPage = forwardRef((props, ref setScrolledHeaderExpanded(false); } }, - [topHeaderHeight, headerPinned, props.onScroll, scrolledHeaderExpanded, selectedSubSectionId], + [headerPinned, scrolledHeaderExpanded, selectedSubSectionId, objectPageRef, scrollEndHandler], ); const onHoverToggleButton: MouseEventHandler = useCallback((e) => { diff --git a/packages/main/src/components/ObjectPage/useHandleTabSelect.ts b/packages/main/src/components/ObjectPage/useHandleTabSelect.ts index 96781c9add0..cacbdaf3583 100644 --- a/packages/main/src/components/ObjectPage/useHandleTabSelect.ts +++ b/packages/main/src/components/ObjectPage/useHandleTabSelect.ts @@ -1,7 +1,7 @@ import { enrichEventWithDetails } from '@ui5/webcomponents-react-base'; import type { debounce } from '@ui5/webcomponents-react-base'; import type { Dispatch, JSXElementConstructor, MutableRefObject, ReactElement, RefObject, SetStateAction } from 'react'; -import { isValidElement, useEffect, useState } from 'react'; +import { isValidElement } from 'react'; import { ObjectPageMode } from '../../enums/ObjectPageMode.js'; import type { TabContainerPropTypes } from '../../webComponents/TabContainer/index.js'; import type { ObjectPageSectionPropTypes } from '../ObjectPageSection/index.js'; @@ -44,16 +44,6 @@ export const useHandleTabSelect = ({ setTabSelectId, setWasUserSectionChange, }: UseHandleTabSelectProps) => { - const [onSectionSelectedArgs, setOnSectionSelectedArgs] = useState< - | false - | [ - Parameters[0], - undefined | string, - string, - ReactElement, - ] - >(false); - const handleOnSubSectionSelected = (e) => { isProgrammaticallyScrolled.current = true; const sectionId = e.detail.sectionId; @@ -105,20 +95,13 @@ export const useHandleTabSelect = ({ const section = childrenArray.find((el) => { return el.props.id == sectionId; }); - setOnSectionSelectedArgs([event, section?.props?.id, index, section]); + handleOnSectionSelected(event, section?.props?.id, index, section); } if (mode === ObjectPageMode.IconTabBar) { setWasUserSectionChange(true); } }; - // effect required - if event is called in `handleTabItemSelect` it's invoked twice in StrictMode - useEffect(() => { - if (onSectionSelectedArgs) { - handleOnSectionSelected(...onSectionSelectedArgs); - setOnSectionSelectedArgs(false); - } - }, [onSectionSelectedArgs]); return handleTabItemSelect; }; diff --git a/packages/main/src/components/ObjectPageTitle/index.tsx b/packages/main/src/components/ObjectPageTitle/index.tsx index 2d92ff5bafe..bded325043e 100644 --- a/packages/main/src/components/ObjectPageTitle/index.tsx +++ b/packages/main/src/components/ObjectPageTitle/index.tsx @@ -2,7 +2,7 @@ import { debounce, Device, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; -import { cloneElement, forwardRef, isValidElement, useEffect, useRef, useState } from 'react'; +import { cloneElement, forwardRef, isValidElement, useEffect, useMemo, useRef, useState } from 'react'; import { FlexBoxAlignItems } from '../../enums/FlexBoxAlignItems.js'; import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js'; import { FlexBoxJustifyContent } from '../../enums/FlexBoxJustifyContent.js'; @@ -40,9 +40,7 @@ const ObjectPageTitle = forwardRef((pr const [componentRef, dynamicPageTitleRef] = useSyncRef(ref); const [showNavigationInTopArea, setShowNavigationInTopArea] = useState(undefined); const isMounted = useRef(false); - const [isPhone, setIsPhone] = useState( - Device.getCurrentRange(dynamicPageTitleRef.current?.getBoundingClientRect().width)?.name === 'Phone', - ); + const [isPhone, setIsPhone] = useState(false); const containerClasses = clsx(classNames.container, isPhone && classNames.phone, className); const toolbarContainerRef = useRef(null); const _header = !props?.['data-header-content-visible'] && snappedHeader ? snappedHeader : header; @@ -84,18 +82,16 @@ const ObjectPageTitle = forwardRef((pr debouncedObserverFn.cancel(); observer.disconnect(); }; - }, [dynamicPageTitleRef.current, showNavigationInTopArea, isMounted]); + }, [dynamicPageTitleRef, showNavigationInTopArea, isMounted]); - const [wcrNavToolbar, setWcrNavToolbar] = useState(null); - useEffect(() => { + const wcrNavToolbar = useMemo(() => { //@ts-expect-error: private identifier if (isValidElement(navigationBar) && navigationBar?.type?._displayName === 'UI5WCRToolbar') { - setWcrNavToolbar( - cloneElement(navigationBar, { - numberOfAlwaysVisibleItems: Infinity, - }), - ); + return cloneElement(navigationBar, { + numberOfAlwaysVisibleItems: Infinity, + }); } + return null; }, [navigationBar]); useEffect(() => { diff --git a/packages/main/src/components/SelectDialog/SelectDialog.cy.tsx b/packages/main/src/components/SelectDialog/SelectDialog.cy.tsx index 8c4f6d610b3..8fe5f5e038a 100644 --- a/packages/main/src/components/SelectDialog/SelectDialog.cy.tsx +++ b/packages/main/src/components/SelectDialog/SelectDialog.cy.tsx @@ -305,6 +305,8 @@ describe('SelectDialog', () => { }); it('invisible messaging', () => { + // do not remove content of ui5wc invisible message (disable `setTimeout` from running) + cy.clock(); cy.mount( @@ -313,14 +315,21 @@ describe('SelectDialog', () => { , ); + + cy.get('ui5-announcement-area .ui5-invisiblemessage-polite').should('exist'); cy.findByTestId('1').click(); + cy.findByTestId('1').should('have.attr', 'selected'); cy.get('ui5-announcement-area').should('contain.text', 'Selected Items 1'); cy.findByTestId('1').click(); - cy.get('ui5-announcement-area').should('not.contain.text', 'Selected Items 1'); + cy.findByTestId('1').should('not.have.attr', 'selected'); cy.findByTestId('1').click(); + cy.findByTestId('1').should('have.attr', 'selected'); cy.findByTestId('2').click(); + cy.findByTestId('2').should('have.attr', 'selected'); cy.findByTestId('3').click(); + cy.findByTestId('3').should('have.attr', 'selected'); cy.findByTestId('4').click(); + cy.findByTestId('4').should('have.attr', 'selected'); cy.get('ui5-announcement-area').should('contain.text', 'Selected Items 4'); }); }); diff --git a/packages/main/src/components/SelectDialog/index.tsx b/packages/main/src/components/SelectDialog/index.tsx index 80dedc5bf0a..aa779f16e10 100644 --- a/packages/main/src/components/SelectDialog/index.tsx +++ b/packages/main/src/components/SelectDialog/index.tsx @@ -12,7 +12,7 @@ import iconSearch from '@ui5/webcomponents-icons/dist/search.js'; import { enrichEventWithDetails, useI18nBundle, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import { forwardRef, useEffect, useState } from 'react'; -import type { ReactNode } from 'react'; +import type { ReactNode, RefObject } from 'react'; import { FlexBoxAlignItems } from '../../enums/FlexBoxAlignItems.js'; import { CANCEL, CLEAR, SEARCH, SELECT, SELECTED, SELECTED_ITEMS } from '../../i18n/i18n-defaults.js'; import type { Ui5CustomEvent } from '../../types/index.js'; @@ -188,7 +188,9 @@ const SelectDialog = forwardRef((props, ref const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); const [searchValue, setSearchValue] = useState(''); const [selectedItems, setSelectedItems] = useState([]); - const [listComponentRef, listRef] = useSyncRef((listProps as any).ref); + const [listComponentRef, listRef] = useSyncRef( + (listProps as SelectDialogPropTypes['listProps'] & { ref: RefObject }).ref, + ); const [internalOpen, setInternalOpen] = useState(open); useEffect(() => { setInternalOpen(open); diff --git a/packages/main/src/components/Splitter/index.tsx b/packages/main/src/components/Splitter/index.tsx index 961740c3a8d..925b9fc340a 100644 --- a/packages/main/src/components/Splitter/index.tsx +++ b/packages/main/src/components/Splitter/index.tsx @@ -59,7 +59,7 @@ const Splitter = forwardRef((props, ref) => { const positionKeys = vertical ? verticalPositionInfo : horizontalPositionInfo; const [isDragging, setIsDragging] = useState(false); - const [isSiblings, setIsSiblings] = useState(['previousSibling', 'nextSibling']); + const isSiblings = isRtl && !vertical ? ['nextSibling', 'previousSibling'] : ['previousSibling', 'nextSibling']; const animationFrameIdRef = useRef(null); const fireOnResize = (prevSibling: HTMLElement, nextSibling: HTMLElement) => { @@ -282,10 +282,6 @@ const Splitter = forwardRef((props, ref) => { }; }, [isDragging]); - useEffect(() => { - setIsSiblings(isRtl && !vertical ? ['nextSibling', 'previousSibling'] : ['previousSibling', 'nextSibling']); - }, [isRtl, vertical]); - const currentTheme = useCurrentTheme(); const isHighContrast = currentTheme === 'sap_fiori_3_hcb' || diff --git a/packages/main/src/components/SplitterElement/index.tsx b/packages/main/src/components/SplitterElement/index.tsx index aabf7a74184..31634e43a3a 100644 --- a/packages/main/src/components/SplitterElement/index.tsx +++ b/packages/main/src/components/SplitterElement/index.tsx @@ -50,17 +50,22 @@ const SplitterElement = forwardRef((pr const { vertical, reset } = useContext(SplitterLayoutContext); const safariStyles = Device.isSafari() ? { width: 'min-content', flex: '1 1 auto' } : {}; const _size = typeof size === 'number' ? `${size}px` : size; - const defaultFlexStyles = _size !== 'auto' ? { flex: `0 1 ${_size}` } : { flex: '1 0 min-content', ...safariStyles }; - const [flexStyles, setFlexStyles] = useState(defaultFlexStyles); const [flexBasisApplied, setFlexBasisApplied] = useState(false); + const [observedFlex, setObservedFlex] = useState(null); + const flexStyles = reset + ? undefined + : _size !== 'auto' + ? { flex: `0 1 ${_size}` } + : (observedFlex ?? { flex: '1 0 min-content', ...safariStyles }); useStylesheet(styleData, SplitterElement.displayName); - useEffect(() => { + if (_size !== 'auto') return; + const elementObserver = new ResizeObserver(([element]) => { if (element.target.getBoundingClientRect().width !== 0 && !flexBasisApplied) { const resetSafariStyles = Device.isSafari() ? { width: 'unset' } : {}; - setFlexStyles({ + setObservedFlex({ flex: `0 0 ${element.target.getBoundingClientRect()[vertical ? 'height' : 'width']}px`, ...resetSafariStyles, }); @@ -68,29 +73,21 @@ const SplitterElement = forwardRef((pr } }); - if (_size === 'auto' && splitterElementRef.current) { + if (splitterElementRef.current) { elementObserver.observe(splitterElementRef.current); - } else { - setFlexStyles({ flex: `0 1 ${_size}` }); } return () => { elementObserver.disconnect(); }; - }, [_size, flexBasisApplied, vertical]); + }, [_size, flexBasisApplied, splitterElementRef, vertical]); useIsomorphicLayoutEffect(() => { if (reset) { - setFlexStyles(undefined); + setObservedFlex(null); setFlexBasisApplied(false); } - }, [reset, _size]); - - useIsomorphicLayoutEffect(() => { - if (flexStyles === undefined) { - setFlexStyles(defaultFlexStyles); - } - }, [flexStyles]); + }, [reset]); return (
{ }) .realMouseUp({ position: 'center' }); + cy.wait(100); + cy.findByTestId('0') .invoke('text') .then((txt) => parseInt(txt, 10)) diff --git a/packages/main/src/components/SplitterLayout/index.tsx b/packages/main/src/components/SplitterLayout/index.tsx index 92f22f39542..f0d84ecfa0b 100644 --- a/packages/main/src/components/SplitterLayout/index.tsx +++ b/packages/main/src/components/SplitterLayout/index.tsx @@ -53,6 +53,8 @@ const SplitterLayout = forwardRef((prop setReset(true); } initialCustomDep.current = false; + // Can't determine external dependencies + // eslint-disable-next-line react-hooks/exhaustive-deps }, options?.resetOnCustomDepsChange ?? []); useEffect(() => { @@ -76,7 +78,7 @@ const SplitterLayout = forwardRef((prop layoutObserver.disconnect(); }; } - }, [vertical, options?.resetOnSizeChange]); + }, [vertical, options?.resetOnSizeChange, sLRef]); useEffect(() => { if (reset) { diff --git a/packages/main/src/components/ThemeProvider/I18n.cy.tsx b/packages/main/src/components/ThemeProvider/I18n.cy.tsx index 771bc9231ae..ce91ef89d25 100644 --- a/packages/main/src/components/ThemeProvider/I18n.cy.tsx +++ b/packages/main/src/components/ThemeProvider/I18n.cy.tsx @@ -72,6 +72,7 @@ describe('I18nProvider', () => { return ( <> {i18nBundle.getText('PLEASE_WAIT')} + {/* eslint-disable-next-line react-hooks/refs */} {renderCounter.current} ); diff --git a/packages/main/src/internal/useObserveHeights.ts b/packages/main/src/internal/useObserveHeights.ts index b5a9ffdd1c9..8aa07bad48c 100644 --- a/packages/main/src/internal/useObserveHeights.ts +++ b/packages/main/src/internal/useObserveHeights.ts @@ -26,6 +26,8 @@ export const useObserveHeights = ( const prevScrollTop = useRef(0); const onScroll = useCallback( + // ToDo: Check how to properly memoize this callback so it supports React Compiler + // eslint-disable-next-line react-hooks/preserve-manual-memoization (e) => { const scrollDown = prevScrollTop.current <= e.target.scrollTop; prevScrollTop.current = e.target.scrollTop; diff --git a/packages/main/src/webComponents/Card/Card.stories.tsx b/packages/main/src/webComponents/Card/Card.stories.tsx index 6f505ddc0e2..1b3533fd67d 100644 --- a/packages/main/src/webComponents/Card/Card.stories.tsx +++ b/packages/main/src/webComponents/Card/Card.stories.tsx @@ -72,7 +72,7 @@ const simpleDataSet = [ ]; export const WithAnalyticalCardHeader = { - render: (args) => { + render: () => { return ( diff --git a/patterns/navigation-layout/package-lock.json b/patterns/navigation-layout/package-lock.json index aeb70a1b839..5a007897dec 100644 --- a/patterns/navigation-layout/package-lock.json +++ b/patterns/navigation-layout/package-lock.json @@ -21,7 +21,7 @@ "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "5.1.2", "eslint": "9.39.2", - "eslint-plugin-react-hooks": "6.1.1", + "eslint-plugin-react-hooks": "7.0.0", "eslint-plugin-react-refresh": "0.5.0", "globals": "17.3.0", "typescript": "5.8.3", @@ -2332,14 +2332,15 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz", - "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.0.tgz", + "integrity": "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, @@ -2638,6 +2639,23 @@ "node": ">=8" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", diff --git a/patterns/navigation-layout/package.json b/patterns/navigation-layout/package.json index 1e505a72fbd..78731078cc3 100644 --- a/patterns/navigation-layout/package.json +++ b/patterns/navigation-layout/package.json @@ -23,7 +23,7 @@ "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "5.1.2", "eslint": "9.39.2", - "eslint-plugin-react-hooks": "6.1.1", + "eslint-plugin-react-hooks": "7.0.0", "eslint-plugin-react-refresh": "0.5.0", "globals": "17.3.0", "typescript": "5.8.3", diff --git a/patterns/selection-assistant/InputSelectionAssistant.tsx b/patterns/selection-assistant/InputSelectionAssistant.tsx index 5f122a1c01b..3e5a088bcbc 100644 --- a/patterns/selection-assistant/InputSelectionAssistant.tsx +++ b/patterns/selection-assistant/InputSelectionAssistant.tsx @@ -1,7 +1,7 @@ import getElementSelection from '@ui5/webcomponents-base/dist/util/SelectionAssistant.js'; import ai from '@ui5/webcomponents-icons/dist/ai.js'; -import type { ButtonDomRef, InputPropTypes } from '@ui5/webcomponents-react'; import { Button, Input, Label, Toast } from '@ui5/webcomponents-react'; +import type { ButtonDomRef, InputPropTypes } from '@ui5/webcomponents-react'; import type { CSSProperties } from 'react'; import { useRef, useState } from 'react'; import { SelectionAssistantContainer } from '@/patterns/selection-assistant/SelectionAssistantContainer.js'; diff --git a/templates/vite-ts/package-lock.json b/templates/vite-ts/package-lock.json index 7a118e863ee..061524ba090 100644 --- a/templates/vite-ts/package-lock.json +++ b/templates/vite-ts/package-lock.json @@ -23,7 +23,7 @@ "@vitejs/plugin-react": "5.1.2", "cypress": "15.9.0", "eslint": "9.39.2", - "eslint-plugin-react-hooks": "6.1.1", + "eslint-plugin-react-hooks": "7.0.0", "eslint-plugin-react-refresh": "0.5.0", "globals": "17.3.0", "typescript": "5.8.3", @@ -2994,14 +2994,15 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz", - "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.0.tgz", + "integrity": "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", "zod": "^3.22.4 || ^4.0.0", "zod-validation-error": "^3.0.3 || ^4.0.0" }, @@ -3632,6 +3633,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/http-signature": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", diff --git a/templates/vite-ts/package.json b/templates/vite-ts/package.json index 175e73b1f95..b52b879e049 100644 --- a/templates/vite-ts/package.json +++ b/templates/vite-ts/package.json @@ -27,7 +27,7 @@ "@ui5/webcomponents-cypress-commands": "2.18.1", "cypress": "15.9.0", "eslint": "9.39.2", - "eslint-plugin-react-hooks": "6.1.1", + "eslint-plugin-react-hooks": "7.0.0", "eslint-plugin-react-refresh": "0.5.0", "globals": "17.3.0", "typescript": "5.8.3", diff --git a/yarn.lock b/yarn.lock index 57312deaecb..21917a8eb31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5403,7 +5403,7 @@ __metadata: dependencies: "@ui5/webcomponents-react-base": "workspace:~" peerDependencies: - "@ui5/webcomponents-ai": ~2.18.0 + "@ui5/webcomponents-ai": ~2.18.0 || ~2.19.0 react: ^18 || ^19 languageName: unknown linkType: soft @@ -5446,8 +5446,8 @@ __metadata: version: 0.0.0-use.local resolution: "@ui5/webcomponents-cypress-commands@workspace:packages/cypress-commands" peerDependencies: - "@ui5/webcomponents": ~2.18.0 - "@ui5/webcomponents-base": ~2.18.0 + "@ui5/webcomponents": ~2.18.0 || ~2.19.0 + "@ui5/webcomponents-base": ~2.18.0 || ~2.19.0 cypress: ^12 || ^13 || ^14 || ^15 peerDependenciesMeta: "@ui5/webcomponents": @@ -5512,7 +5512,7 @@ __metadata: resolution: "@ui5/webcomponents-react-base@workspace:packages/base" peerDependencies: "@types/react": "*" - "@ui5/webcomponents-base": ~2.18.0 + "@ui5/webcomponents-base": ~2.18.0 || ~2.19.0 react: ^18 || ^19 peerDependenciesMeta: "@types/react": @@ -5556,7 +5556,7 @@ __metadata: peerDependencies: "@types/react": "*" "@types/react-dom": "*" - "@ui5/webcomponents-compat": ~2.18.0 + "@ui5/webcomponents-compat": ~2.18.0 || ~2.19.0 "@ui5/webcomponents-react": ~2.18.0 react: ^18 || ^19 react-dom: ^18 || ^19 @@ -5583,10 +5583,10 @@ __metadata: peerDependencies: "@types/react": "*" "@types/react-dom": "*" - "@ui5/webcomponents": ~2.18.0 - "@ui5/webcomponents-base": ~2.18.0 - "@ui5/webcomponents-fiori": ~2.18.0 - "@ui5/webcomponents-icons": ~2.18.0 + "@ui5/webcomponents": ~2.18.0 || ~2.19.0 + "@ui5/webcomponents-base": ~2.18.0 || ~2.19.0 + "@ui5/webcomponents-fiori": ~2.18.0 || ~2.19.0 + "@ui5/webcomponents-icons": ~2.18.0 || ~2.19.0 react: ^18 || ^19 react-dom: ^18 || ^19 peerDependenciesMeta: @@ -10126,17 +10126,18 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react-hooks@npm:6.1.1": - version: 6.1.1 - resolution: "eslint-plugin-react-hooks@npm:6.1.1" +"eslint-plugin-react-hooks@npm:7.0.1": + version: 7.0.1 + resolution: "eslint-plugin-react-hooks@npm:7.0.1" dependencies: "@babel/core": "npm:^7.24.4" "@babel/parser": "npm:^7.24.4" - zod: "npm:^3.22.4 || ^4.0.0" - zod-validation-error: "npm:^3.0.3 || ^4.0.0" + hermes-parser: "npm:^0.25.1" + zod: "npm:^3.25.0 || ^4.0.0" + zod-validation-error: "npm:^3.5.0 || ^4.0.0" peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - checksum: 10c0/579be053bc89c995a6c03996f9ee3f6bac88946b4b1c8b891b42f981e7c05a9c5de46324bbd2a33199855c0a602820c0e3eeb7f840730301b77a9ba3dc7a0ae2 + checksum: 10c0/1e711d1a9d1fa9cfc51fa1572500656577201199c70c795c6a27adfc1df39e5c598f69aab6aa91117753d23cc1f11388579a2bed14921cf9a4efe60ae8618496 languageName: node linkType: hard @@ -11906,6 +11907,22 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-estree@npm:0.25.1" + checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac + languageName: node + linkType: hard + +"hermes-parser@npm:^0.25.1": + version: 0.25.1 + resolution: "hermes-parser@npm:0.25.1" + dependencies: + hermes-estree: "npm:0.25.1" + checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c + languageName: node + linkType: hard + "highlight.js@npm:^11.6.0": version: 11.10.0 resolution: "highlight.js@npm:11.10.0" @@ -20766,7 +20783,7 @@ __metadata: eslint-plugin-no-only-tests: "npm:3.3.0" eslint-plugin-prettier: "npm:5.5.5" eslint-plugin-react: "npm:7.37.5" - eslint-plugin-react-hooks: "npm:6.1.1" + eslint-plugin-react-hooks: "npm:7.0.1" eslint-plugin-storybook: "npm:10.2.3" glob: "npm:13.0.0" globals: "npm:17.3.0" @@ -22233,7 +22250,7 @@ __metadata: languageName: node linkType: hard -"zod-validation-error@npm:^3.0.3 || ^4.0.0": +"zod-validation-error@npm:^3.5.0 || ^4.0.0": version: 4.0.2 resolution: "zod-validation-error@npm:4.0.2" peerDependencies: @@ -22242,10 +22259,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4 || ^4.0.0": - version: 4.1.12 - resolution: "zod@npm:4.1.12" - checksum: 10c0/b64c1feb19e99d77075261eaf613e0b2be4dfcd3551eff65ad8b4f2a079b61e379854d066f7d447491fcf193f45babd8095551a9d47973d30b46b6d8e2c46774 +"zod@npm:^3.25.0 || ^4.0.0": + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 languageName: node linkType: hard