Skip to content

Commit 6c360bd

Browse files
authored
fix: Forwarding ref in Select components (#492)
* Split Select components * Forward refs of Select and MultiSelect
1 parent a6ce6aa commit 6c360bd

File tree

50 files changed

+493
-385
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+493
-385
lines changed
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {
2+
useMergedRefs,
3+
useMutableCallback,
4+
useResizeObserver,
5+
} from '@rocket.chat/fuselage-hooks';
6+
import React, {
7+
useState,
8+
useRef,
9+
useEffect,
10+
useCallback,
11+
memo,
12+
forwardRef,
13+
} from 'react';
14+
15+
import { AnimatedVisibility, Box, Flex, Position } from '../Box';
16+
import Chip from '../Chip';
17+
import { Icon } from '../Icon';
18+
import { InputBox } from '../InputBox';
19+
import Margins from '../Margins';
20+
import { Options, CheckOption, useCursor } from '../Options';
21+
import { Focus, Addon } from '../Select/Select';
22+
23+
const SelectedOptions = memo((props) => <Chip {...props} />);
24+
25+
const prevent = (e) => {
26+
e.preventDefault();
27+
e.stopPropagation();
28+
e.nativeEvent.stopImmediatePropagation();
29+
};
30+
31+
export const MultiSelect = forwardRef(
32+
(
33+
{
34+
value,
35+
filter,
36+
options = [],
37+
error,
38+
disabled,
39+
anchor: Anchor = Focus,
40+
onChange = () => {},
41+
getLabel = ([, label] = []) => label,
42+
getValue = ([value]) => value,
43+
placeholder,
44+
renderOptions: _Options = Options,
45+
...props
46+
},
47+
ref
48+
) => {
49+
const [internalValue, setInternalValue] = useState(value || []);
50+
51+
const currentValue = value !== undefined ? value : internalValue;
52+
const option = options.find((option) => getValue(option) === currentValue);
53+
const index = options.indexOf(option);
54+
55+
const internalChanged = ([value]) => {
56+
if (currentValue.includes(value)) {
57+
const newValue = currentValue.filter((item) => item !== value);
58+
setInternalValue(newValue);
59+
return onChange(newValue);
60+
}
61+
const newValue = [...currentValue, value];
62+
setInternalValue(newValue);
63+
return onChange(newValue);
64+
};
65+
66+
const mapOptions = ([value, label]) => {
67+
if (currentValue.includes(value)) {
68+
return [value, label, true];
69+
}
70+
return [value, label];
71+
};
72+
const applyFilter = ([, option]) =>
73+
!filter || ~option.toLowerCase().indexOf(filter.toLowerCase());
74+
const filteredOptions = options.filter(applyFilter).map(mapOptions);
75+
const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] =
76+
useCursor(index, filteredOptions, internalChanged);
77+
78+
useEffect(reset, [filter]);
79+
80+
const innerRef = useRef();
81+
const anchorRef = useMergedRefs(ref, innerRef);
82+
83+
const { ref: containerRef, borderBoxSize } = useResizeObserver();
84+
85+
return (
86+
<Box
87+
is='div'
88+
rcx-select
89+
className={[error && 'invalid', disabled && 'disabled']}
90+
ref={containerRef}
91+
onClick={useMutableCallback(() =>
92+
visible === AnimatedVisibility.VISIBLE
93+
? hide()
94+
: innerRef.current.focus() & show()
95+
)}
96+
disabled={disabled}
97+
{...props}
98+
>
99+
<Flex.Item grow={1}>
100+
<Margins inline='x4'>
101+
<Flex.Container>
102+
<Box is='div'>
103+
<Box
104+
is='div'
105+
display='flex'
106+
alignItems='center'
107+
flexWrap='wrap'
108+
margin='-x8'
109+
role='listbox'
110+
>
111+
<Margins all='x4'>
112+
<Anchor
113+
disabled={disabled}
114+
ref={anchorRef}
115+
aria-haspopup='listbox'
116+
onClick={show}
117+
onBlur={hide}
118+
onKeyUp={handleKeyUp}
119+
onKeyDown={handleKeyDown}
120+
order={1}
121+
rcx-input-box--undecorated
122+
children={!value ? option || placeholder : null}
123+
/>
124+
{currentValue.map((value) => (
125+
<SelectedOptions
126+
tabIndex={-1}
127+
role='option'
128+
key={value}
129+
onMouseDown={(e) =>
130+
prevent(e) & internalChanged([value]) && false
131+
}
132+
children={getLabel(
133+
options.find(([val]) => val === value)
134+
)}
135+
/>
136+
))}
137+
</Margins>
138+
</Box>
139+
</Box>
140+
</Flex.Container>
141+
</Margins>
142+
</Flex.Item>
143+
<Flex.Item grow={0} shrink={0}>
144+
<Margins inline='x4'>
145+
<Addon
146+
children={
147+
<Icon
148+
name={
149+
visible === AnimatedVisibility.VISIBLE
150+
? 'cross'
151+
: 'chevron-down'
152+
}
153+
size='x20'
154+
/>
155+
}
156+
/>
157+
</Margins>
158+
</Flex.Item>
159+
<AnimatedVisibility visibility={visible}>
160+
<Position anchor={containerRef}>
161+
<_Options
162+
width={borderBoxSize.inlineSize}
163+
onMouseDown={prevent}
164+
multiple
165+
filter={filter}
166+
renderItem={CheckOption}
167+
role='listbox'
168+
options={filteredOptions}
169+
onSelect={internalChanged}
170+
cursor={cursor}
171+
/>
172+
</Position>
173+
</AnimatedVisibility>
174+
</Box>
175+
);
176+
}
177+
);
178+
179+
export const MultiSelectFiltered = ({ options, placeholder, ...props }) => {
180+
const [filter, setFilter] = useState('');
181+
const anchor = useCallback(
182+
forwardRef(({ children, filter, ...props }, ref) => (
183+
<Flex.Item grow={1}>
184+
<InputBox.Input
185+
ref={ref}
186+
placeholder={placeholder}
187+
value={filter}
188+
onInput={(e) => setFilter(e.currentTarget.value)}
189+
{...props}
190+
rcx-input-box--undecorated
191+
/>
192+
</Flex.Item>
193+
)),
194+
[]
195+
);
196+
return (
197+
<MultiSelect filter={filter} options={options} {...props} anchor={anchor} />
198+
);
199+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ComponentProps, ForwardRefExoticComponent } from 'react';
2+
3+
import { Box } from '../Box';
4+
5+
type MultiSelectOptions = readonly (readonly [string, string])[];
6+
7+
type MultiSelectProps = Omit<ComponentProps<typeof Box>, 'onChange'> & {
8+
error?: string;
9+
options: MultiSelectOptions;
10+
onChange: (value: MultiSelectOptions[number][0]) => void;
11+
};
12+
13+
export const MultiSelect: ForwardRefExoticComponent<MultiSelectProps>;
14+
15+
export const MultiSelectFiltered: ForwardRefExoticComponent<MultiSelectProps>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './MultiSelect';

0 commit comments

Comments
 (0)