Skip to content

Commit c173d15

Browse files
authored
chore: Adds internal use-merge-refs util (#131)
1 parent 28ff166 commit c173d15

File tree

3 files changed

+85
-0
lines changed

3 files changed

+85
-0
lines changed

src/internal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ export { isFocusable, getAllFocusables, getFirstFocusable, getLastFocusable } fr
3737
export { default as handleKey } from './utils/handle-key';
3838
export { default as circleIndex } from './utils/circle-index';
3939
export { default as Portal, PortalProps } from './portal';
40+
export { useMergeRefs } from './use-merge-refs';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React from 'react';
5+
import { render } from '@testing-library/react';
6+
7+
import { useMergeRefs } from '../index';
8+
9+
const DemoNull = React.forwardRef((props, ref) => {
10+
const mergedRef = useMergeRefs(null, ref, undefined);
11+
return (
12+
<>
13+
<div ref={mergedRef} className="target"></div>
14+
</>
15+
);
16+
});
17+
18+
const Demo = React.forwardRef((props, ref) => {
19+
const ref2 = React.createRef<HTMLDivElement>();
20+
const mergedRef = useMergeRefs(ref, ref2);
21+
return (
22+
<>
23+
<div ref={mergedRef} className="target"></div>
24+
</>
25+
);
26+
});
27+
28+
describe('use merge refs', function () {
29+
it('does not cause component to crash when all refs are null or undefined', () => {
30+
render(<DemoNull ref={null} />);
31+
expect(document.querySelector('.target')).not.toBe(null);
32+
});
33+
34+
it('merges ref with null refs', () => {
35+
const ref1 = React.createRef<HTMLDivElement>();
36+
render(<DemoNull ref={ref1} />);
37+
expect(ref1.current!.classList).toContain('target');
38+
});
39+
40+
it('merges two refs', () => {
41+
const ref1 = React.createRef<HTMLDivElement>();
42+
render(<Demo ref={ref1} />);
43+
expect(ref1.current!.classList).toContain('target');
44+
});
45+
46+
it('ref callback has been called', () => {
47+
const ref1 = jest.fn();
48+
render(<Demo ref={ref1} />);
49+
expect(ref1).toHaveBeenCalledTimes(1);
50+
expect(ref1).toHaveBeenCalledWith(expect.objectContaining({ className: 'target' }));
51+
});
52+
});

src/internal/use-merge-refs/index.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useMemo } from 'react';
5+
6+
/**
7+
* useMergeRefs merges multiple refs into single ref callback.
8+
*
9+
* For example
10+
* const mergedRef = useMergeRefs(ref1, ref2, ref3)
11+
* <div ref={refs}>...</div>
12+
*/
13+
export function useMergeRefs<T = any>(
14+
...refs: Array<React.RefCallback<T> | React.MutableRefObject<T> | null | undefined>
15+
): React.RefCallback<T> | null {
16+
return useMemo(() => {
17+
if (refs.every(ref => ref === null || ref === undefined)) {
18+
return null;
19+
}
20+
return (value: T | null) => {
21+
refs.forEach(ref => {
22+
if (typeof ref === 'function') {
23+
ref(value);
24+
} else if (ref !== null && ref !== undefined) {
25+
(ref as React.MutableRefObject<any>).current = value;
26+
}
27+
});
28+
};
29+
// ESLint expects an array literal which we can not provide here
30+
// eslint-disable-next-line react-hooks/exhaustive-deps
31+
}, refs);
32+
}

0 commit comments

Comments
 (0)