Skip to content

Commit

Permalink
feat(ios): dismiss detection support (#671)
Browse files Browse the repository at this point in the history
* feat(ios): add picker dismissal detection

* fix test, improve docs

* fix test

* change evt listeners
  • Loading branch information
vonovak authored Oct 1, 2022
1 parent ab6ad9b commit 09bb6ea
Show file tree
Hide file tree
Showing 17 changed files with 87 additions and 32 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ List of possible values for iOS (maps to [preferredDatePickerStyle](https://deve
Date change handler.

This is called when the user changes the date or time in the UI. It receives the event and the date as parameters.
It is also called when user dismisses the picker, which you can detect by checking the `event.type` property.
The values can be: `'set' | 'dismissed' | 'neutralButtonPressed'`. (`neutralButtonPressed` is only available on Android).

```js
setDate = (event, date) => {};
Expand Down
9 changes: 9 additions & 0 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TextInput,
useColorScheme,
Switch,
Alert,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';
import SegmentedControl from './SegmentedControl';
Expand Down Expand Up @@ -99,6 +100,14 @@ export const App = () => {
if (Platform.OS === 'android') {
setShow(false);
}
if (event.type === 'dismissed') {
Alert.alert('picker was dismissed', undefined, [
{
text: 'great',
},
]);
return;
}

if (event.type === 'neutralButtonPressed') {
setDate(new Date(0));
Expand Down
9 changes: 5 additions & 4 deletions example/e2e/detoxTest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ describe('e2e tests', () => {
}
});

it('nothing should happen if date does not change', async () => {
if (isIOS()) {
await userOpensPicker({mode: 'date', display: 'compact'});
it('nothing should happen if picker is dismissed / cancelled', async () => {
await userOpensPicker({mode: 'date', display: 'default'});

if (isIOS()) {
await element(
by.traits(['staticText']).withAncestor(by.label('Date Picker')),
).tap();
Expand All @@ -65,7 +65,6 @@ describe('e2e tests', () => {
await nextMonthArrow.tap();
await userDismissesCompactDatePicker();
} else {
await userOpensPicker({mode: 'date', display: 'default'});
const calendarHorizontalScrollView = element(
by
.type('android.widget.ScrollView')
Expand All @@ -76,6 +75,7 @@ describe('e2e tests', () => {
await userTapsCancelButtonAndroid();
}

await elementByText('great').tap();
await expect(getDateText()).toHaveText('11/13/2021');
});

Expand Down Expand Up @@ -123,6 +123,7 @@ describe('e2e tests', () => {
} else {
await userChangesTimeValue({minutes: '22'});
await userTapsCancelButtonAndroid();
await elementByText('great').tap();
}
await expect(getTimeText()).toHaveText('11:00');
});
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require_relative '../../node_modules/react-native-test-app/test_app'

# Flipper causes the build to fail on release when fabric is enabled
# Flipper causes the build to fail on release when fabric is enabled
# https://github.com/facebook/react-native/issues/33764
use_flipper!()
# use_flipper!()

workspace 'date-time-picker-example.xcworkspace'

Expand Down
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ PODS:
- React-Core
- React-jsi
- ReactTestApp-Resources (1.0.0-dev)
- RNDateTimePicker (6.4.0):
- RNDateTimePicker (6.4.2):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
Expand Down Expand Up @@ -923,12 +923,12 @@ SPEC CHECKSUMS:
ReactCommon: de55f940495d7bf87b5d7bf55b5b15cdd50d7d7b
ReactTestApp-DevSupport: 8a8cff38c37cd8145a12ac7d7d0503dd08f97d65
ReactTestApp-Resources: ff5f151e465e890010b417ce65ca6c5de6aeccbb
RNDateTimePicker: 597591d8a3ae159acd7d7b19706024932da816af
RNDateTimePicker: 0913d8322a3b21bb3d2010aca96c5090248223b6
RNLocalize: cbcb55d0e19c78086ea4eea20e03fe8000bbbced
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 82c9e8f652789f67d98bed5aef9d6653f71b04a9
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: 6bca2b1533bd782af99c755ccf9b322ffa84edc5
PODFILE CHECKSUM: d34f3e71e434db4f0211ec41f8c89e1a077c236f

COCOAPODS: 1.11.3
16 changes: 15 additions & 1 deletion ios/RNDateTimePicker.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@interface RNDateTimePicker ()

@property (nonatomic, copy) RCTBubblingEventBlock onChange;
@property (nonatomic, copy) RCTBubblingEventBlock onPickerDismiss;
@property (nonatomic, assign) NSInteger reactMinuteInterval;

@end
Expand All @@ -22,8 +23,14 @@ @implementation RNDateTimePicker
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
[self addTarget:self action:@selector(didChange)
#ifndef RCT_NEW_ARCH_ENABLED
// somehow, with Fabric, the callbacks are executed here as well as in RNDateTimePickerComponentView
// so do not register it with Fabric, to avoid potential problems
[self addTarget:self action:@selector(didChange)
forControlEvents:UIControlEventValueChanged];
[self addTarget:self action:@selector(onDismiss:) forControlEvents:UIControlEventEditingDidEnd];
#endif

_reactMinuteInterval = 1;
}
return self;
Expand All @@ -38,6 +45,13 @@ - (void)didChange
}
}

- (void)onDismiss:(RNDateTimePicker *)sender
{
if (_onPickerDismiss) {
_onPickerDismiss(@{});
}
}

- (void)setDatePickerMode:(UIDatePickerMode)datePickerMode
{
[super setDatePickerMode:datePickerMode];
Expand Down
1 change: 1 addition & 0 deletions ios/RNDateTimePickerManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ + (NSString*) datepickerStyleToString: (UIDatePickerStyle) style API_AVAILABLE(
RCT_EXPORT_VIEW_PROPERTY(minuteInterval, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPickerDismiss, RCTBubblingEventBlock)

RCT_REMAP_VIEW_PROPERTY(mode, datePickerMode, UIDatePickerMode)
RCT_REMAP_VIEW_PROPERTY(timeZoneOffsetInMinutes, timeZone, NSTimeZone)
Expand Down
13 changes: 12 additions & 1 deletion ios/fabric/RNDateTimePickerComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_dummyPicker = [RNDateTimePicker new];

[_picker addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
[_picker addTarget:self action:@selector(onDismiss:) forControlEvents:UIControlEventEditingDidEnd];

// Default Picker mode
_picker.datePickerMode = UIDatePickerModeDate;
Expand All @@ -52,6 +53,16 @@ - (instancetype)initWithFrame:(CGRect)frame
return self;
}

-(void)onDismiss:(RNDateTimePicker *)sender
{
if (!_eventEmitter) {
return;
}
RNDateTimePickerEventEmitter::OnPickerDismiss event = {};
std::dynamic_pointer_cast<const RNDateTimePickerEventEmitter>(_eventEmitter)
->onPickerDismiss(event);
}

-(void)onChange:(RNDateTimePicker *)sender
{
if (!_eventEmitter) {
Expand Down Expand Up @@ -103,7 +114,7 @@ -(void)updateTextColorForPicker:(UIDatePicker *)picker color:(UIColor *)color
return;
}
}

if (color) {
[picker setValue:color forKey:@"textColor"];
[picker setValue:@(NO) forKey:@"highlightsToday"];
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"react": "18.1.0",
"react-native": "^0.70.0",
"react-native-localize": "^2.2.0",
"react-native-test-app": "^1.6.16",
"react-native-test-app": "^1.6.19",
"react-native-windows": "^0.70.0-preview.2",
"react-test-renderer": "18.1.0",
"semantic-release": "^19.0.3"
Expand Down
3 changes: 2 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ export const ANDROID_DISPLAY = Object.freeze({
});

export const EVENT_TYPE_SET = 'set';
export const EVENT_TYPE_DISMISSED = 'dismissed';
export const ANDROID_EVT_TYPE = Object.freeze({
set: EVENT_TYPE_SET,
dismissed: EVENT_TYPE_DISMISSED,
neutralButtonPressed: 'neutralButtonPressed',
dismissed: 'dismissed',
});

export const IOS_DISPLAY = Object.freeze({
Expand Down
2 changes: 0 additions & 2 deletions src/datetimepicker.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {useEffect} from 'react';

import type {AndroidNativeProps} from './types';
import {validateAndroidProps} from './androidUtils';
import invariant from 'invariant';
import {DateTimePickerAndroid} from './DateTimePickerAndroid';

export default function RNDateTimePickerAndroid(
Expand All @@ -29,7 +28,6 @@ export default function RNDateTimePickerAndroid(
negativeButtonLabel,
onError,
} = props;
invariant(value, 'A date or time must be specified as `value` prop.');
const valueTimestamp = value.getTime();

useEffect(() => {
Expand Down
30 changes: 24 additions & 6 deletions src/datetimepicker.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
*/
import RNDateTimePicker from './picker';
import {dateToMilliseconds, sharedPropsValidation} from './utils';
import {IOS_DISPLAY, ANDROID_MODE, EVENT_TYPE_SET} from './constants';
import invariant from 'invariant';
import {
IOS_DISPLAY,
ANDROID_MODE,
EVENT_TYPE_SET,
EVENT_TYPE_DISMISSED,
} from './constants';
import * as React from 'react';
import {Platform} from 'react-native';

Expand Down Expand Up @@ -62,18 +66,31 @@ export default function Picker({

const _onChange = (event: NativeEventIOS) => {
const timestamp = event.nativeEvent.timestamp;
// $FlowFixMe Cannot assign object literal to `unifiedEvent` because number [1] is incompatible with undefined [2] in property `nativeEvent.timestamp`.
const unifiedEvent: DateTimePickerEvent = {...event, type: EVENT_TYPE_SET};
const unifiedEvent: DateTimePickerEvent = {
...event,
type: EVENT_TYPE_SET,
};

const date = timestamp !== undefined ? new Date(timestamp) : undefined;

onChange && onChange(unifiedEvent, date);
};

invariant(value, 'A date or time should be specified as `value`.');
const onDismiss = () => {
// TODO introduce separate onDismissed event listener
onChange &&
onChange(
{
type: EVENT_TYPE_DISMISSED,
nativeEvent: {
timestamp: value.getTime(),
},
},
value,
);
};

return (
// $FlowFixMe - dozen of flow errors
<RNDateTimePicker
testID={testID}
style={style}
Expand All @@ -85,6 +102,7 @@ export default function Picker({
minuteInterval={minuteInterval}
timeZoneOffsetInMinutes={timeZoneOffsetInMinutes}
onChange={_onChange}
onPickerDismiss={onDismiss}
textColor={textColor}
accentColor={accentColor}
themeVariant={themeVariant}
Expand Down
2 changes: 0 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ export type BaseProps = Readonly<ViewProps & DateOptions>;

export type IOSNativeProps = Readonly<
BaseProps & {
date?: Date;

/**
* The date picker locale.
*/
Expand Down
1 change: 1 addition & 0 deletions src/specs/DateTimePickerNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type DateTimePickerEvent = $ReadOnly<{|
type NativeProps = $ReadOnly<{|
...ViewProps,
onChange?: ?BubblingEventHandler<DateTimePickerEvent>,
onPickerDismiss?: ?BubblingEventHandler<null>,
maximumDate?: ?Double,
minimumDate?: ?Double,
date?: ?Double,
Expand Down
10 changes: 5 additions & 5 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,24 @@ export type NativeEventIOS = SyntheticEvent<

export type DateTimePickerEvent = {
type: AndroidEvtTypes,
nativeEvent: {
nativeEvent: $ReadOnly<{
timestamp?: number,
...
},
}>,
...
};

type BaseOptions = {|
/**
* The currently selected date.
*/
value?: ?Date,
value: Date,

/**
* Date change handler.
* change handler.
*
* This is called when the user changes the date or time in the UI.
* Or when they clear / dismiss the dialog.
* The first argument is an Event, the second a selected Date.
*/
onChange?: ?(event: DateTimePickerEvent, date?: Date) => void,
Expand Down Expand Up @@ -90,7 +91,6 @@ export type BaseProps = $ReadOnly<{|

export type IOSNativeProps = $ReadOnly<{|
...BaseProps,
date?: ?Date,

/**
* The date picker locale.
Expand Down
1 change: 1 addition & 0 deletions test/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exports[`DateTimePicker given a component for android / iOS renders a component
enabled={true}
mode="date"
onChange={[Function]}
onPickerDismiss={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
/>
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9366,10 +9366,10 @@ react-native-localize@^2.2.0:
resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-2.2.1.tgz#6fe646833691c6ee8a474df3c8b069402cb1dba8"
integrity sha512-BuPaQWvxLZG1NrCDGqgAnecDrNQu3LED9/Pyl4H2LwTMHcEngXpE5PfVntW2GiLumdr6nUOkWmMnh8PynZqrsw==

react-native-test-app@^1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/react-native-test-app/-/react-native-test-app-1.6.16.tgz#258bf3485b40666cee5a16ad1105bd08263684bb"
integrity sha512-Yl3uCIUrbyxnegamsTe9Am5qeftVG63z0sHsk420RqMog6wj+ttvSgrQ/15XXrxWeagNGtme0ub2YZRxTbFY7g==
react-native-test-app@^1.6.19:
version "1.6.19"
resolved "https://registry.yarnpkg.com/react-native-test-app/-/react-native-test-app-1.6.19.tgz#7105a833c7143673f466ff725dc8504d0cca497f"
integrity sha512-hsSFX1+JhP04N7ThPhpI+X7O3zqQ8J15irbGAhT+a1iJxFLIJHc3oHnEocrYLkBASihmY3l/0ngolsxUmI+gkg==
dependencies:
ajv "^8.0.0"
chalk "^4.1.0"
Expand Down

0 comments on commit 09bb6ea

Please sign in to comment.