@astral/ui 4.103.1 → 4.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/components/Accordion/Accordion.js +2 -2
  2. package/components/Accordion/styles.d.ts +0 -4
  3. package/components/Accordion/styles.js +6 -7
  4. package/components/Accordion/useLogic/useLogic.d.ts +1 -1
  5. package/components/Onboarding/ui/Overlay/Overlay.d.ts +1 -5
  6. package/components/Onboarding/ui/Overlay/Overlay.js +2 -5
  7. package/components/Onboarding/ui/Overlay/constants.d.ts +0 -1
  8. package/components/Onboarding/ui/Overlay/constants.js +0 -1
  9. package/components/Onboarding/ui/Overlay/styles.js +0 -11
  10. package/components/Onboarding/ui/StepStrategy/StepStrategy.js +2 -2
  11. package/components/Onboarding/ui/StepStrategy/useLogic/useLogic.d.ts +0 -4
  12. package/components/Onboarding/ui/StepStrategy/useLogic/useLogic.js +14 -5
  13. package/components/Onboarding/ui/hooks/useAnimatedRect/index.d.ts +1 -0
  14. package/components/Onboarding/ui/hooks/useAnimatedRect/index.js +1 -0
  15. package/components/Onboarding/ui/hooks/useAnimatedRect/useAnimatedRect.d.ts +21 -0
  16. package/components/Onboarding/ui/hooks/useAnimatedRect/useAnimatedRect.js +66 -0
  17. package/components/Onboarding/ui/hooks/useStepTransition/useStepTransition.d.ts +3 -17
  18. package/components/Onboarding/ui/hooks/useStepTransition/useStepTransition.js +8 -23
  19. package/components/Onboarding/ui/hooks/useTargetAnchor/useTargetAnchor.js +3 -6
  20. package/components/Onboarding/utils/geometry/rect/index.d.ts +1 -1
  21. package/components/Onboarding/utils/geometry/rect/index.js +1 -1
  22. package/components/Onboarding/utils/geometry/rect/rect.d.ts +4 -0
  23. package/components/Onboarding/utils/geometry/rect/rect.js +9 -0
  24. package/components/Onboarding/utils/scrollIntoView/scrollIntoView.d.ts +5 -6
  25. package/components/Onboarding/utils/scrollIntoView/scrollIntoView.js +19 -49
  26. package/node/components/Accordion/Accordion.js +1 -1
  27. package/node/components/Accordion/styles.d.ts +0 -4
  28. package/node/components/Accordion/styles.js +7 -8
  29. package/node/components/Accordion/useLogic/useLogic.d.ts +1 -1
  30. package/node/components/Onboarding/ui/Overlay/Overlay.d.ts +1 -5
  31. package/node/components/Onboarding/ui/Overlay/Overlay.js +2 -5
  32. package/node/components/Onboarding/ui/Overlay/constants.d.ts +0 -1
  33. package/node/components/Onboarding/ui/Overlay/constants.js +0 -1
  34. package/node/components/Onboarding/ui/Overlay/styles.js +0 -11
  35. package/node/components/Onboarding/ui/StepStrategy/StepStrategy.js +2 -2
  36. package/node/components/Onboarding/ui/StepStrategy/useLogic/useLogic.d.ts +0 -4
  37. package/node/components/Onboarding/ui/StepStrategy/useLogic/useLogic.js +14 -5
  38. package/node/components/Onboarding/ui/hooks/useAnimatedRect/index.d.ts +1 -0
  39. package/node/components/Onboarding/ui/hooks/useAnimatedRect/index.js +5 -0
  40. package/node/components/Onboarding/ui/hooks/useAnimatedRect/useAnimatedRect.d.ts +21 -0
  41. package/node/components/Onboarding/ui/hooks/useAnimatedRect/useAnimatedRect.js +70 -0
  42. package/node/components/Onboarding/ui/hooks/useStepTransition/useStepTransition.d.ts +3 -17
  43. package/node/components/Onboarding/ui/hooks/useStepTransition/useStepTransition.js +8 -23
  44. package/node/components/Onboarding/ui/hooks/useTargetAnchor/useTargetAnchor.js +3 -6
  45. package/node/components/Onboarding/utils/geometry/rect/index.d.ts +1 -1
  46. package/node/components/Onboarding/utils/geometry/rect/index.js +2 -1
  47. package/node/components/Onboarding/utils/geometry/rect/rect.d.ts +4 -0
  48. package/node/components/Onboarding/utils/geometry/rect/rect.js +11 -1
  49. package/node/components/Onboarding/utils/scrollIntoView/scrollIntoView.d.ts +5 -6
  50. package/node/components/Onboarding/utils/scrollIntoView/scrollIntoView.js +19 -49
  51. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
3
  import { Collapse } from '../Collapse';
