react-native-ui-lib
Version:
<p align="center"> <img src="https://user-images.githubusercontent.com/1780255/105469025-56759000-5ca0-11eb-993d-3568c1fd54f4.png" height="250px" style="display:block"/> </p> <p align="center">UI Toolset & Components Library for React Native</p> <p a
472 lines (415 loc) • 13.1 kB
JavaScript
import _pt from "prop-types";
import _ from 'lodash';
import React, { Component } from 'react';
import { StyleSheet, Animated, Easing } from 'react-native';
import { BorderRadiuses, Colors, Dividers, Spacings } from "../../style";
import { createShimmerPlaceholder, LinearGradientPackage } from "../../optionalDependencies";
import View from "../view";
import { Constants, asBaseComponent } from "../../commons/new";
import { extractAccessibilityProps } from "../../commons/modifiers";
const LinearGradient = LinearGradientPackage?.default;
let ShimmerPlaceholder;
const ANIMATION_DURATION = 400;
export let Template;
(function (Template) {
Template["LIST_ITEM"] = "listItem";
Template["TEXT_CONTENT"] = "content";
})(Template || (Template = {}));
export let Size;
(function (Size) {
Size["SMALL"] = "small";
Size["LARGE"] = "large";
})(Size || (Size = {}));
export let ContentType;
(function (ContentType) {
ContentType["AVATAR"] = "avatar";
ContentType["THUMBNAIL"] = "thumbnail";
})(ContentType || (ContentType = {}));
/**
* @description: Allows showing a temporary skeleton view while your real view is loading.
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/SkeletonViewScreen.tsx
* @image: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Skeleton/Skeleton.gif?raw=true
* @notes: View requires installing the 'react-native-shimmer-placeholder' and 'react-native-linear-gradient' library
*/
class SkeletonView extends Component {
static propTypes = {
/**
* The content has been loaded, start fading out the skeleton and fading in the content
*/
showContent: _pt.bool,
/**
* A function that will render the content once the content is ready (i.e. showContent is true).
* The method will be called with the Skeleton's customValue (i.e. renderContent(props?.customValue))
*/
renderContent: _pt.func,
/**
* Custom value of any type to pass on to SkeletonView and receive back in the renderContent callback.
*/
customValue: _pt.any,
/**
* @deprecated
* - Please use customValue instead.
* - Custom value of any type to pass on to SkeletonView and receive back in the renderContent callback.
*/
contentData: _pt.any,
/**
* The type of the skeleton view.
* Types: LIST_ITEM and TEXT_CONTENT (using SkeletonView.templates.###)
*/
template: _pt.oneOf(["listItem", "content"]),
/**
* Props that are available when using template={SkeletonView.templates.LIST_ITEM}
*/
listProps: _pt.shape({
/**
* The size of the skeleton view.
* Types: SMALL and LARGE (using SkeletonView.sizes.###)
*/
size: _pt.oneOf(["small", "large"]),
/**
* Add content to the skeleton.
* Types: AVATAR and THUMBNAIL (using SkeletonView.contentTypes.###)
*/
contentType: _pt.oneOf(["avatar", "thumbnail"]),
/**
* Whether to hide the list item template separator
*/
hideSeparator: _pt.bool,
/**
* Whether to show the last list item template separator
*/
showLastSeparator: _pt.bool,
/**
* Extra content to be rendered on the end of the list item
*/
renderEndContent: _pt.func
}),
/**
* An object that holds the number of times the skeleton will appear, and (optionally) the key.
* The key will actually be `${key}-${index}` if a key is given or `${index}` if no key is given.
* IMPORTANT: your data (i.e. children \ renderContent) will NOT be duplicated.
* Note: testID will be `${testID}-${index}`
*/
times: _pt.number,
/**
* A key for the duplicated SkeletonViews.
* This is needed because the `key` prop is not accessible.
*/
timesKey: _pt.string,
/**
* @deprecated
* - Pass via listProps instead.
* - The size of the skeleton view.
* - Types: SMALL and LARGE (using SkeletonView.sizes.###)
*/
size: _pt.oneOf(["small", "large"]),
/**
* @deprecated
* - Pass via listProps instead.
* - Add content to the skeleton.
* - Types: AVATAR and THUMBNAIL (using SkeletonView.contentTypes.###)
*/
contentType: _pt.oneOf(["avatar", "thumbnail"]),
/**
* @deprecated
* - Pass via listProps instead.
* - Whether to hide the list item template separator
*/
hideSeparator: _pt.bool,
/**
* @deprecated
* - Pass via listProps instead.
* - Whether to show the last list item template separator
*/
showLastSeparator: _pt.bool,
/**
* The height of the skeleton view
*/
height: _pt.number,
/**
* The width of the skeleton view
*/
width: _pt.number,
/**
* The border radius of the skeleton view
*/
borderRadius: _pt.number,
/**
* Whether the skeleton is a circle (will override the borderRadius)
*/
circle: _pt.bool,
/**
* Used to locate this view in end-to-end tests
*/
testID: _pt.string
};
static defaultProps = {
size: Size.SMALL,
// listProps: {size: Size.SMALL}, TODO: once size is deprecated remove it and add this
borderRadius: BorderRadiuses.br10
};
static templates = Template;
static sizes = Size;
static contentTypes = ContentType;
constructor(props) {
super(props);
this.state = {
isAnimating: props.showContent === false,
opacity: new Animated.Value(0)
};
if (_.isUndefined(LinearGradientPackage?.default)) {
console.error(`RNUILib SkeletonView's requires installing "react-native-linear-gradient" dependency`);
} else if (_.isUndefined(createShimmerPlaceholder)) {
console.error(`RNUILib SkeletonView's requires installing "react-native-shimmer-placeholder" dependency`);
} else {
ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient);
}
}
componentDidMount() {
if (this.state.isAnimating) {
this.fadeInAnimation = this.fade(true);
}
}
componentDidUpdate(prevProps) {
if (this.props.showContent && !prevProps.showContent) {
this.fadeInAnimation?.stop();
this.fade(false, this.showChildren);
}
}
fade(isFadeIn, onAnimationEnd) {
const animation = Animated.timing(this.state.opacity, {
toValue: isFadeIn ? 1 : 0,
easing: Easing.ease,
duration: ANIMATION_DURATION,
useNativeDriver: true
});
animation.start(onAnimationEnd);
return animation;
}
showChildren = () => {
this.setState({
isAnimating: false
});
};
getAccessibilityProps = accessibilityLabel => {
return {
accessible: true,
accessibilityLabel,
...extractAccessibilityProps(this.props)
};
};
getDefaultSkeletonProps = input => {
const {
circleOverride,
style
} = input || {};
const {
circle,
width = 0,
height = 0
} = this.props;
let {
borderRadius
} = this.props;
let size;
if (circle || circleOverride) {
borderRadius = BorderRadiuses.br100;
size = Math.max(width, height);
}
return {
shimmerColors: [Colors.grey70, Colors.grey60, Colors.grey70],
isReversed: Constants.isRTL,
style: [{
borderRadius
}, style],
width: size || width,
height: size || height
};
};
get size() {
const {
listProps,
size
} = this.props;
return listProps?.size || size;
}
get contentSize() {
return this.size === Size.LARGE ? 48 : 40;
}
get contentType() {
const {
listProps,
contentType
} = this.props;
return listProps?.contentType || contentType;
}
get hideSeparator() {
const {
listProps,
hideSeparator
} = this.props;
return listProps?.hideSeparator || hideSeparator;
}
get showLastSeparator() {
const {
listProps,
showLastSeparator
} = this.props;
return listProps?.showLastSeparator || showLastSeparator;
}
renderListItemLeftContent = () => {
const contentType = this.contentType;
if (contentType) {
const contentSize = this.contentSize;
const circleOverride = contentType === ContentType.AVATAR;
const style = {
marginRight: this.size === Size.LARGE ? 16 : 14
};
return <ShimmerPlaceholder {...this.getDefaultSkeletonProps({
circleOverride,
style
})} width={contentSize} height={contentSize} />;
}
};
renderStrip = (isMain, length, marginTop) => {
return <ShimmerPlaceholder {...this.getDefaultSkeletonProps()} width={length} height={isMain ? 12 : 8} style={[{
marginTop
}]} />;
};
renderListItemContentStrips = () => {
const {
listProps
} = this.props;
const contentType = this.contentType;
const size = this.size;
const hideSeparator = this.hideSeparator;
const customLengths = contentType === ContentType.AVATAR ? [undefined, 50] : undefined;
const height = size === Size.LARGE ? 95 : 75;
const lengths = _.merge([90, 180, 160], customLengths);
const topMargins = [0, size === Size.LARGE ? 16 : 8, 8];
return <View flex height={height} centerV style={!hideSeparator && Dividers.d10} row>
<View>
{this.renderStrip(true, lengths[0], topMargins[0])}
{this.renderStrip(false, lengths[1], topMargins[1])}
{size === Size.LARGE && this.renderStrip(false, lengths[2], topMargins[2])}
</View>
{listProps?.renderEndContent?.()}
</View>;
};
renderListItemTemplate = () => {
const {
style,
...others
} = this.props;
return <View style={[styles.listItem, style]} {...this.getAccessibilityProps('Loading list item')} {...others}>
{this.renderListItemLeftContent()}
{this.renderListItemContentStrips()}
</View>;
};
renderTextContentTemplate = () => {
return <View {...this.getAccessibilityProps('Loading content')} {...this.props}>
{this.renderStrip(true, 235, 0)}
{this.renderStrip(true, 260, 12)}
{this.renderStrip(true, 190, 12)}
</View>;
};
renderTemplate = () => {
const {
template
} = this.props;
switch (template) {
case Template.LIST_ITEM:
return this.renderListItemTemplate();
case Template.TEXT_CONTENT:
return this.renderTextContentTemplate();
default:
// just so we won't crash
return this.renderAdvanced();
}
};
renderAdvanced = () => {
const {
children,
renderContent,
showContent,
style,
...others
} = this.props;
const data = showContent && _.isFunction(renderContent) ? renderContent(this.props) : children;
return <View style={style} {...this.getAccessibilityProps('Loading content')}>
<ShimmerPlaceholder {...this.getDefaultSkeletonProps()} {...others}>
{showContent && data}
</ShimmerPlaceholder>
</View>;
};
renderWithFading = skeleton => {
const {
isAnimating
} = this.state;
const {
children,
renderContent,
customValue,
contentData
} = this.props;
if (isAnimating) {
return <Animated.View style={{
opacity: this.state.opacity
}} pointerEvents="none">
{skeleton}
</Animated.View>;
} else if (_.isFunction(renderContent)) {
const _customValue = customValue || contentData;
return renderContent(_customValue);
} else {
return children;
}
};
renderSkeleton() {
const {
template,
showContent,
children,
renderContent
} = this.props;
let skeleton;
if (template) {
skeleton = this.renderTemplate();
} else {
skeleton = this.renderAdvanced();
}
if (_.isUndefined(showContent) || _.isUndefined(children) && _.isUndefined(renderContent)) {
return skeleton;
} else {
return this.renderWithFading(skeleton);
}
}
renderNothing = () => null;
render() {
if (_.isUndefined(LinearGradientPackage?.default) || _.isUndefined(createShimmerPlaceholder)) {
return null;
}
const {
times,
timesKey,
renderContent,
testID
} = this.props;
if (times) {
return _.times(times, index => {
const key = timesKey ? `${timesKey}-${index}` : `${index}`;
return <SkeletonView {...this.props} key={key} testID={`${testID}-${index}`} renderContent={index === 0 ? renderContent : this.renderNothing} hideSeparator={this.hideSeparator || !this.showLastSeparator && index === times - 1} times={undefined} />;
});
} else {
return this.renderSkeleton();
}
}
}
export default asBaseComponent(SkeletonView);
const styles = StyleSheet.create({
listItem: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: Spacings.s5
}
});