diff --git a/src/internal/index.ts b/src/internal/index.ts index 442f8a6..7aab1e2 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -37,3 +37,4 @@ export { isFocusable, getAllFocusables, getFirstFocusable, getLastFocusable } fr export { default as handleKey } from './utils/handle-key'; export { default as circleIndex } from './utils/circle-index'; export { default as Portal, PortalProps } from './portal'; +export { useMergeRefs } from './use-merge-refs'; diff --git a/src/internal/use-merge-refs/__tests__/use-merge-refs.test.tsx b/src/internal/use-merge-refs/__tests__/use-merge-refs.test.tsx new file mode 100644 index 0000000..569d775 --- /dev/null +++ b/src/internal/use-merge-refs/__tests__/use-merge-refs.test.tsx @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { useMergeRefs } from '../index'; + +const DemoNull = React.forwardRef((props, ref) => { + const mergedRef = useMergeRefs(null, ref, undefined); + return ( + <> +
+ + ); +}); + +const Demo = React.forwardRef((props, ref) => { + const ref2 = React.createRef(); + const mergedRef = useMergeRefs(ref, ref2); + return ( + <> +
+ + ); +}); + +describe('use merge refs', function () { + it('does not cause component to crash when all refs are null or undefined', () => { + render(); + expect(document.querySelector('.target')).not.toBe(null); + }); + + it('merges ref with null refs', () => { + const ref1 = React.createRef(); + render(); + expect(ref1.current!.classList).toContain('target'); + }); + + it('merges two refs', () => { + const ref1 = React.createRef(); + render(); + expect(ref1.current!.classList).toContain('target'); + }); + + it('ref callback has been called', () => { + const ref1 = jest.fn(); + render(); + expect(ref1).toHaveBeenCalledTimes(1); + expect(ref1).toHaveBeenCalledWith(expect.objectContaining({ className: 'target' })); + }); +}); diff --git a/src/internal/use-merge-refs/index.tsx b/src/internal/use-merge-refs/index.tsx new file mode 100644 index 0000000..92deb0f --- /dev/null +++ b/src/internal/use-merge-refs/index.tsx @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useMemo } from 'react'; + +/** + * useMergeRefs merges multiple refs into single ref callback. + * + * For example + * const mergedRef = useMergeRefs(ref1, ref2, ref3) + *
...
+ */ +export function useMergeRefs( + ...refs: Array | React.MutableRefObject | null | undefined> +): React.RefCallback | null { + return useMemo(() => { + if (refs.every(ref => ref === null || ref === undefined)) { + return null; + } + return (value: T | null) => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value); + } else if (ref !== null && ref !== undefined) { + (ref as React.MutableRefObject).current = value; + } + }); + }; + // ESLint expects an array literal which we can not provide here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, refs); +}