Skip to content

Commit 9f79c21

Browse files
committed
Add full-height popover component.
1 parent 0e0cecd commit 9f79c21

File tree

6 files changed

+4870
-0
lines changed

6 files changed

+4870
-0
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import * as React from "react";
2+
import { View, ViewStyle, Text, StyleSheet } from "react-native";
3+
import type { ControlStyle } from "../../types/ControlStyle";
4+
import { useRefresh } from "../../hooks/useRefresh";
5+
import { Hitbox } from "../Hitbox";
6+
import { SimpleModal } from "../SimpleModal";
7+
import {
8+
createControlPlaceholderTextStyleInstance,
9+
createControlStateStyleInstance,
10+
createControlStyleInstance,
11+
createControlTextStyleInstance,
12+
createFullHeightPopoverStateStyleInstance,
13+
} from "../helpers";
14+
15+
/**
16+
* Creates a new React component which displays a button which can be pressed to
17+
* show an element in a pop-over which fills the display vertically.
18+
* @param controlStyle The styling to use.
19+
* @returns The created React component.
20+
*/
21+
export const createFullHeightPopoverComponent = (
22+
controlStyle: ControlStyle
23+
): React.FunctionComponent<{
24+
/**
25+
* The text shown in the button. When null, the placeholder is shown instead.
26+
*/
27+
readonly label: null | string;
28+
29+
/**
30+
* The placeholder text shown in the button when there is no text.
31+
*/
32+
readonly placeholder: string;
33+
34+
/**
35+
* When true, the button cannot be pressed and the body is not shown.
36+
*/
37+
readonly disabled: boolean;
38+
39+
/**
40+
* When true, the control is styled as though it is valid. It is otherwise
41+
* styles as though it is invalid.
42+
*/
43+
readonly valid: boolean;
44+
}> => {
45+
const styles = StyleSheet.create({
46+
blurredValidHitbox: createControlStyleInstance(
47+
controlStyle,
48+
controlStyle.blurredValid
49+
),
50+
blurredInvalidHitbox: createControlStateStyleInstance(
51+
controlStyle,
52+
controlStyle.blurredInvalid
53+
),
54+
focusedValidHitbox: createControlStateStyleInstance(
55+
controlStyle,
56+
controlStyle.focusedValid
57+
),
58+
focusedInvalidHitbox: createControlStateStyleInstance(
59+
controlStyle,
60+
controlStyle.focusedInvalid
61+
),
62+
disabledValidHitbox: createControlStateStyleInstance(
63+
controlStyle,
64+
controlStyle.disabledValid
65+
),
66+
disabledInvalidHitbox: createControlStateStyleInstance(
67+
controlStyle,
68+
controlStyle.disabledInvalid
69+
),
70+
disabledValidText: createControlTextStyleInstance(
71+
controlStyle,
72+
controlStyle.disabledValid
73+
),
74+
disabledInvalidText: createControlTextStyleInstance(
75+
controlStyle,
76+
controlStyle.disabledInvalid
77+
),
78+
blurredValidText: createControlTextStyleInstance(
79+
controlStyle,
80+
controlStyle.blurredValid
81+
),
82+
blurredInvalidText: createControlTextStyleInstance(
83+
controlStyle,
84+
controlStyle.blurredInvalid
85+
),
86+
focusedValidText: createControlTextStyleInstance(
87+
controlStyle,
88+
controlStyle.focusedValid
89+
),
90+
focusedInvalidText: createControlTextStyleInstance(
91+
controlStyle,
92+
controlStyle.focusedInvalid
93+
),
94+
disabledValidPlaceholderText: createControlPlaceholderTextStyleInstance(
95+
controlStyle,
96+
controlStyle.disabledValid
97+
),
98+
disabledInvalidPlaceholderText: createControlPlaceholderTextStyleInstance(
99+
controlStyle,
100+
controlStyle.disabledInvalid
101+
),
102+
blurredValidPlaceholderText: createControlPlaceholderTextStyleInstance(
103+
controlStyle,
104+
controlStyle.blurredValid
105+
),
106+
blurredInvalidPlaceholderText: createControlPlaceholderTextStyleInstance(
107+
controlStyle,
108+
controlStyle.blurredInvalid
109+
),
110+
focusedValidPlaceholderText: createControlPlaceholderTextStyleInstance(
111+
controlStyle,
112+
controlStyle.focusedValid
113+
),
114+
focusedInvalidPlaceholderText: createControlPlaceholderTextStyleInstance(
115+
controlStyle,
116+
controlStyle.focusedInvalid
117+
),
118+
validView: createFullHeightPopoverStateStyleInstance(
119+
controlStyle.focusedValid
120+
),
121+
invalidView: createFullHeightPopoverStateStyleInstance(
122+
controlStyle.focusedInvalid
123+
),
124+
});
125+
126+
return ({ label, placeholder, disabled, valid, children }) => {
127+
const refresh = useRefresh();
128+
129+
const state = React.useRef<{
130+
open: boolean;
131+
layout: null | {
132+
readonly pageX: number;
133+
readonly width: number;
134+
};
135+
}>({
136+
open: false,
137+
layout: null,
138+
});
139+
140+
// Ensure that the drop-down does not re-open itself if it is disabled while
141+
// open, then re-enabled.
142+
if (disabled) {
143+
state.current.open = false;
144+
}
145+
146+
let additionalModalViewStyle: null | ViewStyle;
147+
148+
if (!disabled && state.current.open && state.current.layout !== null) {
149+
additionalModalViewStyle = {
150+
left: state.current.layout.pageX,
151+
width: state.current.layout.width,
152+
};
153+
} else {
154+
additionalModalViewStyle = null;
155+
}
156+
157+
const inline = (
158+
<Hitbox
159+
style={
160+
disabled
161+
? valid
162+
? styles.disabledValidHitbox
163+
: styles.disabledInvalidHitbox
164+
: additionalModalViewStyle === null
165+
? valid
166+
? styles.blurredValidHitbox
167+
: styles.blurredInvalidHitbox
168+
: valid
169+
? styles.focusedValidHitbox
170+
: styles.focusedInvalidHitbox
171+
}
172+
onMeasure={(x, y, width, height, pageX, pageY) => {
173+
x;
174+
y;
175+
height;
176+
pageY;
177+
178+
if (
179+
state.current.layout === null ||
180+
pageX !== state.current.layout.pageX ||
181+
width !== state.current.layout.width
182+
) {
183+
state.current.layout = {
184+
pageX,
185+
width,
186+
};
187+
188+
refresh();
189+
}
190+
}}
191+
onPress={() => {
192+
state.current.open = true;
193+
194+
refresh();
195+
}}
196+
disabled={disabled}
197+
>
198+
<Text
199+
style={
200+
disabled
201+
? valid
202+
? label === null
203+
? styles.disabledValidPlaceholderText
204+
: styles.disabledValidText
205+
: label === null
206+
? styles.disabledInvalidPlaceholderText
207+
: styles.disabledInvalidText
208+
: additionalModalViewStyle === null
209+
? valid
210+
? label === null
211+
? styles.blurredValidPlaceholderText
212+
: styles.blurredValidText
213+
: label === null
214+
? styles.blurredInvalidPlaceholderText
215+
: styles.blurredInvalidText
216+
: valid
217+
? label === null
218+
? styles.focusedValidPlaceholderText
219+
: styles.focusedValidText
220+
: label === null
221+
? styles.focusedInvalidPlaceholderText
222+
: styles.focusedInvalidText
223+
}
224+
>
225+
{label ?? placeholder}
226+
</Text>
227+
</Hitbox>
228+
);
229+
230+
if (additionalModalViewStyle === null) {
231+
return inline;
232+
} else {
233+
return (
234+
<React.Fragment>
235+
{inline}
236+
<SimpleModal
237+
onClose={() => {
238+
state.current.open = false;
239+
240+
refresh();
241+
}}
242+
>
243+
<View
244+
style={[
245+
valid ? styles.validView : styles.invalidView,
246+
additionalModalViewStyle,
247+
]}
248+
>
249+
{children}
250+
</View>
251+
</SimpleModal>
252+
</React.Fragment>
253+
);
254+
}
255+
};
256+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# `react-native-app-helpers/createFullHeightPopoverComponent`
2+
3+
Creates a new React component which displays a button which can be pressed to
4+
show an element in a pop-over which fills the display vertically.
5+
6+
This is particularly useful for searchable and creatable selects, where the
7+
button may be obscured by the keyboard (and as much vertical height as possible
8+
is desirable for the list of options).
9+
10+
## Usage
11+
12+
```tsx
13+
import { createFullHeightPopoverComponent } from "react-native-app-helpers";
14+
15+
const ExampleFullHeightPopover = createDropDownComponent(
16+
{
17+
fontFamily: `Example Font Family`,
18+
fontSize: 37,
19+
paddingVertical: 12,
20+
paddingHorizontal: 29,
21+
blurredValid: {
22+
textColor: `#FFEE00`,
23+
placeholderColor: `#E7AA32`,
24+
backgroundColor: `#32AE12`,
25+
radius: 5,
26+
border: {
27+
width: 4,
28+
color: `#FF00FF`,
29+
},
30+
},
31+
blurredInvalid: {
32+
textColor: `#99FE88`,
33+
placeholderColor: `#CACA3A`,
34+
backgroundColor: `#259284`,
35+
radius: 10,
36+
border: {
37+
width: 6,
38+
color: `#9A9A8E`,
39+
},
40+
},
41+
focusedValid: {
42+
textColor: `#55EA13`,
43+
placeholderColor: `#273346`,
44+
backgroundColor: `#CABA99`,
45+
radius: 3,
46+
border: {
47+
width: 5,
48+
color: `#646464`,
49+
},
50+
},
51+
focusedInvalid: {
52+
textColor: `#ABAADE`,
53+
placeholderColor: `#47ADAD`,
54+
backgroundColor: `#32AA88`,
55+
radius: 47,
56+
border: {
57+
width: 12,
58+
color: `#98ADAA`,
59+
},
60+
},
61+
disabledValid: {
62+
textColor: `#AE2195`,
63+
placeholderColor: `#FFAAEE`,
64+
backgroundColor: `#772728`,
65+
radius: 100,
66+
border: {
67+
width: 14,
68+
color: `#5E5E5E`,
69+
},
70+
},
71+
disabledInvalid: {
72+
textColor: `#340297`,
73+
placeholderColor: `#233832`,
74+
backgroundColor: `#938837`,
75+
radius: 2,
76+
border: {
77+
width: 19,
78+
color: `#573829`,
79+
},
80+
},
81+
}
82+
);
83+
84+
const ExampleScreen = () => (
85+
<ExampleFullHeightPopover
86+
button={<Text>Click or touch to open the pop-over.</Text>}
87+
body={(position) => (
88+
<Text>
89+
This is shown in a column spanning the full height of the display when
90+
the button is pressed.
91+
</Text>
92+
)}
93+
disabled={false}
94+
/>
95+
);
96+
```

0 commit comments

Comments
 (0)