Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port the Modal component #44

Closed
wants to merge 11 commits into from
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"@react-navigation/stack": "~5.12.8",
"expo": "~41.0.1",
"expo-asset": "~8.3.1",
"expo-blur": "~9.0.3",
"expo-constants": "~10.1.3",
"expo-font": "~9.1.0",
"expo-linking": "~2.2.3",
Expand Down Expand Up @@ -140,7 +141,8 @@
"@testing-library/react": "^12.0.0",
"@types/react": "~16.9.35",
"@types/react-native": "~0.63.2",
"@types/styled-components": "^5.1.10",
"@types/react-portal": "^4.0.3",
"@types/styled-components": "5.1.6",
"@types/styled-components-react-native": "^5.1.1",
"@typescript-eslint/parser": "^4.28.3",
"babel-loader": "^8.2.2",
Expand Down
4 changes: 4 additions & 0 deletions src/baseElements/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from 'styled-components/native';
import { Animated } from 'react-native';
import { BlurView as BlurViewComponent } from 'expo-blur';
import { withGlobalStyle } from '../context';

// Use these elements over native styled.xx elements, as they apply
Expand All @@ -18,3 +19,6 @@ export const Button = withGlobalStyle(styled.Pressable``); // TODO: investigate

export const AnimatedView = withGlobalStyle(styled(Animated.View)``);
// export const AnimatedSpan = withGlobalStyle(styled(animated.span)``);

export const Modal = withGlobalStyle(styled.Modal``);
export const BlurView = withGlobalStyle(styled(BlurViewComponent));
143 changes: 143 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { ReactElement, ReactNode } from 'react';
import styled from 'styled-components/native';

import { View as NativeViewElement } from 'react-native';
import { BlurTint, BlurView as BlurViewBase } from 'expo-blur';

import { View, Button, Modal as NativeModal } from '../../baseElements';
import { SubcomponentPropsType, StyledSubcomponentType } from '../commonTypes';

const defaultOnClick = () => {};

const Container = styled(View)<{ location: 'top' | 'center' | 'bottom' }>`
display: flex;
height: 100%;
justify-content: ${({ location }) => {
switch (location) {
case 'top':
return 'flex-start';
case 'center':
return 'center';
case 'bottom':
return 'flex-end';
default:
return 'center';
}
}};
`;

const Underlay = styled(Button)`
height: 100%;
width: 100%;
`;

const BlurView = styled(BlurViewBase)`
height: 100%;
`;

export interface ModalProps {
StyledContainer?: StyledSubcomponentType;
StyledUnderlay?: StyledSubcomponentType;
StyledBlurView?: StyledSubcomponentType;

containerProps?: SubcomponentPropsType;
underlayProps?: SubcomponentPropsType;
blurViewProps?: SubcomponentPropsType;

containerRef?: React.RefObject<NativeViewElement>;
underlayRef?: React.RefObject<NativeViewElement>;
blurViewRef?: React.RefObject<NativeViewElement>;

animationType?: 'none' | 'slide' | 'fade';
location?: 'top' | 'center' | 'bottom';

children: ReactNode;

onPressOutside?: () => void;
onClose?: () => void;

backgroundBlur?: number;
backgroundDarkness?: BlurTint;
style?: Record<string, unknown>;
}

const Modal = ({
StyledContainer = Container,
StyledUnderlay = Underlay,
StyledBlurView = BlurView,

containerProps = {},
underlayProps = {},
blurViewProps = {},

containerRef,
underlayRef,
blurViewRef,

animationType = 'fade',
location = 'center',

children,

onPressOutside = defaultOnClick,
onClose = defaultOnClick,

backgroundBlur = 0.5,
backgroundDarkness = 'default',
}: ModalProps): ReactElement => {
const { styles: containerStyles }: { styles?: Record<string, unknown> } = containerProps;
const { styles: underlayStyles }: { styles?: Record<string, unknown> } = underlayProps;
const { styles: blurViewStyles }: { styles?: Record<string, unknown> } = blurViewProps;

const blurIntensity = Math.round(backgroundBlur * 100);

return (
<NativeModal
animationType="fade"
visible
transparent
onRequestClose={onClose}
hardwareAccelerated
>
{blurIntensity > 0 && (
<StyledBlurView
intensity={blurIntensity}
tint={backgroundDarkness}
ref={blurViewRef}
{...blurViewProps}
style={blurViewStyles}
/>
)}
<NativeModal
animationType={animationType}
visible
transparent
onRequestClose={onClose}
onDismiss={onClose}
hardwareAccelerated
>
<StyledUnderlay
onPress={onPressOutside}
ref={underlayRef}
{...underlayProps}
style={underlayStyles}
>
<StyledContainer
ref={containerRef}
{...containerProps}
style={containerStyles}
location={location}
>
{children}
</StyledContainer>
</StyledUnderlay>
</NativeModal>
</NativeModal>
);
};

