mirror of
https://github.com/expo/react-native-action-sheet.git
synced 2026-11-14 19:11:42 -10:00
feat: iOS use custom action sheet (#281)
BREAKING CHANGE: This implementation is backwards compatible, but just to be safe I'm marking this as breaking so that it bumps the major version.
This commit is contained in:
1 parent
86fabab32b
commit
372dc80650
5 files changed
+429
-374
No files matched your search
@@ -10,7 +10,7 @@ React Native Action Sheet is a cross-platform React Native component that uses t
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="https://raw.githubusercontent.com/expo/react-native-action-sheet/master/gif/ios.gif" width="200" height="400"/> | <img src="https://raw.githubusercontent.com/expo/react-native-action-sheet/master/gif/android.gif" width="200" height="400"/> | <img src="https://raw.githubusercontent.com/expo/react-native-action-sheet/master/gif/web.gif" width="400" height="400"/> |
|
||||
|
||||
## <img src="data:image/png;base64,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" height="20" width="20" /> [Check out the example snack here!](https://snack.expo.dev/@expo-action-sheet/example)
|
||||
## [Check out the example snack here!](https://snack.expo.dev/@expo-action-sheet/example)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -122,7 +122,7 @@ This library can also be used in the browser with Expo for web.
|
||||
| `anchor` | number | iPad only option that allows for docking the action sheet to a node. See [ShowActionSheetButton.tsx](/example/ShowActionSheetButton.tsx) for an example on how to implement this. |
|
||||
| `userInterfaceStyle` | string | The interface style used for the action sheet, can be set to `light` or `dark`, otherwise the default system style will be used. |
|
||||
|
||||
### Android/Web-Only Props
|
||||
### Custom Action Sheet Only (Android/Web) Props
|
||||
|
||||
The below props allow modification of the Android ActionSheet. They have no effect on the look on iOS as the native iOS Action Sheet does not have options for modifying these options.
|
||||
|
||||
@@ -144,9 +144,21 @@ The below props allow modification of the Android ActionSheet. They have no effe
|
||||
|
||||
The following props can be set directly on the `ActionSheetProvider`
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `useNativeDriver` | boolean | Windows only option that provides the option to disable the [native animation](https://reactnative.dev/docs/animated#using-the-native-driver) driver for React Native Windows projects targeting _Windows 10 Version-1809 ; Build-10.0.17763.0_ and earlier. `useNativeDriver` is [supported in Version-1903 and later](https://microsoft.github.io/react-native-windows/docs/win10-compat) so if your project is targeting that, you don't need to set this prop. |
|
||||
| Name | Type | Description |
|
||||
| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `useCustomActionSheet` | boolean | iOS only prop that uses the custom pure JS action sheet (Android/Web version) instead of the native ActionSheetIOS component. Defaults to `false`. |
|
||||
| `useNativeDriver` | boolean | Windows only option that provides the option to disable the [native animation](https://reactnative.dev/docs/animated#using-the-native-driver) driver for React Native Windows projects targeting _Windows 10 Version-1809 ; Build-10.0.17763.0_ and earlier. `useNativeDriver` is [supported in Version-1903 and later](https://microsoft.github.io/react-native-windows/docs/win10-compat) so if your project is targeting that, you don't need to set this prop. |
|
||||
|
||||
```jsx
|
||||
// example of using useCustomActionSheet on iOS
|
||||
export default function AppContainer() {
|
||||
return (
|
||||
<ActionSheetProvider useCustomActionSheet={true}>
|
||||
<App />
|
||||
</ActionSheetProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Callback
|
||||
|
||||
|
||||
+48
-10
@@ -3,7 +3,9 @@ import {
|
||||
connectActionSheet,
|
||||
ActionSheetProps,
|
||||
} from '@expo/react-native-action-sheet';
|
||||
import { Entypo } from '@expo/vector-icons';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
StyleSheet,
|
||||
@@ -13,11 +15,15 @@ import {
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
Share,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import ShowActionSheetButton from './ShowActionSheetButton';
|
||||
|
||||
type Props = ActionSheetProps;
|
||||
type Props = ActionSheetProps & {
|
||||
useCustomActionSheet: boolean;
|
||||
setUseCustomActionSheet: (next: boolean) => void;
|
||||
};
|
||||
|
||||
interface State {
|
||||
selectedIndex?: number | null;
|
||||
@@ -77,6 +83,35 @@ class App extends React.Component<Props, State> {
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: Platform.OS === 'ios' ? 'flex' : 'none',
|
||||
}}>
|
||||
{this._renderSectionHeader('Use Custom Action Sheet')}
|
||||
<Text style={{ marginBottom: 10 }}>
|
||||
On iOS the default action sheet will be the native UI. However, you can optionally
|
||||
enable the custom JS action sheet by setting the useCustomActionSheet prop on the
|
||||
provider.
|
||||
</Text>
|
||||
<Entypo.Button
|
||||
backgroundColor={this.props.useCustomActionSheet ? '#3e3e3e' : 'white'}
|
||||
name={this.props.useCustomActionSheet ? 'check' : 'circle'}
|
||||
color={this.props.useCustomActionSheet ? '#fff' : '#3e3e3e'}
|
||||
style={{
|
||||
borderColor: '#3e3e3e',
|
||||
borderWidth: 2,
|
||||
}}
|
||||
onPress={() => this.props.setUseCustomActionSheet(!this.props.useCustomActionSheet)}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
color: this.props.useCustomActionSheet ? '#fff' : '#3e3e3e',
|
||||
}}>
|
||||
Use Custom Action Sheet
|
||||
</Text>
|
||||
</Entypo.Button>
|
||||
</View>
|
||||
{this._renderSectionHeader('Universal Options')}
|
||||
<ShowActionSheetButton
|
||||
title="Options Only"
|
||||
@@ -217,16 +252,19 @@ class App extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedApp = connectActionSheet<object>(App);
|
||||
const ConnectedApp = connectActionSheet<any>(App);
|
||||
|
||||
export default class AppContainer extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<ActionSheetProvider>
|
||||
<ConnectedApp />
|
||||
</ActionSheetProvider>
|
||||
);
|
||||
}
|
||||
export default function WrappedApp() {
|
||||
const [useCustomActionSheet, setUseCustomActionSheet] = useState(false);
|
||||
|
||||
return (
|
||||
<ActionSheetProvider useCustomActionSheet={useCustomActionSheet}>
|
||||
<ConnectedApp
|
||||
useCustomActionSheet={useCustomActionSheet}
|
||||
setUseCustomActionSheet={setUseCustomActionSheet}
|
||||
/>
|
||||
</ActionSheetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
BackHandler,
|
||||
Easing,
|
||||
Modal,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
|
||||
import { ActionSheetOptions } from '../types';
|
||||
import ActionGroup from './ActionGroup';
|
||||
|
||||
interface State {
|
||||
isVisible: boolean;
|
||||
isAnimating: boolean;
|
||||
options: ActionSheetOptions | null;
|
||||
onSelect: ((i: number) => void) | null;
|
||||
overlayOpacity: any;
|
||||
sheetOpacity: any;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
readonly children: React.ReactNode;
|
||||
readonly useNativeDriver?: boolean;
|
||||
readonly pointerEvents?: ViewProps['pointerEvents'];
|
||||
}
|
||||
|
||||
const OPACITY_ANIMATION_IN_TIME = 225;
|
||||
const OPACITY_ANIMATION_OUT_TIME = 195;
|
||||
const EASING_OUT = Easing.bezier(0.25, 0.46, 0.45, 0.94);
|
||||
const EASING_IN = Easing.out(EASING_OUT);
|
||||
const ESCAPE_KEY = 'Escape';
|
||||
|
||||
// Has same API as https://facebook.github.io/react-native/docs/actionsheetios.html
|
||||
export default class CustomActionSheet extends React.Component<Props, State> {
|
||||
_actionSheetHeight = 360;
|
||||
|
||||
state: State = {
|
||||
isVisible: false,
|
||||
isAnimating: false,
|
||||
options: null,
|
||||
onSelect: null,
|
||||
overlayOpacity: new Animated.Value(0),
|
||||
sheetOpacity: new Animated.Value(0),
|
||||
};
|
||||
|
||||
_deferAfterAnimation?: () => void = undefined;
|
||||
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'web') {
|
||||
document.addEventListener('keydown', this._handleWebKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (Platform.OS === 'web') {
|
||||
document.removeEventListener('keydown', this._handleWebKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
_handleWebKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key === ESCAPE_KEY && this.state.isVisible) {
|
||||
event.preventDefault();
|
||||
this._selectCancelButton();
|
||||
}
|
||||
};
|
||||
|
||||
_setActionSheetHeight = ({ nativeEvent }: any) =>
|
||||
(this._actionSheetHeight = nativeEvent.layout.height);
|
||||
|
||||
render() {
|
||||
const { isVisible, overlayOpacity, options } = this.state;
|
||||
const useModal = options ? options.autoFocus || options.useModal === true : false;
|
||||
const overlay = isVisible ? (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{
|
||||
opacity: overlayOpacity,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
// While the sheet is visible, hide the rest of the app's content from screen readers.
|
||||
const appContent = (
|
||||
<View
|
||||
style={styles.flexContainer}
|
||||
importantForAccessibility={isVisible ? 'no-hide-descendants' : 'auto'}>
|
||||
{React.Children.only(this.props.children)}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View pointerEvents={this.props.pointerEvents} style={styles.flexContainer}>
|
||||
{appContent}
|
||||
{isVisible && !useModal && (
|
||||
<>
|
||||
{overlay}
|
||||
{this._renderSheet()}
|
||||
</>
|
||||
)}
|
||||
{isVisible && useModal && (
|
||||
<Modal animationType="none" transparent onRequestClose={this._selectCancelButton}>
|
||||
{overlay}
|
||||
{this._renderSheet()}
|
||||
</Modal>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSheet() {
|
||||
const { options, isAnimating, sheetOpacity } = this.state;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
options: optionsArray,
|
||||
icons,
|
||||
tintIcons,
|
||||
destructiveButtonIndex,
|
||||
disabledButtonIndices,
|
||||
destructiveColor,
|
||||
textStyle,
|
||||
tintColor,
|
||||
title,
|
||||
titleTextStyle,
|
||||
message,
|
||||
messageTextStyle,
|
||||
autoFocus,
|
||||
showSeparators,
|
||||
containerStyle,
|
||||
separatorStyle,
|
||||
cancelButtonIndex,
|
||||
cancelButtonTintColor,
|
||||
} = options;
|
||||
return (
|
||||
<TouchableWithoutFeedback importantForAccessibility="yes" onPress={this._selectCancelButton}>
|
||||
<Animated.View
|
||||
needsOffscreenAlphaCompositing={isAnimating}
|
||||
style={[
|
||||
styles.sheetContainer,
|
||||
{
|
||||
opacity: sheetOpacity,
|
||||
transform: [
|
||||
{
|
||||
translateY: sheetOpacity.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [this._actionSheetHeight, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
<View style={styles.sheet} onLayout={this._setActionSheetHeight}>
|
||||
<ActionGroup
|
||||
options={optionsArray}
|
||||
icons={icons}
|
||||
tintIcons={tintIcons === undefined ? true : tintIcons}
|
||||
cancelButtonIndex={cancelButtonIndex}
|
||||
cancelButtonTintColor={cancelButtonTintColor}
|
||||
destructiveButtonIndex={destructiveButtonIndex}
|
||||
destructiveColor={destructiveColor}
|
||||
disabledButtonIndices={disabledButtonIndices}
|
||||
onSelect={this._onSelect}
|
||||
startIndex={0}
|
||||
length={optionsArray.length}
|
||||
textStyle={textStyle || {}}
|
||||
tintColor={tintColor}
|
||||
title={title || undefined}
|
||||
titleTextStyle={titleTextStyle}
|
||||
message={message || undefined}
|
||||
messageTextStyle={messageTextStyle}
|
||||
autoFocus={autoFocus}
|
||||
showSeparators={showSeparators}
|
||||
containerStyle={containerStyle}
|
||||
separatorStyle={separatorStyle}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
showActionSheetWithOptions = (options: ActionSheetOptions, onSelect: (i: number) => void) => {
|
||||
const { isVisible, overlayOpacity, sheetOpacity } = this.state;
|
||||
const { useNativeDriver = true } = this.props;
|
||||
|
||||
if (isVisible) {
|
||||
this._deferAfterAnimation = this.showActionSheetWithOptions.bind(this, options, onSelect);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
options,
|
||||
onSelect,
|
||||
isVisible: true,
|
||||
isAnimating: true,
|
||||
});
|
||||
overlayOpacity.setValue(0);
|
||||
sheetOpacity.setValue(0);
|
||||
Animated.parallel([
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 0.32,
|
||||
easing: EASING_OUT,
|
||||
duration: OPACITY_ANIMATION_IN_TIME,
|
||||
useNativeDriver,
|
||||
}),
|
||||
Animated.timing(sheetOpacity, {
|
||||
toValue: 1,
|
||||
easing: EASING_OUT,
|
||||
duration: OPACITY_ANIMATION_IN_TIME,
|
||||
useNativeDriver,
|
||||
}),
|
||||
]).start((result) => {
|
||||
if (result.finished) {
|
||||
this.setState({
|
||||
isAnimating: false,
|
||||
});
|
||||
this._deferAfterAnimation = undefined;
|
||||
}
|
||||
});
|
||||
// @ts-ignore: Argument of type '"actionSheetHardwareBackPress"' is not assignable to parameter of type '"hardwareBackPress"'
|
||||
BackHandler.addEventListener('actionSheetHardwareBackPress', this._selectCancelButton);
|
||||
};
|
||||
|
||||
_selectCancelButton = () => {
|
||||
const { options } = this.state;
|
||||
|
||||
if (!options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof options.cancelButtonIndex === 'undefined') {
|
||||
return false;
|
||||
} else if (typeof options.cancelButtonIndex === 'number') {
|
||||
return this._onSelect(options.cancelButtonIndex);
|
||||
} else {
|
||||
return this._animateOut();
|
||||
}
|
||||
};
|
||||
|
||||
_onSelect = (index: number): boolean => {
|
||||
const { isAnimating, onSelect } = this.state;
|
||||
|
||||
if (isAnimating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onSelect) {
|
||||
this._deferAfterAnimation = onSelect.bind(this, index);
|
||||
}
|
||||
|
||||
return this._animateOut();
|
||||
};
|
||||
|
||||
_animateOut = (): boolean => {
|
||||
const { isAnimating, overlayOpacity, sheetOpacity } = this.state;
|
||||
const { useNativeDriver = true } = this.props;
|
||||
|
||||
if (isAnimating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore: Argument of type '"actionSheetHardwareBackPress"' is not assignable to parameter of type '"hardwareBackPress"'
|
||||
BackHandler.removeEventListener('actionSheetHardwareBackPress', this._selectCancelButton);
|
||||
this.setState({
|
||||
isAnimating: true,
|
||||
});
|
||||
Animated.parallel([
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 0,
|
||||
easing: EASING_IN,
|
||||
duration: OPACITY_ANIMATION_OUT_TIME,
|
||||
useNativeDriver,
|
||||
}),
|
||||
Animated.timing(sheetOpacity, {
|
||||
toValue: 0,
|
||||
easing: EASING_IN,
|
||||
duration: OPACITY_ANIMATION_OUT_TIME,
|
||||
useNativeDriver,
|
||||
}),
|
||||
]).start((result) => {
|
||||
if (result.finished) {
|
||||
this.setState({
|
||||
isVisible: false,
|
||||
isAnimating: false,
|
||||
});
|
||||
|
||||
if (this._deferAfterAnimation) {
|
||||
this._deferAfterAnimation();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flexContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
sheetContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
backgroundColor: 'transparent',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sheet: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
+2
-334
@@ -1,334 +1,2 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
BackHandler,
|
||||
Easing,
|
||||
Modal,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
|
||||
import { ActionSheetOptions } from '../types';
|
||||
import ActionGroup from './ActionGroup';
|
||||
|
||||
interface State {
|
||||
isVisible: boolean;
|
||||
isAnimating: boolean;
|
||||
options: ActionSheetOptions | null;
|
||||
onSelect: ((i: number) => void) | null;
|
||||
overlayOpacity: any;
|
||||
sheetOpacity: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
readonly useNativeDriver: boolean | undefined;
|
||||
readonly pointerEvents?: ViewProps['pointerEvents'];
|
||||
}
|
||||
|
||||
const OPACITY_ANIMATION_IN_TIME = 225;
|
||||
const OPACITY_ANIMATION_OUT_TIME = 195;
|
||||
const EASING_OUT = Easing.bezier(0.25, 0.46, 0.45, 0.94);
|
||||
const EASING_IN = Easing.out(EASING_OUT);
|
||||
const ESCAPE_KEY = 'Escape';
|
||||
|
||||
// Has same API as https://facebook.github.io/react-native/docs/actionsheetios.html
|
||||
export default class ActionSheet extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
useNativeDriver: true,
|
||||
};
|
||||
|
||||
_actionSheetHeight = 360;
|
||||
|
||||
state: State = {
|
||||
isVisible: false,
|
||||
isAnimating: false,
|
||||
options: null,
|
||||
onSelect: null,
|
||||
overlayOpacity: new Animated.Value(0),
|
||||
sheetOpacity: new Animated.Value(0),
|
||||
};
|
||||
|
||||
_deferAfterAnimation?: () => void = undefined;
|
||||
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'web') {
|
||||
document.addEventListener('keydown', this._handleWebKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (Platform.OS === 'web') {
|
||||
document.removeEventListener('keydown', this._handleWebKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
_handleWebKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key === ESCAPE_KEY && this.state.isVisible) {
|
||||
event.preventDefault();
|
||||
this._selectCancelButton();
|
||||
}
|
||||
};
|
||||
|
||||
_setActionSheetHeight = ({ nativeEvent }: any) =>
|
||||
(this._actionSheetHeight = nativeEvent.layout.height);
|
||||
|
||||
render() {
|
||||
const { isVisible, overlayOpacity, options } = this.state;
|
||||
const useModal = options ? options.autoFocus || options.useModal === true : false;
|
||||
const overlay = isVisible ? (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{
|
||||
opacity: overlayOpacity,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
// While the sheet is visible, hide the rest of the app's content from screen readers.
|
||||
const appContent = (
|
||||
<View
|
||||
style={styles.flexContainer}
|
||||
importantForAccessibility={isVisible ? 'no-hide-descendants' : 'auto'}>
|
||||
{React.Children.only(this.props.children)}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View pointerEvents={this.props.pointerEvents} style={styles.flexContainer}>
|
||||
{appContent}
|
||||
{isVisible && !useModal && (
|
||||
<>
|
||||
{overlay}
|
||||
{this._renderSheet()}
|
||||
</>
|
||||
)}
|
||||
{isVisible && useModal && (
|
||||
<Modal animationType="none" transparent onRequestClose={this._selectCancelButton}>
|
||||
{overlay}
|
||||
{this._renderSheet()}
|
||||
</Modal>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSheet() {
|
||||
const { options, isAnimating, sheetOpacity } = this.state;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
options: optionsArray,
|
||||
icons,
|
||||
tintIcons,
|
||||
destructiveButtonIndex,
|
||||
disabledButtonIndices,
|
||||
destructiveColor,
|
||||
textStyle,
|
||||
tintColor,
|
||||
title,
|
||||
titleTextStyle,
|
||||
message,
|
||||
messageTextStyle,
|
||||
autoFocus,
|
||||
showSeparators,
|
||||
containerStyle,
|
||||
separatorStyle,
|
||||
cancelButtonIndex,
|
||||
cancelButtonTintColor,
|
||||
} = options;
|
||||
return (
|
||||
<TouchableWithoutFeedback importantForAccessibility="yes" onPress={this._selectCancelButton}>
|
||||
<Animated.View
|
||||
needsOffscreenAlphaCompositing={isAnimating}
|
||||
style={[
|
||||
styles.sheetContainer,
|
||||
{
|
||||
opacity: sheetOpacity,
|
||||
transform: [
|
||||
{
|
||||
translateY: sheetOpacity.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [this._actionSheetHeight, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
<View style={styles.sheet} onLayout={this._setActionSheetHeight}>
|
||||
<ActionGroup
|
||||
options={optionsArray}
|
||||
icons={icons}
|
||||
tintIcons={tintIcons === undefined ? true : tintIcons}
|
||||
cancelButtonIndex={cancelButtonIndex}
|
||||
cancelButtonTintColor={cancelButtonTintColor}
|
||||
destructiveButtonIndex={destructiveButtonIndex}
|
||||
destructiveColor={destructiveColor}
|
||||
disabledButtonIndices={disabledButtonIndices}
|
||||
onSelect={this._onSelect}
|
||||
startIndex={0}
|
||||
length={optionsArray.length}
|
||||
textStyle={textStyle || {}}
|
||||
tintColor={tintColor}
|
||||
title={title || undefined}
|
||||
titleTextStyle={titleTextStyle}
|
||||
message={message || undefined}
|
||||
messageTextStyle={messageTextStyle}
|
||||
autoFocus={autoFocus}
|
||||
showSeparators={showSeparators}
|
||||
containerStyle={containerStyle}
|
||||
separatorStyle={separatorStyle}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
showActionSheetWithOptions = (options: ActionSheetOptions, onSelect: (i: number) => void) => {
|
||||
const { isVisible, overlayOpacity, sheetOpacity } = this.state;
|
||||
|
||||
if (isVisible) {
|
||||
this._deferAfterAnimation = this.showActionSheetWithOptions.bind(this, options, onSelect);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
options,
|
||||
onSelect,
|
||||
isVisible: true,
|
||||
isAnimating: true,
|
||||
});
|
||||
overlayOpacity.setValue(0);
|
||||
sheetOpacity.setValue(0);
|
||||
Animated.parallel([
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 0.32,
|
||||
easing: EASING_OUT,
|
||||
duration: OPACITY_ANIMATION_IN_TIME,
|
||||
useNativeDriver: this.props.useNativeDriver,
|
||||
}),
|
||||
Animated.timing(sheetOpacity, {
|
||||
toValue: 1,
|
||||
easing: EASING_OUT,
|
||||
duration: OPACITY_ANIMATION_IN_TIME,
|
||||
useNativeDriver: this.props.useNativeDriver,
|
||||
}),
|
||||
]).start((result) => {
|
||||
if (result.finished) {
|
||||
this.setState({
|
||||
isAnimating: false,
|
||||
});
|
||||
this._deferAfterAnimation = undefined;
|
||||
}
|
||||
});
|
||||
// @ts-ignore: Argument of type '"actionSheetHardwareBackPress"' is not assignable to parameter of type '"hardwareBackPress"'
|
||||
BackHandler.addEventListener('actionSheetHardwareBackPress', this._selectCancelButton);
|
||||
};
|
||||
|
||||
_selectCancelButton = () => {
|
||||
const { options } = this.state;
|
||||
|
||||
if (!options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof options.cancelButtonIndex === 'undefined') {
|
||||
return false;
|
||||
} else if (typeof options.cancelButtonIndex === 'number') {
|
||||
return this._onSelect(options.cancelButtonIndex);
|
||||
} else {
|
||||
return this._animateOut();
|
||||
}
|
||||
};
|
||||
|
||||
_onSelect = (index: number): boolean => {
|
||||
const { isAnimating, onSelect } = this.state;
|
||||
|
||||
if (isAnimating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onSelect) {
|
||||
this._deferAfterAnimation = onSelect.bind(this, index);
|
||||
}
|
||||
|
||||
return this._animateOut();
|
||||
};
|
||||
|
||||
_animateOut = (): boolean => {
|
||||
const { isAnimating, overlayOpacity, sheetOpacity } = this.state;
|
||||
|
||||
if (isAnimating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore: Argument of type '"actionSheetHardwareBackPress"' is not assignable to parameter of type '"hardwareBackPress"'
|
||||
BackHandler.removeEventListener('actionSheetHardwareBackPress', this._selectCancelButton);
|
||||
this.setState({
|
||||
isAnimating: true,
|
||||
});
|
||||
Animated.parallel([
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 0,
|
||||
easing: EASING_IN,
|
||||
duration: OPACITY_ANIMATION_OUT_TIME,
|
||||
useNativeDriver: this.props.useNativeDriver,
|
||||
}),
|
||||
Animated.timing(sheetOpacity, {
|
||||
toValue: 0,
|
||||
easing: EASING_IN,
|
||||
duration: OPACITY_ANIMATION_OUT_TIME,
|
||||
useNativeDriver: this.props.useNativeDriver,
|
||||
}),
|
||||
]).start((result) => {
|
||||
if (result.finished) {
|
||||
this.setState({
|
||||
isVisible: false,
|
||||
isAnimating: false,
|
||||
});
|
||||
|
||||
if (this._deferAfterAnimation) {
|
||||
this._deferAfterAnimation();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flexContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
sheetContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
backgroundColor: 'transparent',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sheet: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
import CustomActionSheet from './CustomActionSheet';
|
||||
export default CustomActionSheet;
|
||||
+29
-25
@@ -1,41 +1,45 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import ActionSheet from './ActionSheet';
|
||||
// NativeActionSheet will always be custom when on Android/web
|
||||
import NativeActionSheet from './ActionSheet';
|
||||
import CustomActionSheet from './ActionSheet/CustomActionSheet';
|
||||
import { Provider } from './context';
|
||||
import { ActionSheetOptions, ActionSheetProps } from './types';
|
||||
import { ActionSheetOptions } from './types';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
useNativeDriver?: boolean;
|
||||
useCustomActionSheet?: boolean;
|
||||
}
|
||||
|
||||
export default class ActionSheetProvider extends React.Component<Props> {
|
||||
_actionSheetRef: React.RefObject<ActionSheet>;
|
||||
_context: ActionSheetProps;
|
||||
export default function ActionSheetProvider({
|
||||
children,
|
||||
useNativeDriver,
|
||||
useCustomActionSheet = false,
|
||||
}: Props) {
|
||||
const actionSheetRef = React.useRef<NativeActionSheet>(null);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this._actionSheetRef = React.createRef();
|
||||
this._context = {
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
showActionSheetWithOptions: (options: ActionSheetOptions, callback: (i: number) => void) => {
|
||||
if (this._actionSheetRef.current != null) {
|
||||
this._actionSheetRef.current.showActionSheetWithOptions(options, callback);
|
||||
if (actionSheetRef.current) {
|
||||
actionSheetRef.current.showActionSheetWithOptions(options, callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
[actionSheetRef]
|
||||
);
|
||||
|
||||
getContext(): ActionSheetProps {
|
||||
return this._context;
|
||||
}
|
||||
const ActionSheet = React.useMemo(
|
||||
() => (useCustomActionSheet ? CustomActionSheet : NativeActionSheet),
|
||||
[useCustomActionSheet]
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider value={this._context}>
|
||||
<ActionSheet ref={this._actionSheetRef} useNativeDriver={this.props.useNativeDriver}>
|
||||
{React.Children.only(this.props.children)}
|
||||
</ActionSheet>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Provider value={context}>
|
||||
<ActionSheet ref={actionSheetRef} useNativeDriver={useNativeDriver}>
|
||||
{React.Children.only(children)}
|
||||
</ActionSheet>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user