@astral/ui 4.66.0 → 4.67.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 (73) hide show
  1. package/components/NewPopover/NewPopover.d.ts +2 -0
  2. package/components/NewPopover/NewPopover.js +23 -0
  3. package/components/NewPopover/constants.d.ts +6 -0
  4. package/components/NewPopover/constants.js +7 -0
  5. package/components/NewPopover/index.d.ts +2 -0
  6. package/components/NewPopover/index.js +1 -0
  7. package/components/NewPopover/public.d.ts +2 -0
  8. package/components/NewPopover/public.js +1 -0
  9. package/components/NewPopover/styles.d.ts +12 -0
  10. package/components/NewPopover/styles.js +80 -0
  11. package/components/NewPopover/types.d.ts +53 -0
  12. package/components/NewPopover/types.js +1 -0
  13. package/components/NewPopover/useLogic/index.d.ts +1 -0
  14. package/components/NewPopover/useLogic/index.js +1 -0
  15. package/components/NewPopover/useLogic/useLogic.d.ts +11 -0
  16. package/components/NewPopover/useLogic/useLogic.js +54 -0
  17. package/components/NewPopover/utils/index.d.ts +1 -0
  18. package/components/NewPopover/utils/index.js +1 -0
  19. package/components/NewPopover/utils/resolveAnchorNode.d.ts +6 -0
  20. package/components/NewPopover/utils/resolveAnchorNode.js +8 -0
  21. package/components/Popover/Popover.d.ts +4 -8
  22. package/components/Popover/Popover.js +3 -0
  23. package/components/Popover/index.d.ts +1 -0
  24. package/components/Popover/index.js +1 -0
  25. package/components/Popover/public.d.ts +2 -1
  26. package/components/Popover/public.js +1 -1
  27. package/components/Popover/types.d.ts +8 -0
  28. package/components/Popover/types.js +1 -0
  29. package/components/index.d.ts +0 -1
  30. package/components/index.js +0 -1
  31. package/components/useClickAwayEffect/useClickAwayEffect.d.ts +6 -1
  32. package/components/useClickAwayEffect/useClickAwayEffect.js +19 -9
  33. package/components/useFocusTrapEffect/index.d.ts +1 -0
  34. package/components/useFocusTrapEffect/index.js +1 -0
  35. package/components/useFocusTrapEffect/useFocusTrapEffect.d.ts +17 -0
  36. package/components/useFocusTrapEffect/useFocusTrapEffect.js +24 -0
  37. package/node/components/NewPopover/NewPopover.d.ts +2 -0
  38. package/node/components/NewPopover/NewPopover.js +27 -0
  39. package/node/components/NewPopover/constants.d.ts +6 -0
  40. package/node/components/NewPopover/constants.js +10 -0
  41. package/node/components/NewPopover/index.d.ts +2 -0
  42. package/node/components/NewPopover/index.js +5 -0
  43. package/node/components/NewPopover/public.d.ts +2 -0
  44. package/node/components/NewPopover/public.js +5 -0
  45. package/node/components/NewPopover/styles.d.ts +12 -0
  46. package/node/components/NewPopover/styles.js +86 -0
  47. package/node/components/NewPopover/types.d.ts +53 -0
  48. package/node/components/NewPopover/types.js +2 -0
  49. package/node/components/NewPopover/useLogic/index.d.ts +1 -0
  50. package/node/components/NewPopover/useLogic/index.js +17 -0
  51. package/node/components/NewPopover/useLogic/useLogic.d.ts +11 -0
  52. package/node/components/NewPopover/useLogic/useLogic.js +58 -0
  53. package/node/components/NewPopover/utils/index.d.ts +1 -0
  54. package/node/components/NewPopover/utils/index.js +17 -0
  55. package/node/components/NewPopover/utils/resolveAnchorNode.d.ts +6 -0
  56. package/node/components/NewPopover/utils/resolveAnchorNode.js +12 -0
  57. package/node/components/Popover/Popover.d.ts +4 -8
  58. package/node/components/Popover/Popover.js +3 -0
  59. package/node/components/Popover/index.d.ts +1 -0
  60. package/node/components/Popover/index.js +1 -0
  61. package/node/components/Popover/public.d.ts +2 -1
  62. package/node/components/Popover/public.js +3 -15
  63. package/node/components/Popover/types.d.ts +8 -0
  64. package/node/components/Popover/types.js +2 -0
  65. package/node/components/index.d.ts +0 -1
  66. package/node/components/index.js +0 -1
  67. package/node/components/useClickAwayEffect/useClickAwayEffect.d.ts +6 -1
  68. package/node/components/useClickAwayEffect/useClickAwayEffect.js +19 -9
  69. package/node/components/useFocusTrapEffect/index.d.ts +1 -0
  70. package/node/components/useFocusTrapEffect/index.js +17 -0
  71. package/node/components/useFocusTrapEffect/useFocusTrapEffect.d.ts +17 -0
  72. package/node/components/useFocusTrapEffect/useFocusTrapEffect.js +28 -0
  73. package/package.json +3 -2
