-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A Modal implementation using CSS animations and ARIA. The app is hidden from screen readers via `aria-modal`. Focus is contained within the modal. When the `Escape` key is pressed, the `onRequestClose` function is called on the top-most modal. Close #1646 Fix #1020
- Loading branch information
1 parent
98d74da
commit d6e83d8
Showing
13 changed files
with
1,141 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { Meta, Props, 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 | ||
|
||
## Props | ||
|
||
| Name | Type | Default | | ||
| ------------------------- | --------- | ------- | | ||
| animationType | ?string | 'none' | | ||
| disabled | ?boolean | false | | ||
| onDismiss | ?Function | | | ||
| onRequestClose | ?Function | | | ||
| onShow | ?Function | | | ||
| transparent | ?boolean | false | | ||
| visible | ?boolean | false | | ||
|
||
|
||
### 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="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="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="transparent"> | ||
<Stories.transparentModal /> | ||
</Story> | ||
</Preview> | ||
|
||
### visible | ||
|
||
Whether or not the modal is visible. | ||
|
||
When set to `false` the contents are not rendered and the modal removes itself | ||
from the screen. | ||
|
||
<Preview withSource='none'> | ||
<Story name="visible"> | ||
<Stories.simpleModal /> | ||
</Story> | ||
</Preview> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import React, { useState } from 'react'; | ||
import { Modal, Text, Button } from 'react-native'; | ||
|
||
function Animated({ animationType }) { | ||
const [isVisible, setIsVisible] = useState(false); | ||
|
||
return ( | ||
<> | ||
<Button onPress={() => setIsVisible(true)} title={`Open Modal with '${animationType}'`} /> | ||
<Modal | ||
animationType={animationType} | ||
onRequestClose={() => setIsVisible(false)} | ||
visible={isVisible} | ||
> | ||
<Text>This is in the {animationType} Modal. Hello!</Text> | ||
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} /> | ||
</Modal> | ||
</> | ||
); | ||
} | ||
|
||
export default function() { | ||
return ( | ||
<> | ||
<Animated animationType={'none'} /> | ||
<Text> </Text> | ||
<Animated animationType={'slide'} /> | ||
<Text> </Text> | ||
<Animated animationType={'fade'} /> | ||
</> | ||
); | ||
} |
55 changes: 55 additions & 0 deletions
55
packages/docs/src/components/Modal/examples/Modalception.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React, { useState, useMemo } from 'react'; | ||
import { Modal, View, Text, Button } from 'react-native'; | ||
|
||
const WIGGLE_ROOM = 128; | ||
|
||
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={[style.backdrop]} /> | ||
<View style={[style.modal, offset]}> | ||
<View style={[style.container]}> | ||
<Text>This is in Modal {depth}. Hello!</Text> | ||
<Modalception depth={depth + 1} /> | ||
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} /> | ||
</View> | ||
</View> | ||
</Modal> | ||
</> | ||
); | ||
} | ||
|
||
const style = { | ||
backdrop: { | ||
zIndex: -1, | ||
position: 'fixed', | ||
background: 'rgba(255, 255, 255, 0.6)', | ||
top: 0, | ||
left: 0, | ||
right: 0, | ||
bottom: 0 | ||
}, | ||
modal: { | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
position: 'relative' | ||
}, | ||
container: { | ||
width: 600, | ||
background: 'white', | ||
border: '1px solid black', | ||
margin: '40px', | ||
padding: '20px' | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import React, { useState } from 'react'; | ||
import { Modal, Text, Button } from 'react-native'; | ||
|
||
export default function() { | ||
const [isVisible, setIsVisible] = useState(false); | ||
|
||
return ( | ||
<> | ||
<Button onPress={() => setIsVisible(true)} title={'Open Modal'} /> | ||
<Modal onRequestClose={() => setIsVisible(false)} visible={isVisible}> | ||
<Text>Hello, World!</Text> | ||
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} /> | ||
</Modal> | ||
</> | ||
); | ||
} |
16 changes: 16 additions & 0 deletions
16
packages/docs/src/components/Modal/examples/Transparent.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import React, { useState } from 'react'; | ||
import { Modal, Text, Button } from 'react-native'; | ||
|
||
export default function() { | ||
const [isVisible, setIsVisible] = useState(false); | ||
|
||
return ( | ||
<> | ||
<Button onPress={() => setIsVisible(true)} title={'Open Modal'} /> | ||
<Modal transparent visible={isVisible}> | ||
<Text>This Modal has a Transparent Background.</Text> | ||
<Button onPress={() => setIsVisible(false)} title={'Close Modal'} /> | ||
</Modal> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
130 changes: 130 additions & 0 deletions
130
packages/react-native-web/src/exports/Modal/ModalAnimation.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/** | ||
* Copyright (c) Nicolas Gallagher. | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow | ||
*/ | ||
|
||
import { useEffect, useState, useCallback } from 'react'; | ||
|
||
import StyleSheet from '../StyleSheet'; | ||
import createElement from '../createElement'; | ||
|
||
const ANIMATION_DURATION = 300; | ||
|
||
function getAnimationStyle(animationType, visible) { | ||
if (animationType === 'slide') { | ||
return visible ? animatedSlideInStyles : animatedSlideOutStyles; | ||
} | ||
if (animationType === 'fade') { | ||
return visible ? animatedFadeInStyles : animatedFadeOutStyles; | ||
} | ||
return visible ? styles.container : styles.hidden; | ||
} | ||
|
||
export type ModalAnimationProps = {| | ||
children?: any, | ||
animationType: ?('none' | 'slide' | 'fade'), | ||
visible?: ?boolean, | ||
onShow?: ?() => void, | ||
onDismiss?: ?() => void | ||
|}; | ||
|
||
function ModalAnimation(props: ModalAnimationProps) { | ||
const { children, animationType, visible, onShow, onDismiss } = props; | ||
|
||
const [isRendered, setIsRendered] = useState(visible); | ||
|
||
const isAnimated = animationType !== 'none'; | ||
|
||
const animationEndCallback = useCallback(() => { | ||
if (visible) { | ||
if (onShow != null) { | ||
onShow(); | ||
} | ||
} else { | ||
setIsRendered(false); | ||
if (onDismiss != null) { | ||
onDismiss(); | ||
} | ||
} | ||
}, [onDismiss, onShow, visible]); | ||
|
||
useEffect(() => { | ||
if (visible) { | ||
setIsRendered(true); | ||
} | ||
if (!isAnimated) { | ||
// Manually call `animationEndCallback` if no animation | ||
animationEndCallback(); | ||
} | ||
}, [isAnimated, visible, animationEndCallback]); | ||
|
||
return isRendered | ||
? createElement('div', { | ||
children, | ||
onAnimationEnd: animationEndCallback, | ||
style: getAnimationStyle(animationType, visible) | ||
}) | ||
: null; | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
position: 'fixed', | ||
top: 0, | ||
right: 0, | ||
bottom: 0, | ||
left: 0 | ||
}, | ||
animatedIn: { | ||
animationDuration: `${ANIMATION_DURATION}ms`, | ||
animationTimingFunction: 'ease-in' | ||
}, | ||
animatedOut: { | ||
pointerEvents: 'none', | ||
animationDuration: `${ANIMATION_DURATION}ms`, | ||
animationTimingFunction: 'ease-out' | ||
}, | ||
fadeIn: { | ||
opacity: 1, | ||
animationKeyframes: { | ||
'0%': { opacity: 0 }, | ||
'100%': { opacity: 1 } | ||
} | ||
}, | ||
fadeOut: { | ||
opacity: 0, | ||
animationKeyframes: { | ||
'0%': { opacity: 1 }, | ||
'100%': { opacity: 0 } | ||
} | ||
}, | ||
slideIn: { | ||
transform: [{ translateY: '0%' }], | ||
animationKeyframes: { | ||
'0%': { transform: [{ translateY: '100%' }] }, | ||
'100%': { transform: [{ translateY: '0%' }] } | ||
} | ||
}, | ||
slideOut: { | ||
transform: [{ translateY: '100%' }], | ||
animationKeyframes: { | ||
'0%': { transform: [{ translateY: '0%' }] }, | ||
'100%': { transform: [{ translateY: '100%' }] } | ||
} | ||
}, | ||
hidden: { | ||
display: 'none' | ||
} | ||
}); | ||
|
||
const animatedSlideInStyles = [styles.container, styles.animatedIn, styles.slideIn]; | ||
const animatedSlideOutStyles = [styles.container, styles.animatedOut, styles.slideOut]; | ||
const animatedFadeInStyles = [styles.container, styles.animatedIn, styles.fadeIn]; | ||
const animatedFadeOutStyles = [styles.container, styles.animatedOut, styles.fadeOut]; | ||
|
||
export default ModalAnimation; |
Oops, something went wrong.