Skip to content

Commit

Permalink
feat(select): add select component
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Bien committed Nov 30, 2020
1 parent 1363853 commit 1b5e4ca
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 1 deletion.
34 changes: 34 additions & 0 deletions example/src/examples/SelectExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import { Panel, Select, Fieldset } from 'react95-native';

const options = ['apple', 'orange', 'banana', 'pear', 'watermelon'].map(o => ({
label: o,
value: o,
}));

const SelectExample = () => {
const [value, setValue] = useState(options[0].value);
return (
<Panel style={{ flex: 1, padding: 20 }}>
<Fieldset label='Disabled:' style={[{ padding: 20 }]}>
<Select
disabled
options={options}
value={value}
onChange={newValue => setValue(newValue)}
style={[{ width: 150 }]}
/>
</Fieldset>
<Fieldset label='Default:' style={[{ padding: 20 }]}>
<Select
options={options}
value={value}
onChange={newValue => setValue(newValue)}
style={[{ width: 150 }]}
/>
</Fieldset>
</Panel>
);
};

export default SelectExample;
2 changes: 2 additions & 0 deletions example/src/examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import WindowExample from './WindowExample';
import FieldsetExample from './FieldsetExample';
import MenuExample from './MenuExample';
import ProgressExample from './ProgressExample';
import SelectExample from './SelectExample';