@@ -0,0 +1,2 @@
1
+ import { type NewPopoverProps } from './types';
2
+ export declare const NewPopover: ({ children, onClose, isOpen, title, className, style, placement, anchorEl, disablePortal, disableAutoFocus, }: NewPopoverProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BottomDrawer } from '../BottomDrawer';
3
+ import { classNames } from '../utils/classNames';
4
+ import { cva } from '../utils/cva';
5
+ import { popoverClassnames } from './constants';
6
+ import { AnimatedWrapper, InnerContainer, StyledMuiPopper } from './styles';
7
+ import { useLogic } from './useLogic';
8
+ const animatedWrapperCva = cva(popoverClassnames.animatedWrapper, {
9
+ variants: {
10
+ isOpen: {
11
+ true: popoverClassnames.animatedWrapperOpen,
12
+ },
13
+ },
14
+ });
15
+ export const NewPopover = ({ children, onClose, isOpen, title, className, style, placement = 'bottom', anchorEl, disablePortal, disableAutoFocus, }) => {
16
+ const { isMobile, isOpened, shouldRender, handleAnimationEnd, handleClose, setContainerRef, } = useLogic({ isOpen, anchorEl, onClose, disableAutoFocus });
17
+ if (isMobile) {
18
+ return (_jsx(BottomDrawer, { title: title, onClose: handleClose, open: isOpened, className: className, style: style, ModalProps: {
19
+ disableAutoFocus,
20
+ }, children: children }));
21
+ }
22
+ return (_jsx(StyledMuiPopper, { className: popoverClassnames.root, open: shouldRender, placement: placement, anchorEl: anchorEl, disablePortal: disablePortal, children: _jsx(AnimatedWrapper, { className: classNames(className, animatedWrapperCva({ isOpen: isOpened })), style: style, onAnimationEnd: handleAnimationEnd, children: _jsx(InnerContainer, { ref: setContainerRef, tabIndex: -1, className: popoverClassnames.innerContainer, children: children }) }) }));
23
+ };
@@ -0,0 +1,6 @@
1
+ export declare const popoverClassnames: {
2
+ root: string;
3
+ innerContainer: string;
4
+ animatedWrapper: string;
5
+ animatedWrapperOpen: string;
6
+ };
@@ -0,0 +1,7 @@
1
+ import { createUIKitClassname } from '../utils/createUIKitClassname';
2
+ export const popoverClassnames = {
3
+ root: createUIKitClassname('popover'),
4
+ innerContainer: createUIKitClassname('popover__inner-container'),
5
+ animatedWrapper: createUIKitClassname('popover__animated-wrapper'),
6
+ animatedWrapperOpen: createUIKitClassname('popover__animated-wrapper_is-open'),
7
+ };
@@ -0,0 +1,2 @@
1
+ export { NewPopover } from './NewPopover';
2
+ export type { NewPopoverProps } from './types';
@@ -0,0 +1 @@
1
+ export { NewPopover } from './NewPopover';
@@ -0,0 +1,2 @@
1
+ export { NewPopover } from './NewPopover';
2
+ export type { NewPopoverProps } from './types';
@@ -0,0 +1 @@
1
+ export { NewPopover } from './NewPopover';
@@ -0,0 +1,12 @@
1
+ /// <reference types="react" />
2
+ export declare const StyledMuiPopper: import("../styled").StyledComponent<import("@mui/material/Popper").PopperProps & import("react").RefAttributes<HTMLDivElement> & {
3
+ theme?: import("@emotion/react").Theme | undefined;
4
+ }, {}, {}>;
5
+ export declare const AnimatedWrapper: import("../styled").StyledComponent<{
6
+ theme?: import("@emotion/react").Theme | undefined;
7
+ as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
8
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
9
+ export declare const InnerContainer: import("../styled").StyledComponent<{
10
+ theme?: import("@emotion/react").Theme | undefined;
11
+ as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
12
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
@@ -0,0 +1,80 @@
1
+ import MuiPopper from '@mui/material/Popper';
2
+ import { styled } from '../styled';
3
+ import { popoverClassnames } from './constants';
4
+ export const StyledMuiPopper = styled(MuiPopper) `
5
+ &.${popoverClassnames.root} {
6
+ z-index: ${({ theme }) => theme.zIndex.tooltip};
7
+ }
8
+
9
+ &&[data-popper-placement*='top'] .${popoverClassnames.animatedWrapper} {
10
+ transform-origin: bottom center;
11
+
12
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
13
+ }
14
+
15
+ &&[data-popper-placement*='bottom'] .${popoverClassnames.animatedWrapper} {
16
+ transform-origin: top center;
17
+
18
+ margin-top: ${({ theme }) => theme.spacing(2)};
19
+ }
20
+
21
+ &&[data-popper-placement*='left'] .${popoverClassnames.animatedWrapper} {
22
+ transform-origin: right center;
23
+
24
+ margin-right: ${({ theme }) => theme.spacing(2)};
25
+ }
26
+
27
+ &&[data-popper-placement*='right'] .${popoverClassnames.animatedWrapper} {
28
+ transform-origin: left center;
29
+
30
+ margin-left: ${({ theme }) => theme.spacing(2)};
31
+ }
32
+ `;
33
+ export const AnimatedWrapper = styled.div `
34
+ animation: popover-grow-out
35
+ ${({ theme }) => theme.transitions.duration.enteringScreen}ms
36
+ ${({ theme }) => theme.transitions.easing.easeInOut} forwards;
37
+
38
+ &.${popoverClassnames.animatedWrapperOpen} {
39
+ animation: popover-grow-in
40
+ ${({ theme }) => theme.transitions.duration.enteringScreen}ms
41
+ ${({ theme }) => theme.transitions.easing.easeInOut} forwards;
42
+ }
43
+
44
+ @keyframes popover-grow-in {
45
+ from {
46
+ transform: scale(0.75, 0.5625);
47
+
48
+ opacity: 0;
49
+ }
50
+
51
+ to {
52
+ transform: none;
53
+
54
+ opacity: 1;
55
+ }
56
+ }
57
+
58
+ @keyframes popover-grow-out {
59
+ from {
60
+ transform: none;
61
+
62
+ opacity: 1;
63
+ }
64
+
65
+ to {
66
+ transform: scale(0.75, 0.5625);
67
+
68
+ opacity: 0;
69
+ }
70
+ }
71
+ `;
72
+ export const InnerContainer = styled.div `
73
+ &.${popoverClassnames.innerContainer} {
74
+ overflow: hidden;
75
+
76
+ background: ${({ theme }) => theme.palette.background.paper};
77
+ border-radius: ${({ theme }) => theme.shape.medium};
78
+ box-shadow: ${({ theme }) => theme.elevation[200]};
79
+ }
80
+ `;
@@ -0,0 +1,53 @@
1
+ import type { CSSProperties, ReactNode, SyntheticEvent } from 'react';
2
+ export type NewPopoverOnClose = (event: Event | SyntheticEvent) => void;
3
+ export type NewPopoverPlacement = 'auto' | 'auto-start' | 'auto-end' | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'right' | 'right-start' | 'right-end' | 'left' | 'left-start' | 'left-end';
4
+ type NewPopoverVirtualElement = {
5
+ getBoundingClientRect: () => DOMRect;
6
+ };
7
+ export type NewPopoverAnchorEl = null | NewPopoverVirtualElement | HTMLElement | (() => HTMLElement) | (() => NewPopoverVirtualElement);
8
+ export type NewPopoverProps = {
9
+ /**
10
+ * Якорь позиционирования
11
+ */
12
+ anchorEl?: NewPopoverAnchorEl;
13
+ /**
14
+ * Показывать popover. Если не передан — видимость определяется наличием anchorEl.
15
+ */
16
+ isOpen?: boolean;
17
+ /**
18
+ * Позиция относительно anchor
19
+ * @default 'bottom'
20
+ */
21
+ placement?: NewPopoverPlacement;
22
+ /**
23
+ * Контент popover
24
+ */
25
+ children?: ReactNode;
26
+ /**
27
+ * className корневого popper
28
+ */
29
+ className?: string;
30
+ /**
31
+ * style корневого popper
32
+ */
33
+ style?: CSSProperties;
34
+ /**
35
+ * Заголовок для отображения в мобильной версии
36
+ */
37
+ title?: string;
38
+ /**
39
+ * Обработчик закрытия компонента
40
+ */
41
+ onClose?: NewPopoverOnClose;
42
+ /**
43
+ * Рендерить popper внутри родителя, без portal
44
+ * @default false
45
+ */
46
+ disablePortal?: boolean;
47
+ /**
48
+ * Отключить автофокус при открытии popover (десктоп)
49
+ * @default false
50
+ */
51
+ disableAutoFocus?: boolean;
52
+ };
53
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from './useLogic';
@@ -0,0 +1 @@
1
+ export * from './useLogic';
@@ -0,0 +1,11 @@
1
+ import { type NewPopoverProps } from '../types';
2
+ type UseLogicParams = Pick<NewPopoverProps, 'isOpen' | 'anchorEl' | 'onClose' | 'disableAutoFocus'>;
3
+ export declare const useLogic: ({ isOpen, anchorEl, onClose, disableAutoFocus, }: UseLogicParams) => {
4
+ isMobile: boolean;
5
+ isOpened: boolean;
6
+ shouldRender: boolean;
7
+ handleAnimationEnd: () => void;
8
+ handleClose: (event: Event) => void;
9
+ setContainerRef: (node: HTMLDivElement | null) => void;
10
+ };
11
+ export {};
@@ -0,0 +1,54 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { useClickAwayEffect } from '../../useClickAwayEffect';
3
+ import { useEscapeClickEffect } from '../../useEscapeClickEffect';
4
+ import { useFocusTrapEffect } from '../../useFocusTrapEffect';
5
+ import { usePopoverAnimation } from '../../usePopoverAnimation';
6
+ import { useViewportType } from '../../useViewportType';
7
+ import { resolveAnchorNode } from '../utils';
8
+ export const useLogic = ({ isOpen, anchorEl, onClose, disableAutoFocus, }) => {
9
+ const containerRef = useRef(null);
10
+ const [isContainerMounted, setIsContainerMounted] = useState(false);
11
+ const setContainerRef = useCallback((node) => {
12
+ containerRef.current = node;
13
+ setIsContainerMounted(Boolean(node));
14
+ }, []);
15
+ const isOpened = isOpen ?? Boolean(anchorEl);
16
+ const isControlled = isOpen !== undefined;
17
+ const { isMobile } = useViewportType();
18
+ const { shouldRender, handleAnimationEnd } = usePopoverAnimation(isOpened);
19
+ const isEffectActive = !isMobile && isOpened && isContainerMounted;
20
+ const anchorNode = useMemo(() => resolveAnchorNode(anchorEl), [anchorEl]);
21
+ const excludeNodes = useMemo(() => [anchorNode], [anchorNode]);
22
+ const handleClose = (event) => {
23
+ onClose?.(event);
24
+ };
25
+ useClickAwayEffect({
26
+ ref: containerRef,
27
+ onClickAway: handleClose,
28
+ isActive: isEffectActive && !isControlled,
29
+ preventBubbling: true,
30
+ excludeNodes,
31
+ });
32
+ useEscapeClickEffect({
33
+ onEscape: handleClose,
34
+ isActive: isEffectActive,
35
+ preventBubbling: true,
36
+ });
37
+ useFocusTrapEffect({
38
+ ref: containerRef,
39
+ isActive: isEffectActive && !disableAutoFocus,
40
+ returnFocusOnDeactivate: true,
41
+ escapeDeactivates: false,
42
+ clickOutsideDeactivates: false,
43
+ allowOutsideClick: true,
44
+ fallbackFocus: () => containerRef.current || document.body,
45
+ });
46
+ return {
47
+ isMobile,
48
+ isOpened,
49
+ shouldRender,
50
+ handleAnimationEnd,
51
+ handleClose,
52
+ setContainerRef,
53
+ };
54
+ };
@@ -0,0 +1 @@
1
+ export * from './resolveAnchorNode';
@@ -0,0 +1 @@
1
+ export * from './resolveAnchorNode';
@@ -0,0 +1,6 @@
1
+ import { type NewPopoverAnchorEl } from '../types';
2
+ /**
3
+ * Резолвит anchorEl в DOM-ноду, если это возможно.
4
+ * Для virtual-элементов вернёт null.
5
+ */
6
+ export declare const resolveAnchorNode: (anchorEl: NewPopoverAnchorEl | undefined) => Node | null;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Резолвит anchorEl в DOM-ноду, если это возможно.
3
+ * Для virtual-элементов вернёт null.
4
+ */
5
+ export const resolveAnchorNode = (anchorEl) => {
6
+ const value = typeof anchorEl === 'function' ? anchorEl() : anchorEl;
7
+ return value instanceof Node ? value : null;
8
+ };
@@ -1,9 +1,5 @@
1
- import type { PopoverProps as MuiPopoverProps } from '@mui/material/Popover';
2
- import { type WithoutEmotionSpecific } from '../types/WithoutEmotionSpecific';
3
- export type PopoverProps = WithoutEmotionSpecific<MuiPopoverProps> & {
4
- /**
5
- * Заголовок для отображения в мобильной версии
6
- */
7
- title?: string;
8
- };
1
+ import type { PopoverProps } from './types';
2
+ /**
3
+ * @deprecated Причина отказа от поддержки: MUI Popover использует backdrop, который перехватывает первый внешний клик и создает UX-эффект "двойного клика". Используйте компонент `NewPopover`.
4
+ */
9
5
  export declare const Popover: ({ children, onClose, open, title, className, ...restProps }: PopoverProps) => import("react/jsx-runtime").JSX.Element;
@@ -2,6 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { BottomDrawer } from '../BottomDrawer';
3
3
  import { useViewportType } from '../useViewportType';
4
4
  import { StyledMuiPopover } from './styles';
5
+ /**
6
+ * @deprecated Причина отказа от поддержки: MUI Popover использует backdrop, который перехватывает первый внешний клик и создает UX-эффект "двойного клика". Используйте компонент `NewPopover`.
7
+ */
5
8
  export const Popover = ({ children, onClose, open, title, className, ...restProps }) => {
6
9
  const { isMobile } = useViewportType();
7
10
  if (isMobile) {
@@ -1 +1,2 @@
1
1
  export * from './Popover';
2
+ export * from './types';
@@ -1 +1,2 @@
1
1
  export * from './Popover';
2
+ export * from './types';
@@ -1 +1,2 @@
1
- export * from './Popover';
1
+ export { Popover } from './Popover';
2
+ export type { PopoverProps } from './types';
@@ -1 +1 @@
1
- export * from './Popover';
1
+ export { Popover } from './Popover';
@@ -0,0 +1,8 @@
1
+ import type { PopoverProps as MuiPopoverProps } from '@mui/material/Popover';
2
+ import { type WithoutEmotionSpecific } from '../types/WithoutEmotionSpecific';
3
+ export type PopoverProps = WithoutEmotionSpecific<MuiPopoverProps> & {
4
+ /**
5
+ * Заголовок для отображения в мобильной версии
6
+ */
7
+ title?: string;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -20,7 +20,6 @@ export * from './Checkbox';
20
20
  export * from './CheckboxField';
21
21
  export * from './Chevron';
22
22
  export * from './ClickAwayListener';
23
- export * from './ClickAwayListener';
24
23
  export { CodeField, type CodeFieldProps } from './CodeField';
25
24
  export * from './CollapsableAlert';
26
25
  export * from './Collapse';
@@ -20,7 +20,6 @@ export * from './Checkbox';
20
20
  export * from './CheckboxField';
21
21
  export * from './Chevron';
22
22
  export * from './ClickAwayListener';
23
- export * from './ClickAwayListener';
24
23
  export { CodeField } from './CodeField';
25
24
  export * from './CollapsableAlert';
26
25
  export * from './Collapse';
@@ -1,5 +1,6 @@
1
1
  import { type RefObject } from 'react';
2
2
  import { type CloseEventReason } from '../types/CloseEventReason';
3
+ export type ExcludedNode = Node | RefObject<Node | null> | null | undefined;
3
4
  export type UseClickAwayListenerOptions = {
4
5
  /**
5
6
  * реф на дом ноду, клик вне которой надо отслеживать
@@ -17,9 +18,13 @@ export type UseClickAwayListenerOptions = {
17
18
  * флаг необходимости предотвращать всплытие, подойдет когда используется внутри модалки
18
19
  */
19
20
  preventBubbling?: boolean;
21
+ /**
22
+ * Ноды (или рефы), клик по которым не приводит к срабатыванию onClickAway
23
+ */
24
+ excludeNodes?: ExcludedNode[];
20
25
  };
21
26
  /**
22
27
  * хук позволяющий подписаться на клик вне указанного рефа,
23
28
  * подойдет для использования в кастомных попперах
24
29
  */
25
- export declare const useClickAwayEffect: ({ ref, onClickAway, preventBubbling, isActive, }: UseClickAwayListenerOptions) => void;
30
+ export declare const useClickAwayEffect: ({ ref, onClickAway, preventBubbling, isActive, excludeNodes, }: UseClickAwayListenerOptions) => void;
@@ -1,26 +1,36 @@
1
1
  import { useEffect } from 'react';
2
+ const resolveNode = (item) => {
3
+ if (!item) {
4
+ return null;
5
+ }
6
+ return item instanceof Node ? item : item.current;
7
+ };
2
8
  /**
3
9
  * хук позволяющий подписаться на клик вне указанного рефа,
4
10
  * подойдет для использования в кастомных попперах
5
11
  */
6
- export const useClickAwayEffect = ({ ref, onClickAway, preventBubbling, isActive, }) => {
12
+ export const useClickAwayEffect = ({ ref, onClickAway, preventBubbling, isActive, excludeNodes, }) => {
7
13
  useEffect(() => {
8
14
  const node = ref?.current;
9
15
  if (!isActive || !node) {
10
16
  return;
11
17
  }
12
- const onClick = (e) => {
13
- if (!node.contains(e.target)) {
14
- if (preventBubbling) {
15
- e.stopPropagation();
16
- e.stopImmediatePropagation();
17
- }
18
- onClickAway(e, 'clickAway');
18
+ const onClick = (event) => {
19
+ const target = event.target;
20
+ const isClickInside = node.contains(target);
21
+ const isClickOnExcluded = excludeNodes?.some((item) => resolveNode(item)?.contains(target));
22
+ const isClickAway = !isClickInside && !isClickOnExcluded;
23
+ if (preventBubbling) {
24
+ event.stopPropagation();
25
+ event.stopImmediatePropagation();
26
+ }
27
+ if (isClickAway) {
28
+ onClickAway(event, 'clickAway');
19
29
  }
20
30
  };
21
31
  window.addEventListener('pointerdown', onClick);
22
32
  return () => {
23
33
  window.removeEventListener('pointerdown', onClick);
24
34
  };
25
- }, [isActive, ref]);
35
+ }, [isActive, ref, excludeNodes, onClickAway, preventBubbling]);
26
36
  };
@@ -0,0 +1 @@
1
+ export * from './useFocusTrapEffect';
@@ -0,0 +1 @@
1
+ export * from './useFocusTrapEffect';
@@ -0,0 +1,17 @@
1
+ import { type Options } from 'focus-trap';
2
+ import { type RefObject } from 'react';
3
+ export type UseFocusTrapEffectOptions = {
4
+ /**
5
+ * Реф на DOM-ноду контейнера ловушки фокуса
6
+ */
7
+ ref: RefObject<HTMLElement | null>;
8
+ /**
9
+ * Флаг активности ловушки
10
+ */
11
+ isActive: boolean;
12
+ } & Pick<Options, 'initialFocus' | 'fallbackFocus' | 'escapeDeactivates' | 'clickOutsideDeactivates' | 'returnFocusOnDeactivate' | 'allowOutsideClick' | 'preventScroll' | 'delayInitialFocus'>;
13
+ /**
14
+ * Хук для удержания фокуса внутри указанного контейнера.
15
+ * Опции соответствуют конфигурации focus-trap.
16
+ */
17
+ export declare const useFocusTrapEffect: ({ ref, isActive, ...options }: UseFocusTrapEffectOptions) => void;
@@ -0,0 +1,24 @@
1
+ import { createFocusTrap } from 'focus-trap';
2
+ import { useEffect, useRef } from 'react';
3
+ /**
4
+ * Хук для удержания фокуса внутри указанного контейнера.
5
+ * Опции соответствуют конфигурации focus-trap.
6
+ */
7
+ export const useFocusTrapEffect = ({ ref, isActive, ...options }) => {
8
+ const trapRef = useRef(null);
9
+ useEffect(() => {
10
+ const node = ref.current;
11
+ if (!isActive || !node) {
12
+ trapRef.current?.deactivate();
13
+ trapRef.current = null;
14
+ return;
15
+ }
16
+ const trap = createFocusTrap(node, options);
17
+ trapRef.current = trap;
18
+ trap.activate();
19
+ return () => {
20
+ trap.deactivate();
21
+ trapRef.current = null;
22
+ };
23
+ }, [isActive, ref]);
24
+ };
@@ -0,0 +1,2 @@
1
+ import { type NewPopoverProps } from './types';
2
+ export declare const NewPopover: ({ children, onClose, isOpen, title, className, style, placement, anchorEl, disablePortal, disableAutoFocus, }: NewPopoverProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NewPopover = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const BottomDrawer_1 = require("../BottomDrawer");
6
+ const classNames_1 = require("../utils/classNames");
7
+ const cva_1 = require("../utils/cva");
8
+ const constants_1 = require("./constants");
9
+ const styles_1 = require("./styles");
10
+ const useLogic_1 = require("./useLogic");
11
+ const animatedWrapperCva = (0, cva_1.cva)(constants_1.popoverClassnames.animatedWrapper, {
12
+ variants: {
13
+ isOpen: {
14
+ true: constants_1.popoverClassnames.animatedWrapperOpen,
15
+ },
16
+ },
17
+ });
18
+ const NewPopover = ({ children, onClose, isOpen, title, className, style, placement = 'bottom', anchorEl, disablePortal, disableAutoFocus, }) => {
19
+ const { isMobile, isOpened, shouldRender, handleAnimationEnd, handleClose, setContainerRef, } = (0, useLogic_1.useLogic)({ isOpen, anchorEl, onClose, disableAutoFocus });
20
+ if (isMobile) {
21
+ return ((0, jsx_runtime_1.jsx)(BottomDrawer_1.BottomDrawer, { title: title, onClose: handleClose, open: isOpened, className: className, style: style, ModalProps: {
22
+ disableAutoFocus,
23
+ }, children: children }));
24
+ }
25
+ return ((0, jsx_runtime_1.jsx)(styles_1.StyledMuiPopper, { className: constants_1.popoverClassnames.root, open: shouldRender, placement: placement, anchorEl: anchorEl, disablePortal: disablePortal, children: (0, jsx_runtime_1.jsx)(styles_1.AnimatedWrapper, { className: (0, classNames_1.classNames)(className, animatedWrapperCva({ isOpen: isOpened })), style: style, onAnimationEnd: handleAnimationEnd, children: (0, jsx_runtime_1.jsx)(styles_1.InnerContainer, { ref: setContainerRef, tabIndex: -1, className: constants_1.popoverClassnames.innerContainer, children: children }) }) }));
26
+ };
27
+ exports.NewPopover = NewPopover;
@@ -0,0 +1,6 @@
1
+ export declare const popoverClassnames: {
2
+ root: string;
3
+ innerContainer: string;
4
+ animatedWrapper: string;
5
+ animatedWrapperOpen: string;
6
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.popoverClassnames = void 0;
4
+ const createUIKitClassname_1 = require("../utils/createUIKitClassname");
5
+ exports.popoverClassnames = {
6
+ root: (0, createUIKitClassname_1.createUIKitClassname)('popover'),
7
+ innerContainer: (0, createUIKitClassname_1.createUIKitClassname)('popover__inner-container'),
8
+ animatedWrapper: (0, createUIKitClassname_1.createUIKitClassname)('popover__animated-wrapper'),
9
+ animatedWrapperOpen: (0, createUIKitClassname_1.createUIKitClassname)('popover__animated-wrapper_is-open'),
10
+ };
@@ -0,0 +1,2 @@
1
+ export { NewPopover } from './NewPopover';
2
+ export type { NewPopoverProps } from './types';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NewPopover = void 0;
4
+ var NewPopover_1 = require("./NewPopover");
5
+ Object.defineProperty(exports, "NewPopover", { enumerable: true, get: function () { return NewPopover_1.NewPopover; } });
@@ -0,0 +1,2 @@
1
+ export { NewPopover } from './NewPopover';
2
+ export type { NewPopoverProps } from './types';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NewPopover = void 0;
4
+ var NewPopover_1 = require("./NewPopover");
5
+ Object.defineProperty(exports, "NewPopover", { enumerable: true, get: function () { return NewPopover_1.NewPopover; } });
@@ -0,0 +1,12 @@
1
+ /// <reference types="react" />
2
+ export declare const StyledMuiPopper: import("@emotion/styled/dist/declarations/src/types").StyledComponent<import("@mui/material/Popper").PopperProps & import("react").RefAttributes<HTMLDivElement> & {
3
+ theme?: import("@emotion/react").Theme | undefined;
4
+ }, {}, {}>;
5
+ export declare const AnimatedWrapper: import("@emotion/styled/dist/declarations/src/types").StyledComponent<{
6
+ theme?: import("@emotion/react").Theme | undefined;
7
+ as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
8
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
9
+ export declare const InnerContainer: import("@emotion/styled/dist/declarations/src/types").StyledComponent<{
10
+ theme?: import("@emotion/react").Theme | undefined;
11
+ as?: import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> | undefined;
12
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InnerContainer = exports.AnimatedWrapper = exports.StyledMuiPopper = void 0;
7
+ const Popper_1 = __importDefault(require("@mui/material/Popper"));
8
+ const styled_1 = require("../styled");
9
+ const constants_1 = require("./constants");
10
+ exports.StyledMuiPopper = (0, styled_1.styled)(Popper_1.default) `
11
+ &.${constants_1.popoverClassnames.root} {
12
+ z-index: ${({ theme }) => theme.zIndex.tooltip};
13
+ }
14
+
15
+ &&[data-popper-placement*='top'] .${constants_1.popoverClassnames.animatedWrapper} {
16
+ transform-origin: bottom center;
17
+
18
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
19
+ }
20
+
21
+ &&[data-popper-placement*='bottom'] .${constants_1.popoverClassnames.animatedWrapper} {
22
+ transform-origin: top center;
23
+
24
+ margin-top: ${({ theme }) => theme.spacing(2)};
25
+ }
26
+
27
+ &&[data-popper-placement*='left'] .${constants_1.popoverClassnames.animatedWrapper} {
28
+ transform-origin: right center;
29
+
30
+ margin-right: ${({ theme }) => theme.spacing(2)};
31
+ }
32
+
33
+ &&[data-popper-placement*='right'] .${constants_1.popoverClassnames.animatedWrapper} {
34
+ transform-origin: left center;
35
+
36
+ margin-left: ${({ theme }) => theme.spacing(2)};
37
+ }
38
+ `;
39
+ exports.AnimatedWrapper = styled_1.styled.div `
40
+ animation: popover-grow-out
41
+ ${({ theme }) => theme.transitions.duration.enteringScreen}ms
42
+ ${({ theme }) => theme.transitions.easing.easeInOut} forwards;
43
+
44
+ &.${constants_1.popoverClassnames.animatedWrapperOpen} {
45
+ animation: popover-grow-in
46
+ ${({ theme }) => theme.transitions.duration.enteringScreen}ms
47
+ ${({ theme }) => theme.transitions.easing.easeInOut} forwards;
48
+ }
49
+
50
+ @keyframes popover-grow-in {
51
+ from {
52
+ transform: scale(0.75, 0.5625);
53
+
54
+ opacity: 0;
55
+ }
56
+
57
+ to {
58
+ transform: none;
59
+
60
+ opacity: 1;
61
+ }
62
+ }
63
+
64
+ @keyframes popover-grow-out {
65
+ from {
66
+ transform: none;
67
+
68
+ opacity: 1;
69
+ }
70
+
71
+ to {
72
+ transform: scale(0.75, 0.5625);
73
+
74
+ opacity: 0;
75
+ }
76
+ }
77
+ `;
78
+ exports.InnerContainer = styled_1.styled.div `
79
+ &.${constants_1.popoverClassnames.innerContainer} {
80
+ overflow: hidden;
81
+
82
+ background: ${({ theme }) => theme.palette.background.paper};
83
+ border-radius: ${({ theme }) => theme.shape.medium};
84
+ box-shadow: ${({ theme }) => theme.elevation[200]};
85
+ }
86
+ `;
@@ -0,0 +1,53 @@
1
+ import type { CSSProperties, ReactNode, SyntheticEvent } from 'react';
2
+ export type NewPopoverOnClose = (event: Event | SyntheticEvent) => void;
3
+ export type NewPopoverPlacement = 'auto' | 'auto-start' | 'auto-end' | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'right' | 'right-start' | 'right-end' | 'left' | 'left-start' | 'left-end';
4
+ type NewPopoverVirtualElement = {
5
+ getBoundingClientRect: () => DOMRect;
6
+ };
7
+ export type NewPopoverAnchorEl = null | NewPopoverVirtualElement | HTMLElement | (() => HTMLElement) | (() => NewPopoverVirtualElement);
8
+ export type NewPopoverProps = {
9
+ /**
10
+ * Якорь позиционирования
11
+ */
12
+ anchorEl?: NewPopoverAnchorEl;
13
+ /**
14
+ * Показывать popover. Если не передан — видимость определяется наличием anchorEl.
15
+ */
16
+ isOpen?: boolean;
17
+ /**
18
+ * Позиция относительно anchor
19
+ * @default 'bottom'
20
+ */
21
+ placement?: NewPopoverPlacement;
22
+ /**
23
+ * Контент popover
24
+ */
25
+ children?: ReactNode;
26
+ /**
27
+ * className корневого popper
28
+ */
29
+ className?: string;
30
+ /**
31
+ * style корневого popper
32
+ */
33
+ style?: CSSProperties;
34
+ /**
35
+ * Заголовок для отображения в мобильной версии
36
+ */
37
+ title?: string;
38
+ /**
39
+ * Обработчик закрытия компонента
40
+ */
41
+ onClose?: NewPopoverOnClose;
42
+ /**
43
+ * Рендерить popper внутри родителя, без portal
44
+ * @default false
45
+ */
46
+ disablePortal?: boolean;
47
+ /**
48
+ * Отключить автофокус при открытии popover (десктоп)
49
+ * @default false
50
+ */
51
+ disableAutoFocus?: boolean;
52
+ };
53
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export * from './useLogic';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./useLogic"), exports);
@@ -0,0 +1,11 @@
1
+ import { type NewPopoverProps } from '../types';
2
+ type UseLogicParams = Pick<NewPopoverProps, 'isOpen' | 'anchorEl' | 'onClose' | 'disableAutoFocus'>;
3
+ export declare const useLogic: ({ isOpen, anchorEl, onClose, disableAutoFocus, }: UseLogicParams) => {
4
+ isMobile: boolean;
5
+ isOpened: boolean;
6
+ shouldRender: boolean;
7
+ handleAnimationEnd: () => void;
8
+ handleClose: (event: Event) => void;
9
+ setContainerRef: (node: HTMLDivElement | null) => void;
10
+ };
11
+ export {};
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useLogic = void 0;
4
+ const react_1 = require("react");
5
+ const useClickAwayEffect_1 = require("../../useClickAwayEffect");
6
+ const useEscapeClickEffect_1 = require("../../useEscapeClickEffect");
7
+ const useFocusTrapEffect_1 = require("../../useFocusTrapEffect");
8
+ const usePopoverAnimation_1 = require("../../usePopoverAnimation");
9
+ const useViewportType_1 = require("../../useViewportType");
10
+ const utils_1 = require("../utils");
11
+ const useLogic = ({ isOpen, anchorEl, onClose, disableAutoFocus, }) => {
12
+ const containerRef = (0, react_1.useRef)(null);
13
+ const [isContainerMounted, setIsContainerMounted] = (0, react_1.useState)(false);
14
+ const setContainerRef = (0, react_1.useCallback)((node) => {
15
+ containerRef.current = node;
16
+ setIsContainerMounted(Boolean(node));
17
+ }, []);
18
+ const isOpened = isOpen ?? Boolean(anchorEl);
19
+ const isControlled = isOpen !== undefined;
20
+ const { isMobile } = (0, useViewportType_1.useViewportType)();
21
+ const { shouldRender, handleAnimationEnd } = (0, usePopoverAnimation_1.usePopoverAnimation)(isOpened);
22
+ const isEffectActive = !isMobile && isOpened && isContainerMounted;
23
+ const anchorNode = (0, react_1.useMemo)(() => (0, utils_1.resolveAnchorNode)(anchorEl), [anchorEl]);
24
+ const excludeNodes = (0, react_1.useMemo)(() => [anchorNode], [anchorNode]);
25
+ const handleClose = (event) => {
26
+ onClose?.(event);
27
+ };
28
+ (0, useClickAwayEffect_1.useClickAwayEffect)({
29
+ ref: containerRef,
30
+ onClickAway: handleClose,
31
+ isActive: isEffectActive && !isControlled,
32
+ preventBubbling: true,
33
+ excludeNodes,
34
+ });
35
+ (0, useEscapeClickEffect_1.useEscapeClickEffect)({
36
+ onEscape: handleClose,
37
+ isActive: isEffectActive,
38
+ preventBubbling: true,
39
+ });
40
+ (0, useFocusTrapEffect_1.useFocusTrapEffect)({
41
+ ref: containerRef,
42
+ isActive: isEffectActive && !disableAutoFocus,
43
+ returnFocusOnDeactivate: true,
44
+ escapeDeactivates: false,
45
+ clickOutsideDeactivates: false,
46
+ allowOutsideClick: true,
47
+ fallbackFocus: () => containerRef.current || document.body,
48
+ });
49
+ return {
50
+ isMobile,
51
+ isOpened,
52
+ shouldRender,
53
+ handleAnimationEnd,
54
+ handleClose,
55
+ setContainerRef,
56
+ };
57
+ };
58
+ exports.useLogic = useLogic;
@@ -0,0 +1 @@
1
+ export * from './resolveAnchorNode';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./resolveAnchorNode"), exports);
@@ -0,0 +1,6 @@
1
+ import { type NewPopoverAnchorEl } from '../types';
2
+ /**
3
+ * Резолвит anchorEl в DOM-ноду, если это возможно.
4
+ * Для virtual-элементов вернёт null.
5
+ */
6
+ export declare const resolveAnchorNode: (anchorEl: NewPopoverAnchorEl | undefined) => Node | null;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAnchorNode = void 0;
4
+ /**
5
+ * Резолвит anchorEl в DOM-ноду, если это возможно.
6
+ * Для virtual-элементов вернёт null.
7
+ */
8
+ const resolveAnchorNode = (anchorEl) => {
9
+ const value = typeof anchorEl === 'function' ? anchorEl() : anchorEl;
10
+ return value instanceof Node ? value : null;
11
+ };
12
+ exports.resolveAnchorNode = resolveAnchorNode;
@@ -1,9 +1,5 @@
1
- import type { PopoverProps as MuiPopoverProps } from '@mui/material/Popover';
2
- import { type WithoutEmotionSpecific } from '../types/WithoutEmotionSpecific';
3
- export type PopoverProps = WithoutEmotionSpecific<MuiPopoverProps> & {
4
- /**
5
- * Заголовок для отображения в мобильной версии
6
- */
7
- title?: string;
8
- };
1
+ import type { PopoverProps } from './types';
2
+ /**
3
+ * @deprecated Причина отказа от поддержки: MUI Popover использует backdrop, который перехватывает первый внешний клик и создает UX-эффект "двойного клика". Используйте компонент `NewPopover`.
4
+ */
9
5
  export declare const Popover: ({ children, onClose, open, title, className, ...restProps }: PopoverProps) => import("react/jsx-runtime").JSX.Element;
@@ -5,6 +5,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const BottomDrawer_1 = require("../BottomDrawer");
6
6
  const useViewportType_1 = require("../useViewportType");
7
7
  const styles_1 = require("./styles");
8
+ /**
9
+ * @deprecated Причина отказа от поддержки: MUI Popover использует backdrop, который перехватывает первый внешний клик и создает UX-эффект "двойного клика". Используйте компонент `NewPopover`.
10
+ */
8
11
  const Popover = ({ children, onClose, open, title, className, ...restProps }) => {
9
12
  const { isMobile } = (0, useViewportType_1.useViewportType)();
10
13
  if (isMobile) {
@@ -1 +1,2 @@
1
1
  export * from './Popover';
2
+ export * from './types';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./Popover"), exports);
18
+ __exportStar(require("./types"), exports);
@@ -1 +1,2 @@
1
- export * from './Popover';
1
+ export { Popover } from './Popover';
2
+ export type { PopoverProps } from './types';
@@ -1,17 +1,5 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
2
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./Popover"), exports);
3
+ exports.Popover = void 0;
4
+ var Popover_1 = require("./Popover");
5
+ Object.defineProperty(exports, "Popover", { enumerable: true, get: function () { return Popover_1.Popover; } });
@@ -0,0 +1,8 @@
1
+ import type { PopoverProps as MuiPopoverProps } from '@mui/material/Popover';
2
+ import { type WithoutEmotionSpecific } from '../types/WithoutEmotionSpecific';
3
+ export type PopoverProps = WithoutEmotionSpecific<MuiPopoverProps> & {
4
+ /**
5
+ * Заголовок для отображения в мобильной версии
6
+ */
7
+ title?: string;
8
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -20,7 +20,6 @@ export * from './Checkbox';
20
20
  export * from './CheckboxField';
21
21
  export * from './Chevron';
22
22
  export * from './ClickAwayListener';
23
- export * from './ClickAwayListener';
24
23
  export { CodeField, type CodeFieldProps } from './CodeField';
25
24
  export * from './CollapsableAlert';
26
25
  export * from './Collapse';
@@ -53,7 +53,6 @@ __exportStar(require("./Checkbox"), exports);
53
53
  __exportStar(require("./CheckboxField"), exports);
54
54
  __exportStar(require("./Chevron"), exports);
55
55
  __exportStar(require("./ClickAwayListener"), exports);
56
- __exportStar(require("./ClickAwayListener"), exports);
57
56
  var CodeField_1 = require("./CodeField");
58
57
  Object.defineProperty(exports, "CodeField", { enumerable: true, get: function () { return CodeField_1.CodeField; } });
59
58
  __exportStar(require("./CollapsableAlert"), exports);
@@ -1,5 +1,6 @@
1
1
  import { type RefObject } from 'react';
2
2
  import { type CloseEventReason } from '../types/CloseEventReason';
3
+ export type ExcludedNode = Node | RefObject<Node | null> | null | undefined;
3
4
  export type UseClickAwayListenerOptions = {
4
5
  /**
5
6
  * реф на дом ноду, клик вне которой надо отслеживать
@@ -17,9 +18,13 @@ export type UseClickAwayListenerOptions = {
17
18
  * флаг необходимости предотвращать всплытие, подойдет когда используется внутри модалки
18
19
  */
19
20
  preventBubbling?: boolean;
21
+ /**
22
+ * Ноды (или рефы), клик по которым не приводит к срабатыванию onClickAway
23
+ */
24
+ excludeNodes?: ExcludedNode[];
20
25
  };
21
26
  /**
22
27
  * хук позволяющий подписаться на клик вне указанного рефа,
23
28
  * подойдет для использования в кастомных попперах
24
29
  */
25
- export declare const useClickAwayEffect: ({ ref, onClickAway, preventBubbling, isActive, }: UseClickAwayListenerOptions) => void;
30
+ export declare const useClickAwayEffect: ({ ref, onClickAway, preventBubbling, isActive, excludeNodes, }: UseClickAwayListenerOptions) => void;
@@ -2,29 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useClickAwayEffect = void 0;
4
4
  const react_1 = require("react");
5
+ const resolveNode = (item) => {
6
+ if (!item) {
7
+ return null;
8
+ }
9
+ return item instanceof Node ? item : item.current;
10
+ };
5
11
  /**
6
12
  * хук позволяющий подписаться на клик вне указанного рефа,
7
13
  * подойдет для использования в кастомных попперах
8
14
  */
9
- const useClickAwayEffect = ({ ref, onClickAway, preventBubbling, isActive, }) => {
15
+ const useClickAwayEffect = ({ ref, onClickAway, preventBubbling, isActive, excludeNodes, }) => {
10
16
  (0, react_1.useEffect)(() => {
11
17
  const node = ref?.current;
12
18
  if (!isActive || !node) {
13
19
  return;
14
20
  }
15
- const onClick = (e) => {
16
- if (!node.contains(e.target)) {
17
- if (preventBubbling) {
18
- e.stopPropagation();
19
- e.stopImmediatePropagation();
20
- }
21
- onClickAway(e, 'clickAway');
21
+ const onClick = (event) => {
22
+ const target = event.target;
23
+ const isClickInside = node.contains(target);
24
+ const isClickOnExcluded = excludeNodes?.some((item) => resolveNode(item)?.contains(target));
25
+ const isClickAway = !isClickInside && !isClickOnExcluded;
26
+ if (preventBubbling) {
27
+ event.stopPropagation();
28
+ event.stopImmediatePropagation();
29
+ }
30
+ if (isClickAway) {
31
+ onClickAway(event, 'clickAway');
22
32
  }
23
33
  };
24
34
  window.addEventListener('pointerdown', onClick);
25
35
  return () => {
26
36
  window.removeEventListener('pointerdown', onClick);
27
37
  };
28
- }, [isActive, ref]);
38
+ }, [isActive, ref, excludeNodes, onClickAway, preventBubbling]);
29
39
  };
30
40
  exports.useClickAwayEffect = useClickAwayEffect;
@@ -0,0 +1 @@
1
+ export * from './useFocusTrapEffect';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./useFocusTrapEffect"), exports);
@@ -0,0 +1,17 @@
1
+ import { type Options } from 'focus-trap';
2
+ import { type RefObject } from 'react';
3
+ export type UseFocusTrapEffectOptions = {
4
+ /**
5
+ * Реф на DOM-ноду контейнера ловушки фокуса
6
+ */
7
+ ref: RefObject<HTMLElement | null>;
8
+ /**
9
+ * Флаг активности ловушки
10
+ */
11
+ isActive: boolean;
12
+ } & Pick<Options, 'initialFocus' | 'fallbackFocus' | 'escapeDeactivates' | 'clickOutsideDeactivates' | 'returnFocusOnDeactivate' | 'allowOutsideClick' | 'preventScroll' | 'delayInitialFocus'>;
13
+ /**
14
+ * Хук для удержания фокуса внутри указанного контейнера.
15
+ * Опции соответствуют конфигурации focus-trap.
16
+ */
17
+ export declare const useFocusTrapEffect: ({ ref, isActive, ...options }: UseFocusTrapEffectOptions) => void;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFocusTrapEffect = void 0;
4
+ const focus_trap_1 = require("focus-trap");
5
+ const react_1 = require("react");
6
+ /**
7
+ * Хук для удержания фокуса внутри указанного контейнера.
8
+ * Опции соответствуют конфигурации focus-trap.
9
+ */
10
+ const useFocusTrapEffect = ({ ref, isActive, ...options }) => {
11
+ const trapRef = (0, react_1.useRef)(null);
12
+ (0, react_1.useEffect)(() => {
13
+ const node = ref.current;
14
+ if (!isActive || !node) {
15
+ trapRef.current?.deactivate();
16
+ trapRef.current = null;
17
+ return;
18
+ }
19
+ const trap = (0, focus_trap_1.createFocusTrap)(node, options);
20
+ trapRef.current = trap;
21
+ trap.activate();
22
+ return () => {
23
+ trap.deactivate();
24
+ trapRef.current = null;
25
+ };
26
+ }, [isActive, ref]);
27
+ };
28
+ exports.useFocusTrapEffect = useFocusTrapEffect;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astral/ui",
3
- "version": "4.66.0",
3
+ "version": "4.67.0",
4
4
  "browser": "./index.js",
5
5
  "main": "./node/index.js",
6
6
  "dependencies": {
@@ -23,7 +23,8 @@
23
23
  "react-virtuoso": "4.18.1",
24
24
  "remeda": "^1.61.0",
25
25
  "react-pdf": "9.2.1",
26
- "react-hook-form": "^7.66.1"
26
+ "react-hook-form": "^7.66.1",
27
+ "focus-trap": "^7.8.0"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",