From 61936d63f6b7eb390b351138c48572442d5b1e4c Mon Sep 17 00:00:00 2001 From: Madhuri Sandbhor Date: Tue, 25 Jun 2024 12:01:47 +0200 Subject: [PATCH] chore: standardise forwardRefs across components (#1748) --- .changeset/rotten-eagles-matter.md | 5 + .../components/Breadcrumbs/Breadcrumbs.tsx | 42 +-- .../src/components/Breadcrumbs/Crumb.tsx | 28 +- .../src/components/Breadcrumbs/CrumbLink.tsx | 6 +- .../Breadcrumbs/CrumbSimpleMenu.tsx | 12 +- .../src/components/Card/Card.tsx | 5 +- .../src/components/Card/CardAction.tsx | 8 +- .../src/components/Card/CardCheckbox.tsx | 8 +- .../EmptyStateLayout/EmptyStateLayout.tsx | 59 ++-- .../src/components/Popover/Popover.tsx | 40 +-- .../src/components/RawTable/RawCell.tsx | 264 +++++++++--------- .../src/components/RawTable/RawTable.tsx | 191 +++++++------ .../src/components/SimpleMenu/SimpleMenu.tsx | 83 +++--- .../src/components/Table/Cell.tsx | 12 +- .../src/components/Table/Table.tsx | 6 +- 15 files changed, 401 insertions(+), 368 deletions(-) create mode 100644 .changeset/rotten-eagles-matter.md diff --git a/.changeset/rotten-eagles-matter.md b/.changeset/rotten-eagles-matter.md new file mode 100644 index 000000000..6f6c129bd --- /dev/null +++ b/.changeset/rotten-eagles-matter.md @@ -0,0 +1,5 @@ +--- +'@strapi/design-system': minor +--- + +chore: standardise forwardRefs across components diff --git a/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx b/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx index 098594d33..bd4fb91fc 100644 --- a/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -21,25 +21,27 @@ export interface BreadcrumbsProps extends FlexProps { label?: string; } -export const Breadcrumbs = ({ label, children, ...props }: BreadcrumbsProps) => { - const childrenArray = React.Children.toArray(children); - - return ( - - - {React.Children.map(childrenArray, (child, index) => { - const shouldDisplayDivider = childrenArray.length > 1 && index + 1 < childrenArray.length; - - return ( - - {child} - {shouldDisplayDivider && } - - ); - })} - - - ); -}; +export const Breadcrumbs = React.forwardRef( + ({ label, children, ...props }, forwardedRef) => { + const childrenArray = React.Children.toArray(children); + + return ( + + + {React.Children.map(childrenArray, (child, index) => { + const shouldDisplayDivider = childrenArray.length > 1 && index + 1 < childrenArray.length; + + return ( + + {child} + {shouldDisplayDivider && } + + ); + })} + + + ); + }, +); Breadcrumbs.displayName = 'Breadcrumbs'; diff --git a/packages/design-system/src/components/Breadcrumbs/Crumb.tsx b/packages/design-system/src/components/Breadcrumbs/Crumb.tsx index b08628f43..d254de9b0 100644 --- a/packages/design-system/src/components/Breadcrumbs/Crumb.tsx +++ b/packages/design-system/src/components/Breadcrumbs/Crumb.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import { Box } from '../Box'; import { Typography, TypographyProps } from '../Typography'; @@ -5,18 +7,20 @@ export interface CrumbProps extends TypographyProps { isCurrent?: boolean; } -export const Crumb = ({ children, isCurrent = false, ...props }: CrumbProps) => ( - - - {children} - - +export const Crumb = React.forwardRef( + ({ children, isCurrent = false, ...props }, forwardedRef) => ( + + + {children} + + + ), ); Crumb.displayName = 'Crumb'; diff --git a/packages/design-system/src/components/Breadcrumbs/CrumbLink.tsx b/packages/design-system/src/components/Breadcrumbs/CrumbLink.tsx index abf5133f8..0a03de13d 100644 --- a/packages/design-system/src/components/Breadcrumbs/CrumbLink.tsx +++ b/packages/design-system/src/components/Breadcrumbs/CrumbLink.tsx @@ -19,6 +19,10 @@ const StyledLink = styled(BaseLink)` } `; -export const CrumbLink = ({ children, ...props }: BaseLinkProps) => {children}; +export const CrumbLink = React.forwardRef(({ children, ...props }, forwardedRef) => ( + + {children} + +)); CrumbLink.displayName = 'CrumbLink'; diff --git a/packages/design-system/src/components/Breadcrumbs/CrumbSimpleMenu.tsx b/packages/design-system/src/components/Breadcrumbs/CrumbSimpleMenu.tsx index e69893ac6..c7c2bd8e7 100644 --- a/packages/design-system/src/components/Breadcrumbs/CrumbSimpleMenu.tsx +++ b/packages/design-system/src/components/Breadcrumbs/CrumbSimpleMenu.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { styled } from 'styled-components'; -import { SimpleMenu, SimpleMenuProps } from '../SimpleMenu'; +import { SimpleMenu, type SimpleMenuProps } from '../SimpleMenu'; const StyledButton = styled(SimpleMenu)` padding: ${({ theme }) => `${theme.spaces[1]} ${theme.spaces[2]}`}; @@ -20,10 +20,12 @@ export interface CrumbSimpleMenuProps extends SimpleMenuProps { endIcon?: React.ReactNode; } -export const CrumbSimpleMenu = ({ children, ...props }: CrumbSimpleMenuProps) => ( - - {children} - +export const CrumbSimpleMenu = React.forwardRef( + ({ children, ...props }, forwardedRef) => ( + + {children} + + ), ); CrumbSimpleMenu.displayName = 'CrumbSimpleMenu'; diff --git a/packages/design-system/src/components/Card/Card.tsx b/packages/design-system/src/components/Card/Card.tsx index b56d089bd..4ec81af57 100644 --- a/packages/design-system/src/components/Card/Card.tsx +++ b/packages/design-system/src/components/Card/Card.tsx @@ -9,7 +9,7 @@ export interface CardProps extends BoxProps { id?: string; } -export const Card = ({ id, ...props }: CardProps) => { +export const Card = React.forwardRef(({ id, ...props }, forwardedRef) => { const generatedId = useId(id); const context = React.useMemo(() => ({ id: generatedId }), [generatedId]); @@ -17,6 +17,7 @@ export const Card = ({ id, ...props }: CardProps) => { return ( { /> ); -}; +}); diff --git a/packages/design-system/src/components/Card/CardAction.tsx b/packages/design-system/src/components/Card/CardAction.tsx index f5928f76d..877f7b570 100644 --- a/packages/design-system/src/components/Card/CardAction.tsx +++ b/packages/design-system/src/components/Card/CardAction.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import { styled } from 'styled-components'; import { PropsToTransientProps } from '../../types'; @@ -9,9 +11,9 @@ type CardActionProps = Omit, 'direction' | 'gap' | 'position'> position: CardActionPosition; }; -const CardActionImpl = ({ position, ...restProps }: CardActionProps) => { - return ; -}; +const CardActionImpl = React.forwardRef(({ position, ...restProps }, forwardedRef) => { + return ; +}); const CardAction = styled(Flex)>` position: absolute; diff --git a/packages/design-system/src/components/Card/CardCheckbox.tsx b/packages/design-system/src/components/Card/CardCheckbox.tsx index 6c3c9ac31..1da110df4 100644 --- a/packages/design-system/src/components/Card/CardCheckbox.tsx +++ b/packages/design-system/src/components/Card/CardCheckbox.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import { Checkbox, CheckboxProps } from '../Checkbox'; import { CardAction } from './CardAction'; @@ -5,15 +7,15 @@ import { useCard } from './CardContext'; interface CardCheckboxProps extends CheckboxProps {} -const CardCheckbox = (props: CardCheckboxProps) => { +const CardCheckbox = React.forwardRef((props, forwardedRef) => { const { id } = useCard(); return ( - + ); -}; +}); export { CardCheckbox }; export type { CardCheckboxProps }; diff --git a/packages/design-system/src/components/EmptyStateLayout/EmptyStateLayout.tsx b/packages/design-system/src/components/EmptyStateLayout/EmptyStateLayout.tsx index 932fadb88..9654d0072 100644 --- a/packages/design-system/src/components/EmptyStateLayout/EmptyStateLayout.tsx +++ b/packages/design-system/src/components/EmptyStateLayout/EmptyStateLayout.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import { styled } from 'styled-components'; import { Box, BoxComponent } from '../Box'; @@ -16,35 +18,32 @@ const EmptyStateIconWrapper = styled(Box)` } `; -export const EmptyStateLayout = ({ - icon, - content, - action, - hasRadius = true, - shadow = 'tableShadow', -}: EmptyStateLayoutProps) => { - return ( - - {icon ? ( - - {icon} - - ) : null} +export const EmptyStateLayout = React.forwardRef( + ({ icon, content, action, hasRadius = true, shadow = 'tableShadow' }: EmptyStateLayoutProps, forwardedRef) => { + return ( + + {icon ? ( + + {icon} + + ) : null} - - - {content} - - + + + {content} + + - {action} - - ); -}; + {action} + + ); + }, +); diff --git a/packages/design-system/src/components/Popover/Popover.tsx b/packages/design-system/src/components/Popover/Popover.tsx index b075445de..852ca7ff3 100644 --- a/packages/design-system/src/components/Popover/Popover.tsx +++ b/packages/design-system/src/components/Popover/Popover.tsx @@ -4,6 +4,7 @@ import * as Popover from '@radix-ui/react-popover'; import { styled } from 'styled-components'; import { stripReactIdOfColon } from '../../helpers/strings'; +import { useComposedRefs } from '../../hooks/useComposeRefs'; import { useId } from '../../hooks/useId'; import { useIntersection } from '../../hooks/useIntersection'; import { ANIMATIONS } from '../../styles/motion'; @@ -86,24 +87,27 @@ interface ScrollAreaImplProps extends ScrollAreaProps { onReachEnd?: (entry: IntersectionObserverEntry) => void; } -const ScrollAreaImpl = ({ children, intersectionId, onReachEnd, ...props }: ScrollAreaImplProps) => { - const popoverRef = React.useRef(null!); - - const generatedIntersectionId = useId(); - useIntersection(popoverRef, onReachEnd ?? (() => {}), { - selectorToWatch: `#${stripReactIdOfColon(generatedIntersectionId)}`, - skipWhen: !intersectionId || !onReachEnd, - }); - - return ( - - {children} - {intersectionId && onReachEnd && ( - - )} - - ); -}; +const ScrollAreaImpl = React.forwardRef( + ({ children, intersectionId, onReachEnd, ...props }, forwardedRef) => { + const popoverRef = React.useRef(null!); + const composedRef = useComposedRefs(popoverRef, forwardedRef); + + const generatedIntersectionId = useId(); + useIntersection(popoverRef, onReachEnd ?? (() => {}), { + selectorToWatch: `#${stripReactIdOfColon(generatedIntersectionId)}`, + skipWhen: !intersectionId || !onReachEnd, + }); + + return ( + + {children} + {intersectionId && onReachEnd && ( + + )} + + ); + }, +); const PopoverScrollArea = styled(ScrollArea)` height: 20rem; diff --git a/packages/design-system/src/components/RawTable/RawCell.tsx b/packages/design-system/src/components/RawTable/RawCell.tsx index 7f060f0fb..5731615ff 100644 --- a/packages/design-system/src/components/RawTable/RawCell.tsx +++ b/packages/design-system/src/components/RawTable/RawCell.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { getFocusableNodes, getFocusableNodesWithKeyboardNav } from '../../helpers/getFocusableNodes'; import { KeyboardKeys } from '../../helpers/keyboardKeys'; +import { useComposedRefs } from '../../hooks/useComposeRefs'; import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'; import { Box, BoxProps } from '../Box'; @@ -20,162 +21,165 @@ interface RawTdProps extends BoxProps<'td' | 'th'> { }; } -const RawTd = ({ coords = { col: 0, row: 0 }, tag = 'td', ...props }: RawTdProps) => { - const tdRef = React.useRef(null!); - const { rowIndex, colIndex, setTableValues } = useTable(); - const [isActive, setIsActive] = React.useState(false); +const RawTd = React.forwardRef( + ({ coords = { col: 0, row: 0 }, tag = 'td', ...props }, forwardedRef) => { + const tdRef = React.useRef(null!); + const composedRef = useComposedRefs(forwardedRef, tdRef); + const { rowIndex, colIndex, setTableValues } = useTable(); + const [isActive, setIsActive] = React.useState(false); - const handleKeyDown: React.KeyboardEventHandler = (e) => { - const focusableNodes = getFocusableNodes(tdRef.current, true); + const handleKeyDown: React.KeyboardEventHandler = (e) => { + const focusableNodes = getFocusableNodes(tdRef.current, true); - /** - * If the cell does not have focusable children or if it has focusable children - * without keyboard navigation, we should not run the handler. - */ - if ( - focusableNodes.length === 0 || - (focusableNodes.length === 1 && getFocusableNodesWithKeyboardNav(focusableNodes).length === 0) - ) { - return; /** - * This allows cells that **only** have buttons in them to still be - * navigable with the keyboard arrow keys (left / right) as if they were grid cells. - * - * If there are nextNodes (next child node) then we stop the table's keyboard navigation - * handlers from happening. + * If the cell does not have focusable children or if it has focusable children + * without keyboard navigation, we should not run the handler. */ - } - if (focusableNodes.length > 1 && !focusableNodes.find((node) => node.tagName !== 'BUTTON')) { - e.preventDefault(); - const focussedButtonIndex = focusableNodes.findIndex((node) => node === document.activeElement); + if ( + focusableNodes.length === 0 || + (focusableNodes.length === 1 && getFocusableNodesWithKeyboardNav(focusableNodes).length === 0) + ) { + return; + /** + * This allows cells that **only** have buttons in them to still be + * navigable with the keyboard arrow keys (left / right) as if they were grid cells. + * + * If there are nextNodes (next child node) then we stop the table's keyboard navigation + * handlers from happening. + */ + } + if (focusableNodes.length > 1 && !focusableNodes.find((node) => node.tagName !== 'BUTTON')) { + e.preventDefault(); + const focussedButtonIndex = focusableNodes.findIndex((node) => node === document.activeElement); + + if (e.key === KeyboardKeys.RIGHT) { + const nextNode = focusableNodes[focussedButtonIndex + 1]; + + if (nextNode) { + e.stopPropagation(); + nextNode.focus(); + } + } else if (e.key === KeyboardKeys.LEFT) { + const nextNode = focusableNodes[focussedButtonIndex - 1]; + + if (nextNode) { + e.stopPropagation(); + nextNode.focus(); + } + } - if (e.key === KeyboardKeys.RIGHT) { - const nextNode = focusableNodes[focussedButtonIndex + 1]; + return; + } - if (nextNode) { - e.stopPropagation(); - nextNode.focus(); - } - } else if (e.key === KeyboardKeys.LEFT) { - const nextNode = focusableNodes[focussedButtonIndex - 1]; + const isEnterKey = e.key === KeyboardKeys.ENTER; - if (nextNode) { - e.stopPropagation(); - nextNode.focus(); + if (isEnterKey && !isActive) { + setIsActive(true); + /** + * Cells should be "escapeable" with the escape key or enter key + */ + } else if ((e.key === KeyboardKeys.ESCAPE || isEnterKey) && isActive) { + /** + * It's expected behaviour that the cell can't be escaped with `enter` if + * the element that is focussed is an anchor tag. + */ + if (isEnterKey && document.activeElement?.tagName === 'A') { + return; } + + setIsActive(false); + tdRef.current.focus(); + } else if (isActive) { + /** + * This stops the table navigation from working + */ + e.stopPropagation(); } + }; - return; - } + const isFocused = rowIndex === coords.row - 1 && colIndex === coords.col - 1; - const isEnterKey = e.key === KeyboardKeys.ENTER; + /** + * Handles tabindex of the rendered cell element + */ + useIsomorphicLayoutEffect(() => { + const focusableNodes = getFocusableNodes(tdRef.current, true); - if (isEnterKey && !isActive) { - setIsActive(true); /** - * Cells should be "escapeable" with the escape key or enter key + * We should focus the cell if there are no focussable children inside + * If there is only one focusable child and it has it's own keyboard navigation + * Or if there is more than one focusable child unless those children + * are exclusively buttons. */ - } else if ((e.key === KeyboardKeys.ESCAPE || isEnterKey) && isActive) { + if ( + focusableNodes.length === 0 || + (focusableNodes.length === 1 && getFocusableNodesWithKeyboardNav(focusableNodes).length !== 0) || + (focusableNodes.length > 1 && Boolean(focusableNodes.find((node) => node.tagName !== 'BUTTON'))) + ) { + tdRef.current.setAttribute('tabIndex', !isActive && isFocused ? '0' : '-1'); + + focusableNodes.forEach((node, index) => { + node.setAttribute('tabIndex', isActive ? '0' : '-1'); + + /** + * When a cell is active we want to focus the + * first focusable element simulating a focus trap + */ + if (isActive && index === 0) { + node.focus(); + } + }); + } else { + focusableNodes.forEach((node) => { + node.setAttribute('tabIndex', isFocused ? '0' : '-1'); + }); + } + }, [isActive, isFocused]); + + const handleFocusableNodeFocus = React.useCallback(() => { + const focusableNodes = getFocusableNodes(tdRef.current, true); + /** - * It's expected behaviour that the cell can't be escaped with `enter` if - * the element that is focussed is an anchor tag. + * If there's 1 or more focusable children and at least one has keyboard navigation + * or the children are exclusively button elements the cell should be using the "active" system */ - if (isEnterKey && document.activeElement?.tagName === 'A') { - return; + if ( + focusableNodes.length >= 1 && + (getFocusableNodesWithKeyboardNav(focusableNodes).length !== 0 || + !focusableNodes.find((node) => node.tagName !== 'BUTTON')) + ) { + setIsActive(true); } - - setIsActive(false); - tdRef.current.focus(); - } else if (isActive) { /** - * This stops the table navigation from working + * This function is wrapped in `useCallback` so we can safely + * assume that the reference will not change */ - e.stopPropagation(); - } - }; - - const isFocused = rowIndex === coords.row - 1 && colIndex === coords.col - 1; - - /** - * Handles tabindex of the rendered cell element - */ - useIsomorphicLayoutEffect(() => { - const focusableNodes = getFocusableNodes(tdRef.current, true); - - /** - * We should focus the cell if there are no focussable children inside - * If there is only one focusable child and it has it's own keyboard navigation - * Or if there is more than one focusable child unless those children - * are exclusively buttons. - */ - if ( - focusableNodes.length === 0 || - (focusableNodes.length === 1 && getFocusableNodesWithKeyboardNav(focusableNodes).length !== 0) || - (focusableNodes.length > 1 && Boolean(focusableNodes.find((node) => node.tagName !== 'BUTTON'))) - ) { - tdRef.current.setAttribute('tabIndex', !isActive && isFocused ? '0' : '-1'); - - focusableNodes.forEach((node, index) => { - node.setAttribute('tabIndex', isActive ? '0' : '-1'); - - /** - * When a cell is active we want to focus the - * first focusable element simulating a focus trap - */ - if (isActive && index === 0) { - node.focus(); - } - }); - } else { - focusableNodes.forEach((node) => { - node.setAttribute('tabIndex', isFocused ? '0' : '-1'); - }); - } - }, [isActive, isFocused]); + setTableValues({ rowIndex: coords.row - 1, colIndex: coords.col - 1 }); + }, [coords, setTableValues]); - const handleFocusableNodeFocus = React.useCallback(() => { - const focusableNodes = getFocusableNodes(tdRef.current, true); - - /** - * If there's 1 or more focusable children and at least one has keyboard navigation - * or the children are exclusively button elements the cell should be using the "active" system - */ - if ( - focusableNodes.length >= 1 && - (getFocusableNodesWithKeyboardNav(focusableNodes).length !== 0 || - !focusableNodes.find((node) => node.tagName !== 'BUTTON')) - ) { - setIsActive(true); - } /** - * This function is wrapped in `useCallback` so we can safely - * assume that the reference will not change + * This handles the case where you click on a focusable + * node that has it's own keyboard nav (e.g. Input) */ - setTableValues({ rowIndex: coords.row - 1, colIndex: coords.col - 1 }); - }, [coords, setTableValues]); - - /** - * This handles the case where you click on a focusable - * node that has it's own keyboard nav (e.g. Input) - */ - useIsomorphicLayoutEffect(() => { - const cell = tdRef.current; - const focusableNodes = getFocusableNodes(cell, true); - - focusableNodes.forEach((node) => { - node.addEventListener('focus', handleFocusableNodeFocus); - }); - - return () => { + useIsomorphicLayoutEffect(() => { + const cell = tdRef.current; const focusableNodes = getFocusableNodes(cell, true); + focusableNodes.forEach((node) => { - node.removeEventListener('focus', handleFocusableNodeFocus); + node.addEventListener('focus', handleFocusableNodeFocus); }); - }; - }, [handleFocusableNodeFocus]); - return ; -}; + return () => { + const focusableNodes = getFocusableNodes(cell, true); + focusableNodes.forEach((node) => { + node.removeEventListener('focus', handleFocusableNodeFocus); + }); + }; + }, [handleFocusableNodeFocus]); + + return ; + }, +); /* ------------------------------------------------------------------------------------------------- * RawTh diff --git a/packages/design-system/src/components/RawTable/RawTable.tsx b/packages/design-system/src/components/RawTable/RawTable.tsx index 92040c616..0f5111223 100644 --- a/packages/design-system/src/components/RawTable/RawTable.tsx +++ b/packages/design-system/src/components/RawTable/RawTable.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { KeyboardKeys } from '../../helpers/keyboardKeys'; +import { useComposedRefs } from '../../hooks/useComposeRefs'; import { focusFocusable } from './focusFocusable'; import { RawTableContext } from './RawTableContext'; @@ -13,124 +14,120 @@ export interface RawTableProps extends React.TableHTMLAttributes { - const tableRef = React.useRef(null); - const mountedRef = React.useRef(false); - /** - * Rows will always have n+1 line because of the elements - */ - const [rowIndex, setRowIndex] = React.useState(initialRow); - const [colIndex, setColIndex] = React.useState(initialCol); - - const setTableValues = React.useCallback(({ colIndex, rowIndex }) => { - setColIndex(colIndex); - setRowIndex(rowIndex); - }, []); - - React.useEffect(() => { - if (mountedRef.current) { - focusFocusable(tableRef.current); - } - - if (!mountedRef.current) { - mountedRef.current = true; - } - }, [colIndex, rowIndex]); - - const handleKeyDown = (e) => { - switch (e.key) { - case KeyboardKeys.RIGHT: { - e.preventDefault(); - setColIndex((prevColIndex) => (prevColIndex < colCount - 1 ? prevColIndex + 1 : prevColIndex)); - - break; +export const RawTable = React.forwardRef( + ({ colCount, rowCount, jumpStep = 3, initialCol = 0, initialRow = 0, ...props }, forwardedRef) => { + const tableRef = React.useRef(null); + const mountedRef = React.useRef(false); + const composedRef = useComposedRefs(tableRef, forwardedRef); + /** + * Rows will always have n+1 line because of the elements + */ + const [rowIndex, setRowIndex] = React.useState(initialRow); + const [colIndex, setColIndex] = React.useState(initialCol); + + const setTableValues = React.useCallback(({ colIndex, rowIndex }) => { + setColIndex(colIndex); + setRowIndex(rowIndex); + }, []); + + React.useEffect(() => { + if (mountedRef.current) { + focusFocusable(tableRef.current); } - case KeyboardKeys.LEFT: { - e.preventDefault(); - setColIndex((prevColIndex) => (prevColIndex > 0 ? prevColIndex - 1 : prevColIndex)); - - break; + if (!mountedRef.current) { + mountedRef.current = true; } + }, [colIndex, rowIndex]); - case KeyboardKeys.UP: { - e.preventDefault(); - setRowIndex((prevRowIndex) => (prevRowIndex > 0 ? prevRowIndex - 1 : prevRowIndex)); + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case KeyboardKeys.RIGHT: { + e.preventDefault(); + setColIndex((prevColIndex) => (prevColIndex < colCount - 1 ? prevColIndex + 1 : prevColIndex)); - break; - } + break; + } - case KeyboardKeys.DOWN: { - e.preventDefault(); - setRowIndex((prevRowIndex) => (prevRowIndex < rowCount - 1 ? prevRowIndex + 1 : prevRowIndex)); + case KeyboardKeys.LEFT: { + e.preventDefault(); + setColIndex((prevColIndex) => (prevColIndex > 0 ? prevColIndex - 1 : prevColIndex)); - break; - } + break; + } - case KeyboardKeys.HOME: { - e.preventDefault(); + case KeyboardKeys.UP: { + e.preventDefault(); + setRowIndex((prevRowIndex) => (prevRowIndex > 0 ? prevRowIndex - 1 : prevRowIndex)); - if (e.ctrlKey) { - setRowIndex(0); + break; } - setColIndex(0); + case KeyboardKeys.DOWN: { + e.preventDefault(); + setRowIndex((prevRowIndex) => (prevRowIndex < rowCount - 1 ? prevRowIndex + 1 : prevRowIndex)); - break; - } + break; + } + + case KeyboardKeys.HOME: { + e.preventDefault(); + + if (e.ctrlKey) { + setRowIndex(0); + } - case KeyboardKeys.END: { - e.preventDefault(); + setColIndex(0); - if (e.ctrlKey) { - setRowIndex(rowCount - 1); + break; } - setColIndex(colCount - 1); + case KeyboardKeys.END: { + e.preventDefault(); - break; - } + if (e.ctrlKey) { + setRowIndex(rowCount - 1); + } - case KeyboardKeys.PAGE_DOWN: { - e.preventDefault(); + setColIndex(colCount - 1); - setRowIndex((prevRowIndex) => (prevRowIndex + jumpStep < rowCount ? prevRowIndex + jumpStep : rowCount - 1)); + break; + } - break; - } + case KeyboardKeys.PAGE_DOWN: { + e.preventDefault(); - case KeyboardKeys.PAGE_UP: { - e.preventDefault(); + setRowIndex((prevRowIndex) => (prevRowIndex + jumpStep < rowCount ? prevRowIndex + jumpStep : rowCount - 1)); - setRowIndex((prevRowIndex) => (prevRowIndex - jumpStep > 0 ? prevRowIndex - jumpStep : 0)); + break; + } - break; - } + case KeyboardKeys.PAGE_UP: { + e.preventDefault(); - default: - break; - } - }; - - const context = React.useMemo(() => ({ rowIndex, colIndex, setTableValues }), [colIndex, rowIndex, setTableValues]); - - return ( - - - - ); -}; + setRowIndex((prevRowIndex) => (prevRowIndex - jumpStep > 0 ? prevRowIndex - jumpStep : 0)); + + break; + } + + default: + break; + } + }; + + const context = React.useMemo(() => ({ rowIndex, colIndex, setTableValues }), [colIndex, rowIndex, setTableValues]); + + return ( + +
+ + ); + }, +); diff --git a/packages/design-system/src/components/SimpleMenu/SimpleMenu.tsx b/packages/design-system/src/components/SimpleMenu/SimpleMenu.tsx index d49de6326..b6e390b1a 100644 --- a/packages/design-system/src/components/SimpleMenu/SimpleMenu.tsx +++ b/packages/design-system/src/components/SimpleMenu/SimpleMenu.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { stripReactIdOfColon } from '../../helpers/strings'; +import { useComposedRefs } from '../../hooks/useComposeRefs'; import { useId } from '../../hooks/useId'; import { useIntersection } from '../../hooks/useIntersection'; @@ -23,51 +24,57 @@ interface SimpleMenuProps onReachEnd?: (entry: IntersectionObserverEntry) => void; } -const SimpleMenu = ({ children, onOpen, onClose, popoverPlacement, onReachEnd, ...props }: SimpleMenuProps) => { - /** - * Used for the intersection observer - */ - const contentRef = React.useRef(null); +const SimpleMenu = React.forwardRef( + ({ children, onOpen, onClose, popoverPlacement, onReachEnd, ...props }, forwardedRef) => { + const triggerRef = React.useRef(null); + const composedRef = useComposedRefs(forwardedRef, triggerRef); + /** + * Used for the intersection observer + */ + const contentRef = React.useRef(null); - const [internalIsOpen, setInternalIsOpen] = React.useState(false); + const [internalIsOpen, setInternalIsOpen] = React.useState(false); - const handleReachEnd = (entry: IntersectionObserverEntry) => { - if (onReachEnd) { - onReachEnd(entry); - } - }; + const handleReachEnd = (entry: IntersectionObserverEntry) => { + if (onReachEnd) { + onReachEnd(entry); + } + }; - const handleOpenChange = (isOpen: boolean) => { - if (isOpen && typeof onOpen === 'function') { - onOpen(); - } else if (!isOpen && typeof onClose === 'function') { - onClose(); - } + const handleOpenChange = (isOpen: boolean) => { + if (isOpen && typeof onOpen === 'function') { + onOpen(); + } else if (!isOpen && typeof onClose === 'function') { + onClose(); + } - setInternalIsOpen(isOpen); - }; + setInternalIsOpen(isOpen); + }; - const generatedId = useId(); - const intersectionId = `intersection-${stripReactIdOfColon(generatedId)}`; + const generatedId = useId(); + const intersectionId = `intersection-${stripReactIdOfColon(generatedId)}`; - useIntersection(contentRef, handleReachEnd, { - selectorToWatch: `#${intersectionId}`, - /** - * We need to know when the select is open because only then will viewportRef - * not be null. Because it uses a portal that (sensibly) is not mounted 24/7. - */ - skipWhen: !internalIsOpen, - }); + useIntersection(contentRef, handleReachEnd, { + selectorToWatch: `#${intersectionId}`, + /** + * We need to know when the select is open because only then will viewportRef + * not be null. Because it uses a portal that (sensibly) is not mounted 24/7. + */ + skipWhen: !internalIsOpen, + }); - return ( - - {props.label} - - {children} - - - ); -}; + return ( + + + {props.label} + + + {children} + + + ); + }, +); const MenuItem = Menu.Item; type MenuItemProps = Menu.ItemProps; diff --git a/packages/design-system/src/components/Table/Cell.tsx b/packages/design-system/src/components/Table/Cell.tsx index c33e81ea8..f35eb5490 100644 --- a/packages/design-system/src/components/Table/Cell.tsx +++ b/packages/design-system/src/components/Table/Cell.tsx @@ -26,21 +26,21 @@ export interface ThProps extends RawTdProps { action?: React.ReactNode; } -export const Th = ({ children, action, ...props }: ThProps) => { +export const Th = React.forwardRef(({ children, action, ...props }, forwardedRef) => { return ( - + {children} {action} ); -}; +}); -export const Td = ({ children, ...props }: RawTdProps) => { +export const Td = React.forwardRef(({ children, ...props }, forwardedRef) => { return ( - + {children} ); -}; +}); diff --git a/packages/design-system/src/components/Table/Table.tsx b/packages/design-system/src/components/Table/Table.tsx index 625cb6317..af8b133fd 100644 --- a/packages/design-system/src/components/Table/Table.tsx +++ b/packages/design-system/src/components/Table/Table.tsx @@ -52,7 +52,7 @@ export interface TableProps extends RawTableProps { footer?: React.ReactNode; } -export const Table = ({ footer, ...props }: TableProps) => { +export const Table = React.forwardRef(({ footer, ...props }, forwardedRef) => { const tableRef = React.useRef(null!); const [overflowing, setOverflowing] = React.useState(); @@ -86,10 +86,10 @@ export const Table = ({ footer, ...props }: TableProps) => { - + {footer} ); -}; +});