Skip to content

Commit a11e2ce

Browse files
authored
fix: Ref was not being passed through for Picker (#2122)
- Pass through ref to SpectrumPicker correctly - Added a `useMultiRef` hook for handling multiple refs being passed in
1 parent 8fe9bad commit a11e2ce

4 files changed

Lines changed: 110 additions & 14 deletions

File tree

packages/components/src/spectrum/comboBox/ComboBox.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,32 @@ import {
33
ComboBox as SpectrumComboBox,
44
SpectrumComboBoxProps,
55
} from '@adobe/react-spectrum';
6-
import type { FocusableRef } from '@react-types/shared';
6+
import type { DOMRef } from '@react-types/shared';
77
import cl from 'classnames';
88
import type { NormalizedItem } from '../utils';
99
import { PickerPropsT, usePickerProps } from '../picker';
10+
import useMultiRef from '../picker/useMultiRef';
1011

1112
export type ComboBoxProps = PickerPropsT<SpectrumComboBoxProps<NormalizedItem>>;
1213

1314
export const ComboBox = React.forwardRef(function ComboBox(
1415
{ UNSAFE_className, ...props }: ComboBoxProps,
15-
ref: FocusableRef<HTMLElement>
16+
ref: DOMRef<HTMLDivElement>
1617
): JSX.Element {
17-
const { defaultSelectedKey, disabledKeys, selectedKey, ...comboBoxProps } =
18-
usePickerProps(props);
19-
18+
const {
19+
defaultSelectedKey,
20+
disabledKeys,
21+
selectedKey,
22+
ref: scrollRef,
23+
...comboBoxProps
24+
} = usePickerProps(props);
25+
const pickerRef = useMultiRef(ref, scrollRef);
2026
return (
2127
<SpectrumComboBox
2228
// eslint-disable-next-line react/jsx-props-no-spreading
2329
{...comboBoxProps}
2430
UNSAFE_className={cl('dh-combobox', UNSAFE_className)}
25-
ref={ref}
31+
ref={pickerRef}
2632
// Type assertions are necessary here since Spectrum types don't account
2733
// for number and boolean key values even though they are valid runtime
2834
// values.

packages/components/src/spectrum/picker/Picker.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import {
22
Picker as SpectrumPicker,
33
SpectrumPickerProps,
44
} from '@adobe/react-spectrum';
5+
import type { DOMRef } from '@react-types/shared';
56
import cl from 'classnames';
7+
import React from 'react';
68
import type { NormalizedItem } from '../utils';
79
import type { PickerProps } from './PickerProps';
10+
import useMultiRef from './useMultiRef';
811
import { usePickerProps } from './usePickerProps';
912

1013
/**
@@ -14,17 +17,23 @@ import { usePickerProps } from './usePickerProps';
1417
* for the Spectrum Picker component.
1518
* See https://react-spectrum.adobe.com/react-spectrum/Picker.html
1619
*/
17-
export function Picker({
18-
UNSAFE_className,
19-
...props
20-
}: PickerProps): JSX.Element {
21-
const { defaultSelectedKey, disabledKeys, selectedKey, ...pickerProps } =
22-
usePickerProps<PickerProps, HTMLDivElement>(props);
23-
20+
export const Picker = React.forwardRef(function Picker(
21+
{ UNSAFE_className, ...props }: PickerProps,
22+
ref: DOMRef<HTMLDivElement>
23+
): JSX.Element {
24+
const {
25+
defaultSelectedKey,
26+
disabledKeys,
27+
selectedKey,
28+
ref: scrollRef,
29+
...pickerProps
30+
} = usePickerProps(props);
31+
const pickerRef = useMultiRef(ref, scrollRef);
2432
return (
2533
<SpectrumPicker
2634
// eslint-disable-next-line react/jsx-props-no-spreading
2735
{...pickerProps}
36+
ref={pickerRef}
2837
UNSAFE_className={cl('dh-picker', UNSAFE_className)}
2938
// Type assertions are necessary here since Spectrum types don't account
3039
// for number and boolean key values even though they are valid runtime
@@ -40,6 +49,6 @@ export function Picker({
4049
}
4150
/>
4251
);
43-
}
52+
});
4453

4554
export default Picker;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import useMultiRef from './useMultiRef';
3+
4+
describe('useMultiRef', () => {
5+
it('should assign the ref to all refs passed in', () => {
6+
const ref1 = jest.fn();
7+
const ref2 = jest.fn();
8+
const ref3 = jest.fn();
9+
const { result } = renderHook(() => useMultiRef(ref1, ref2, ref3));
10+
const multiRef = result.current;
11+
const element = document.createElement('div');
12+
multiRef(element);
13+
expect(ref1).toHaveBeenCalledWith(element);
14+
expect(ref2).toHaveBeenCalledWith(element);
15+
expect(ref3).toHaveBeenCalledWith(element);
16+
});
17+
18+
it('should assign the ref to all refs passed in with null', () => {
19+
const ref1 = jest.fn();
20+
const ref2 = jest.fn();
21+
const ref3 = jest.fn();
22+
const { result } = renderHook(() => useMultiRef(ref1, ref2, ref3));
23+
const multiRef = result.current;
24+
multiRef(null);
25+
expect(ref1).toHaveBeenCalledWith(null);
26+
expect(ref2).toHaveBeenCalledWith(null);
27+
expect(ref3).toHaveBeenCalledWith(null);
28+
});
29+
30+
it('should work with non-function refs', () => {
31+
const ref1 = { current: null };
32+
const ref2 = { current: null };
33+
const ref3 = { current: null };
34+
const { result } = renderHook(() =>
35+
useMultiRef<HTMLDivElement | null>(ref1, ref2, ref3)
36+
);
37+
const multiRef = result.current;
38+
const element = document.createElement('div');
39+
multiRef(element);
40+
expect(ref1.current).toBe(element);
41+
expect(ref2.current).toBe(element);
42+
expect(ref3.current).toBe(element);
43+
});
44+
45+
it('should handle a mix of function and non-function refs', () => {
46+
const ref1 = jest.fn();
47+
const ref2 = { current: null };
48+
const ref3 = jest.fn();
49+
const { result } = renderHook(() =>
50+
useMultiRef<HTMLDivElement | null>(ref1, ref2, ref3)
51+
);
52+
const multiRef = result.current;
53+
const element = document.createElement('div');
54+
multiRef(element);
55+
expect(ref1).toHaveBeenCalledWith(element);
56+
expect(ref2.current).toBe(element);
57+
expect(ref3).toHaveBeenCalledWith(element);
58+
});
59+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { MutableRefObject, Ref, RefCallback, useCallback } from 'react';
2+
3+
/**
4+
* Takes in multiple refs and then returns one ref that can be assigned to the component.
5+
* In turn all the refs passed in will be assigned when the ref returned is assigned.
6+
* @param refs The refs to assign
7+
*/
8+
function useMultiRef<T>(...refs: readonly Ref<T>[]): RefCallback<T> {
9+
return useCallback(newRef => {
10+
refs.forEach(ref => {
11+
if (typeof ref === 'function') {
12+
ref(newRef);
13+
} else if (ref != null) {
14+
// eslint-disable-next-line no-param-reassign
15+
(ref as MutableRefObject<T | null>).current = newRef;
16+
}
17+
});
18+
// eslint-disable-next-line react-hooks/exhaustive-deps
19+
}, refs);
20+
}
21+
22+
export default useMultiRef;

0 commit comments

Comments
 (0)