Skip to content
Closed
300 changes: 255 additions & 45 deletions src/Card/CardCarousel/tests/__snapshots__/CardCarousel.test.jsx.snap

Large diffs are not rendered by default.

86 changes: 28 additions & 58 deletions src/Card/CardImageCap.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Skeleton from 'react-loading-skeleton';
import CardContext from './CardContext';
import cardSrcFallbackImg from './fallback-default.png';

const SKELETON_HEIGHT_VALUE = 140;
const LOGO_SKELETON_HEIGHT_VALUE = 41;
import CardImageWithSkeleton from './CardImageWithSkeleton';
import { LOGO_SKELETON_HEIGHT_VALUE, SKELETON_HEIGHT_VALUE } from './constants';

const CardImageCap = React.forwardRef(({
src,
Expand All @@ -24,69 +22,41 @@ const CardImageCap = React.forwardRef(({
imageLoadingType,
}, ref) => {
const { orientation, isLoading } = useContext(CardContext);
const [showImageCap, setShowImageCap] = useState(false);
const [showLogoCap, setShowLogoCap] = useState(false);

const wrapperClassName = `pgn__card-wrapper-image-cap ${orientation}`;

if (isLoading) {
return (
<div
className={classNames(wrapperClassName, className)}
data-testid="image-loader-wrapper"
>
<Skeleton
containerClassName="pgn__card-image-cap-loader"
height={orientation === 'horizontal' ? '100%' : skeletonHeight}
width={skeletonWidth}
/>
{logoSkeleton && (
<Skeleton
containerClassName="pgn__card-logo-cap"
height={logoSkeletonHeight}
width={logoSkeletonWidth}
/>
)}
</div>
);
}

const handleSrcFallback = (event, altSrc, imageKey) => {
const { currentTarget } = event;

if (!altSrc || currentTarget.src.endsWith(altSrc)) {
if (imageKey === 'imageCap') {
currentTarget.src = cardSrcFallbackImg;
} else {
setShowLogoCap(false);
}
const imageSkeletonHeight = useMemo(() => (
orientation === 'horizontal' ? '100%' : skeletonHeight
), [orientation, skeletonHeight]);

return;
}

currentTarget.src = altSrc;
};
const wrapperClassName = `pgn__card-wrapper-image-cap ${orientation}`;

return (
<div className={classNames(className, wrapperClassName)} ref={ref}>
{!!src && (
<img
className={classNames('pgn__card-image-cap', { show: showImageCap })}
<CardImageWithSkeleton
src={src}
onError={(event) => handleSrcFallback(event, fallbackSrc, 'imageCap')}
onLoad={() => setShowImageCap(true)}
alt={srcAlt}
loading={imageLoadingType}
fallback={fallbackSrc}
className="pgn__card-image-cap"
useDefaultSrc
withSkeleton
skeletonWidth={skeletonWidth}
skeletonHeight={imageSkeletonHeight}
imageLoadingType={imageLoadingType}
skeletonClassName="pgn__card-image-cap-loader"
isLoading={isLoading}
/>
)}
{!!logoSrc && (
<img
className={classNames('pgn__card-logo-cap', { show: showLogoCap })}
{!!logoSrc && !isLoading && (
<CardImageWithSkeleton
src={logoSrc}
onError={(event) => handleSrcFallback(event, fallbackLogoSrc, 'logoCap')}
onLoad={() => setShowLogoCap(true)}
alt={logoAlt}
loading={imageLoadingType}
fallback={fallbackLogoSrc}
withSkeleton={logoSkeleton}
className="pgn__card-logo-cap"
skeletonWidth={logoSkeletonWidth}
skeletonHeight={logoSkeletonHeight}
imageLoadingType={imageLoadingType}
skeletonClassName="pgn__card-logo-cap"
/>
)}
</div>
Expand All @@ -109,9 +79,9 @@ CardImageCap.propTypes = {
/** Specifies logo image alt text. */
logoAlt: PropTypes.string,
/** Specifies height of Image skeleton in loading state. */
skeletonHeight: PropTypes.number,
skeletonHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Specifies width of Image skeleton in loading state. */
skeletonWidth: PropTypes.number,
skeletonWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Specifies whether the cap should be displayed during loading. */
logoSkeleton: PropTypes.bool,
/** Specifies height of Logo skeleton in loading state. */
Expand Down
74 changes: 74 additions & 0 deletions src/Card/CardImageWithSkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Skeleton from 'react-loading-skeleton';

import Image from '../Image';
import useImageLoader from '../hooks/useImageLoader';

function CardImageWithSkeleton({
src,
alt,
fallback,
className,
withSkeleton,
useDefaultSrc,
skeletonWidth,
skeletonHeight,
imageLoadingType,
skeletonClassName,
isLoading = false,
}) {
const config = useMemo(
() => ({
mainSrc: src, fallbackSrc: fallback, useDefaultSrc,
}),
[src, fallback, useDefaultSrc],
);

const { ref, isSrcLoading } = useImageLoader(config);

return (
<>
<Skeleton
containerClassName={classNames(skeletonClassName, { show: (isSrcLoading || isLoading) && withSkeleton })}
height={skeletonHeight}
width={skeletonWidth}
/>
<Image
ref={ref}
className={classNames(className, { show: !isSrcLoading && !isLoading })}
alt={alt}
loading={imageLoadingType}
/>
</>
);
}

CardImageWithSkeleton.propTypes = {
className: PropTypes.string,
src: PropTypes.string.isRequired,
fallback: PropTypes.string,
useDefaultSrc: PropTypes.bool,
alt: PropTypes.string.isRequired,
skeletonHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
skeletonWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
skeletonClassName: PropTypes.string,
imageLoadingType: PropTypes.oneOf(['eager', 'lazy']),
isLoading: PropTypes.bool,
withSkeleton: PropTypes.bool,
};

CardImageWithSkeleton.defaultProps = {
className: undefined,
skeletonClassName: undefined,
fallback: undefined,
useDefaultSrc: false,
imageLoadingType: 'eager',
skeletonHeight: undefined,
skeletonWidth: undefined,
isLoading: false,
withSkeleton: false,
};

export default CardImageWithSkeleton;
2 changes: 1 addition & 1 deletion src/Card/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ $card-footer-text-font-size: $x-small-font-size;
$card-image-horizontal-max-width: 240px !default;
$card-image-horizontal-min-width: $card-image-horizontal-max-width !default;
$card-image-vertical-max-height: 140px !default;
$loading-skeleton-spacer: .313rem !default;
$loading-skeleton-spacer: -4px !default;
Comment thread
khudym marked this conversation as resolved.

$card-focus-border-offset: 5px !default;
$card-focus-border-width: 2px !default;
Expand Down
2 changes: 2 additions & 0 deletions src/Card/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SKELETON_HEIGHT_VALUE = 140;
export const LOGO_SKELETON_HEIGHT_VALUE = 41;
11 changes: 8 additions & 3 deletions src/Card/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -337,10 +337,15 @@ a.pgn__card {
}

.pgn__card-image-cap-loader {
margin-top: $loading-skeleton-spacer;
display: none;

&.show {
display: block;
height: 100%;
}

.react-loading-skeleton {
margin-bottom: -$loading-skeleton-spacer;
position: relative;
top: -$loading-skeleton-spacer;
height: 100%;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
Expand Down
70 changes: 3 additions & 67 deletions src/Card/tests/CardImageCap.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { render, fireEvent, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import CardImageCap from '../CardImageCap';
import CardContext from '../CardContext';

Expand Down Expand Up @@ -61,71 +61,7 @@ describe('<CardImageCap />', () => {
/>,
);

expect(screen.getByAltText('Src alt text').src).toEqual('http://src.image/');
expect(screen.getByAltText('Logo alt text').src).toEqual('http://logo.image/');
});

it('render with loading state', () => {
render(
<CardImageCapWrapper
src="http://fake.image"
logoSrc="http://fake.image"
srcAlt="Src alt text"
logoAlt="Logo alt text"
isLoading
logoSkeleton
/>,
);

expect(screen.queryByAltText('Src alt text')).toBeNull();
expect(screen.queryByAltText('Logo alt text')).toBeNull();

expect(screen.getByTestId('image-loader-wrapper')).toBeInTheDocument();
expect(screen.getByTestId('image-loader-wrapper').firstChild).toHaveClass('pgn__card-image-cap-loader');
expect(screen.getByTestId('image-loader-wrapper').lastChild).toHaveClass('pgn__card-logo-cap');
});

it('replaces image with fallback one in case of error', () => {
render(
<CardImageCapWrapper
src="http://src.image/"
srcAlt="Src alt text"
fallbackSrc="http://src.image.fallback/"
logoSrc="http://logo.image/"
fallbackLogoSrc="http://logo.image.fallback/"
logoAlt="Logo alt text"
/>,
);
const srcImg = screen.getByAltText('Src alt text');
expect(srcImg.src).toEqual('http://src.image/');
fireEvent.load(srcImg);
fireEvent.error(srcImg);
expect(srcImg.src).toEqual('http://src.image.fallback/');

const logoImg = screen.getByAltText('Logo alt text');
expect(logoImg.src).toEqual('http://logo.image/');
fireEvent.load(srcImg);
fireEvent.error(logoImg);
expect(logoImg.src).toEqual('http://logo.image.fallback/');
});

it('hiding component if it isn`t fallbackLogoSrc and logoSrc don`t work', () => {
render(<CardImageCapWrapper logoSrc="fakeURL" logoAlt="Logo alt text" />);

const logoImg = screen.getByAltText('Logo alt text');
fireEvent.load(logoImg);
expect(logoImg.className).toEqual('pgn__card-logo-cap show');
fireEvent.error(logoImg);
expect(logoImg.className).toEqual('pgn__card-logo-cap');
});

it('hiding component if it isn`t fallbackSrc and src don`t work', () => {
render(<CardImageCapWrapper src="fakeURL" fallbackSrc="fakeURL" srcAlt="Src alt text" />);

const srcImg = screen.getByAltText('Src alt text');
fireEvent.load(srcImg);
fireEvent.error(srcImg);
// test-file-stub is what our fileMock.js returns for all images
expect(srcImg.src.endsWith('test-file-stub')).toEqual(true);
expect(screen.getByAltText('Src alt text')).toBeInTheDocument();
expect(screen.getByAltText('Logo alt text')).toBeInTheDocument();
});
});
Loading