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}
);
-};
+});