diff --git a/README.md b/README.md index fc605ce4..b3fd7603 100755 --- a/README.md +++ b/README.md @@ -74,6 +74,29 @@ const MyLoader = () => ( ) ``` +**Per-rect color overrides** + +```jsx +import ContentLoader, { Rect } from 'react-content-loader' + +const MyLoader = () => ( + + + + + +) +``` + **Still not clear?** Take a look at this working example at [codesandbox.io](https://codesandbox.io/s/moojk887z9) Or try the components editable demo hands-on and install it from [bit.dev](https://bit.dev/danilowoz/react-content-loader) diff --git a/src/native/ContentLoader.tsx b/src/native/ContentLoader.tsx index e911827e..c9965a81 100644 --- a/src/native/ContentLoader.tsx +++ b/src/native/ContentLoader.tsx @@ -1,7 +1,8 @@ import * as React from 'react' -import { Circle, Path, Rect } from 'react-native-svg' +import { Circle, Path } from 'react-native-svg' import { Facebook, IContentLoaderProps } from '.' +import Rect from './Rect' import Svg from './Svg' const ContentLoader: React.FC = props => diff --git a/src/native/Rect.tsx b/src/native/Rect.tsx new file mode 100644 index 00000000..a79b3b61 --- /dev/null +++ b/src/native/Rect.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { Rect as SvgRect } from 'react-native-svg' + +export type IRectProps = React.ComponentProps & { + foregroundColor?: string + backgroundColor?: string +} + +const Rect: React.FC = ({ + foregroundColor, + backgroundColor, + ...props +}) => + +Rect.displayName = 'Rect' + +export default Rect diff --git a/src/native/Svg.tsx b/src/native/Svg.tsx index 68fbf791..3be18393 100644 --- a/src/native/Svg.tsx +++ b/src/native/Svg.tsx @@ -4,12 +4,13 @@ import Svg, { ClipPath, Defs, LinearGradient, - Rect, + Rect as SvgRect, Stop, } from 'react-native-svg' import uid from '../shared/uid' import { IContentLoaderProps } from './' +import Rect, { IRectProps } from './Rect' const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient) @@ -85,6 +86,36 @@ class NativeSvg extends Component { ...props } = this.props + const clipChildren: React.ReactNode[] = [] + const overrideRects: Array<{ + element: React.ReactElement + foregroundColor: string + backgroundColor: string + idClip: string + idGradient: string + }> = [] + + React.Children.forEach(children, child => { + if ( + isValidElement(child) && + child.type === Rect && + (child.props.foregroundColor != null || + child.props.backgroundColor != null) + ) { + const index = overrideRects.length + overrideRects.push({ + element: child, + foregroundColor: child.props.foregroundColor ?? foregroundColor, + backgroundColor: child.props.backgroundColor ?? backgroundColor, + idClip: `${this.fixedId}-diff-override-${index}`, + idGradient: `${this.fixedId}-animated-diff-override-${index}`, + }) + return + } + + clipChildren.push(child) + }) + const x1Animation = this.animatedValue.interpolate({ extrapolate: 'clamp', inputRange: [-1, 2], @@ -109,7 +140,7 @@ class NativeSvg extends Component { {beforeMask && isValidElement(beforeMask) ? beforeMask : null} - { clipPath={`url(#${this.idGradient})`} /> + {overrideRects.map(override => ( + + ))} + - {children} + {clipChildren} + + {overrideRects.map(override => ( + + {override.element} + + ))} { + + {overrideRects.map(override => ( + + + + + + ))} ) diff --git a/src/native/__tests__/ContentLoader.test.tsx b/src/native/__tests__/ContentLoader.test.tsx index d269a4c6..eb670bc1 100644 --- a/src/native/__tests__/ContentLoader.test.tsx +++ b/src/native/__tests__/ContentLoader.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import * as renderer from 'react-test-renderer' import * as ShallowRenderer from 'react-test-renderer/shallow' +import { Rect as SvgRect } from 'react-native-svg' import ContentLoader, { Circle, Rect } from '../ContentLoader' @@ -18,9 +19,11 @@ describe('ContentLoader', () => { it('should render custom element', () => { const rect = customWrapper.findAllByType(Rect) + const svgRect = customWrapper.findAllByType(SvgRect) const circle = customWrapper.findAllByType(Circle) - expect(rect.length).toBe(3) + expect(rect.length).toBe(2) + expect(svgRect.length).toBe(3) expect(circle.length).toBe(1) }) }) diff --git a/src/native/__tests__/Svg.test.tsx b/src/native/__tests__/Svg.test.tsx index 71afb00b..6b2e7143 100644 --- a/src/native/__tests__/Svg.test.tsx +++ b/src/native/__tests__/Svg.test.tsx @@ -106,4 +106,60 @@ describe('Svg', () => { }).toThrow('No instances found with props: {"x":"123"}') }) }) + + describe('rect color overrides', () => { + it('uses default colors when no overrides are provided', () => { + const wrapperWithDefaults = renderer.create( + + + + ).root + + const linearGradient = wrapperWithDefaults.findByType(LinearGradient) + const stops = linearGradient.findAllByType(Stop) + + expect(stops[0].props.stopColor).toBe('#222') + expect(stops[1].props.stopColor).toBe('#111') + expect(stops[2].props.stopColor).toBe('#222') + }) + + it('allows a rect to override colors without affecting others', () => { + const wrapperWithOverrides = renderer.create( + + + + + ).root + + const allLinearGradient = + wrapperWithOverrides.findAllByType(LinearGradient) + const overrideGradient = allLinearGradient.find(gradient => + gradient.props.id.includes('override') + ) + const defaultGradient = allLinearGradient.find( + gradient => !gradient.props.id.includes('override') + ) + + expect(allLinearGradient.length).toBe(2) + expect(overrideGradient).toBeTruthy() + expect(defaultGradient).toBeTruthy() + + const defaultStops = defaultGradient!.findAllByType(Stop) + const overrideStops = overrideGradient!.findAllByType(Stop) + + expect(defaultStops[0].props.stopColor).toBe('#222') + expect(defaultStops[1].props.stopColor).toBe('#111') + expect(defaultStops[2].props.stopColor).toBe('#222') + expect(overrideStops[0].props.stopColor).toBe('#444') + expect(overrideStops[1].props.stopColor).toBe('#333') + expect(overrideStops[2].props.stopColor).toBe('#444') + }) + }) }) diff --git a/src/native/index.ts b/src/native/index.ts index a5c1a842..fd0debc1 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -1,6 +1,7 @@ import { SvgProps } from 'react-native-svg' import ContentLoader from './ContentLoader' +import Rect, { IRectProps } from './Rect' export interface IContentLoaderProps extends SvgProps { animate?: boolean @@ -21,5 +22,7 @@ export { default as Code } from './presets/CodeStyle' export { default as List } from './presets/ListStyle' export { default as BulletList } from './presets/BulletListStyle' -export { Circle, Rect, Path } from './ContentLoader' +export { Circle, Path } from './ContentLoader' +export { Rect } +export type { IRectProps } export default ContentLoader diff --git a/src/web/Rect.tsx b/src/web/Rect.tsx new file mode 100644 index 00000000..4194b0a7 --- /dev/null +++ b/src/web/Rect.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +export interface IRectProps extends React.SVGAttributes { + foregroundColor?: string + backgroundColor?: string +} + +const Rect: React.FC = ({ + foregroundColor, + backgroundColor, + ...props +}) => + +Rect.displayName = 'Rect' + +export default Rect diff --git a/src/web/Svg.tsx b/src/web/Svg.tsx index 749fd0c7..7c847160 100644 --- a/src/web/Svg.tsx +++ b/src/web/Svg.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import uid from '../shared/uid' import { IContentLoaderProps } from './' +import Rect, { IRectProps } from './Rect' const SVG: React.FC = ({ animate = true, @@ -31,6 +32,36 @@ const SVG: React.FC = ({ const from = `${gradientRatio * -1} 0` const to = `${gradientRatio} 0` + const clipChildren: React.ReactNode[] = [] + const overrideRects: Array<{ + element: React.ReactElement + foregroundColor: string + backgroundColor: string + idClip: string + idGradient: string + }> = [] + + React.Children.forEach(children, child => { + if ( + React.isValidElement(child) && + child.type === Rect && + (child.props.foregroundColor != null || + child.props.backgroundColor != null) + ) { + const index = overrideRects.length + overrideRects.push({ + element: child, + foregroundColor: child.props.foregroundColor ?? foregroundColor, + backgroundColor: child.props.backgroundColor ?? backgroundColor, + idClip: `${fixedId}-diff-override-${index}`, + idGradient: `${fixedId}-animated-diff-override-${index}`, + }) + return + } + + clipChildren.push(child) + }) + return ( = ({ style={{ fill: `url(${baseUrl}#${idGradient})` }} /> + {overrideRects.map(override => ( + + ))} + - {children} + {clipChildren} + + {overrideRects.map(override => ( + + {override.element} + + ))} = ({ /> )} + + {overrideRects.map(override => ( + + + + + + + + {animate && ( + + )} + + ))} ) diff --git a/src/web/__tests__/Svg.test.tsx b/src/web/__tests__/Svg.test.tsx index 766cd5d0..2266ae4b 100644 --- a/src/web/__tests__/Svg.test.tsx +++ b/src/web/__tests__/Svg.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import * as renderer from 'react-test-renderer' -import Svg from '..' +import Svg, { Rect } from '..' interface PredicateArgs { type: any @@ -170,4 +170,60 @@ describe('Svg', () => { }).toThrow('No instances found with props: {"role":"beforeMask"}') }) }) + + describe('rect color overrides', () => { + it('uses default colors when no overrides are provided', () => { + const wrapperWithDefaults = renderer.create( + + + + ).root + + const linearGradient = wrapperWithDefaults.findByType('linearGradient') + const stops = linearGradient.findAllByType('stop') + + expect(stops[0].props.stopColor).toBe('#222') + expect(stops[1].props.stopColor).toBe('#111') + expect(stops[2].props.stopColor).toBe('#222') + }) + + it('allows a rect to override colors without affecting others', () => { + const wrapperWithOverrides = renderer.create( + + + + + ).root + + const allLinearGradient = + wrapperWithOverrides.findAllByType('linearGradient') + const overrideGradient = allLinearGradient.find(gradient => + gradient.props.id.includes('override') + ) + const defaultGradient = allLinearGradient.find( + gradient => !gradient.props.id.includes('override') + ) + + expect(allLinearGradient.length).toBe(2) + expect(overrideGradient).toBeTruthy() + expect(defaultGradient).toBeTruthy() + + const defaultStops = defaultGradient!.findAllByType('stop') + const overrideStops = overrideGradient!.findAllByType('stop') + + expect(defaultStops[0].props.stopColor).toBe('#222') + expect(defaultStops[1].props.stopColor).toBe('#111') + expect(defaultStops[2].props.stopColor).toBe('#222') + expect(overrideStops[0].props.stopColor).toBe('#444') + expect(overrideStops[1].props.stopColor).toBe('#333') + expect(overrideStops[2].props.stopColor).toBe('#444') + }) + }) }) diff --git a/src/web/index.ts b/src/web/index.ts index 37082810..db4fde44 100755 --- a/src/web/index.ts +++ b/src/web/index.ts @@ -1,6 +1,7 @@ import { SVGAttributes, ReactElement } from 'react' import ContentLoader from './ContentLoader' +import Rect from './Rect' export interface IContentLoaderProps extends SVGAttributes { animate?: boolean @@ -22,5 +23,7 @@ export { default as Instagram } from './presets/InstagramStyle' export { default as Code } from './presets/CodeStyle' export { default as List } from './presets/ListStyle' export { default as BulletList } from './presets/BulletListStyle' +export { Rect } +export type { IRectProps } from './Rect' export default ContentLoader