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

[BD-46] fix: show skeleton while loading src in CardImageCap (hook) #2846

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
300 changes: 255 additions & 45 deletions src/Card/CardCarousel/tests/__snapshots__/CardCarousel.test.jsx.snap

Large diffs are not rendered by default.

85 changes: 27 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,40 @@ 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
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 +78,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
70 changes: 70 additions & 0 deletions src/Card/CardImageWithSkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Skeleton from 'react-loading-skeleton';

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

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

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

return (
<>
<Skeleton
containerClassName={classNames(skeletonClassName, { show: isSrcLoading || isLoading })}
height={skeletonHeight}
width={skeletonWidth}
/>
<img
khudym marked this conversation as resolved.
Show resolved Hide resolved
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,
};

CardImageWithSkeleton.defaultProps = {
className: undefined,
skeletonClassName: undefined,
fallback: undefined,
useDefaultSrc: false,
imageLoadingType: 'eager',
skeletonHeight: undefined,
skeletonWidth: undefined,
isLoading: 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;
khudym marked this conversation as resolved.
Show resolved Hide 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