Skip to content

Commit

Permalink
feat: custom components (#23)
Browse files Browse the repository at this point in the history
* feat: add custom compontents: NoOptionsComponent and OptionComponent

* fix: eslint errors
  • Loading branch information
irekrog authored Nov 27, 2021
1 parent fa1927a commit 1abad7f
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 14 deletions.
4 changes: 4 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SelectProvider } from '@mobile-reality/react-native-select-pro';

import { Basic } from './examples/Basic';
import { Callbacks } from './examples/Callbacks';
import { CustomComponent } from './examples/CustomComponent';
import { CustomStyles } from './examples/CustomStyles';
import { ModalExample } from './examples/ModalExample';
import { Overflow } from './examples/Overflow';
Expand Down Expand Up @@ -48,6 +49,7 @@ const examples = [
'Custom Styles',
'Ref',
'Callbacks',
'Custom Component',
];

export default function App() {
Expand Down Expand Up @@ -80,6 +82,8 @@ export default function App() {
return <Ref />;
case 'Callbacks':
return <Callbacks />;
case 'Custom Component':
return <CustomComponent />;
default:
return null;
}
Expand Down
38 changes: 38 additions & 0 deletions example/src/examples/CustomComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { SafeAreaView, Text, TouchableOpacity, View } from 'react-native';
import { OptionComponentProps, Select } from '@mobile-reality/react-native-select-pro';

import { DATA } from '../App';

const MyOption = ({ option, onPressOption }: OptionComponentProps) => {
return (
<TouchableOpacity onPress={onPressOption}>
<Text>{option.label}</Text>
</TouchableOpacity>
);
};

const NoOptions = () => {
return (
<View>
<Text>Custom component no options</Text>
</View>
);
};

export const CustomComponent = () => {
return (
<SafeAreaView>
<Select
OptionComponent={(props) => <MyOption {...props} />}
options={DATA}
selectControlStyle={{ width: 200 }}
/>
<Select
NoOptionsComponent={<NoOptions />}
options={[]}
selectControlStyle={{ width: 200 }}
/>
</SafeAreaView>
);
};
30 changes: 29 additions & 1 deletion src/__tests__/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { MeasureOnSuccessCallback, View } from 'react-native';
import { MeasureOnSuccessCallback, Pressable, Text, View } from 'react-native';
import type { OptionComponentProps } from '@mobile-reality/react-native-select-pro';
import { fireEvent, render } from '@testing-library/react-native';

import { Select } from '../components/select';
Expand Down Expand Up @@ -89,4 +90,31 @@ describe('Select', () => {
const listWrapper2 = queryByA11yLabel('Options list');
expect(listWrapper2).toBeFalsy();
});

it('should select option with custom option component', () => {
const MyCustomOption = ({ onPressOption, option }: OptionComponentProps) => {
return (
<Pressable accessibilityLabel={'Option'} onPress={onPressOption}>
<Text>{option.label}</Text>
</Pressable>
);
};
const { getByA11yLabel } = render(
<SelectProvider>
<Select
OptionComponent={(props) => <MyCustomOption {...props} />}
options={[DATA[0]]}
/>
</SelectProvider>,
);

const open = getByA11yLabel('Open a dropdown');
fireEvent.press(open);

const list = getByA11yLabel('Options list');
expect(list).toBeTruthy();

const option = getByA11yLabel('Option');
fireEvent.press(option);
});
});
45 changes: 33 additions & 12 deletions src/components/option/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import type { OptionsList } from '../options-list';

