Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/huge-cats-say.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@alfalab/core-components-skeleton': patch
'@alfalab/core-components': patch
---

##### hooks/useSkeleton

- Добавлено дефолтное состояние в `useSkeleton`, чтобы SSR сразу отдавал текстовый скелетон.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions packages/skeleton/src/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const colorStyles = {

export type BorderRadiusSize = 0 | 2 | 4 | 6 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 36 | 64 | 'pill';

export type SkeletonProps = {
export interface SkeletonProps {
/**
* Флаг, явно задающий состояние, при котором контент закрывается прелоадером
*/
Expand Down Expand Up @@ -59,7 +59,7 @@ export type SkeletonProps = {
* @default 8
*/
borderRadius?: BorderRadiusSize;
};
}

export const Skeleton: React.FC<SkeletonProps> = ({
visible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { render } from '@testing-library/react';
const Skeleton = (props: TextSkeletonProps) => {
const { renderSkeleton, textRef } = useSkeleton(true, props);

const skeleton = renderSkeleton({});
const skeleton = renderSkeleton({ dataTestId: 'test-skeleton' });

if (skeleton) return skeleton;

Expand Down
59 changes: 17 additions & 42 deletions packages/skeleton/src/hooks/use-skeleton/use-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,39 @@ import { useLayoutEffect_SAFE_FOR_SSR } from '@alfalab/hooks';

import { Skeleton } from '../../Component';
import { type TextSkeletonProps } from '../../types/text-skeleton-props';
import {
getFallbackSkeletonParams,
measureSkeletonParams,
type TextSkeletonParams,
} from '../../utils';

import styles from './use-skeleton.module.css';

type TextSkeletonParams = {
height: number;
padding: string;
rows: number;
};

type SkeletonProps = {
wrapperClassName?: string;
dataTestId?: string;
};

export function useSkeleton(showSkeleton?: boolean, skeletonProps?: TextSkeletonProps) {
const [skeletonParams, setSkeletonParams] = useState<TextSkeletonParams>();
const [skeletonParams, setSkeletonParams] = useState<TextSkeletonParams | undefined>(() =>
showSkeleton ? getFallbackSkeletonParams(skeletonProps) : undefined,
);
const textRef = useRef<HTMLElement>(null);

useLayoutEffect_SAFE_FOR_SSR(() => {
if (showSkeleton && textRef.current) {
const style = getComputedStyle(textRef.current);

const textHeight = textRef.current.offsetHeight;
const fontSize = parseInt(style.fontSize, 10);
const lineHeight = parseInt(style.lineHeight, 10);

let padding =
(lineHeight - fontSize) % 2 === 0
? (lineHeight - fontSize) / 2
: (lineHeight - fontSize - 1) / 2;

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

/**
* Расчет отступов с учётом размера глифа от базовой линии до верхней границы
* Это позволяет отображать более приближённый размер скелетона к начертанию текста
* @see DS-12535
*/
if (context && textRef.current.textContent) {
context.font = style.font;
const metrics = context.measureText(textRef.current.textContent);
if (!showSkeleton) {
setSkeletonParams(undefined);

padding = (lineHeight - metrics.actualBoundingBoxAscent) / 2;
}
return;
}

const rows = skeletonProps?.rows
? skeletonProps?.rows
: Math.ceil(textHeight / lineHeight);
if (textRef.current) {
setSkeletonParams(measureSkeletonParams(textRef.current, skeletonProps));

setSkeletonParams({
height: lineHeight - padding * 2,
padding: `${padding}px 0`,
rows,
});
} else {
setSkeletonParams(undefined);
return;
}

setSkeletonParams((prev) => prev ?? getFallbackSkeletonParams(skeletonProps));
}, [showSkeleton, skeletonProps?.rows]);

const renderSkeleton = (props: SkeletonProps) => {
Expand Down
80 changes: 80 additions & 0 deletions packages/skeleton/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { type TextSkeletonProps } from './types/text-skeleton-props';

type PaddingParams = {
lineHeight: number;
fontSize: number;
};

/**
* Возвращает вертикальный padding для выравнивания текста внутри скелетона.
*/
export const getPadding = ({ lineHeight, fontSize }: PaddingParams) => {
if (lineHeight <= fontSize) {
return 0;
}

const diff = lineHeight - fontSize;

return diff % 2 === 0 ? diff / 2 : (diff - 1) / 2;
};

export type TextSkeletonParams = {
height: number;
padding: string;
rows: number;
};

/**
* Возвращает параметры скелетона по умолчанию, когда нет реальных размеров текста.
*/
export const getFallbackSkeletonParams = (
skeletonProps?: TextSkeletonProps,
): TextSkeletonParams => {
const padding = getPadding({ lineHeight: 24, fontSize: 16 });

return {
height: 24 - padding * 2,
padding: `${padding}px 0`,
rows: skeletonProps?.rows ?? 1,
};
};

/**
* Вычисляет параметры скелетона по реальному DOM-узлу.
*
* @param node элемент, по которому считаются размеры текста
* @param skeletonProps пропсы скелетона (для переопределения rows)
* @returns высота, паддинги и количество строк скелетона
*/
export const measureSkeletonParams = (
node: HTMLElement,
skeletonProps?: TextSkeletonProps,
): TextSkeletonParams => {
const style = getComputedStyle(node);
const textHeight = node.offsetHeight;
const fontSize = parseInt(style.fontSize, 10);
const lineHeight = parseInt(style.lineHeight, 10);

let padding = getPadding({ lineHeight, fontSize });

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

/**
* Расчет отступов с учётом размера глифа от базовой линии до верхней границы
* Это позволяет отображать более приближённый размер скелетона к начертанию текста
* @see DS-12535
*/
if (context && node.textContent) {
context.font = style.font;
const metrics = context.measureText(node.textContent);

padding = (lineHeight - metrics.actualBoundingBoxAscent) / 2;
}

return {
height: lineHeight - padding * 2,
padding: `${padding}px 0`,
rows: skeletonProps?.rows ?? Math.ceil(textHeight / lineHeight),
};
};
Loading