Skip to content

Commit

Permalink
refactor(modal): migrate to functional component (#2557)
Browse files Browse the repository at this point in the history
  • Loading branch information
crutchcorn authored Oct 19, 2021
1 parent ee9c2d0 commit 072e40b
Showing 1 changed file with 97 additions and 124 deletions.
221 changes: 97 additions & 124 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
getBottomSpace,
} from 'react-native-iphone-x-helper';
import Surface from './Surface';
import { withTheme } from '../core/theming';
import { useTheme } from '../core/theming';
import useAnimatedValue from '../utils/useAnimatedValue';

type Props = {
/**
Expand Down Expand Up @@ -47,15 +48,6 @@ type Props = {
* Use this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom.
*/
style?: StyleProp<ViewStyle>;
/**
* @optional
*/
theme: ReactNativePaper.Theme;
};

type State = {
opacity: Animated.Value;
rendered: boolean;
};

const DEFAULT_DURATION = 220;
Expand Down Expand Up @@ -101,60 +93,54 @@ const BOTTOM_INSET = getBottomSpace();
* export default MyComponent;
* ```
*/
class Modal extends React.Component<Props, State> {
static defaultProps = {
dismissable: true,
visible: false,
overlayAccessibilityLabel: 'Close modal',
};
export default function Modal({
dismissable = true,
visible = false,
overlayAccessibilityLabel = 'Close modal',
onDismiss,
children,
contentContainerStyle,
style,
}: Props) {
const visibleRef = React.useRef(visible);

static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (nextProps.visible && !prevState.rendered) {
return {
rendered: true,
};
}
React.useEffect(() => {
visibleRef.current = visible;
});

return null;
}
const { colors, animation } = useTheme();

state = {
opacity: new Animated.Value(this.props.visible ? 1 : 0),
rendered: this.props.visible,
};
const opacity = useAnimatedValue(visible ? 1 : 0);

componentDidUpdate(prevProps: Props) {
if (prevProps.visible !== this.props.visible) {
if (this.props.visible) {
this.showModal();
} else {
this.hideModal();
}
}
}
const [rendered, setRendered] = React.useState(visible);

private subscription: NativeEventSubscription | undefined;
if (visible && !rendered) {
setRendered(true);
}

private handleBack = () => {
if (this.props.dismissable) {
this.hideModal();
const handleBack = () => {
if (dismissable) {
hideModal();
}
return true;
};

private showModal = () => {
if (this.subscription?.remove) {
this.subscription.remove();
const subscription = React.useRef<NativeEventSubscription | undefined>(
undefined
);

const showModal = () => {
if (subscription.current?.remove) {
subscription.current.remove();
} else {
BackHandler.removeEventListener('hardwareBackPress', this.handleBack);
BackHandler.removeEventListener('hardwareBackPress', handleBack);
}
this.subscription = BackHandler.addEventListener(
subscription.current = BackHandler.addEventListener(
'hardwareBackPress',
this.handleBack
handleBack
);

const { opacity } = this.state;
const { scale } = this.props.theme.animation;
const { scale } = animation;

Animated.timing(opacity, {
toValue: 1,
Expand All @@ -164,15 +150,14 @@ class Modal extends React.Component<Props, State> {
}).start();
};

private hideModal = () => {
if (this.subscription?.remove) {
this.subscription?.remove();
const hideModal = () => {
if (subscription.current?.remove) {
subscription.current?.remove();
} else {
BackHandler.removeEventListener('hardwareBackPress', this.handleBack);
BackHandler.removeEventListener('hardwareBackPress', handleBack);
}

const { opacity } = this.state;
const { scale } = this.props.theme.animation;
const { scale } = animation;

Animated.timing(opacity, {
toValue: 0,
Expand All @@ -184,89 +169,77 @@ class Modal extends React.Component<Props, State> {
return;
}

if (this.props.visible && this.props.onDismiss) {
this.props.onDismiss();
if (visible && onDismiss) {
onDismiss();
}

if (this.props.visible) {
this.showModal();
if (visibleRef.current) {
showModal();
} else {
this.setState({
rendered: false,
});
setRendered(false);
}
});
};

componentWillUnmount() {
if (this.subscription?.remove) {
this.subscription.remove();
} else {
BackHandler.removeEventListener('hardwareBackPress', this.handleBack);
}
}

render() {
const { rendered, opacity } = this.state;
const prevVisible = React.useRef<boolean | null>(null);

if (!rendered) return null;

const {
children,
dismissable,
style,
theme,
contentContainerStyle,
overlayAccessibilityLabel,
} = this.props;
const { colors } = theme;
return (
<Animated.View
pointerEvents={this.props.visible ? 'auto' : 'none'}
accessibilityViewIsModal
accessibilityLiveRegion="polite"
style={StyleSheet.absoluteFill}
onAccessibilityEscape={this.hideModal}
React.useEffect(() => {
if (prevVisible.current !== visible) {
if (visible) {
showModal();
} else {
hideModal();
}
}
prevVisible.current = visible;
});

if (!rendered) return null;

return (
<Animated.View
pointerEvents={visible ? 'auto' : 'none'}
accessibilityViewIsModal
accessibilityLiveRegion="polite"
style={StyleSheet.absoluteFill}
onAccessibilityEscape={hideModal}
>
<TouchableWithoutFeedback
accessibilityLabel={overlayAccessibilityLabel}
accessibilityRole="button"
disabled={!dismissable}
onPress={dismissable ? hideModal : undefined}
importantForAccessibility="no"
>
<TouchableWithoutFeedback
accessibilityLabel={overlayAccessibilityLabel}
accessibilityRole="button"
disabled={!dismissable}
onPress={dismissable ? this.hideModal : undefined}
importantForAccessibility="no"
>
<Animated.View
style={[
styles.backdrop,
{ backgroundColor: colors.backdrop, opacity },
]}
/>
</TouchableWithoutFeedback>
<View
<Animated.View
style={[
styles.wrapper,
{ marginTop: TOP_INSET, marginBottom: BOTTOM_INSET },
style,
styles.backdrop,
{ backgroundColor: colors.backdrop, opacity },
]}
pointerEvents="box-none"
/>
</TouchableWithoutFeedback>
<View
style={[
styles.wrapper,
{ marginTop: TOP_INSET, marginBottom: BOTTOM_INSET },
style,
]}
pointerEvents="box-none"
>
<Surface
style={
[{ opacity }, styles.content, contentContainerStyle] as StyleProp<
ViewStyle
>
}
>
<Surface
style={
[{ opacity }, styles.content, contentContainerStyle] as StyleProp<
ViewStyle
>
}
>
{children}
</Surface>
</View>
</Animated.View>
);
}
{children}
</Surface>
</View>
</Animated.View>
);
}

export default withTheme(Modal);

const styles = StyleSheet.create({
backdrop: {
flex: 1,
Expand Down

0 comments on commit 072e40b

Please sign in to comment.