@yandex/ui
Version:
Yandex UI components
88 lines (86 loc) • 4.89 kB
JavaScript
import { __read } from "tslib";
import React, { useCallback, useState, useEffect, useMemo, useRef, } from 'react';
import { cn } from '@bem-react/classname';
import { mergeAllRefs } from '../lib/mergeRefs';
import './Image.css';
var cnImage = cn('Image');
var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
var ANIMATION_DELAY_MS = 50;
/**
* Компонент для красивой загрузки картинок
* - Пока загружается картинка, пользователь будет видеть `<stub/>`
* - После загрузки картинки она будет плавно отображена поверх `<stub/>`
* - После плавного показана картинки `<stub/>` будет удалён из `DOM`
* - Если картинка загружается из кэша, то пользователь увидит сразу картинку без показа `<stub/>`
* - Если картинку не удалось загрузить, то покажется стандартная картинка из свойства `fallback`
* @param {IImageProps} props
*/
export var Image = function (props) {
var src = props.src, src2x = props.src2x, srcSet = props.srcSet, sizes = props.sizes, fallbackSrc = props.fallbackSrc, className = props.className, imageClassName = props.imageClassName, stub = props.stub, alt = props.alt, ariaLabel = props.ariaLabel, onClick = props.onClick, _a = props.loading, loading = _a === void 0 ? 'lazy' : _a, width = props.width, height = props.height, borderRadius = props.borderRadius, innerRef = props.innerRef;
var _b = __read(useState(false), 2), isLoaded = _b[0], setLoaded = _b[1];
var _c = __read(useState(false), 2), isFailed = _c[0], setFailed = _c[1];
var _d = __read(useState(false), 2), needAnimation = _d[0], setNeedAnimation = _d[1];
var _e = __read(useState(false), 2), canRemoveStub = _e[0], setCanRemoveStub = _e[1];
// нужен реф по условию, а значит useRef не годится
// важный момент: в innerRef должна быть актуальная ссылка на ref
var imageRef = useRef(null);
// объединяем рефы
var mergedRefs = useMemo(function () { return mergeAllRefs(imageRef, innerRef); }, [imageRef, innerRef]);
var imageSrc = src || emptyImage;
var legacySrcSet = src2x ? src2x + " 2x" : undefined;
var imageSrcSet = srcSet || legacySrcSet;
var isRendered = useCallback(function () {
if (!imageRef.current) {
return false;
}
var _a = imageRef.current, naturalWidth = _a.naturalWidth, naturalHeight = _a.naturalHeight;
return naturalWidth > 0 && naturalHeight > 0;
}, [imageRef]);
useEffect(function () {
var timer = setTimeout(function () {
if (!isLoaded && !isRendered()) {
setNeedAnimation(true);
}
}, ANIMATION_DELAY_MS);
return function () { return clearTimeout(timer); };
}, [isLoaded, isRendered, setNeedAnimation]);
var handleLoad = useCallback(function () {
setLoaded(true);
}, []);
var handleError = useCallback(function () {
setFailed(true);
}, []);
var containerClassNames = cnImage('Container', [className]);
var imageClassNames = cnImage({
loaded: isLoaded,
loading: needAnimation,
animated: needAnimation && isLoaded,
},
// Если нет обертки, то добавляем className в изображение
[!stub ? className : undefined, imageClassName]);
var handleOnAnimationEnd = useCallback(function () {
setCanRemoveStub(true);
}, []);
var sizeAttrs = {};
// безопасно устанавливаем стили, чтобы избежать undefined в html
if (width !== undefined) {
sizeAttrs.width = width;
}
if (height !== undefined) {
sizeAttrs.height = height;
}
if (borderRadius !== undefined) {
sizeAttrs.borderRadius = borderRadius;
}
var image = (React.createElement("img", { style: sizeAttrs, alt: alt, "aria-hidden": ariaLabel ? 'false' : 'true', "aria-label": ariaLabel, className: imageClassNames, onAnimationEnd: handleOnAnimationEnd, onClick: onClick, onError: handleError, onLoad: handleLoad, ref: mergedRefs, src: isFailed && fallbackSrc ? fallbackSrc : imageSrc, srcSet: imageSrcSet, sizes: sizes,
// TypeScript пока не знает про такой атрибут
// @ts-ignore
loading: loading }));
if (stub) {
return (React.createElement("div", { className: containerClassNames, style: sizeAttrs },
needAnimation && !canRemoveStub && stub,
image));
}
return image;
};
Image.displayName = cnImage();