4
- import { Chevron, ChevronWrapper, ContentWrapper, Header, SubtitleWrapper, Summary, Wrapper, } from './styles';
4
+ import { Chevron, ChevronWrapper, ContentWrapper, Header, SubtitleWrapper, Summary, } from './styles';
5
5
  import { useLogic } from './useLogic';
6
6
  export const Accordion = forwardRef(({ summary, startAdornment, isExpanded, onChange, children, summaryVariant = 'h6', subtitle, }, forwardedRef) => {
7
7
  const { actualIsExpanded, hasStartAdornment, handleClick, handleKeyDown, hasSubtitle, } = useLogic({ startAdornment, isExpanded, onChange, subtitle });
8
- return (_jsxs(Wrapper, { ref: forwardedRef, tabIndex: 0, onKeyDown: handleKeyDown, children: [_jsxs(Header, { role: "button", onClick: handleClick, "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: [startAdornment, typeof summary === 'string' ? (_jsx(Summary, { variant: summaryVariant, children: summary })) : (summary), _jsx(ChevronWrapper, { children: _jsx(Chevron, { "$isActive": actualIsExpanded }) }), hasSubtitle && (_jsx(SubtitleWrapper, { "$withStartAdornment": hasStartAdornment, children: subtitle }))] }), _jsx(Collapse, { in: actualIsExpanded, unmountOnExit: true, children: _jsx(ContentWrapper, { "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: children }) })] }));
8
+ return (_jsxs("div", { ref: forwardedRef, children: [_jsxs(Header, { role: "button", tabIndex: 0, onClick: handleClick, onKeyDown: handleKeyDown, "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: [startAdornment, typeof summary === 'string' ? (_jsx(Summary, { variant: summaryVariant, children: summary })) : (summary), _jsx(ChevronWrapper, { children: _jsx(Chevron, { "$isActive": actualIsExpanded }) }), hasSubtitle && (_jsx(SubtitleWrapper, { "$withStartAdornment": hasStartAdornment, children: subtitle }))] }), _jsx(Collapse, { in: actualIsExpanded, unmountOnExit: true, children: _jsx(ContentWrapper, { "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: children }) })] }));
9
9
  });
@@ -34,10 +34,6 @@ export declare const ChevronWrapper: import("../styled").StyledComponent<{
34
34
  theme?: import("@emotion/react").Theme | undefined;
35
35
  as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
36
36
  }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
37
- export declare const Wrapper: import("../styled").StyledComponent<{
38
- theme?: import("@emotion/react").Theme | undefined;
39
- as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
40
- }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
41
37
  export declare const Chevron: import("../styled").StyledComponent<{
42
38
  children?: import("react").ReactNode;
43
39
  color?: string | undefined;
@@ -11,6 +11,8 @@ export const Header = styled.header `
11
11
  grid-template-columns: ${({ $withStartAdornment }) => $withStartAdornment ? '20px 1fr 20px' : ' 1fr 20px'};
12
12
  grid-template-rows: ${({ $hasSubtitle }) => $hasSubtitle ? '20px 1fr' : '1fr'};
13
13
 
14
+ border-radius: ${({ theme }) => theme.shape.medium};
15
+
14
16
  ${({ theme }) => theme.breakpoints.down('mobile')} {
15
17
  grid-row-gap: ${({ theme }) => theme.spacing(1)};
16
18
  grid-template-rows: ${({ $hasSubtitle }) => $hasSubtitle ? '24px 1fr' : '1fr'};
@@ -18,6 +20,10 @@ export const Header = styled.header `
18
20
  /* Убирает highlight на мобильных устройствах */
19
21
  -webkit-tap-highlight-color: transparent;
20
22
  }
23
+
24
+ &:focus-visible {
25
+ outline: 2px solid ${({ theme }) => theme.palette.components.focused};
26
+ }
21
27
 
