[instant] Viewport specific errors (#1228)

feat(instant): Shows different error animation based on viewport
This commit is contained in:
Steve Klebanoff
2018-11-08 15:37:56 -08:00
committed by GitHub
parent c448a409c1
commit 117e2f583f
8 changed files with 118 additions and 49 deletions

View File

@@ -1,5 +1,6 @@
import { Keyframes } from 'styled-components';
import { InterpolationValue } from 'styled-components';
import { media, OptionallyScreenSpecific, stylesForMedia } from '../../style/media';
import { css, keyframes, styled } from '../../style/theme';
export interface TransitionInfo {
@@ -51,30 +52,57 @@ export interface PositionAnimationSettings {
right?: TransitionInfo;
timingFunction: string;
duration?: string;
position?: string;
}
export interface PositionAnimationProps extends PositionAnimationSettings {
position: string;
const generatePositionAnimationCss = (positionSettings: PositionAnimationSettings) => {
return css`
animation-name: ${slideKeyframeGenerator(
positionSettings.position || 'relative',
positionSettings.top,
positionSettings.bottom,
positionSettings.left,
positionSettings.right,
)};
animation-duration: ${positionSettings.duration || '0.3s'};
animation-timing-function: ${positionSettings.timingFunction};
animation-delay: 0s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
position: ${positionSettings.position || 'relative'};
width: 100%;
`;
};
export interface PositionAnimationProps {
positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
zIndex?: OptionallyScreenSpecific<number>;
}
const defaultAnimation = (positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>) => {
const bestDefault = 'default' in positionSettings ? positionSettings.default : positionSettings;
return generatePositionAnimationCss(bestDefault);
};
const animationForSize = (
positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>,
sizeKey: 'sm' | 'md' | 'lg',
mediaFn: (...args: any[]) => InterpolationValue[],
) => {
// checking default makes sure we have a PositionAnimationSettings object
// and then we check to see if we have a setting for the specific `sizeKey`
const animationSettingsForSize = 'default' in positionSettings && positionSettings[sizeKey];
return animationSettingsForSize && mediaFn`${generatePositionAnimationCss(animationSettingsForSize)}`;
};
export const PositionAnimation =
styled.div <
PositionAnimationProps >
`
animation-name: ${props =>
css`
${slideKeyframeGenerator(props.position, props.top, props.bottom, props.left, props.right)};
`};
animation-duration: ${props => props.duration || '0.3s'};
animation-timing-function: ${props => props.timingFunction};
animation-delay: 0s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
position: ${props => props.position};
height: 100%;
width: 100%;
&& {
${props => props.zIndex && stylesForMedia<number>('z-index', props.zIndex)}
${props => defaultAnimation(props.positionSettings)}
${props => animationForSize(props.positionSettings, 'sm', media.small)}
${props => animationForSize(props.positionSettings, 'md', media.medium)}
${props => animationForSize(props.positionSettings, 'lg', media.large)}
}
`;
PositionAnimation.defaultProps = {
position: 'relative',
};

View File

@@ -1,22 +1,24 @@
import * as React from 'react';
import { OptionallyScreenSpecific } from '../../style/media';
import { PositionAnimation, PositionAnimationSettings } from './position_animation';
export type SlideAnimationState = 'slidIn' | 'slidOut' | 'none';
export interface SlideAnimationProps {
position: string;
animationState: SlideAnimationState;
slideInSettings: PositionAnimationSettings;
slideOutSettings: PositionAnimationSettings;
slideInSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
zIndex?: OptionallyScreenSpecific<number>;
}
export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => {
if (props.animationState === 'none') {
return <React.Fragment>{props.children}</React.Fragment>;
}
const propsToUse = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings;
const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings;
return (
<PositionAnimation position={props.position} {...propsToUse}>
<PositionAnimation positionSettings={positionSettings} zIndex={props.zIndex}>
{props.children}
</PositionAnimation>
);

View File

@@ -1,6 +1,8 @@
import * as React from 'react';
import { ScreenSpecification } from '../style/media';
import { ColorOption } from '../style/theme';
import { zIndex } from '../style/z_index';
import { PositionAnimationSettings } from './animations/position_animation';
import { SlideAnimation, SlideAnimationState } from './animations/slide_animation';
@@ -21,6 +23,7 @@ export const Error: React.StatelessComponent<ErrorProps> = props => (
backgroundColor={ColorOption.lightOrange}
width="100%"
borderRadius="6px"
marginTop="10px"
marginBottom="10px"
>
<Flex justify="flex-start">
@@ -39,25 +42,51 @@ export interface SlidingErrorProps extends ErrorProps {
}
export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props => {
const slideAmount = '120px';
const slideUpSettings: PositionAnimationSettings = {
const desktopSlideIn: PositionAnimationSettings = {
timingFunction: 'ease-in',
top: {
from: slideAmount,
to: '0px',
},
position: 'relative',
};
const slideDownSettings: PositionAnimationSettings = {
const desktopSlideOut: PositionAnimationSettings = {
timingFunction: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
top: {
from: '0px',
to: slideAmount,
},
position: 'relative',
};
const mobileSlideIn: PositionAnimationSettings = {
duration: '0.5s',
timingFunction: 'ease-in',
top: { from: '-120px', to: '0px' },
position: 'fixed',
};
const moblieSlideOut: PositionAnimationSettings = {
duration: '0.5s',
timingFunction: 'ease-in',
top: { from: '0px', to: '-120px' },
position: 'fixed',
};
const slideUpSettings: ScreenSpecification<PositionAnimationSettings> = {
default: desktopSlideIn,
sm: mobileSlideIn,
};
const slideOutSettings: ScreenSpecification<PositionAnimationSettings> = {
default: desktopSlideOut,
sm: moblieSlideOut,
};
return (
<SlideAnimation
position="relative"
slideInSettings={slideUpSettings}
slideOutSettings={slideDownSettings}
slideOutSettings={slideOutSettings}
zIndex={{ sm: zIndex.errorPopUp, default: zIndex.errorPopBehind }}
animationState={props.animationState}
>
<Error icon={props.icon} message={props.message} />

View File

@@ -51,6 +51,7 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props =
from: slideAmount,
to: '0px',
},
position: 'absolute',
};
const slideDownSettings: PositionAnimationSettings = {
duration: '0.3s',
@@ -59,10 +60,10 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props =
from: '0px',
to: slideAmount,
},
position: 'absolute',
};
return (
<SlideAnimation
position="absolute"
slideInSettings={slideUpSettings}
slideOutSettings={slideDownSettings}
animationState={animationState}

View File

@@ -67,9 +67,9 @@ export const Container =
${props => cssRuleIfExists(props, 'cursor')}
${props => cssRuleIfExists(props, 'overflow')}
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
${props => props.display && stylesForMedia('display', props.display)}
${props => (props.width ? stylesForMedia('width', props.width) : '')}
${props => (props.height ? stylesForMedia('height', props.height) : '')}
${props => props.display && stylesForMedia<string>('display', props.display)}
${props => props.width && stylesForMedia<string>('width', props.width)}
${props => props.height && stylesForMedia<string>('height', props.height)}
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')};
&:hover {

View File

@@ -35,7 +35,7 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain
height={{ default: 'auto', sm: '100%' }}
position="relative"
>
<Container zIndex={zIndex.errorPopup} position="relative">
<Container position="relative">
<LatestError />
</Container>
<Container

View File

@@ -14,30 +14,38 @@ const generateMediaWrapper = (screenWidth: ScreenWidths) => (...args: any[]) =>
}
`;
const media = {
export const media = {
small: generateMediaWrapper(ScreenWidths.Sm),
medium: generateMediaWrapper(ScreenWidths.Md),
large: generateMediaWrapper(ScreenWidths.Lg),
};
export interface ScreenSpecifications {
default: string;
sm?: string;
md?: string;
lg?: string;
export interface ScreenSpecification<T> {
default: T;
sm?: T;
md?: T;
lg?: T;
}
export type MediaChoice = string | ScreenSpecifications;
export const stylesForMedia = (cssPropertyName: string, choice: MediaChoice): InterpolationValue[] => {
if (typeof choice === 'string') {
export type OptionallyScreenSpecific<T> = T | ScreenSpecification<T>;
export type MediaChoice = OptionallyScreenSpecific<string>;
/**
* Given a css property name and a OptionallyScreenSpecific value,
* generates css properties with screen-specific viewport styling
*/
export function stylesForMedia<T extends string | number>(
cssPropertyName: string,
choice: OptionallyScreenSpecific<T>,
): InterpolationValue[] {
if (typeof choice === 'object') {
return css`
${cssPropertyName}: ${choice};
`;
}
return css`
${cssPropertyName}: ${choice.default};
${choice.lg && media.large`${cssPropertyName}: ${choice.lg}`}
${choice.md && media.medium`${cssPropertyName}: ${choice.md}`}
${choice.sm && media.small`${cssPropertyName}: ${choice.sm}`}
`;
};
} else {
return css`
${cssPropertyName}: ${choice};
`;
}
}

View File

@@ -1,5 +1,6 @@
export const zIndex = {
errorPopup: 1,
errorPopBehind: 1,
mainContainer: 2,
panel: 3,
errorPopUp: 4,
};