From 03320bb7d49780e59626db08240c340d777cdf1c Mon Sep 17 00:00:00 2001 From: Vadim Kalushko Date: Mon, 1 Dec 2025 12:08:00 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20useSkeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/huge-cats-say.md | 8 +++ .../hooks/use-skeleton/use-skeleton.test.tsx | 2 +- .../src/hooks/use-skeleton/use-skeleton.tsx | 59 +++++------------ packages/skeleton/src/utils.ts | 64 +++++++++++++++++++ 4 files changed, 90 insertions(+), 43 deletions(-) create mode 100644 .changeset/huge-cats-say.md create mode 100644 packages/skeleton/src/utils.ts diff --git a/.changeset/huge-cats-say.md b/.changeset/huge-cats-say.md new file mode 100644 index 0000000000..8783bfd7a0 --- /dev/null +++ b/.changeset/huge-cats-say.md @@ -0,0 +1,8 @@ +--- +'@alfalab/core-components-skeleton': patch +'@alfalab/core-components': patch +--- + +##### hooks/useSkeleton + +Добавлено дефолтное состояние в `useSkeleton`, чтобы SSR сразу отдавал текстовый скелетон без мерцания. diff --git a/packages/skeleton/src/hooks/use-skeleton/use-skeleton.test.tsx b/packages/skeleton/src/hooks/use-skeleton/use-skeleton.test.tsx index 02f33a8a53..c8e9b5935c 100644 --- a/packages/skeleton/src/hooks/use-skeleton/use-skeleton.test.tsx +++ b/packages/skeleton/src/hooks/use-skeleton/use-skeleton.test.tsx @@ -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; diff --git a/packages/skeleton/src/hooks/use-skeleton/use-skeleton.tsx b/packages/skeleton/src/hooks/use-skeleton/use-skeleton.tsx index 84503bff5d..68bd4355de 100644 --- a/packages/skeleton/src/hooks/use-skeleton/use-skeleton.tsx +++ b/packages/skeleton/src/hooks/use-skeleton/use-skeleton.tsx @@ -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(); + const [skeletonParams, setSkeletonParams] = useState(() => + showSkeleton ? getFallbackSkeletonParams(skeletonProps) : undefined, + ); const textRef = useRef(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) => { diff --git a/packages/skeleton/src/utils.ts b/packages/skeleton/src/utils.ts new file mode 100644 index 0000000000..da6e0e11b8 --- /dev/null +++ b/packages/skeleton/src/utils.ts @@ -0,0 +1,64 @@ +import { type TextSkeletonProps } from './types/text-skeleton-props'; + +export type TextSkeletonParams = { + height: number; + padding: string; + rows: number; +}; + +const DEFAULT_FONT_SIZE = 16; +const DEFAULT_LINE_HEIGHT = 24; + +export const getPadding = (lineHeight: number, fontSize: number) => { + if (lineHeight <= fontSize) { + return 0; + } + + const diff = lineHeight - fontSize; + + return diff % 2 === 0 ? diff / 2 : (diff - 1) / 2; +}; + +export const getFallbackSkeletonParams = ( + skeletonProps?: TextSkeletonProps, +): TextSkeletonParams => { + const padding = getPadding(DEFAULT_LINE_HEIGHT, DEFAULT_FONT_SIZE); + + return { + height: DEFAULT_LINE_HEIGHT - padding * 2, + padding: `${padding}px 0`, + rows: skeletonProps?.rows ?? 1, + }; +}; + +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), + }; +}; From eaba1eb93282f1c8130076804b0f09341a740371 Mon Sep 17 00:00:00 2001 From: Vadim Kalushko Date: Thu, 11 Dec 2025 10:14:08 +0300 Subject: [PATCH 2/3] refactor --- .changeset/huge-cats-say.md | 2 +- .../select-with-tags-dark-preview-snap.png | 4 +- packages/skeleton/src/Component.tsx | 2 +- packages/skeleton/src/utils.ts | 38 +++++++++++++------ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/.changeset/huge-cats-say.md b/.changeset/huge-cats-say.md index 8783bfd7a0..08aab3d769 100644 --- a/.changeset/huge-cats-say.md +++ b/.changeset/huge-cats-say.md @@ -5,4 +5,4 @@ ##### hooks/useSkeleton -Добавлено дефолтное состояние в `useSkeleton`, чтобы SSR сразу отдавал текстовый скелетон без мерцания. +- Добавлено дефолтное состояние в `useSkeleton`, чтобы SSR сразу отдавал текстовый скелетон. diff --git a/packages/select-with-tags/src/__image_snapshots__/select-with-tags-dark-preview-snap.png b/packages/select-with-tags/src/__image_snapshots__/select-with-tags-dark-preview-snap.png index 60369aa41a..7148fd8a9b 100644 --- a/packages/select-with-tags/src/__image_snapshots__/select-with-tags-dark-preview-snap.png +++ b/packages/select-with-tags/src/__image_snapshots__/select-with-tags-dark-preview-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36970985aedd3f85a53548871a02854d7b0bdb9cdd1f2971832b33d4b6e4910a -size 11475 +oid sha256:cec7be5d63b321b14ca4ca74fd0c7dbd82df60b23e3e592e41915f9ed10442ff +size 8705 diff --git a/packages/skeleton/src/Component.tsx b/packages/skeleton/src/Component.tsx index d6230a56aa..9635bb1ec7 100644 --- a/packages/skeleton/src/Component.tsx +++ b/packages/skeleton/src/Component.tsx @@ -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 { /** * Флаг, явно задающий состояние, при котором контент закрывается прелоадером */ diff --git a/packages/skeleton/src/utils.ts b/packages/skeleton/src/utils.ts index da6e0e11b8..d4b92caaf7 100644 --- a/packages/skeleton/src/utils.ts +++ b/packages/skeleton/src/utils.ts @@ -1,15 +1,14 @@ import { type TextSkeletonProps } from './types/text-skeleton-props'; -export type TextSkeletonParams = { - height: number; - padding: string; - rows: number; +type PaddingParams = { + lineHeight: number; + fontSize: number; }; -const DEFAULT_FONT_SIZE = 16; -const DEFAULT_LINE_HEIGHT = 24; - -export const getPadding = (lineHeight: number, fontSize: number) => { +/** + * Возвращает вертикальный padding для выравнивания текста внутри скелетона. + */ +export const getPadding = ({ lineHeight, fontSize }: PaddingParams) => { if (lineHeight <= fontSize) { return 0; } @@ -19,18 +18,34 @@ export const getPadding = (lineHeight: number, fontSize: number) => { 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(DEFAULT_LINE_HEIGHT, DEFAULT_FONT_SIZE); + const padding = getPadding({ lineHeight: 24, fontSize: 16 }); return { - height: DEFAULT_LINE_HEIGHT - padding * 2, + 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, @@ -40,7 +55,8 @@ export const measureSkeletonParams = ( const fontSize = parseInt(style.fontSize, 10); const lineHeight = parseInt(style.lineHeight, 10); - let padding = getPadding(lineHeight, fontSize); + let padding = getPadding({ lineHeight, fontSize }); + const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); From 1206a0f411b861b00ea0102da00e9cd15cfafbe3 Mon Sep 17 00:00:00 2001 From: Vadim Kalushko Date: Thu, 11 Dec 2025 10:28:15 +0300 Subject: [PATCH 3/3] fix EsLint --- packages/skeleton/src/Component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/skeleton/src/Component.tsx b/packages/skeleton/src/Component.tsx index 9635bb1ec7..90ff9207a5 100644 --- a/packages/skeleton/src/Component.tsx +++ b/packages/skeleton/src/Component.tsx @@ -59,7 +59,7 @@ export interface SkeletonProps { * @default 8 */ borderRadius?: BorderRadiusSize; -}; +} export const Skeleton: React.FC = ({ visible,