Modal.Underlay = Underlay;
Modal.BlurView = BlurView;
Modal.Container = Container;

export default Modal;
3 changes: 3 additions & 0 deletions src/components/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Modal from './Modal';

export default Modal;
3 changes: 3 additions & 0 deletions src/components/commonTypes.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
import { StyledComponentBase } from 'styled-components';

export type SubcomponentPropsType = Record<string, unknown>;
export type StyledSubcomponentType = string & StyledComponentBase<any, SubcomponentPropsType>;
4 changes: 2 additions & 2 deletions storybook/rn-addons.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import "@storybook/addon-ondevice-actions/register";
import "@storybook/addon-ondevice-knobs/register";
import '@storybook/addon-ondevice-actions/register';
import '@storybook/addon-ondevice-knobs/register';
92 changes: 92 additions & 0 deletions storybook/stories/Modal/Modal.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useState } from 'react';

import { ImageBackground } from 'react-native';

import styled from 'styled-components/native';

import { boolean, select, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react-native';

import colors from '../../../src/enums/colors';
import variants from '../../../src/enums/variants';
import Modal from '../../../src/components/Modal';
import Button from '../../../src/components/Button';
import Card from '../../../src/components/Card';

const StyledButtonContainer = styled(Button.Container)`
align-self: flex-start;
`;

const DefaultModal = () => {
const [isOpen, setIsOpen] = useState(false);

const handleClose = () => {
setIsOpen(false);
action('close')();
};

const handlePressOutside = boolean('onPressOutside function', true) ? handleClose : undefined;

return (
<>
<Card elevation={1} header="Use this button to open the modal again">
<Button
color={colors.primaryDark}
variant={variants.fill}
onPress={() => {
setIsOpen(true);
action('open')();
}}
StyledContainer={StyledButtonContainer}
>
Open modal
</Button>
</Card>
{isOpen && (
<Modal
backgroundDarkness={select('backgroundDarkness', ['default', 'light', 'dark'], 'dark')}
backgroundBlur={number('backgroundBlur', 0.5, {
range: true,
min: 0,
max: 5,
step: 0.1,
})}
onPressOutside={handlePressOutside}
onClose={handleClose}
animationType={select('animationType', ['none', 'slide', 'fade'], 'fade')}
location={select('location', ['top', 'center', 'bottom'], 'center')}
>
<Card
header="Hello world!"
footer={
<Button color={colors.primaryDark} onPress={handleClose}>
Okay...
</Button>
}
elevation={1}
>
The content of the modal (the card and everything inside it) is customizable. The close
&times; is built-in but can be easily overwritten. It is the very model of a modern
major React modal.
</Card>
</Modal>
)}
</>
);
};

storiesOf('Modal', module)
.addParameters({ component: Modal })
.addDecorator(getStory => (
<ImageBackground
source={{ uri: 'https://source.unsplash.com/weekly?landscape' }}
style={{
flex: 1,
justifyContent: 'center',
}}
>
{getStory()}
</ImageBackground>
))
.add('Default', () => <DefaultModal />);
2 changes: 1 addition & 1 deletion storybook/stories/Welcome/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class Welcome extends React.Component {
},
};

showApp = (event) => {
showApp = event => {
const { showApp } = this.props;
event.preventDefault();

Expand Down
1 change: 1 addition & 0 deletions storybook/stories/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './Button/Button.stories';
import './Card/Card.stories';
import './Modal/Modal.stories';
import './StorybookButton/Button.stories';
import './Welcome/Welcome.stories';
Loading