Skip to content

Commit 1b5e4ca

Browse files
author
Artur Bien
committed
feat(select): add select component
1 parent 1363853 commit 1b5e4ca

File tree

7 files changed

+299
-1
lines changed

7 files changed

+299
-1
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { useState } from 'react';
2+
import { Panel, Select, Fieldset } from 'react95-native';
3+
4+
const options = ['apple', 'orange', 'banana', 'pear', 'watermelon'].map(o => ({
5+
label: o,
6+
value: o,
7+
}));
8+
9+
const SelectExample = () => {
10+
const [value, setValue] = useState(options[0].value);
11+
return (
12+
<Panel style={{ flex: 1, padding: 20 }}>
13+
<Fieldset label='Disabled:' style={[{ padding: 20 }]}>
14+
<Select
15+
disabled
16+
options={options}
17+
value={value}
18+
onChange={newValue => setValue(newValue)}
19+
style={[{ width: 150 }]}
20+
/>
21+
</Fieldset>
22+
<Fieldset label='Default:' style={[{ padding: 20 }]}>
23+
<Select
24+
options={options}
25+
value={value}
26+
onChange={newValue => setValue(newValue)}
27+
style={[{ width: 150 }]}
28+
/>
29+
</Fieldset>
30+
</Panel>
31+
);
32+
};
33+
34+
export default SelectExample;

example/src/examples/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import WindowExample from './WindowExample';
1111
import FieldsetExample from './FieldsetExample';
1212
import MenuExample from './MenuExample';
1313
import ProgressExample from './ProgressExample';
14+
import SelectExample from './SelectExample';
1415