22
28
  & .${svgIconClassnames.root} {
23
29
  font-size: 20px;
@@ -54,13 +60,6 @@ export const ChevronWrapper = styled.div `
54
60
  height: 24px;
55
61
  }
56
62
  `;
57
- export const Wrapper = styled.div `
58
- border-radius: ${({ theme }) => theme.shape.medium};
59
-
60
- &:focus-visible {
61
- outline: 2px solid ${({ theme }) => theme.palette.components.focused};
62
- }
63
- `;
64
63
  export const Chevron = styled(DownOutlineMd, {
65
64
  shouldForwardProp: (prop) => !['$isActive'].includes(prop),
66
65
  }) `
@@ -3,7 +3,7 @@ import { type AccordionProps } from '../types';
3
3
  type UseLogicParams = Pick<AccordionProps, 'startAdornment' | 'isExpanded' | 'onChange' | 'subtitle'>;
4
4
  export declare const useLogic: ({ startAdornment, isExpanded, onChange, subtitle, }: UseLogicParams) => {
5
5
  handleClick: (event: SyntheticEvent<HTMLElement>) => void;
6
- handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
6
+ handleKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
7
7
  actualIsExpanded: boolean;
8
8
  isUncontrolledExpanded: boolean;
9
9
  hasStartAdornment: boolean;
@@ -14,10 +14,6 @@ export type OverlayProps = {
14
14
  * @default true
15
15
  */
16
16
  isVisible?: boolean;
17
- /**
18
- * Идёт переезд hole на новую цель.
19
- */
20
- isHoleMoving?: boolean;
21
17
  /**
22
18
  * Клик по overlay.
23
19
  */
@@ -26,4 +22,4 @@ export type OverlayProps = {
26
22
  /**
27
23
  * SVG-overlay с hole и порталом в document.body.
28
24
  */
29
- export declare const Overlay: ({ hole, isInteractive, isVisible, isHoleMoving, onClose, }: OverlayProps) => import("react").ReactPortal;
25
+ export declare const Overlay: ({ hole, isInteractive, isVisible, onClose, }: OverlayProps) => import("react").ReactPortal;
@@ -10,15 +10,12 @@ const rootCva = cva(overlayClassnames.root, {
10
10
  isHidden: {
11
11
  true: overlayClassnames.hidden,
12
12
  },
13
- isHoleMoving: {
14
- true: overlayClassnames.holeMoving,
15
- },
16
13
  },
17
14
  });
18
15
  /**
19
16
  * SVG-overlay с hole и порталом в document.body.
20
17
  */
21
- export const Overlay = ({ hole, isInteractive, isVisible = true, isHoleMoving = false, onClose, }) => {
18
+ export const Overlay = ({ hole, isInteractive, isVisible = true, onClose, }) => {
22
19
  const { viewport, clampedHole, path } = useLogic(hole);
23
- return createPortal(_jsxs(OverlayRoot, { viewBox: `0 0 ${viewport.width} ${viewport.height}`, preserveAspectRatio: "none", className: rootCva({ isHidden: !isVisible, isHoleMoving }), children: [_jsx(Backdrop, { d: path, fillRule: "evenodd", onClick: onClose, className: overlayClassnames.backdrop }), !isInteractive && clampedHole && (_jsx(HoleBlocker, { x: clampedHole.x, y: clampedHole.y, width: clampedHole.width, height: clampedHole.height, rx: DEFAULT_HIGHLIGHT_RADIUS, className: overlayClassnames.holeBlocker }))] }), document.body);
20
+ return createPortal(_jsxs(OverlayRoot, { viewBox: `0 0 ${viewport.width} ${viewport.height}`, preserveAspectRatio: "none", className: rootCva({ isHidden: !isVisible }), children: [_jsx(Backdrop, { d: path, fillRule: "evenodd", onClick: onClose, className: overlayClassnames.backdrop }), !isInteractive && clampedHole && (_jsx(HoleBlocker, { x: clampedHole.x, y: clampedHole.y, width: clampedHole.width, height: clampedHole.height, rx: DEFAULT_HIGHLIGHT_RADIUS, className: overlayClassnames.holeBlocker }))] }), document.body);
24
21
  };
@@ -1,7 +1,6 @@
1
1
  export declare const overlayClassnames: {
2
2
  root: string;
3
3
  hidden: string;
4
- holeMoving: string;
5
4
  backdrop: string;
6
5
  holeBlocker: string;
7
6
  };
@@ -2,7 +2,6 @@ import { createUIKitClassname } from '../../../utils/createUIKitClassname';
2
2
  export const overlayClassnames = {
3
3
  root: createUIKitClassname('onboarding-overlay'),
4
4
  hidden: createUIKitClassname('onboarding-overlay_hidden'),
5
- holeMoving: createUIKitClassname('onboarding-overlay_hole-moving'),
6
5
  backdrop: createUIKitClassname('onboarding-overlay__backdrop'),
7
6
  holeBlocker: createUIKitClassname('onboarding-overlay__hole-blocker'),
8
7
  };
@@ -35,11 +35,6 @@ export const Backdrop = styled.path `
35
35
  cursor: pointer;
36
36
 
37
37
  fill: ${({ theme }) => theme.palette.background.modalShadow};
38
-
39
- .${overlayClassnames.holeMoving} & {
40
- transition: d ${({ theme }) => theme.transitions.duration.standard}ms
41
- ${({ theme }) => theme.transitions.easing.easeInOut};
42
- }
43
38
  `;
44
39
  /**
45
40
  * Блокатор кликов для неинтерактивных шагов.
@@ -48,10 +43,4 @@ export const HoleBlocker = styled.rect `
48
43
  pointer-events: auto;
49
44
 
50
45
  fill: transparent;
51
-
52
- .${overlayClassnames.holeMoving} & {
53
- transition-timing-function: ${({ theme }) => theme.transitions.easing.easeInOut};
54
- transition-duration: ${({ theme }) => theme.transitions.duration.standard}ms;
55
- transition-property: x, y, width, height, rx;
56
- }
57
46
  `;
@@ -11,7 +11,7 @@ const STEP_VIEWS = {
11
11
  * Стратегия отрисовки по типу шага, единый overlay для всего тура.
12
12
  */
13
13
  export const StepStrategy = ({ activeStep, isClosing }) => {
14
- const { geometry, shouldShowOverlay, isVisible, isHoleMoving, onOverlayClose, } = useLogic(activeStep, isClosing);
14
+ const { geometry, shouldShowOverlay, isVisible, onOverlayClose } = useLogic(activeStep, isClosing);
15
15
  const StepView = STEP_VIEWS[activeStep.step.type];
16
- return (_jsxs(_Fragment, { children: [shouldShowOverlay && (_jsx(Overlay, { hole: geometry.hole, isInteractive: activeStep.isInteractive, isVisible: isVisible, isHoleMoving: isHoleMoving, onClose: onOverlayClose })), _jsx(StepView, { activeStep: activeStep, isClosing: isClosing, geometry: geometry })] }));
16
+ return (_jsxs(_Fragment, { children: [shouldShowOverlay && (_jsx(Overlay, { hole: geometry.hole, isInteractive: activeStep.isInteractive, isVisible: isVisible, onClose: onOverlayClose })), _jsx(StepView, { activeStep: activeStep, isClosing: isClosing, geometry: geometry })] }));
17
17
  };
@@ -32,10 +32,6 @@ export type UseLogicResult = {
32
32
  * Overlay виден.
33
33
  */
34
34
  isVisible: boolean;
35
- /**
36
- * Hole анимируется.
37
- */
38
- isHoleMoving: boolean;
39
35
  /**
40
36
  * Клик по overlay — запрос выхода.
41
37
  */
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef } from 'react';
2
2
  import { useTheme } from '../../../../theme/hooks/useTheme';
3
3
  import { createVirtualAnchor, } from '../../../utils/createVirtualAnchor';
4
4
  import { inflateRect, parseOffset } from '../../../utils/geometry';
5
+ import { useAnimatedRect } from '../../hooks/useAnimatedRect';
5
6
  import { useStepTransition } from '../../hooks/useStepTransition';
6
7
  import { useTargetAnchor } from '../../hooks/useTargetAnchor';
7
8
  /**
@@ -19,12 +20,21 @@ export const useLogic = (activeStep, isClosing) => {
19
20
  timeout: step.waitForTimeout,
20
21
  });
21
22
  const padding = parseOffset(step.highlightOffset);
22
- const hole = useMemo(() => (rect ? inflateRect(rect, padding) : null), [rect, padding]);
23
+ // Отступ учитываем до переезда: иначе он подставляется сразу от нового шага,
24
+ // и область скачком меняет размер вместо плавного.
25
+ const targetHole = useMemo(() => (rect ? inflateRect(rect, padding) : null), [rect, padding]);
26
+ // Переезд hole между шагами: цель шага может ехать сама (шторка, прокрутка),
27
+ // поэтому область анимируется здесь, а не css-переходом.
28
+ const hole = useAnimatedRect(targetHole, {
29
+ stepKey: step.name,
30
+ durationMs: theme.transitions.duration.short,
31
+ // Шаг без hole: область гаснет сразу, а не едет к отсутствующей цели.
32
+ hasHole: isHighlight,
33
+ });
23
34
  const anchorEl = useMemo(() => (hole ? createVirtualAnchor(hole) : null), [hole]);
24
- const { isPopoverHidden, isHoleMoving } = useStepTransition(step.name, {
25
- // Popover скрыт пока hole движется.
35
+ const isPopoverHidden = useStepTransition(step.name, {
36
+ // Popover скрыт пока hole едет к новой цели.
26
37
  popoverHiddenMs: theme.transitions.duration.short,
27
- holeMovingMs: theme.transitions.duration.short,
28
38
  hasHole: isHighlight,
29
39
  });
30
40
  const onOverlayClose = useCallback(() => {
@@ -45,7 +55,6 @@ export const useLogic = (activeStep, isClosing) => {
45
55
  },
46
56
  shouldShowOverlay: hasShownOverlayRef.current,
47
57
  isVisible: !isClosing,
48
- isHoleMoving,
49
58
  onOverlayClose,
50
59
  };
51
60
  };
@@ -0,0 +1 @@
1
+ export { useAnimatedRect } from './useAnimatedRect';
@@ -0,0 +1 @@
1
+ export { useAnimatedRect } from './useAnimatedRect';
@@ -0,0 +1,21 @@
1
+ import { type Rect } from '../../../utils/geometry';
2
+ export type AnimatedRectOptions = {
3
+ /**
4
+ * Ключ шага: его смена запускает переезд hole на новую цель.
5
+ */
6
+ stepKey: string;
7
+ /**
8
+ * Длительность переезда (мс).
9
+ */
10
+ durationMs: number;
11
+ /**
12
+ * Есть ли hole в текущем шаге (highlight). У шага без hole область гаснет
13
+ * сразу: ехать некуда.
14
+ * @default true
15
+ */
16
+ hasHole?: boolean;
17
+ };
18
+ /**
19
+ * Плавный переход области hole от цели прошлого шага к цели нового.
20
+ */
21
+ export declare const useAnimatedRect: (target: Rect | null, { stepKey, durationMs, hasHole }: AnimatedRectOptions) => Rect | null;
@@ -0,0 +1,66 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { interpolateRect } from '../../../utils/geometry';
3
+ /**
4
+ * Сглаживание переезда, аналог css-функции `ease-in-out`.
5
+ */
6
+ const easeInOut = (progress) => progress < 0.5 ? 4 * progress ** 3 : 1 - (-2 * progress + 2) ** 3 / 2;
7
+ /**
8
+ * Плавный переход области hole от цели прошлого шага к цели нового.
9
+ */
10
+ export const useAnimatedRect = (target, { stepKey, durationMs, hasHole = true }) => {
11
+ const [travelRect, setTravelRect] = useState(null);
12
+ const fromRect = useRef(null);
13
+ const targetRect = useRef(target);
14
+ const shownRect = useRef(target);
15
+ const prevStepKey = useRef(stepKey);
16
+ const [travelId, setTravelId] = useState(0);
17
+ targetRect.current = target;
18
+ // Стартуем синхронно во время render, иначе виден кадр с hole на новой цели.
19
+ if (prevStepKey.current !== stepKey) {
20
+ prevStepKey.current = stepKey;
21
+ if (!hasHole) {
22
+ // Шаг без hole (standalone): гасим область и отменяем начатый переезд.
23
+ fromRect.current = null;
24
+ setTravelRect(null);
25
+ setTravelId((id) => id + 1);
26
+ }
27
+ else if (shownRect.current) {
28
+ // Область уже показана — едем от неё; иначе (первый hole в туре) цель
29
+ // показывается сразу, без переезда.
30
+ fromRect.current = shownRect.current;
31
+ setTravelRect(shownRect.current);
32
+ setTravelId((id) => id + 1);
33
+ }
34
+ }
35
+ useEffect(() => {
36
+ // Первый render (mount) — переезд не запускаем.
37
+ if (travelId === 0) {
38
+ return;
39
+ }
40
+ const from = fromRect.current;
41
+ if (!from) {
42
+ return;
43
+ }
44
+ let frame = 0;
45
+ let startTime = 0;
46
+ const travel = (time) => {
47
+ startTime = startTime || time;
48
+ const progress = Math.min((time - startTime) / durationMs, 1);
49
+ const to = targetRect.current;
50
+ // Цель ещё не готова (элемент не смонтирован, идёт прокрутка) — держим
51
+ // прежнюю область, чтобы hole не гас на время ожидания.
52
+ setTravelRect(to ? interpolateRect(from, to, easeInOut(progress)) : from);
53
+ if (progress < 1) {
54
+ frame = requestAnimationFrame(travel);
55
+ return;
56
+ }
57
+ // Переезд закончен, дальше область ведёт трекинг цели.
58
+ setTravelRect(null);
59
+ };
60
+ frame = requestAnimationFrame(travel);
61
+ return () => cancelAnimationFrame(frame);
62
+ }, [travelId, durationMs]);
63
+ const rect = hasHole ? (travelRect ?? target) : null;
64
+ shownRect.current = rect;
65
+ return rect;
66
+ };
@@ -1,12 +1,8 @@
1
1
  export type StepTransitionDurations = {
2
2
  /**
3
- * Время скрытия popover при смене шага (не меньше `holeMovingMs`).
3
+ * Время скрытия popover при смене шага.
4
4
  */
5
5
  popoverHiddenMs: number;
6
- /**
7
- * Время движения hole к новому элементу.
8
- */
9
- holeMovingMs: number;
10
6
  /**
11
7
  * Есть ли hole в текущем шаге (highlight). Скрытие popover нужно только для
12
8
  * переезда hole (highlight → highlight).
@@ -14,17 +10,7 @@ export type StepTransitionDurations = {
14
10
  */
15
11
  hasHole?: boolean;
16
12
  };
17
- export type StepTransitionPhase = {
18
- /**
19
- * Popover скрыт, пока hole движется.
20
- */
21
- isPopoverHidden: boolean;
22
- /**
23
- * Идёт анимированный переезд hole.
24
- */
25
- isHoleMoving: boolean;
26
- };
27
13
  /**
28
- * Координирует скрытие popover и движение hole при переходе между highlight-шагами.
14
+ * Прячет popover на время переезда hole между highlight-шагами.
29
15
  */
30
- export declare const useStepTransition: (stepKey: string, { popoverHiddenMs, holeMovingMs, hasHole }: StepTransitionDurations) => StepTransitionPhase;
16
+ export declare const useStepTransition: (stepKey: string, { popoverHiddenMs, hasHole }: StepTransitionDurations) => boolean;
@@ -1,17 +1,9 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
- const IDLE_PHASE = {
3
- isPopoverHidden: false,
4
- isHoleMoving: false,
5
- };
6
- const HIDDEN_PHASE = {
7
- isPopoverHidden: true,
8
- isHoleMoving: true,
9
- };
10
2
  /**
11
- * Координирует скрытие popover и движение hole при переходе между highlight-шагами.
3
+ * Прячет popover на время переезда hole между highlight-шагами.
12
4
  */
13
- export const useStepTransition = (stepKey, { popoverHiddenMs, holeMovingMs, hasHole = true }) => {
14
- const [phase, setPhase] = useState(IDLE_PHASE);
5
+ export const useStepTransition = (stepKey, { popoverHiddenMs, hasHole = true }) => {
6
+ const [isPopoverHidden, setIsPopoverHidden] = useState(false);
15
7
  const prevKey = useRef(stepKey);
16
8
  const prevHadHole = useRef(hasHole);
17
9
  // Триггер для эффекта: `prevKey` мутируется во время render, поэтому в эффекте не сравнить.
@@ -22,25 +14,18 @@ export const useStepTransition = (stepKey, { popoverHiddenMs, holeMovingMs, hasH
22
14
  const isHoleTravel = prevHadHole.current && hasHole;
23
15
  prevKey.current = stepKey;
24
16
  prevHadHole.current = hasHole;
17
+ setIsPopoverHidden(isHoleTravel);
25
18
  if (isHoleTravel) {
26
- setPhase(HIDDEN_PHASE);
27
19
  setTransitionId((id) => id + 1);
28
20
  }
29
- else {
30
- setPhase(IDLE_PHASE);
31
- }
32
21
  }
33
22
  useEffect(() => {
34
23
  // Первый render (mount) — переход не запускаем.
35
24
  if (transitionId === 0) {
36
25
  return;
37
26
  }
38
- const popoverTimer = setTimeout(() => setPhase((prev) => ({ ...prev, isPopoverHidden: false })), popoverHiddenMs);
39
- const holeTimer = setTimeout(() => setPhase((prev) => ({ ...prev, isHoleMoving: false })), holeMovingMs);
40
- return () => {
41
- clearTimeout(popoverTimer);
42
- clearTimeout(holeTimer);
43
- };
44
- }, [transitionId, popoverHiddenMs, holeMovingMs]);
45
- return phase;
27
+ const popoverTimer = setTimeout(() => setIsPopoverHidden(false), popoverHiddenMs);
28
+ return () => clearTimeout(popoverTimer);
29
+ }, [transitionId, popoverHiddenMs]);
30
+ return isPopoverHidden;
46
31
  };
@@ -21,17 +21,14 @@ export const useTargetAnchor = (targetName, { onError, shouldTrack = true, scrol
21
21
  // Устанавливаем найденный элемент сразу, без задержки.
22
22
  setElement(getTargetElement(targetName));
23
23
  waitForElement(targetName, { timeout })
24
- .then(async (target) => {
24
+ .then((target) => {
25
25
  if (isCancelled) {
26
26
  return;
27
27
  }
28
+ setElement(target);
28
29
  if (scrollOnEnter) {
29
- await scrollIntoView(target);
30
+ scrollIntoView(target);
30
31
  }
31
- if (isCancelled) {
32
- return;
33
- }
34
- setElement(target);
35
32
  stopRemovalWatch = observeRemoval(target, () => {
36
33
  if (!isCancelled) {
37
34
  setElement(null);
@@ -1,2 +1,2 @@
1
- export { clampToScrollParents, clampToViewport, inflateRect, isSameRect, rectFromDomRect, } from './rect';
1
+ export { clampToScrollParents, clampToViewport, inflateRect, interpolateRect, isSameRect, rectFromDomRect, } from './rect';
2
2
  export type { Rect } from './rect';
@@ -1 +1 @@
1
- export { clampToScrollParents, clampToViewport, inflateRect, isSameRect, rectFromDomRect, } from './rect';
1
+ export { clampToScrollParents, clampToViewport, inflateRect, interpolateRect, isSameRect, rectFromDomRect, } from './rect';
@@ -20,6 +20,10 @@ export declare const isSameRect: (a: Rect | null, b: Rect) => boolean;
20
20
  * Расширяет область на padding во все стороны.
21
21
  */
22
22
  export declare const inflateRect: (rect: Rect, padding: number) => Rect;
23
+ /**
24
+ * Область между `from` и `to` по доле переезда `progress` (0..1).
25
+ */
26
+ export declare const interpolateRect: (from: Rect, to: Rect, progress: number) => Rect;
23
27
  /**
24
28
  * Обрезает область hole по границам viewport.
25
29
  */
@@ -24,6 +24,15 @@ export const inflateRect = (rect, padding) => ({
24
24
  width: rect.width + padding * 2,
25
25
  height: rect.height + padding * 2,
26
26
  });
27
+ /**
28
+ * Область между `from` и `to` по доле переезда `progress` (0..1).
29
+ */
30
+ export const interpolateRect = (from, to, progress) => ({
31
+ x: from.x + (to.x - from.x) * progress,
32
+ y: from.y + (to.y - from.y) * progress,
33
+ width: from.width + (to.width - from.width) * progress,
34
+ height: from.height + (to.height - from.height) * progress,
35
+ });
27
36
  /**
28
37
  * Пересечение двух областей. Не пересекаются — ширина/высота 0.
29
38
  */
@@ -1,13 +1,12 @@
1
1
  /**
2
- * Доводит элемент highlight-шага до центра viewport и ждёт, пока прокрутка
3
- * реально доедет.
2
+ * Доводит элемент highlight-шага до центра viewport.
4
3
  *
5
4
  * Зачем: цель может быть за пределами экрана или внутри прокрученного
6
5
  * контейнера — её надо показать, прежде чем подсвечивать. Нативный
7
6
  * `scrollIntoView` сам прокручивает всю цепочку вложенных scroll-контейнеров.
8
7
  *
9
- * Почему ждём: прокрутка плавная (анимированная), позиция элемента меняется
10
- * кадр за кадром. Дожидаемся её стабилизации, чтобы hole overlay и popover встали
11
- * по финальной позиции, а не по промежуточной «едущей».
8
+ * Окончания прокрутки не ждём: hole и popover привязаны к элементу, а
9
+ * `useTrackedRect` ведёт их за ним покадрово — к финальной позиции они приедут
10
+ * вместе с ним.
12
11
  */
13
- export declare const scrollIntoView: (element: HTMLElement, behavior?: ScrollBehavior) => Promise<void>;
12
+ export declare const scrollIntoView: (element: HTMLElement, behavior?: ScrollBehavior) => void;
@@ -2,51 +2,23 @@ import { SCROLL_BEHAVIOR } from '../../constants';
2
2
  import { getViewport } from '../geometry';
3
3
  import { getScrollParents } from '../getScrollParents';
4
4
  /**
5
- * Сколько кадров подряд позиция не меняется, чтобы счесть прокрутку стабилизировавшейся.
5
+ * Позиционирование, при котором элемент не двигается прокруткой страницы.
6
6
  */
7
- const STABLE_FRAMES = 3;
7
+ const PINNED_POSITIONS = ['fixed', 'sticky'];
8
8
  /**
9
- * Предохранитель: если позиция так и не стабилизировалась (постоянные
10
- * layout-сдвиги), делаем resolve по тайм-ауту, чтобы Promise не завис и тур не застрял.
9
+ * Элемент закреплён на экране: лежит внутри шторки, модалки или липкой шапки.
10
+ * Прокрутка такой элемент не подвинет, звать её незачем.
11
11
  */
12
- const SETTLE_TIMEOUT_MS = 1000;
13
- /**
14
- * Дожидается окончания плавной прокрутки.
15
- *
16
- * У браузера нет надёжного сигнала «плавная прокрутка доехала».
17
- * Поэтому отслеживаем позицию элемента покадрово и считаем прокрутку
18
- * законченной, когда `top` перестаёт меняться 3 кадра
19
- * подряд — либо когда срабатывает предохранитель 1000мс.
20
- */
21
- const waitForScrollSettle = (element) => new Promise((resolve) => {
22
- let raf = 0;
23
- let stableFrames = 0;
24
- let prevTop = Number.NaN;
25
- const timer = setTimeout(() => {
26
- cancelAnimationFrame(raf);
27
- resolve();
28
- }, SETTLE_TIMEOUT_MS);
29
- const tick = () => {
30
- const { top } = element.getBoundingClientRect();
31
- // Позиция не изменилась за кадр — копим «стабильные» кадры; дёрнулась —
32
- // сбрасываем счётчик и запоминаем новую позицию.
33
- if (top === prevTop) {
34
- stableFrames += 1;
35
- }
36
- else {
37
- stableFrames = 0;
38
- prevTop = top;
12
+ const checkIsPinned = (element) => {
13
+ let node = element;
14
+ while (node) {
15
+ if (PINNED_POSITIONS.includes(getComputedStyle(node).position)) {
16
+ return true;
39
17
  }
40
- // Достаточно кадров без движения → анимация закончилась.
41
- if (stableFrames >= STABLE_FRAMES) {
42
- clearTimeout(timer);
43
- resolve();
44
- return;
45
- }
46
- raf = requestAnimationFrame(tick);
47
- };
48
- raf = requestAnimationFrame(tick);
49
- });
18
+ node = node.parentElement;
19
+ }
20
+ return false;
21
+ };
50
22
  /**
51
23
  * Элемент целиком видим: помещается и во viewport, и в рамки каждого своего
52
24
  * scroll-предка.
@@ -65,21 +37,19 @@ const isFullyVisible = (element) => {
65
37
  rect.right <= frame.right);
66
38
  };
67
39
  /**
68
- * Доводит элемент highlight-шага до центра viewport и ждёт, пока прокрутка
69
- * реально доедет.
40
+ * Доводит элемент highlight-шага до центра viewport.
70
41
  *
71
42
  * Зачем: цель может быть за пределами экрана или внутри прокрученного
72
43
  * контейнера — её надо показать, прежде чем подсвечивать. Нативный
73
44
  * `scrollIntoView` сам прокручивает всю цепочку вложенных scroll-контейнеров.
74
45
  *
75
- * Почему ждём: прокрутка плавная (анимированная), позиция элемента меняется
76
- * кадр за кадром. Дожидаемся её стабилизации, чтобы hole overlay и popover встали
77
- * по финальной позиции, а не по промежуточной «едущей».
46
+ * Окончания прокрутки не ждём: hole и popover привязаны к элементу, а
47
+ * `useTrackedRect` ведёт их за ним покадрово — к финальной позиции они приедут
48
+ * вместе с ним.
78
49
  */
79
- export const scrollIntoView = async (element, behavior = SCROLL_BEHAVIOR) => {
80
- if (isFullyVisible(element)) {
50
+ export const scrollIntoView = (element, behavior = SCROLL_BEHAVIOR) => {
51
+ if (checkIsPinned(element) || isFullyVisible(element)) {
81
52
  return;
82
53
  }
83
54
  element.scrollIntoView({ behavior, block: 'center', inline: 'center' });
84
- await waitForScrollSettle(element);
85
55
  };
@@ -8,5 +8,5 @@ const styles_1 = require("./styles");
8
8
  const useLogic_1 = require("./useLogic");
9
9
  exports.Accordion = (0, react_1.forwardRef)(({ summary, startAdornment, isExpanded, onChange, children, summaryVariant = 'h6', subtitle, }, forwardedRef) => {
10
10
  const { actualIsExpanded, hasStartAdornment, handleClick, handleKeyDown, hasSubtitle, } = (0, useLogic_1.useLogic)({ startAdornment, isExpanded, onChange, subtitle });
11
- return ((0, jsx_runtime_1.jsxs)(styles_1.Wrapper, { ref: forwardedRef, tabIndex: 0, onKeyDown: handleKeyDown, children: [(0, jsx_runtime_1.jsxs)(styles_1.Header, { role: "button", onClick: handleClick, "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: [startAdornment, typeof summary === 'string' ? ((0, jsx_runtime_1.jsx)(styles_1.Summary, { variant: summaryVariant, children: summary })) : (summary), (0, jsx_runtime_1.jsx)(styles_1.ChevronWrapper, { children: (0, jsx_runtime_1.jsx)(styles_1.Chevron, { "$isActive": actualIsExpanded }) }), hasSubtitle && ((0, jsx_runtime_1.jsx)(styles_1.SubtitleWrapper, { "$withStartAdornment": hasStartAdornment, children: subtitle }))] }), (0, jsx_runtime_1.jsx)(Collapse_1.Collapse, { in: actualIsExpanded, unmountOnExit: true, children: (0, jsx_runtime_1.jsx)(styles_1.ContentWrapper, { "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: children }) })] }));
11
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: forwardedRef, children: [(0, jsx_runtime_1.jsxs)(styles_1.Header, { role: "button", tabIndex: 0, onClick: handleClick, onKeyDown: handleKeyDown, "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: [startAdornment, typeof summary === 'string' ? ((0, jsx_runtime_1.jsx)(styles_1.Summary, { variant: summaryVariant, children: summary })) : (summary), (0, jsx_runtime_1.jsx)(styles_1.ChevronWrapper, { children: (0, jsx_runtime_1.jsx)(styles_1.Chevron, { "$isActive": actualIsExpanded }) }), hasSubtitle && ((0, jsx_runtime_1.jsx)(styles_1.SubtitleWrapper, { "$withStartAdornment": hasStartAdornment, children: subtitle }))] }), (0, jsx_runtime_1.jsx)(Collapse_1.Collapse, { in: actualIsExpanded, unmountOnExit: true, children: (0, jsx_runtime_1.jsx)(styles_1.ContentWrapper, { "$withStartAdornment": hasStartAdornment, "$hasSubtitle": hasSubtitle, children: children }) })] }));
12
12
  });
@@ -34,10 +34,6 @@ export declare const ChevronWrapper: import("@emotion/styled/dist/declarations/s
34
34
  theme?: import("@emotion/react").Theme | undefined;
35
35
  as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
36
36
  }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
37
- export declare const Wrapper: import("@emotion/styled/dist/declarations/src/types").StyledComponent<{
38
- theme?: import("@emotion/react").Theme | undefined;
39
- as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
40
- }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
41
37
  export declare const Chevron: import("@emotion/styled/dist/declarations/src/types").StyledComponent<{
42
38
  children?: import("react").ReactNode;
43
39
  color?: string | undefined;