Skip to content

Commit

Permalink
fix: Forwarding ref in Select components (#492)
Browse files Browse the repository at this point in the history
* Split Select components

* Forward refs of Select and MultiSelect
  • Loading branch information
tassoevan authored Jul 8, 2021
1 parent a6ce6aa commit 6c360bd
Show file tree
Hide file tree
Showing 50 changed files with 493 additions and 385 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
199 changes: 199 additions & 0 deletions packages/fuselage/src/components/MultiSelect/MultiSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {
useMergedRefs,
useMutableCallback,
useResizeObserver,
} from '@rocket.chat/fuselage-hooks';
import React, {
useState,
useRef,
useEffect,
useCallback,
memo,
forwardRef,
} from 'react';

import { AnimatedVisibility, Box, Flex, Position } from '../Box';
import Chip from '../Chip';
import { Icon } from '../Icon';
import { InputBox } from '../InputBox';
import Margins from '../Margins';
import { Options, CheckOption, useCursor } from '../Options';
import { Focus, Addon } from '../Select/Select';

const SelectedOptions = memo((props) => <Chip {...props} />);

const prevent = (e) => {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
};

export const MultiSelect = forwardRef(
(
{
value,
filter,
options = [],
error,
disabled,
anchor: Anchor = Focus,
onChange = () => {},
getLabel = ([, label] = []) => label,
getValue = ([value]) => value,
placeholder,
renderOptions: _Options = Options,
...props
},
ref
) => {
const [internalValue, setInternalValue] = useState(value || []);

const currentValue = value !== undefined ? value : internalValue;
const option = options.find((option) => getValue(option) === currentValue);
const index = options.indexOf(option);

const internalChanged = ([value]) => {
if (currentValue.includes(value)) {
const newValue = currentValue.filter((item) => item !== value);
setInternalValue(newValue);
return onChange(newValue);
}
const newValue = [...currentValue, value];
setInternalValue(newValue);
return onChange(newValue);
};

const mapOptions = ([value, label]) => {
if (currentValue.includes(value)) {
return [value, label, true];
}
return [value, label];
};
const applyFilter = ([, option]) =>
!filter || ~option.toLowerCase().indexOf(filter.toLowerCase());
const filteredOptions = options.filter(applyFilter).map(mapOptions);
const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] =
useCursor(index, filteredOptions, internalChanged);

useEffect(reset, [filter]);

const innerRef = useRef();
const anchorRef = useMergedRefs(ref, innerRef);

const { ref: containerRef, borderBoxSize } = useResizeObserver();

return (
<Box
is='div'
rcx-select
className={[error && 'invalid', disabled && 'disabled']}
ref={containerRef}
onClick={useMutableCallback(() =>
visible === AnimatedVisibility.VISIBLE
? hide()
: innerRef.current.focus() & show()
)}
disabled={disabled}
{...props}
>
<Flex.Item grow={1}>
<Margins inline='x4'>
<Flex.Container>
<Box is='div'>
<Box
is='div'
display='flex'
alignItems='center'
flexWrap='wrap'
margin='-x8'
role='listbox'
>
<Margins all='x4'>
<Anchor
disabled={disabled}
ref={anchorRef}
aria-haspopup='listbox'
onClick={show}
onBlur={hide}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
order={1}
rcx-input-box--undecorated
children={!value ? option || placeholder : null}
/>
{currentValue.map((value) => (
<SelectedOptions
tabIndex={-1}
role='option'
key={value}
onMouseDown={(e) =>
prevent(e) & internalChanged([value]) && false
}
children={getLabel(
options.find(([val]) => val === value)
)}
/>
))}
</Margins>
</Box>
</Box>
</Flex.Container>
</Margins>
</Flex.Item>
<Flex.Item grow={0} shrink={0}>
<Margins inline='x4'>
<Addon
children={
<Icon
name={
visible === AnimatedVisibility.VISIBLE
? 'cross'
: 'chevron-down'
}
size='x20'
/>
}
/>
</Margins>
</Flex.Item>
<AnimatedVisibility visibility={visible}>
<Position anchor={containerRef}>
<_Options
width={borderBoxSize.inlineSize}
onMouseDown={prevent}
multiple
filter={filter}
renderItem={CheckOption}
role='listbox'
options={filteredOptions}
onSelect={internalChanged}
cursor={cursor}
/>
</Position>
</AnimatedVisibility>
</Box>
);
}
);

