Skip to content

Commit 58ad784

Browse files
authored
feat: focus events management on Android picker (#258)
1 parent fc30e39 commit 58ad784

File tree

9 files changed

+301
-13
lines changed

9 files changed

+301
-13
lines changed

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,30 @@ Add `Picker` like this:
146146
</Picker>
147147
```
148148

149+
If you want to open/close picker programmatically on android, pass ref to `Picker`:
150+
151+
```javascript
152+
const pickerRef = useRef();
153+
154+
function open() {
155+
pickerRef.current.focus();
156+
}
157+
158+
function close() {
159+
pickerRef.current.blur();
160+
}
161+
162+
return <Picker
163+
ref={pickerRef}
164+
selectedValue={selectedLanguage}
165+
onValueChange={(itemValue, itemIndex) =>
166+
setSelectedLanguage(itemValue)
167+
}>
168+
<Picker.Item label="Java" value="java" />
169+
<Picker.Item label="JavaScript" value="js" />
170+
</Picker>
171+
```
172+
149173
### Props
150174

151175
* [Inherited `View` props...](https://reactnative.dev/docs/view#props)
@@ -266,6 +290,28 @@ such that the total number of lines does not exceed this number. Default is '1'
266290
| ------- | -------- | -------- |
267291
| number | No | Android |
268292

293+
### `onBlur`
294+
295+
| Type | Required | Platform |
296+
| --------- | -------- | -------- |
297+
| function | no | Android |
298+
299+
### `onFocus`
300+
301+
| Type | Required | Platform |
302+
| --------- | -------- | -------- |
303+
| function | no | Android |
304+
305+
## Methods
306+
307+
### `blur` (Android only)
308+
309+
Programmatically closes picker
310+
311+
### `focus` (Android only)
312+
313+
Programmatically opens picker
314+
269315
## PickerItemProps
270316

271317
Props that can be applied to individual `Picker.Item`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.reactnativecommunity.picker;
2+
3+
import com.facebook.react.bridge.Arguments;
4+
import com.facebook.react.bridge.WritableMap;
5+
import com.facebook.react.uimanager.events.Event;
6+
import com.facebook.react.uimanager.events.RCTEventEmitter;
7+
8+
public class PickerBlurEvent extends Event<PickerBlurEvent> {
9+
public static final String EVENT_NAME = "topBlur";
10+
@Override
11+
public String getEventName() {
12+
return EVENT_NAME;
13+
}
14+
15+
public PickerBlurEvent(int id) {
16+
super(id);
17+
}
18+
19+
@Override
20+
public void dispatch(RCTEventEmitter rctEventEmitter) {
21+
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), getEventData());
22+
}
23+
24+
private WritableMap getEventData() {
25+
WritableMap eventData = Arguments.createMap();
26+
eventData.putInt("target", getViewTag());
27+
return eventData;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.reactnativecommunity.picker;
2+
3+
import com.facebook.react.bridge.Arguments;
4+
import com.facebook.react.bridge.WritableMap;
5+
import com.facebook.react.uimanager.events.Event;
6+
import com.facebook.react.uimanager.events.RCTEventEmitter;
7+
8+
public class PickerFocusEvent extends Event<PickerFocusEvent> {
9+
public static final String EVENT_NAME = "topFocus";
10+
@Override
11+
public String getEventName() {
12+
return EVENT_NAME;
13+
}
14+
15+
public PickerFocusEvent(int id) {
16+
super(id);
17+
}
18+
19+
@Override
20+
public void dispatch(RCTEventEmitter rctEventEmitter) {
21+
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), getEventData());
22+
}
23+
24+
private WritableMap getEventData() {
25+
WritableMap eventData = Arguments.createMap();
26+
eventData.putInt("target", getViewTag());
27+
return eventData;
28+
}
29+
}

android/src/main/java/com/reactnativecommunity/picker/ReactPicker.java

+35
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ public class ReactPicker extends AppCompatSpinner {
3030
private int mMode = Spinner.MODE_DIALOG;
3131
private @Nullable Integer mPrimaryColor;
3232
private @Nullable OnSelectListener mOnSelectListener;
33+
private @Nullable OnFocusListener mOnFocusListener;
3334
private @Nullable Integer mStagedSelection;
3435
private int mOldElementSize = Integer.MIN_VALUE;
36+
private boolean mIsOpen = false;
3537

3638
private final OnItemSelectedListener mItemSelectedListener = new OnItemSelectedListener() {
3739
@Override
@@ -56,6 +58,11 @@ public interface OnSelectListener {
5658
void onItemSelected(int position);
5759
}
5860

61+
public interface OnFocusListener {
62+
void onPickerBlur();
63+
void onPickerFocus();
64+
}
65+
5966
public ReactPicker(Context context) {
6067
super(context);
6168
handleRTL(context);
@@ -117,6 +124,28 @@ public void requestLayout() {
117124
post(measureAndLayout);
118125
}
119126

127+
@Override
128+
public boolean performClick() {
129+
// When picker is opened, emit focus event.
130+
mIsOpen = true;
131+
if (mOnFocusListener != null) {
132+
mOnFocusListener.onPickerFocus();
133+
}
134+
return super.performClick();
135+
}
136+
137+
@Override
138+
public void onWindowFocusChanged(boolean hasWindowFocus) {
139+
// When view that holds picker gains focus and picker was opened,
140+
// then picker lost focus, so emit blur event.
141+
if (mIsOpen && hasWindowFocus) {
142+
mIsOpen = false;
143+
if (mOnFocusListener != null) {
144+
mOnFocusListener.onPickerBlur();
145+
}
146+
}
147+
}
148+
120149
@Override
121150
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
122151
super.onLayout(changed, left, top, right, bottom);
@@ -164,10 +193,16 @@ public void setOnSelectListener(@Nullable OnSelectListener onSelectListener) {
164193
mOnSelectListener = onSelectListener;
165194
}
166195

196+
public void setOnFocusListener(@Nullable OnFocusListener onFocusListener) {
197+
mOnFocusListener = onFocusListener;
198+
}
199+
167200
@Nullable public OnSelectListener getOnSelectListener() {
168201
return mOnSelectListener;
169202
}
170203

204+
@Nullable public OnFocusListener getOnFocusListener() { return mOnFocusListener; }
205+
171206
/**
172207
* Will cache "selection" value locally and set it only once {@link #updateStagedSelection} is
173208
* called

android/src/main/java/com/reactnativecommunity/picker/ReactPickerManager.java

+75-5
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@
1818
import android.widget.Spinner;
1919
import android.widget.TextView;
2020

21+
import androidx.annotation.NonNull;
22+
2123
import com.facebook.infer.annotation.Assertions;
2224
import com.facebook.react.bridge.Arguments;
2325
import com.facebook.react.bridge.ReadableArray;
2426
import com.facebook.react.bridge.ReadableMap;
27+
import com.facebook.react.common.MapBuilder;
2528
import com.facebook.react.modules.i18nmanager.I18nUtil;
2629
import com.facebook.react.uimanager.*;
2730
import com.facebook.react.uimanager.annotations.ReactProp;
2831
import com.facebook.react.uimanager.events.EventDispatcher;
2932

33+
import java.util.Map;
34+
3035
import javax.annotation.Nullable;
3136

3237
/**
@@ -39,6 +44,36 @@
3944
public abstract class ReactPickerManager extends BaseViewManager<ReactPicker, ReactPickerShadowNode> {
4045
private static final ReadableArray EMPTY_ARRAY = Arguments.createArray();
4146

47+
private static final int FOCUS_PICKER = 1;
48+
private static final int BLUR_PICKER = 2;
49+
50+
@Nullable
51+
@Override
52+
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
53+
return MapBuilder.<String, Object>builder()
54+
.put(
55+
"topSelect",
56+
MapBuilder.of(
57+
"phasedRegistrationNames",
58+
MapBuilder.of("bubbled", "onSelect", "captured", "onSelectCapture")))
59+
.put(
60+
"topFocus",
61+
MapBuilder.of(
62+
"phasedRegistrationNames",
63+
MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture")))
64+
.put(
65+
"topBlur",
66+
MapBuilder.of(
67+
"phasedRegistrationNames",
68+
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
69+
.build();
70+
}
71+
72+
@Override
73+
public @Nullable Map<String, Integer> getCommandsMap() {
74+
return MapBuilder.of("focus", FOCUS_PICKER, "blur", BLUR_PICKER);
75+
}
76+
4277
@ReactProp(name = "items")
4378
public void setItems(ReactPicker view, @Nullable ReadableArray items) {
4479
ReactPickerAdapter adapter = (ReactPickerAdapter) view.getAdapter();
@@ -104,10 +139,35 @@ protected void onAfterUpdateTransaction(ReactPicker view) {
104139
protected void addEventEmitters(
105140
final ThemedReactContext reactContext,
106141
final ReactPicker picker) {
107-
picker.setOnSelectListener(
108-
new PickerEventEmitter(
109-
picker,
110-
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()));
142+
final PickerEventEmitter eventEmitter = new PickerEventEmitter(
143+
picker,
144+
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher());
145+
picker.setOnSelectListener(eventEmitter);
146+
picker.setOnFocusListener(eventEmitter);
147+
}
148+
149+
@Override
150+
public void receiveCommand(@NonNull ReactPicker root, int commandId, @androidx.annotation.Nullable ReadableArray args) {
151+
switch (commandId) {
152+
case FOCUS_PICKER:
153+
root.performClick();
154+
break;
155+
case BLUR_PICKER:
156+
root.clearFocus();
157+
break;
158+
}
159+
}
160+
161+
@Override
162+
public void receiveCommand(@NonNull ReactPicker root, String commandId, @androidx.annotation.Nullable ReadableArray args) {
163+
switch (commandId) {
164+
case "focus":
165+
root.performClick();
166+
break;
167+
case "blur":
168+
root.clearFocus();
169+
break;
170+
}
111171
}
112172

113173
@Override
@@ -254,7 +314,7 @@ public void setNumberOfLines(int numberOfLines) {
254314
}
255315
}
256316

257-
private static class PickerEventEmitter implements ReactPicker.OnSelectListener {
317+
private static class PickerEventEmitter implements ReactPicker.OnSelectListener, ReactPicker.OnFocusListener {
258318

259319
private final ReactPicker mReactPicker;
260320
private final EventDispatcher mEventDispatcher;
@@ -269,5 +329,15 @@ public void onItemSelected(int position) {
269329
mEventDispatcher.dispatchEvent( new PickerItemSelectEvent(
270330
mReactPicker.getId(), position));
271331
}
332+
333+
@Override
334+
public void onPickerBlur() {
335+
mEventDispatcher.dispatchEvent( new PickerBlurEvent(mReactPicker.getId()));
336+
}
337+
338+
@Override
339+
public void onPickerFocus() {
340+
mEventDispatcher.dispatchEvent( new PickerFocusEvent(mReactPicker.getId()));
341+
}
272342
}
273343
}

example/ios/Podfile.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ PODS:
217217
- React-cxxreact (= 0.61.5)
218218
- React-jsi (= 0.61.5)
219219
- ReactCommon/jscallinvoker (= 0.61.5)
220-
- RNCPicker (1.9.8):
220+
- RNCPicker (1.14.0):
221221
- React-Core
222222
- Yoga (1.14.0)
223223

@@ -336,9 +336,9 @@ SPEC CHECKSUMS:
336336
React-RCTText: 9ccc88273e9a3aacff5094d2175a605efa854dbe
337337
React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad
338338
ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd
339-
RNCPicker: cdeab10edac8f14ed6839fdf922a789f80c72ec1
339+
RNCPicker: 1a266981fc99330c252f5f98b3f09a377a35d88c
340340
Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b
341341

342342
PODFILE CHECKSUM: 895c98cb9ca5923052256783bd886330d061c5d2
343343

344-
COCOAPODS: 1.10.0
344+
COCOAPODS: 1.10.1

example/src/PickerExample.tsx

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import {Text, View, StyleSheet} from 'react-native';
2+
import {Text, View, StyleSheet, Button} from 'react-native';
33
import {Picker} from '../../js';
44

55
const Item: any = Picker.Item;
@@ -167,21 +167,45 @@ function NoListenerPickerExample() {
167167

168168
function ColorPickerExample() {
169169
const [value, setValue] = React.useState('red');
170+
const [isFocused, setIsFocused] = React.useState(false);
171+
const [isSecondFocused, setIsSecondFocused] = React.useState(false);
172+
const pickerRef = React.useRef(null);
170173

171174
return (
172175
<>
176+
<Button
177+
onPress={() => {
178+
pickerRef.current?.focus?.();
179+
}}
180+
title="Focus"
181+
/>
182+
<Text>{`Is input opened: ${isFocused ? 'YES' : 'NO'}`}</Text>
173183
<Picker
184+
ref={pickerRef}
174185
style={styles.container}
175186
selectedValue={value}
187+
onBlur={() => {
188+
setIsFocused(false);
189+
}}
190+
onFocus={() => {
191+
setIsFocused(true);
192+
}}
176193
onValueChange={(v) => setValue(v)}
177194
mode="dropdown">
178195
<Item label="red" color="red" value="red" />
179196
<Item label="green" color="green" value="green" />
180197
<Item label="blue" color="blue" value="blue" />
181198
</Picker>
199+
<Text>{`Is input opened: ${isSecondFocused ? 'YES' : 'NO'}`}</Text>
182200
<Picker
183201
style={{color: value}}
184202
selectedValue={value}
203+
onBlur={() => {
204+
setIsSecondFocused(false);
205+
}}
206+
onFocus={() => {
207+
setIsSecondFocused(true);
208+
}}
185209
onValueChange={(v) => setValue(v)}
186210
mode="dialog">
187211
<Item label="red" color="red" value="red" />

0 commit comments

Comments
 (0)