type FromSelectComponentProps = Pick<
ComponentProps<typeof OptionsList>,
'optionSelectedStyle' | 'optionStyle' | 'optionTextStyle' | 'onSelect' | 'onPressOption'
| 'optionSelectedStyle'
| 'optionStyle'
| 'optionTextStyle'
| 'onSelect'
| 'onPressOption'
| 'OptionComponent'
>;
type OptionProps = OptionalToRequired<FromSelectComponentProps> & {

export type OptionProps = OptionalToRequired<FromSelectComponentProps> & {
isSelected: boolean;
option: OptionType;
};

export type OnChooseOption = () => void;

export const Option = ({
optionSelectedStyle,
optionStyle,
Expand All @@ -23,25 +31,38 @@ export const Option = ({
onPressOption,
option,
onSelect,
OptionComponent,
}: OptionProps) => {
const { label } = option;

const onChooseOption: OnChooseOption = () => {
onPressOption(option);
if (onSelect) {
onSelect(option);
}
};

if (OptionComponent) {
return (
<OptionComponent
isSelected={isSelected}
onPressOption={onChooseOption}
option={option}
/>
);
}

return (
<TouchableOpacity
accessibilityLabel={`Choose ${option.label} option`}
onPress={() => {
if (onPressOption) {
onPressOption(option);
}
if (onSelect) {
onSelect(option);
}
}}
accessibilityLabel={`Choose ${label} option`}
onPress={onChooseOption}
style={[
styles.option,
optionStyle,
isSelected && [styles.selected, optionSelectedStyle],
]}>
<Text numberOfLines={1} style={[styles.text, optionTextStyle]}>
{option.label}
{label}
</Text>
</TouchableOpacity>
);
Expand Down
11 changes: 10 additions & 1 deletion src/components/options-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type FromSelectComponentProps = Pick<
| 'noOptionsText'
| 'onSelect'
| 'optionsListStyle'
| 'NoOptionsComponent'
| 'OptionComponent'
>;

type OptionsListProps = OptionalToRequired<
Expand All @@ -45,6 +47,8 @@ export const OptionsList = ({
noOptionsText,
onSelect,
optionsListStyle,
NoOptionsComponent,
OptionComponent,
}: OptionsListProps) => {
return (
<>
Expand Down Expand Up @@ -76,6 +80,7 @@ export const OptionsList = ({
const { value } = item;
return (
<Option
OptionComponent={OptionComponent}
isSelected={value === selectedOption?.value}
key={value}
onPressOption={onPressOption}
Expand All @@ -88,7 +93,11 @@ export const OptionsList = ({
);
}}
{...flatListProps}
ListEmptyComponent={<NoOptions noOptionsText={noOptionsText} />}
ListEmptyComponent={
NoOptionsComponent || (
<NoOptions noOptionsText={noOptionsText} />
)
}
/>
</View>
</Portal>
Expand Down
4 changes: 4 additions & 0 deletions src/components/select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const Select = forwardRef((props: SelectProps, ref: ForwardedRef<SelectRe
selectControlStyle,
selectControlTextStyle,
optionsListStyle,
NoOptionsComponent,
OptionComponent,
} = props;
const [{ isOpened, selectedOption, optionsData, openedPosition }, dispatch] = useReducer(
reducer,
Expand Down Expand Up @@ -184,6 +186,8 @@ export const Select = forwardRef((props: SelectProps, ref: ForwardedRef<SelectRe
selectedOption={selectedOption}
/>
<OptionsList
NoOptionsComponent={NoOptionsComponent}
OptionComponent={OptionComponent}
aboveSelectControl={aboveSelectControl}
flatListProps={flatListProps}
isOpened={isOpened}
Expand Down
22 changes: 22 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
import type {
FlatListProps,
ImageStyle,
Expand All @@ -7,6 +8,8 @@ import type {
ViewStyle,
} from 'react-native';

import type { OnChooseOption, OptionProps } from '../components/option';

export type OptionType = {
label: string;
value: string;
Expand All @@ -15,6 +18,10 @@ export type OptionType = {

export type OptionsType = OptionType[];

export type OptionComponentProps = Pick<OptionProps, 'isSelected' | 'option'> & {
onPressOption: OnChooseOption;
};

/**
* `<Select />` component props
*/
Expand Down Expand Up @@ -204,6 +211,21 @@ export interface SelectProps {
* @default "Close a dropdown"
*/
selectControlCloseDropdownA11yLabel?: string;

/**
* NoOptionsComponent
*
* @category Custom Component
*/
NoOptionsComponent?: JSX.Element;

/**
* OptionComponent
*
* @param props OptionComponentProps
* @category Custom Component
*/
OptionComponent?: (props: OptionComponentProps) => JSX.Element;
}

/**
Expand Down

0 comments on commit 1abad7f

Please sign in to comment.