export default [
{ name: 'ButtonExample', component: ButtonExample, title: 'Button' },
Expand All @@ -26,6 +27,7 @@ export default [
{ name: 'FieldsetExample', component: FieldsetExample, title: 'Fieldset' },
{ name: 'MenuExample', component: MenuExample, title: 'Menu' },
{ name: 'ProgressExample', component: ProgressExample, title: 'Progress' },
{ name: 'SelectExample', component: SelectExample, title: 'Select' },
].sort((a, b) => {
/* Sort screens alphabetically */
if (a.title < b.title) return -1;
Expand Down
2 changes: 1 addition & 1 deletion src/Panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const testId = 'panel';

// TODO: common interface with styleElements/Border ?
type Props = {
children: React.ReactNode;
children?: React.ReactNode;
variant?: 'default' | 'well' | 'outside';
style?: StyleProp<ViewStyle>;
};
Expand Down
255 changes: 255 additions & 0 deletions src/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import React, { useState } from 'react';
import {
StyleSheet,
View,
Text,
TouchableHighlight,
ImageBackground,
} from 'react-native';
import { original as theme } from '../common/themes';
import { blockSizes, text, border } from '../common/styles';
import { Border } from '../common/styleElements';

type Option = {
value: any;
label: React.ReactNode;
};

type SelectItemProps = {
option: Option;
onPress: () => void;
isSelected: boolean;
};

const SelectItem = ({ option, onPress, isSelected }: SelectItemProps) => {
const [isPressed, setIsPressed] = useState(false);

return (
<TouchableHighlight
onPress={() => onPress(option)}
onHideUnderlay={() => setIsPressed(false)}
onShowUnderlay={() => setIsPressed(true)}
accessibilityRole='menuitem'
underlayColor='none'
>
<View
style={[
styles.center,
styles.optionWrapper,
{
backgroundColor:
isPressed || isSelected ? theme.hoverBackground : theme.canvas,
},
]}
>
<Text
style={[
styles.optionText,
{
color:
isPressed || isSelected
? theme.canvasTextInvert
: theme.canvasText,
},
]}
>
{option.label}
</Text>
</View>
</TouchableHighlight>
);
};

const dropdownSymbol = {
default:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAALElEQVQoU2NkIAIwEqGGgWRF/7GYCjYE3SRkhXA5bNaBFKKIk+wmnB4lyiQAAsgDCqkPxTcAAAAASUVORK5CYII=',
disabled:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANklEQVQoU2NkIAIwEqGGgTRFLa0t/9FNramuARuCYhKyQpgCDEUgAZBCZAVYFWHzCGkOxxcUANHnDAplQ9G1AAAAAElFTkSuQmCC',
};

type SelectProps = {
options: Array<Option>;
value: any;
disabled?: boolean;
// TODO: what to put below?
onChange: () => void;
style?: Object;
};

const Select = ({
value,
options = [],
disabled = false,
onChange,
style,
}: SelectProps) => {
const [isOpen, setIsOpen] = useState(false);
const [isPressed, setIsPressed] = useState(false);

const selectedIndex = options.findIndex(option => option.value === value);

function handleOptionSelect(option: Option) {
onChange(option.value);
setIsOpen(false);
}

// TODO: close dropdown when user touches outside of component
// TODO: native prop to use native select

return (
<TouchableHighlight
onPress={() => setIsOpen(currentIsOpen => !currentIsOpen)}
activeOpacity={1}
disabled={disabled}
onHideUnderlay={() => setIsPressed(false)}
onShowUnderlay={() => setIsPressed(true)}
// TODO: accessibility
// accessibilityTraits
// accessibilityComponentType
// accessibilityRole
// accessibilityState
underlayColor='none'
>
<View style={[styles.wrapper, style]}>
<Border variant='cutout' />
<View style={[styles.flex]}>
<View
style={[
styles.value,
{ backgroundColor: disabled ? theme.material : theme.canvas },
]}
>
<View
style={[
styles.center,
{
borderWidth: 2,
borderColor: disabled ? theme.material : theme.canvas,
backgroundColor: disabled
? theme.material
: isPressed
? theme.hoverBackground
: theme.canvas,
},

isPressed && border.focusSecondaryOutline,
]}
>
<Text
style={[
styles.textValue,
disabled ? text.disabled : text.default,
!disabled &&
isPressed && {
color: isPressed
? theme.canvasTextInvert
: theme.canvasText,
},
]}
>
{options[selectedIndex].label}
</Text>
</View>
</View>
<View style={[styles.fakeButton]}>
<ImageBackground
// border to compensate for Border
style={[
{
marginTop: isPressed ? 1 : 0,
width: '100%',
height: '100%',
},
]}
imageStyle={{
resizeMode: 'contain',
flex: 1,
}}
source={{
uri: dropdownSymbol[disabled ? 'disabled' : 'default'],
}}
/>
<Border
variant={isPressed ? 'default' : 'outside'}
invert={isPressed}
/>
</View>
</View>

{isOpen && (
<View style={[styles.options]}>
{options.map((option, index) => (
<SelectItem
key={option.value}
option={option}
isSelected={index === selectedIndex}
onPress={handleOptionSelect}
/>
))}
</View>
)}
</View>
</TouchableHighlight>
);
};

export default Select;

const selectHeight = blockSizes.md + 2;

const styles = StyleSheet.create({
wrapper: {
position: 'relative',
height: selectHeight,
alignSelf: 'flex-start',
padding: 4,
},
flex: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignContent: 'center',
alignItems: 'center',
height: '100%',
},
value: {
flexGrow: 1,
flex: 1,
height: '100%',
padding: 2,
},
center: {
flexGrow: 1,
flex: 1,
height: '100%',
justifyContent: 'center',
},
textValue: {
fontSize: 16,
paddingHorizontal: 4,
},
fakeButton: {
position: 'relative',
height: '100%',
width: 33,
padding: 4,
backgroundColor: theme.material,
},
options: {
position: 'absolute',
top: selectHeight,
left: 2,
right: 4,
backgroundColor: theme.canvas,
borderWidth: 2,
borderColor: theme.borderDarkest,
padding: 2,
},
optionWrapper: {
height: selectHeight - 4,
},
optionText: {
fontSize: 16,
paddingLeft: 6,
},
});
1 change: 1 addition & 0 deletions src/Select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Select';
5 changes: 5 additions & 0 deletions src/common/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const commonBorderStyle = { borderWidth: 2 };

export const border = StyleSheet.create({
/* createBorderStyles({ invert: false, windowBorders: false }) */
focusSecondaryOutline: {
...commonBorderStyle,
borderStyle: 'dotted',
borderColor: theme.focusSecondary,
},
focusOutline: {
...commonBorderStyle,
borderStyle: 'dotted',
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { default as Checkbox } from './Checkbox';
export { default as Radio } from './Radio';
export { default as Fieldset } from './Fieldset';
export { default as Progress } from './Progress';
export { default as Select } from './Select';
export { MenuItem, default as Menu } from './Menu';

export * from './common/themes';

0 comments on commit 1b5e4ca

Please sign in to comment.