export const MultiSelectFiltered = ({ options, placeholder, ...props }) => {
const [filter, setFilter] = useState('');
const anchor = useCallback(
forwardRef(({ children, filter, ...props }, ref) => (
<Flex.Item grow={1}>
<InputBox.Input
ref={ref}
placeholder={placeholder}
value={filter}
onInput={(e) => setFilter(e.currentTarget.value)}
{...props}
rcx-input-box--undecorated
/>
</Flex.Item>
)),
[]
);
return (
<MultiSelect filter={filter} options={options} {...props} anchor={anchor} />
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { action } from '@storybook/addon-actions';
import { Meta, Canvas, ArgsTable, Story } from '@storybook/addon-docs/blocks';

import { MultiSelect, MultiSelectFiltered } from '../..';

export const thumb =
'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
export const options = [
[1, 'a teste 1'],
[2, 'b teste 2', true],
[3, 'c teste 3'],
[4, 'd teste 4'],
[5, 'd teste 5'],
[6, 'd teste 6'],
[7, 'd teste 7'],
[8, 'd teste 8'],
[9, 'd teste 9'],
[10, 'd teste 10'],
];
export const optionsEllipses = [
[11, 'Very very very very very very very very very large text'],
...options,
];

<Meta
title='Forms/MultiSelect'
component={MultiSelect}
parameters={{ jest: ['Select/spec'] }}
/>

# MultiSelect

An input for selection of options.

<Canvas>
<Story name='MultiSelect'>
<MultiSelect placeholder='Placeholder here...' options={options} />
</Story>
</Canvas>

<Canvas>
<Story name='MultiSelect Error'>
<MultiSelect error placeholder='Placeholder here...' options={options} />
</Story>
</Canvas>

<Canvas>
<Story name='MultiSelect Disabled'>
<MultiSelect disabled placeholder='Placeholder here...' options={options} />
</Story>
</Canvas>

<ArgsTable of={MultiSelect} />

# MultiSelectFiltered

An input for selection of options.

<Canvas>
<Story name='MultiSelectFiltered'>
<MultiSelectFiltered
placeholder='Placeholder here...'
onChange={action('change')}
options={options}
/>
</Story>
</Canvas>

<Canvas>
<Story name='MultiSelectFiltered error'>
<MultiSelectFiltered
error
placeholder='Placeholder here...'
onChange={action('change')}
options={options}
/>
</Story>
</Canvas>

<Canvas>
<Story name='MultiSelectFiltered disabled'>
<MultiSelectFiltered
disabled
placeholder='Placeholder here...'
onChange={action('change')}
options={options}
/>
</Story>
</Canvas>

<ArgsTable of={MultiSelectFiltered} />
15 changes: 15 additions & 0 deletions packages/fuselage/src/components/MultiSelect/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentProps, ForwardRefExoticComponent } from 'react';

import { Box } from '../Box';

type MultiSelectOptions = readonly (readonly [string, string])[];

type MultiSelectProps = Omit<ComponentProps<typeof Box>, 'onChange'> & {
error?: string;
options: MultiSelectOptions;
onChange: (value: MultiSelectOptions[number][0]) => void;
};

export const MultiSelect: ForwardRefExoticComponent<MultiSelectProps>;

export const MultiSelectFiltered: ForwardRefExoticComponent<MultiSelectProps>;
1 change: 1 addition & 0 deletions packages/fuselage/src/components/MultiSelect/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './MultiSelect';
Loading

0 comments on commit 6c360bd

Please sign in to comment.