1516
export default [
1617
{ name: 'ButtonExample', component: ButtonExample, title: 'Button' },
@@ -26,6 +27,7 @@ export default [
2627
{ name: 'FieldsetExample', component: FieldsetExample, title: 'Fieldset' },
2728
{ name: 'MenuExample', component: MenuExample, title: 'Menu' },
2829
{ name: 'ProgressExample', component: ProgressExample, title: 'Progress' },
30+
{ name: 'SelectExample', component: SelectExample, title: 'Select' },
2931
].sort((a, b) => {
3032
/* Sort screens alphabetically */
3133
if (a.title < b.title) return -1;

src/Panel/Panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const testId = 'panel';
88

99
// TODO: common interface with styleElements/Border ?
1010
type Props = {
11-
children: React.ReactNode;
11+
children?: React.ReactNode;
1212
variant?: 'default' | 'well' | 'outside';
1313
style?: StyleProp<ViewStyle>;
1414
};

src/Select/Select.tsx

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import React, { useState } from 'react';
2+
import {
3+
StyleSheet,
4+
View,
5+
Text,
6+
TouchableHighlight,
7+
ImageBackground,
8+
} from 'react-native';
9+
import { original as theme } from '../common/themes';
10+
import { blockSizes, text, border } from '../common/styles';
11+
import { Border } from '../common/styleElements';
12+
13+
type Option = {
14+
value: any;
15+
label: React.ReactNode;
16+
};
17+
18+
type SelectItemProps = {
19+
option: Option;
20+
onPress: () => void;
21+
isSelected: boolean;
22+
};
23+
24+
const SelectItem = ({ option, onPress, isSelected }: SelectItemProps) => {
25+
const [isPressed, setIsPressed] = useState(false);
26+
27+
return (
28+
<TouchableHighlight
29+
onPress={() => onPress(option)}
30+
onHideUnderlay={() => setIsPressed(false)}
31+
onShowUnderlay={() => setIsPressed(true)}
32+
accessibilityRole='menuitem'
33+
underlayColor='none'
34+
>
35+
<View
36+
style={[
37+
styles.center,
38+
styles.optionWrapper,
39+
{
40+
backgroundColor:
41+
isPressed || isSelected ? theme.hoverBackground : theme.canvas,
42+
},
43+
]}
44+
>
45+
<Text
46+
style={[
47+
styles.optionText,
48+
{
49+
color:
50+
isPressed || isSelected
51+
? theme.canvasTextInvert
52+
: theme.canvasText,
53+
},
54+
]}
55+
>
56+
{option.label}
57+
</Text>
58+
</View>
59+
</TouchableHighlight>
60+
);
61+
};
62+
63+
const dropdownSymbol = {
64+
default:
65+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAALElEQVQoU2NkIAIwEqGGgWRF/7GYCjYE3SRkhXA5bNaBFKKIk+wmnB4lyiQAAsgDCqkPxTcAAAAASUVORK5CYII=',
66+
disabled:
67+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANklEQVQoU2NkIAIwEqGGgTRFLa0t/9FNramuARuCYhKyQpgCDEUgAZBCZAVYFWHzCGkOxxcUANHnDAplQ9G1AAAAAElFTkSuQmCC',
68+
};
69+
70+
type SelectProps = {
71+
options: Array<Option>;
72+
value: any;
73+
disabled?: boolean;
74+
// TODO: what to put below?
75+
onChange: () => void;
76+
style?: Object;
77+
};
78+
79+
const Select = ({
80+
value,
81+
options = [],
82+
disabled = false,
83+
onChange,
84+
style,
85+
}: SelectProps) => {
86+
const [isOpen, setIsOpen] = useState(false);
87+
const [isPressed, setIsPressed] = useState(false);
88+
89+
const selectedIndex = options.findIndex(option => option.value === value);
90+
91+
function handleOptionSelect(option: Option) {
92+
onChange(option.value);
93+
setIsOpen(false);
94+
}
95+
96+
// TODO: close dropdown when user touches outside of component
97+
// TODO: native prop to use native select
98+
99+
return (
100+
<TouchableHighlight
101+
onPress={() => setIsOpen(currentIsOpen => !currentIsOpen)}
102+
activeOpacity={1}
103+
disabled={disabled}
104+
onHideUnderlay={() => setIsPressed(false)}
105+
onShowUnderlay={() => setIsPressed(true)}
106+
// TODO: accessibility
107+
// accessibilityTraits
108+
// accessibilityComponentType
109+
// accessibilityRole
110+
// accessibilityState
111+
underlayColor='none'
112+
>
113+
<View style={[styles.wrapper, style]}>
114+
<Border variant='cutout' />
115+
<View style={[styles.flex]}>
116+
<View
117+
style={[
118+
styles.value,
119+
{ backgroundColor: disabled ? theme.material : theme.canvas },
120+
]}
121+
>
122+
<View
123+
style={[
124+
styles.center,
125+
{
126+
borderWidth: 2,
127+
borderColor: disabled ? theme.material : theme.canvas,
128+
backgroundColor: disabled
129+
? theme.material
130+
: isPressed
131+
? theme.hoverBackground
132+
: theme.canvas,
133+
},
134+
135+
isPressed && border.focusSecondaryOutline,
136+
]}
137+
>
138+
<Text
139+
style={[
140+
styles.textValue,
141+
disabled ? text.disabled : text.default,
142+
!disabled &&
143+
isPressed && {
144+
color: isPressed
145+
? theme.canvasTextInvert
146+
: theme.canvasText,
147+
},
148+
]}
149+
>
150+
{options[selectedIndex].label}
151+
</Text>
152+
</View>
153+
</View>
154+
<View style={[styles.fakeButton]}>
155+
<ImageBackground
156+
// border to compensate for Border
157+
style={[
158+
{
159+
marginTop: isPressed ? 1 : 0,
160+
width: '100%',
161+
height: '100%',
162+
},
163+
]}
164+
imageStyle={{
165+
resizeMode: 'contain',
166+
flex: 1,
167+
}}
168+
source={{
169+
uri: dropdownSymbol[disabled ? 'disabled' : 'default'],
170+
}}
171+
/>
172+
<Border
173+
variant={isPressed ? 'default' : 'outside'}
174+
invert={isPressed}
175+
/>
176+
</View>
177+
</View>
178+
179+
{isOpen && (
180+
<View style={[styles.options]}>
181+
{options.map((option, index) => (
182+
<SelectItem
183+
key={option.value}
184+
option={option}
185+
isSelected={index === selectedIndex}
186+
onPress={handleOptionSelect}
187+
/>
188+
))}
189+
</View>
190+
)}
191+
</View>
192+
</TouchableHighlight>
193+
);
194+
};
195+
196+
export default Select;
197+
198+
const selectHeight = blockSizes.md + 2;
199+
200+
const styles = StyleSheet.create({
201+
wrapper: {
202+
position: 'relative',
203+
height: selectHeight,
204+
alignSelf: 'flex-start',
205+
padding: 4,
206+
},
207+
flex: {
208+
display: 'flex',
209+
flexDirection: 'row',
210+
justifyContent: 'space-between',
211+
alignContent: 'center',
212+
alignItems: 'center',
213+
height: '100%',
214+
},
215+
value: {
216+
flexGrow: 1,
217+
flex: 1,
218+
height: '100%',
219+
padding: 2,
220+
},
221+
center: {
222+
flexGrow: 1,
223+
flex: 1,
224+
height: '100%',
225+
justifyContent: 'center',
226+
},
227+
textValue: {
228+
fontSize: 16,
229+
paddingHorizontal: 4,
230+
},
231+
fakeButton: {
232+
position: 'relative',
233+
height: '100%',
234+
width: 33,
235+
padding: 4,
236+
backgroundColor: theme.material,
237+
},
238+
options: {
239+
position: 'absolute',
240+
top: selectHeight,
241+
left: 2,
242+
right: 4,
243+
backgroundColor: theme.canvas,
244+
borderWidth: 2,
245+
borderColor: theme.borderDarkest,
246+
padding: 2,
247+
},
248+
optionWrapper: {
249+
height: selectHeight - 4,
250+
},
251+
optionText: {
252+
fontSize: 16,
253+
paddingLeft: 6,
254+
},
255+
});

src/Select/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './Select';

src/common/styles.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ const commonBorderStyle = { borderWidth: 2 };
66

77
export const border = StyleSheet.create({
88
/* createBorderStyles({ invert: false, windowBorders: false }) */
9+
focusSecondaryOutline: {
10+
...commonBorderStyle,
11+
borderStyle: 'dotted',
12+
borderColor: theme.focusSecondary,
13+
},
914
focusOutline: {
1015
...commonBorderStyle,
1116
borderStyle: 'dotted',

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { default as Checkbox } from './Checkbox';
1010
export { default as Radio } from './Radio';
1111
export { default as Fieldset } from './Fieldset';
1212
export { default as Progress } from './Progress';
13+
export { default as Select } from './Select';
1314
export { MenuItem, default as Menu } from './Menu';
1415

1516
export * from './common/themes';

0 commit comments

Comments
 (0)