Skip to content

Commit

Permalink
[add] Modal component
Browse files Browse the repository at this point in the history
This adds support for the React Native Modal on web.

The app content is hidden from screen readers by setting the aria-modal flag on
the modal. This focus is trapped within the modal, both when attempting to
focus elsewhere using the mouse as well as when attempting to focus elsewhere
using the keyboard. A built-in "Escape to close" mechanism is been implemented
that calls 'onRequestClose' for the active modal.

Close #1646
Fix #1020
  • Loading branch information
imnotjames authored and necolas committed Sep 9, 2020
1 parent 004683c commit e37d6ec
Show file tree
Hide file tree
Showing 13 changed files with 1,202 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ React Native v0.60
| Image || Missing multiple sources ([#515](https://github.com/necolas/react-native-web/issues/515)) and HTTP headers ([#1019](https://github.com/necolas/react-native-web/issues/1019)). |
| ImageBackground || |
| KeyboardAvoidingView | (✓) | Mock. No equivalent web APIs. |
| Modal | | Not started ([#1020](https://github.com/necolas/react-native-web/issues/1020)). |
| Modal | | |
| Picker || |
| Pressable || |
| RefreshControl || Not started ([#1027](https://github.com/necolas/react-native-web/issues/1027)). |
Expand Down
83 changes: 83 additions & 0 deletions packages/docs/src/components/Modal/Modal.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import * as Stories from './examples';

<Meta title="Components|Modal" />

# Modal

The Modal component is a basic way to present content above an enclosing view.
Modals may be nested within other Modals.

## Props

| Name | Type | Default |
| ------------------------- | -------------- | ------- |
| animationType | ?AnimationType | 'none' |
| children | ?any | |
| onDismiss | ?Function | |
| onRequestClose | ?Function | |
| onShow | ?Function | |
| transparent | ?boolean | false |
| visible | ?boolean | true |

### animationType

The `animationType` prop can be used to add animation to the modal
being opened or dismissed.

* `none` - the modal appears without any animation.
* `slide` - the modal slides up from the bottom of the screen.
* `fade` - the modal fades in.

By default this is `none`.

<Preview withSource='none'>
<Story name="propsExample-animationType">
<Stories.animatedModal />
</Story>
</Preview>

### onDismiss

The `onDismiss` callback is called after the modal has been dismissed and is no longer visible.

### onRequestClose

The `onRequestClose` callback is called when the user is attempting to close the modal -
such as when they hit `Escape`.

Only the top-most Modal responds to hitting `Escape`.

<Preview withSource='none'>
<Story name="propsExample-onRequestClose">
<Stories.modalception />
</Story>
</Preview>

### onShow

The `onShow` callback is called once the modal has been shown and may be visible.

### transparent

The `transparent` prop determines if the modal is rendered with a `transparent` backdrop or
a `white` backdrop.

<Preview withSource='none'>
<Story name="propsExample-transparent">
<Stories.transparentModal />
</Story>
</Preview>

### visible

Whether or not the modal is visible.

When set to `false` the contents are not rendered & the modal removes itself
from the screen.

<Preview withSource='none'>
<Story name="propsExample-visible">
<Stories.simpleModal />
</Story>
</Preview>
50 changes: 50 additions & 0 deletions packages/docs/src/components/Modal/examples/Animated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { Modal, Text, Button, View, StyleSheet } from 'react-native';

function Gap() {
return <View style={styles.gap} />;
}

function AnimatedModal({ animationType }) {
const [isVisible, setIsVisible] = useState(false);

return (
<>
<Button onPress={() => setIsVisible(true)} title={`Open Modal with '${animationType}'`} />
<Modal
animationType={animationType}
onRequestClose={() => setIsVisible(false)}
visible={isVisible}
>
<View style={styles.container}>
<Text>Modal with "animationType" of "{animationType}"</Text>
<Gap />
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} />
</View>
</Modal>
</>
);
}

export default function AnimatedModalStack() {
return (
<>
<AnimatedModal animationType={'none'} />
<Gap />
<AnimatedModal animationType={'slide'} />
<Gap />
<AnimatedModal animationType={'fade'} />
</>
);
}

const styles = StyleSheet.create({
container: {
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
gap: {
height: 10
}
});
52 changes: 52 additions & 0 deletions packages/docs/src/components/Modal/examples/Modalception.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useState, useMemo } from 'react';
import { Modal, View, Text, Button, StyleSheet } from 'react-native';

const WIGGLE_ROOM = 128;

function Gap() {
return <View style={styles.gap} />;
}

export default function Modalception({ depth = 1 }) {
const [isVisible, setIsVisible] = useState(false);

const offset = useMemo(() => {
return {
top: Math.random() * WIGGLE_ROOM - WIGGLE_ROOM / 2,
left: Math.random() * WIGGLE_ROOM - WIGGLE_ROOM / 2
};
}, []);

return (
<>
<Button onPress={() => setIsVisible(true)} title={'Open Modal'} />
<Modal onRequestClose={() => setIsVisible(false)} transparent visible={isVisible}>
<View style={[styles.container, offset]}>
<Text>This is in Modal {depth}</Text>
<Gap />
{isVisible ? <Modalception depth={depth + 1} /> : null}
<Gap />
<Button color="red" onPress={() => setIsVisible(false)} title={'Close Modal'} />
</View>
</Modal>
</>
);
}

const styles = StyleSheet.create({
container: {
alignItems: 'center',
backgroundColor: 'white',
borderColor: '#eee',
borderRadius: 10,
borderWidth: 1,
justifyContent: 'center',
height: 300,
margin: 'auto',
padding: 30,
width: 300
},
gap: {
height: 10
}
});
34 changes: 34 additions & 0 deletions packages/docs/src/components/Modal/examples/Simple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import { Modal, Text, View, Button, StyleSheet } from 'react-native';

function Gap() {
return <View style={styles.gap} />;
}

export default function SimpleModal() {
const [isVisible, setIsVisible] = useState(false);

return (
<>
<Button onPress={() => setIsVisible(true)} title={'Open Modal'} />
<Modal onRequestClose={() => setIsVisible(false)} visible={isVisible}>
<View style={styles.container}>
<Text>Hello, World!</Text>
<Gap />
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} />
</View>
</Modal>
</>
);
}

const styles = StyleSheet.create({
container: {
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
gap: {
height: 10
}
});
41 changes: 41 additions & 0 deletions packages/docs/src/components/Modal/examples/Transparent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { Modal, Text, View, Button, StyleSheet } from 'react-native';

function Gap() {
return <View style={styles.gap} />;
}

export default function TransparentModal() {
const [isVisible, setIsVisible] = useState(false);

return (
<>
<Button onPress={() => setIsVisible(true)} title={'Open Modal'} />
<Modal onRequestClose={() => setIsVisible(false)} transparent visible={isVisible}>
<View style={styles.container}>
<Text style={{ textAlign: 'center' }}>Modal with "transparent" value</Text>
<Gap />
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} />
</View>
</Modal>
</>
);
}

const styles = StyleSheet.create({
container: {
alignItems: 'center',
backgroundColor: 'white',
borderColor: '#eee',
borderRadius: 10,
borderWidth: 1,
justifyContent: 'center',
height: 300,
margin: 'auto',
padding: 30,
width: 300
},
gap: {
height: 10
}
});
4 changes: 4 additions & 0 deletions packages/docs/src/components/Modal/examples/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as transparentModal } from './Transparent';
export { default as simpleModal } from './Simple';
export { default as animatedModal } from './Animated';
export { default as modalception } from './Modalception';
Loading

0 comments on commit e37d6ec

Please sign in to comment.