[add] Modal component

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
This commit is contained in:
James Ward
2020-09-08 11:45:37 -07:00
committed by Nicolas Gallagher
parent 6bd41a622a
commit d97a1ca567
13 changed files with 1217 additions and 3 deletions
@@ -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>
@@ -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
}
});
@@ -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
}
});
@@ -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
}
});
@@ -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
}
});
@@ -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';