Skip to content

Commit 1ba99ad

Browse files
authored
Feat/incubator dialog fixes and improvements (#1704)
* Add ImperativeDialog - fix bug while keeping simple * Add alignment support * Refactor (add useFadeView) and fix pointerEvents * Fix onRequestClose * Support useSafeArea * Add supportedOrientations and accessibilityLabel * Revert "Add supportedOrientations and accessibilityLabel" This reverts commit 9d8b588. * Add modalProps.supportedOrientations to the screen * Bring back containerStyle * Workaround useSafeArea performance issue * Fix types * Remove containerStyle again * Add api
1 parent d8e9a78 commit 1ba99ad

File tree

14 files changed

+454
-187
lines changed

14 files changed

+454
-187
lines changed

demo/src/screens/incubatorScreens/IncubatorDialogScreen.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {Component} from 'react';
2-
import {StyleSheet} from 'react-native';
2+
import {StyleSheet, ModalProps} from 'react-native';
33
import {FlatList} from 'react-native-gesture-handler';
4-
import {View, Text, Card, Button, Incubator, Colors, BorderRadiuses} from 'react-native-ui-lib'; //eslint-disable-line
4+
import {View, Text, Card, Button, Incubator, Colors, BorderRadiuses, Constants} from 'react-native-ui-lib'; //eslint-disable-line
55

66
interface Item {
77
value: string;
@@ -33,10 +33,11 @@ const colors: Item[] = [
3333

3434
export default class IncubatorDialogScreen extends Component {
3535
state = {visible: false};
36+
modalProps: ModalProps = {supportedOrientations: ['portrait', 'landscape']};
3637

3738
renderVerticalItem = ({item}: {item: Item}) => {
3839
return (
39-
<Text text50 margin-20 color={item.value}>
40+
<Text text50 margin-20 color={item.value} onPress={this.closeDialog}>
4041
{item.label}
4142
</Text>
4243
);
@@ -54,6 +55,10 @@ export default class IncubatorDialogScreen extends Component {
5455
this.setState({visible: false});
5556
};
5657

58+
onDismiss = () => {
59+
this.setState({visible: false});
60+
};
61+
5762
render() {
5863
const {visible} = this.state;
5964

@@ -65,7 +70,14 @@ export default class IncubatorDialogScreen extends Component {
6570
<View flex center>
6671
<Button marginV-20 label="Open Dialog" onPress={this.openDialog}/>
6772
</View>
68-
<Incubator.Dialog visible={visible} onDismiss={this.closeDialog} bottom containerStyle={styles.dialogContainer}>
73+
<Incubator.Dialog
74+
useSafeArea
75+
visible={visible}
76+
onDismiss={this.onDismiss}
77+
bottom
78+
centerH
79+
modalProps={this.modalProps}
80+
>
6981
<View style={styles.dialog}>
7082
<Text text60 margin-s2>
7183
Title (swipe here)
@@ -86,14 +98,11 @@ export default class IncubatorDialogScreen extends Component {
8698
}
8799

88100
const styles = StyleSheet.create({
89-
dialogContainer: {
90-
bottom: 20,
91-
alignSelf: 'center'
92-
},
93101
dialog: {
102+
marginBottom: 20,
94103
backgroundColor: Colors.white,
95-
width: 200,
96-
height: 300,
104+
maxHeight: Constants.screenHeight * 0.8,
105+
width: 300,
97106
borderRadius: BorderRadiuses.br20
98107
},
99108
verticalScroll: {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import { ImperativeDialogMethods, DialogDirections, DialogDirectionsEnum } from './types';
3+
export { DialogDirections, DialogDirectionsEnum };
4+
declare const _default: React.ForwardRefExoticComponent<import("./types")._DialogProps & {
5+
children?: React.ReactNode;
6+
} & React.RefAttributes<ImperativeDialogMethods>>;
7+
export default _default;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { AlignmentModifiers } from '../../../commons/modifiers';
2+
export declare enum AlignmentType {
3+
DEFAULT = "default",
4+
BOTTOM = "bottom",
5+
TOP = "top"
6+
}
7+
declare const useAlignmentStyle: (props: AlignmentModifiers) => {
8+
alignmentType: AlignmentType;
9+
alignmentStyle: any[];
10+
};
11+
export default useAlignmentStyle;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference types="react" />
2+
import { ModalProps } from '../../../components/modal';
3+
import { TransitionViewAnimationType } from '../../TransitionView';
4+
import { ImperativeDialogProps } from '../types';
5+
export declare type AnimationType = TransitionViewAnimationType;
6+
export declare type FadeViewProps = Pick<ImperativeDialogProps, 'initialVisibility' | 'testID'> & Pick<ModalProps, 'overlayBackgroundColor'>;
7+
export interface FadeViewMethods {
8+
hideNow: () => void;
9+
}
10+
declare const useFadeView: (props: FadeViewProps) => {
11+
FadeView: JSX.Element;
12+
hideNow: () => void;
13+
fade: (type: AnimationType) => void;
14+
};
15+
export default useFadeView;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference types="react" />
2+
import { ImperativeDialogProps } from '../types';
3+
import { AlignmentType } from './useAlignmentStyle';
4+
export interface SafeAreaProps extends Pick<ImperativeDialogProps, 'useSafeArea'> {
5+
alignmentType: AlignmentType;
6+
}
7+
/**
8+
* TODO: technically useSafeArea can be sent to either PanView or TransitionView.
9+
* however that causes some performance \ UI bugs (when there is a safe area).
10+
* TransitionView is less pronouns than PanView but still not good.
11+
* We think this is because of reanimation 2, we should re-visit this problem later.
12+
*/
13+
declare const useSafeAreaView: (props: SafeAreaProps) => {
14+
topSafeArea: JSX.Element | undefined;
15+
bottomSafeArea: JSX.Element | undefined;
16+
};
17+
export default useSafeAreaView;
Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,15 @@
1-
import { PropsWithChildren } from 'react';
2-
import { StyleProp, ViewStyle } from 'react-native';
3-
import { PanningDirections, PanningDirectionsEnum } from '../panView';
4-
import { ModalProps } from '../../components/modal';
5-
import { AlignmentModifiers } from '../../commons/modifiers';
6-
declare type DialogDirections = PanningDirections;
7-
declare const DialogDirectionsEnum: typeof PanningDirectionsEnum;
1+
/// <reference types="react" />
2+
import { ImperativeDialogProps, DialogDirections, DialogDirectionsEnum } from './types';
83
export { DialogDirections, DialogDirectionsEnum };
9-
interface _DialogProps extends AlignmentModifiers {
4+
export interface DialogProps extends Omit<ImperativeDialogProps, 'initialVisibility'> {
105
/**
11-
* Control visibility of the dialog.
6+
* The visibility of the dialog.
127
*/
138
visible?: boolean;
14-
/**
15-
* Callback that is called after the dialog's dismiss (after the animation has ended).
16-
*/
17-
onDismiss?: (props?: DialogProps) => void;
18-
/**
19-
* The direction from which and to which the dialog is animating \ panning (default bottom).
20-
*/
21-
direction?: DialogDirections;
22-
/**
23-
* The Dialog`s container style (it is set to {position: 'absolute'})
24-
*/
25-
containerStyle?: StyleProp<ViewStyle>;
26-
/**
27-
* Whether or not to ignore background press.
28-
*/
29-
ignoreBackgroundPress?: boolean;
30-
/**
31-
* Additional props for the modal.
32-
*/
33-
modalProps?: ModalProps;
34-
/**
35-
* Used to locate this view in end-to-end tests
36-
* The container has the unchanged id.
37-
* Currently supported inner IDs:
38-
* TODO: add missing <TestID>(s?)
39-
* <TestID>.modal - the Modal's id.
40-
* <TestID>.overlayFadingBackground - the fading background id.
41-
*/
42-
testID?: string;
439
}
44-
export declare type DialogProps = PropsWithChildren<_DialogProps>;
4510
declare const Dialog: {
4611
(props: DialogProps): JSX.Element;
4712
displayName: string;
48-
directions: typeof PanningDirectionsEnum;
13+
directions: typeof import("../panView").PanningDirectionsEnum;
4914
};
5015
export default Dialog;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { PropsWithChildren } from 'react';
2+
import { AlignmentModifiers } from '../../commons/modifiers';
3+
import { ModalProps } from '../../components/modal';
4+
import { ViewProps } from '../../components/view';
5+
import { PanningDirections, PanningDirectionsEnum } from '../panView';
6+
declare type DialogDirections = PanningDirections;
7+
declare const DialogDirectionsEnum: typeof PanningDirectionsEnum;
8+
export { DialogDirections, DialogDirectionsEnum };
9+
export interface _DialogProps extends AlignmentModifiers, Pick<ViewProps, 'useSafeArea'> {
10+
/**
11+
* The initial visibility of the dialog.
12+
*/
13+
initialVisibility?: boolean;
14+
/**
15+
* Callback that is called after the dialog's dismiss (after the animation has ended).
16+
*/
17+
onDismiss?: (props?: ImperativeDialogProps) => void;
18+
/**
19+
* The direction from which and to which the dialog is animating \ panning (default down).
20+
*/
21+
direction?: DialogDirections;
22+
/**
23+
* Whether or not to ignore background press.
24+
*/
25+
ignoreBackgroundPress?: boolean;
26+
/**
27+
* Additional props for the modal.
28+
*/
29+
modalProps?: ModalProps;
30+
/**
31+
* Used to locate this view in end-to-end tests
32+
* The container has the unchanged id.
33+
* Currently supported inner IDs:
34+
* TODO: add missing <TestID>(s?)
35+
* <TestID>.modal - the Modal's id.
36+
* <TestID>.overlayFadingBackground - the fading background id.
37+
*/
38+
testID?: string;
39+
}
40+
export declare type ImperativeDialogProps = PropsWithChildren<_DialogProps>;
41+
export interface ImperativeDialogMethods {
42+
open: () => void;
43+
close: () => void;
44+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React, {useMemo, useCallback, useState, useImperativeHandle, forwardRef} from 'react';
2+
import {StyleSheet} from 'react-native';
3+
import View from '../../components/view';
4+
import Modal from '../../components/modal';
5+
import TransitionView, {TransitionViewAnimationType} from '../TransitionView';
6+
import PanView from '../panView';
7+
import useAlignmentStyle from './helpers/useAlignmentStyle';
8+
import useSafeAreaView from './helpers/useSafeAreaView';
9+
import useFadeView from './helpers/useFadeView';
10+
import {ImperativeDialogProps, ImperativeDialogMethods, DialogDirections, DialogDirectionsEnum} from './types';
11+
export {DialogDirections, DialogDirectionsEnum};
12+
13+
const ImperativeDialog = (props: ImperativeDialogProps, ref: any) => {
14+
const {
15+
initialVisibility = false,
16+
onDismiss,
17+
direction = DialogDirectionsEnum.DOWN,
18+
children,
19+
ignoreBackgroundPress,
20+
modalProps = {},
21+
useSafeArea,
22+
testID
23+
} = props;
24+
const transitionAnimatorRef = React.createRef<typeof TransitionView>();
25+
const {overlayBackgroundColor, ...otherModalProps} = modalProps;
26+
const [visible, setVisible] = useState(initialVisibility);
27+
const {alignmentType, alignmentStyle} = useAlignmentStyle(props);
28+
const {topSafeArea, bottomSafeArea} = useSafeAreaView({useSafeArea, alignmentType});
29+
const {FadeView, hideNow, fade} = useFadeView({
30+
initialVisibility,
31+
testID: `${testID}.overlayFadingBackground`,
32+
overlayBackgroundColor
33+
});
34+
35+
const open = useCallback(() => {
36+
if (!visible) {
37+
setVisible(true);
38+
}
39+
}, [visible, setVisible]);
40+
41+
const close = useCallback(() => {
42+
if (visible) {
43+
transitionAnimatorRef.current?.animateOut();
44+
}
45+
}, [visible, transitionAnimatorRef]);
46+
47+
useImperativeHandle(ref, () => ({
48+
open,
49+
close
50+
}));
51+
52+
const directions = useMemo((): DialogDirections[] => {
53+
return [direction];
54+
}, [direction]);
55+
56+
const onBackgroundPress = useCallback(() => {
57+
close();
58+
}, [close]);
59+
60+
const onPanViewDismiss = useCallback(() => {
61+
hideNow();
62+
setVisible(false);
63+
onDismiss?.();
64+
}, [hideNow, onDismiss, setVisible]);
65+
66+
const onTransitionAnimationEnd = useCallback((type: TransitionViewAnimationType) => {
67+
if (type === 'exit') {
68+
setVisible(false);
69+
onDismiss?.();
70+
}
71+
},
72+
[onDismiss, setVisible]);
73+
74+
const renderDialog = () => {
75+
return (
76+
<PanView
77+
directions={directions}
78+
dismissible
79+
animateToOrigin
80+
containerStyle={styles.panView}
81+
onDismiss={onPanViewDismiss}
82+
>
83+
<TransitionView
84+
ref={transitionAnimatorRef}
85+
enterFrom={direction}
86+
exitTo={direction}
87+
onAnimationStart={fade}
88+
onAnimationEnd={onTransitionAnimationEnd}
89+
>
90+
{topSafeArea}
91+
{children}
92+
{bottomSafeArea}
93+
</TransitionView>
94+
</PanView>
95+
);
96+
};
97+
98+
return (
99+
<Modal
100+
transparent
101+
animationType={'none'}
102+
{...otherModalProps}
103+
testID={`${testID}.modal`}
104+
useGestureHandlerRootView
105+
visible={visible}
106+
onBackgroundPress={ignoreBackgroundPress ? undefined : onBackgroundPress}
107+
onRequestClose={ignoreBackgroundPress ? undefined : onBackgroundPress}
108+
onDismiss={undefined}
109+
>
110+
{FadeView}
111+
<View pointerEvents={'box-none'} style={alignmentStyle}>
112+
{renderDialog()}
113+
</View>
114+
</Modal>
115+
);
116+
};
117+
118+
ImperativeDialog.displayName = 'IGNORE';
119+
120+
export default forwardRef<ImperativeDialogMethods, ImperativeDialogProps>(ImperativeDialog);
121+
122+
const styles = StyleSheet.create({
123+
panView: {
124+
position: 'absolute'
125+
}
126+
});

src/incubator/Dialog/dialog.api.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "Dialog",
3+
"category": "incubator",
4+
"description": "Component for displaying custom content inside a popup dialog",
5+
"note": "Use alignment modifiers to control the dialog position (top, bottom, centerV, centerH, etc... by default the dialog is aligned to center)",
6+
"modifiers": ["alignment"],
7+
"example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/IncubatorDialogScreen.js",
8+
"props": [
9+
{"name": "visible", "type": "boolean", "description": "The visibility of the dialog"},
10+
{"name": "onDismiss", "type": "(props?: ImperativeDialogProps) => void", "description": "Callback that is called after the dialog's dismiss (after the animation has ended)."},
11+
{
12+
"name": "direction",
13+
"type": "up | down | left | right",
14+
"description": "The direction from which and to which the dialog is animating \\ panning (default bottom).",
15+
"default": "down"
16+
},
17+
{"name": "ignoreBackgroundPress", "type": "boolean", "description": "Whether or not to ignore background press."},
18+
{"name": "modalProps", "type": "ModalProps", "description": "Pass props to the dialog modal"},
19+
{"name": "testID", "type": "string", "description": "Used as a testing identifier"}
20+
]
21+
}

0 commit comments

Comments
 (0)