diff --git a/src/internal/components/dropdown/context.tsx b/src/internal/components/dropdown/context.tsx index 610cc48396..4452740382 100644 --- a/src/internal/components/dropdown/context.tsx +++ b/src/internal/components/dropdown/context.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; @@ -14,7 +14,8 @@ export interface DropdownContextProviderProps { } export function DropdownContextProvider({ children, position = 'bottom-right' }: DropdownContextProviderProps) { - return {children}; + const value = useMemo(() => ({ position }), [position]); + return {children}; } export function useDropdownContext() { diff --git a/src/table/use-column-widths.tsx b/src/table/use-column-widths.tsx index db41a79c8c..6597a10922 100644 --- a/src/table/use-column-widths.tsx +++ b/src/table/use-column-widths.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useResizeObserver, useStableCallback } from '@cloudscape-design/component-toolkit/internal'; import { getLogicalBoundingClientRect } from '@cloudscape-design/component-toolkit/internal'; @@ -35,12 +35,12 @@ function readWidths( } function updateWidths( - visibleColumns: readonly ColumnWidthDefinition[], + columnById: Map, oldWidths: Map, newWidth: number, columnId: PropertyKey ) { - const column = visibleColumns.find(column => column.id === columnId); + const column = columnById.get(columnId); let minWidth = DEFAULT_COLUMN_WIDTH; if (typeof column?.width === 'number' && column.width < DEFAULT_COLUMN_WIDTH) { minWidth = column?.width; @@ -83,61 +83,84 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain const containerWidthRef = useRef(0); const [columnWidths, setColumnWidths] = useState>(null); + // Pre-build a Map for column lookups + const columnById = useMemo( + () => new Map(visibleColumns.map(column => [column.id, column])), + [visibleColumns] + ); + const cellsRef = useRef(new Map()); const stickyCellsRef = useRef(new Map()); + // Cache measured widths from real cells to avoid getBoundingClientRect() during render + const measuredWidthsRef = useRef(new Map()); const getCell = (columnId: PropertyKey): null | HTMLElement => cellsRef.current.get(columnId) ?? null; - const setCell = (sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => { + const setCell = useCallback((sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => { const ref = sticky ? stickyCellsRef : cellsRef; if (node) { ref.current.set(columnId, node); } else { ref.current.delete(columnId); } - }; + }, []); - const getColumnStyles = (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => { - const column = visibleColumns.find(column => column.id === columnId); - if (!column) { - return {}; - } + const getColumnStyles = useCallback( + (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => { + const column = columnById.get(columnId); + if (!column) { + return {}; + } - if (sticky) { - return { - width: - cellsRef.current.get(column.id)?.getBoundingClientRect().width || - (columnWidths?.get(column.id) ?? column.width), - }; - } + if (sticky) { + // Use cached measured width to avoid getBoundingClientRect() during render. + // The cache is populated by updateColumnWidths() which runs outside the render path. + return { + width: measuredWidthsRef.current.get(column.id) || columnWidths?.get(column.id) || column.width, + }; + } - if (resizableColumns && columnWidths) { - const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id; - const totalWidth = visibleColumns.reduce( - (sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH), - 0 - ); - if (isLastColumn && containerWidthRef.current > totalWidth) { - return { width: 'auto', minWidth: column?.minWidth }; - } else { - return { width: columnWidths.get(column.id), minWidth: column?.minWidth }; + if (resizableColumns && columnWidths) { + const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id; + const totalWidth = visibleColumns.reduce( + (sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH), + 0 + ); + if (isLastColumn && containerWidthRef.current > totalWidth) { + return { width: 'auto', minWidth: column?.minWidth }; + } else { + return { width: columnWidths.get(column.id), minWidth: column?.minWidth }; + } } - } - return { - width: column.width, - minWidth: column.minWidth, - maxWidth: !resizableColumns ? column.maxWidth : undefined, - }; - }; + return { + width: column.width, + minWidth: column.minWidth, + maxWidth: !resizableColumns ? column.maxWidth : undefined, + }; + }, + [columnById, columnWidths, resizableColumns, visibleColumns] + ); // Imperatively sets width style for a cell avoiding React state. // This allows setting the style as soon container's size change is observed. const updateColumnWidths = useStableCallback(() => { + // First pass: set widths on real cells for (const { id } of visibleColumns) { const element = cellsRef.current.get(id); if (element) { setElementWidths(element, getColumnStyles(false, id)); } } - // Sticky column widths must be synchronized once all real column widths are assigned. + // Second pass: measure real cell widths and cache them for sticky columns. + // This avoids calling getBoundingClientRect() during render. + for (const { id } of visibleColumns) { + const element = cellsRef.current.get(id); + if (element) { + const width = element.getBoundingClientRect().width; + if (width > 0) { + measuredWidthsRef.current.set(id, width); + } + } + } + // Third pass: set sticky column widths using cached measurements for (const { id } of visibleColumns) { const element = stickyCellsRef.current.get(id); if (element) { @@ -189,12 +212,25 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - function updateColumn(columnId: PropertyKey, newWidth: number) { - setColumnWidths(columnWidths => updateWidths(visibleColumns, columnWidths ?? new Map(), newWidth, columnId)); - } + const updateColumn = useCallback( + (columnId: PropertyKey, newWidth: number) => { + setColumnWidths(columnWidths => updateWidths(columnById, columnWidths ?? new Map(), newWidth, columnId)); + }, + [columnById] + ); + + const contextValue = useMemo( + () => ({ + getColumnStyles, + columnWidths: columnWidths ?? new Map(), + updateColumn, + setCell, + }), + [getColumnStyles, columnWidths, updateColumn, setCell] + ); return ( - + {children} ); diff --git a/src/top-navigation/parts/overflow-menu/router.tsx b/src/top-navigation/parts/overflow-menu/router.tsx index 10c41fb8d7..0e1db40a9a 100644 --- a/src/top-navigation/parts/overflow-menu/router.tsx +++ b/src/top-navigation/parts/overflow-menu/router.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { createContext, Dispatch, SetStateAction, useContext, useState } from 'react'; +import React, { createContext, Dispatch, SetStateAction, useContext, useMemo, useState } from 'react'; type View = 'utilities' | 'dropdown-menu'; @@ -60,7 +60,8 @@ interface RouterProps { const Router = ({ children }: RouterProps) => { const [state, setState] = useState({ view: 'utilities', data: null }); - return {children}; + const value = useMemo(() => ({ state, setState }), [state]); + return {children}; }; export default Router;