@hh.ru/magritte-ui-modal 10.1.35 → 11.0.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 (65) hide show
  1. package/index.css +185 -191
  2. package/index.d.ts +5 -2
  3. package/index.js +25 -14
  4. package/index.js.map +1 -1
  5. package/index.mock.d.ts +6 -2
  6. package/index.mock.js +7 -3
  7. package/index.mock.js.map +1 -1
  8. package/internal/ModalBody.d.ts +5 -0
  9. package/internal/ModalBody.js +41 -0
  10. package/internal/ModalBody.js.map +1 -0
  11. package/internal/ModalContent.d.ts +2 -0
  12. package/internal/ModalContent.js +14 -0
  13. package/internal/ModalContent.js.map +1 -0
  14. package/internal/ModalInner.d.ts +28 -0
  15. package/internal/ModalInner.js +46 -0
  16. package/internal/ModalInner.js.map +1 -0
  17. package/internal/ModalSlotsContext.d.ts +46 -0
  18. package/internal/ModalSlotsContext.js +66 -0
  19. package/internal/ModalSlotsContext.js.map +1 -0
  20. package/internal/ModalStickyHeader.d.ts +8 -0
  21. package/internal/ModalStickyHeader.js +40 -0
  22. package/internal/ModalStickyHeader.js.map +1 -0
  23. package/internal/constants.d.ts +38 -0
  24. package/internal/constants.js +53 -0
  25. package/internal/constants.js.map +1 -0
  26. package/internal/isTreeSelectorChild.d.ts +7 -0
  27. package/internal/isTreeSelectorChild.js +13 -0
  28. package/internal/isTreeSelectorChild.js.map +1 -0
  29. package/{useModalOrder.js → internal/useModalOrder.js} +1 -1
  30. package/internal/useModalOrder.js.map +1 -0
  31. package/internal/useModalTransition.d.ts +29 -0
  32. package/internal/useModalTransition.js +85 -0
  33. package/internal/useModalTransition.js.map +1 -0
  34. package/internal/usePageHeaderScrollAnimation.d.ts +14 -0
  35. package/internal/usePageHeaderScrollAnimation.js +48 -0
  36. package/internal/usePageHeaderScrollAnimation.js.map +1 -0
  37. package/modal-Bf_KXESS.js +5 -0
  38. package/modal-Bf_KXESS.js.map +1 -0
  39. package/package.json +12 -8
  40. package/public/Modal.d.ts +10 -0
  41. package/public/Modal.js +105 -0
  42. package/public/Modal.js.map +1 -0
  43. package/public/ModalMedia.d.ts +15 -0
  44. package/public/ModalMedia.js +23 -0
  45. package/public/ModalMedia.js.map +1 -0
  46. package/public/ModalWithImage.d.ts +16 -0
  47. package/public/ModalWithImage.js +56 -0
  48. package/public/ModalWithImage.js.map +1 -0
  49. package/public/types.d.ts +40 -0
  50. package/{types.js → public/types.js} +1 -1
  51. package/Modal.d.ts +0 -3
  52. package/Modal.js +0 -271
  53. package/Modal.js.map +0 -1
  54. package/ModalContentWithHeader-DI3La66k.js +0 -150
  55. package/ModalContentWithHeader-DI3La66k.js.map +0 -1
  56. package/ModalContentWithHeader.d.ts +0 -8
  57. package/ModalContentWithHeader.js +0 -15
  58. package/ModalContentWithHeader.js.map +0 -1
  59. package/ModalHeader.d.ts +0 -6
  60. package/ModalHeader.js +0 -48
  61. package/ModalHeader.js.map +0 -1
  62. package/types.d.ts +0 -71
  63. package/useModalOrder.js.map +0 -1
  64. /package/{useModalOrder.d.ts → internal/useModalOrder.d.ts} +0 -0
  65. /package/{types.js.map → public/types.js.map} +0 -0
package/index.mock.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import './index.css';
2
2
  import { mockComponent } from '@hh.ru/magritte-ui-mock-component';
3
3
 
4
- const Modal = mockComponent('Modal', undefined, {
5
- withChildren: true,
4
+ const Media = mockComponent('Modal.Media', undefined, { withChildren: true });
5
+ const Modal = Object.assign(mockComponent('Modal', undefined, { withChildren: true }), {
6
+ Media,
7
+ });
8
+ const ModalWithImage = Object.assign(mockComponent('ModalWithImage', undefined, { withChildren: true }), {
9
+ Media,
6
10
  });
7
11
 
8
- export { Modal };
12
+ export { Modal, ModalWithImage };
9
13
  //# sourceMappingURL=index.mock.js.map
package/index.mock.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mock.js","sources":["src/index.mock.ts"],"sourcesContent":["import { ForwardRefExoticComponent } from 'react';\n\nimport { mockComponent } from '@hh.ru/magritte-ui-mock-component';\n\nexport const Modal: ForwardRefExoticComponent<Record<string, unknown>> = mockComponent('Modal', undefined, {\n withChildren: true,\n});\n"],"names":[],"mappings":";;MAIa,KAAK,GAAuD,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE;AACvG,IAAA,YAAY,EAAE,IAAI;AACrB,CAAA;;;;"}
1
+ {"version":3,"file":"index.mock.js","sources":["src/index.mock.ts"],"sourcesContent":["import { mockComponent } from '@hh.ru/magritte-ui-mock-component';\n\nconst Media = mockComponent('Modal.Media', undefined, { withChildren: true });\n\nexport const Modal = Object.assign(mockComponent('Modal', undefined, { withChildren: true }), {\n Media,\n});\n\nexport const ModalWithImage = Object.assign(mockComponent('ModalWithImage', undefined, { withChildren: true }), {\n Media,\n});\n"],"names":[],"mappings":";;AAEA,MAAM,KAAK,GAAG,aAAa,CAAC,aAAa,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;MAEjE,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE;IAC1F,KAAK;AACR,CAAA,EAAE;MAEU,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE;IAC5G,KAAK;AACR,CAAA;;;;"}
@@ -0,0 +1,5 @@
1
+ import { type FC, type PropsWithChildren, type ReactNode, type RefCallback } from 'react';
2
+ export declare const ModalBody: FC<PropsWithChildren<{
3
+ header: ReactNode;
4
+ scrollContainerRef?: RefCallback<HTMLDivElement>;
5
+ }>>;
@@ -0,0 +1,41 @@
1
+ import './../index.css';
2
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
+ import { useRef, useMemo } from 'react';
4
+ import classnames from 'classnames';
5
+ import { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';
6
+ import { useContentOverlay, ContentOverlayRoot } from '@hh.ru/magritte-ui-content-overlay';
7
+ import { useRegisterTopOverlayCandidate, TOP_OVERLAY_PRIORITY, useResolvedTopOverlayElement } from './ModalSlotsContext.js';
8
+ import { ModalStickyHeader } from './ModalStickyHeader.js';
9
+ import { useScrollbar, Scrollbar } from '@hh.ru/magritte-ui-scrollbar';
10
+ import { s as styles } from '../modal-Bf_KXESS.js';
11
+ import '@hh.ru/magritte-internal-slot-registry';
12
+ import '@hh.ru/magritte-ui-nav-bar';
13
+
14
+ const ModalBody = ({ children, scrollContainerRef, header }) => {
15
+ const hasHeader = header != null;
16
+ const contentContainerRef = useRef(null);
17
+ const contentOverlay = useContentOverlay();
18
+ // Контейнер слота `top` тела модала — нужен только когда хедера (`header`) нет: тогда верхний оверлей
19
+ // живёт в начале скролл-контента. Если хедер есть — контейнер предоставляет он (навбар или его запасной
20
+ // div), и тело своего кандидата не регистрирует. Реф безопасно вызывать всегда: пока его div не
21
+ // отрендерен (`hasHeader`), нода не привязывается и регистрация не происходит.
22
+ const bodyFallbackRef = useRegisterTopOverlayCandidate(TOP_OVERLAY_PRIORITY.bodyFallback);
23
+ // Резолвим элемент-победитель и подменяем им слот `top` в контейнерах ContentOverlay (единственный
24
+ // писатель — без гонок и без затирания в null: кандидаты сосуществуют, выбирается максимум приоритета).
25
+ const resolvedTopElement = useResolvedTopOverlayElement();
26
+ const containers = useMemo(() => ({ ...contentOverlay, top: resolvedTopElement }), [contentOverlay, resolvedTopElement]);
27
+ // `offsetTop` скроллбара меряем от того же резолвнутого top-overlay элемента (bottomAttachment навбара /
28
+ // запасной div хедера). Он позиционируется по визуальному низу хедера и переживает overlay-навбар (когда
29
+ // его обёртка схлопывается в 0), поэтому работает надёжно независимо от того, как и где выставлен overlay
30
+ // — в отличие от flow-маркера, который уезжал бы вместе со схлопнутым навбаром.
31
+ const headerBottomRef = useMemo(() => ({ current: resolvedTopElement }), [resolvedTopElement]);
32
+ const { hasVerticalScroll, verticalScrollbarProps, ...scrollbar } = useScrollbar({
33
+ axis: 'vertical',
34
+ offsetTop: hasHeader ? headerBottomRef : 24,
35
+ });
36
+ const contentContainerRefCallback = useMultipleRefs(contentContainerRef, scrollbar.scrollContainerRef, scrollContainerRef);
37
+ return (jsx(Fragment, { children: jsx("div", { className: classnames(styles.modalContentWrapper, scrollbar.hoverContainerCssClass), children: jsx(ContentOverlayRoot, { containers: containers, children: jsxs(Fragment, { children: [jsxs("div", { className: classnames(styles.modalScrollContainer), ref: contentContainerRefCallback, "data-qa": "modal-content-scroll-container", children: [hasHeader ? (jsx(ModalStickyHeader, { children: header })) : (jsx("div", { ref: bodyFallbackRef, className: classnames(styles.contentOverlayTop, styles.static) })), children, jsx("div", { ref: contentOverlay.bottomRef, className: styles.contentOverlayBottom })] }), hasVerticalScroll && jsx(Scrollbar, { ...verticalScrollbarProps })] }) }) }) }));
38
+ };
39
+
40
+ export { ModalBody };
41
+ //# sourceMappingURL=ModalBody.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalBody.js","sources":["src/internal/ModalBody.tsx"],"sourcesContent":["import { type FC, type PropsWithChildren, type ReactNode, type RefCallback, useMemo, useRef } from 'react';\nimport classnames from 'classnames';\n\nimport { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';\nimport { ContentOverlayRoot, useContentOverlay } from '@hh.ru/magritte-ui-content-overlay';\nimport {\n TOP_OVERLAY_PRIORITY,\n useRegisterTopOverlayCandidate,\n useResolvedTopOverlayElement,\n} from '@hh.ru/magritte-ui-modal/internal/ModalSlotsContext';\nimport { ModalStickyHeader } from '@hh.ru/magritte-ui-modal/internal/ModalStickyHeader';\nimport { Scrollbar, useScrollbar } from '@hh.ru/magritte-ui-scrollbar';\n\nimport styles from './modal.less';\n\nexport const ModalBody: FC<\n PropsWithChildren<{\n header: ReactNode;\n scrollContainerRef?: RefCallback<HTMLDivElement>;\n }>\n> = ({ children, scrollContainerRef, header }) => {\n const hasHeader = header != null;\n const contentContainerRef = useRef<HTMLDivElement>(null);\n const contentOverlay = useContentOverlay();\n\n // Контейнер слота `top` тела модала — нужен только когда хедера (`header`) нет: тогда верхний оверлей\n // живёт в начале скролл-контента. Если хедер есть — контейнер предоставляет он (навбар или его запасной\n // div), и тело своего кандидата не регистрирует. Реф безопасно вызывать всегда: пока его div не\n // отрендерен (`hasHeader`), нода не привязывается и регистрация не происходит.\n const bodyFallbackRef = useRegisterTopOverlayCandidate(TOP_OVERLAY_PRIORITY.bodyFallback);\n\n // Резолвим элемент-победитель и подменяем им слот `top` в контейнерах ContentOverlay (единственный\n // писатель — без гонок и без затирания в null: кандидаты сосуществуют, выбирается максимум приоритета).\n const resolvedTopElement = useResolvedTopOverlayElement();\n const containers = useMemo(\n () => ({ ...contentOverlay, top: resolvedTopElement }),\n [contentOverlay, resolvedTopElement]\n );\n\n // `offsetTop` скроллбара меряем от того же резолвнутого top-overlay элемента (bottomAttachment навбара /\n // запасной div хедера). Он позиционируется по визуальному низу хедера и переживает overlay-навбар (когда\n // его обёртка схлопывается в 0), поэтому работает надёжно независимо от того, как и где выставлен overlay\n // — в отличие от flow-маркера, который уезжал бы вместе со схлопнутым навбаром.\n const headerBottomRef = useMemo(() => ({ current: resolvedTopElement }), [resolvedTopElement]);\n\n const { hasVerticalScroll, verticalScrollbarProps, ...scrollbar } = useScrollbar({\n axis: 'vertical',\n offsetTop: hasHeader ? headerBottomRef : 24,\n });\n const contentContainerRefCallback = useMultipleRefs(\n contentContainerRef,\n scrollbar.scrollContainerRef,\n scrollContainerRef\n );\n\n return (\n <>\n <div className={classnames(styles.modalContentWrapper, scrollbar.hoverContainerCssClass)}>\n <ContentOverlayRoot containers={containers}>\n <>\n <div\n className={classnames(styles.modalScrollContainer)}\n ref={contentContainerRefCallback}\n data-qa=\"modal-content-scroll-container\"\n >\n {/*\n * Хедер (sticky) либо запасной контейнер слота `top` тела — взаимоисключающе.\n * Когда хедер есть, контейнер слота `top` даёт он сам (навбар → запасной div хедера,\n * резолв по приоритету в реестре); когда хедера нет — его берёт на себя этот div тела.\n */}\n {hasHeader ? (\n <ModalStickyHeader>{header}</ModalStickyHeader>\n ) : (\n <div\n ref={bodyFallbackRef}\n className={classnames(styles.contentOverlayTop, styles.static)}\n />\n )}\n {children}\n <div ref={contentOverlay.bottomRef} className={styles.contentOverlayBottom} />\n </div>\n {hasVerticalScroll && <Scrollbar {...verticalScrollbarProps} />}\n </>\n </ContentOverlayRoot>\n </div>\n </>\n );\n};\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":";;;;;;;;;;;;AAeO,MAAM,SAAS,GAKlB,CAAC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAI;AAC7C,IAAA,MAAM,SAAS,GAAG,MAAM,IAAI,IAAI,CAAC;AACjC,IAAA,MAAM,mBAAmB,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;AACzD,IAAA,MAAM,cAAc,GAAG,iBAAiB,EAAE,CAAC;;;;;IAM3C,MAAM,eAAe,GAAG,8BAA8B,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC;;;AAI1F,IAAA,MAAM,kBAAkB,GAAG,4BAA4B,EAAE,CAAC;IAC1D,MAAM,UAAU,GAAG,OAAO,CACtB,OAAO,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE,kBAAkB,EAAE,CAAC,EACtD,CAAC,cAAc,EAAE,kBAAkB,CAAC,CACvC,CAAC;;;;;AAMF,IAAA,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAE/F,MAAM,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,GAAG,SAAS,EAAE,GAAG,YAAY,CAAC;AAC7E,QAAA,IAAI,EAAE,UAAU;QAChB,SAAS,EAAE,SAAS,GAAG,eAAe,GAAG,EAAE;AAC9C,KAAA,CAAC,CAAC;AACH,IAAA,MAAM,2BAA2B,GAAG,eAAe,CAC/C,mBAAmB,EACnB,SAAS,CAAC,kBAAkB,EAC5B,kBAAkB,CACrB,CAAC;IAEF,QACIA,0BACIA,GAAK,CAAA,KAAA,EAAA,EAAA,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,mBAAmB,EAAE,SAAS,CAAC,sBAAsB,CAAC,EAAA,QAAA,EACpFA,GAAC,CAAA,kBAAkB,IAAC,UAAU,EAAE,UAAU,EAAA,QAAA,EACtCC,IACI,CAAAC,QAAA,EAAA,EAAA,QAAA,EAAA,CAAAD,IAAA,CAAA,KAAA,EAAA,EACI,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,oBAAoB,CAAC,EAClD,GAAG,EAAE,2BAA2B,EAAA,SAAA,EACxB,gCAAgC,EAOvC,QAAA,EAAA,CAAA,SAAS,IACND,IAAC,iBAAiB,EAAA,EAAA,QAAA,EAAE,MAAM,EAAA,CAAqB,KAE/CA,aACI,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,GAChE,CACL,EACA,QAAQ,EACTA,GAAK,CAAA,KAAA,EAAA,EAAA,GAAG,EAAE,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,oBAAoB,EAAA,CAAI,CAC5E,EAAA,CAAA,EACL,iBAAiB,IAAIA,GAAA,CAAC,SAAS,EAAA,EAAA,GAAK,sBAAsB,EAAA,CAAI,IAChE,EACc,CAAA,EAAA,CACnB,EACP,CAAA,EACL;AACN;;;;"}
@@ -0,0 +1,2 @@
1
+ import { type FC, type PropsWithChildren } from 'react';
2
+ export declare const ModalContent: FC<PropsWithChildren>;
@@ -0,0 +1,14 @@
1
+ import './../index.css';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { isTreeSelectorChild } from './isTreeSelectorChild.js';
4
+ import { TextAreaGrowLimiter } from '@hh.ru/magritte-ui-textarea';
5
+ import { s as styles } from '../modal-Bf_KXESS.js';
6
+ import 'react';
7
+ import '@hh.ru/magritte-ui-tree-selector';
8
+
9
+ const ModalContent = ({ children }) => {
10
+ return isTreeSelectorChild(children) ? (jsx("div", { className: styles.treeSelectorContainer, children: children })) : (jsx(TextAreaGrowLimiter, { className: styles.growLimiter, children: children }));
11
+ };
12
+
13
+ export { ModalContent };
14
+ //# sourceMappingURL=ModalContent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalContent.js","sources":["src/internal/ModalContent.tsx"],"sourcesContent":["import { type FC, type PropsWithChildren } from 'react';\n\nimport { isTreeSelectorChild } from '@hh.ru/magritte-ui-modal/internal/isTreeSelectorChild';\nimport { TextAreaGrowLimiter } from '@hh.ru/magritte-ui-textarea';\n\nimport styles from './modal.less';\n\nexport const ModalContent: FC<PropsWithChildren> = ({ children }) => {\n return isTreeSelectorChild(children) ? (\n <div className={styles.treeSelectorContainer}>{children}</div>\n ) : (\n <TextAreaGrowLimiter className={styles.growLimiter}>{children}</TextAreaGrowLimiter>\n );\n};\n"],"names":["_jsx"],"mappings":";;;;;;;MAOa,YAAY,GAA0B,CAAC,EAAE,QAAQ,EAAE,KAAI;AAChE,IAAA,OAAO,mBAAmB,CAAC,QAAQ,CAAC,IAChCA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,qBAAqB,YAAG,QAAQ,EAAA,CAAO,KAE9DA,GAAA,CAAC,mBAAmB,EAAC,EAAA,SAAS,EAAE,MAAM,CAAC,WAAW,EAAA,QAAA,EAAG,QAAQ,EAAA,CAAuB,CACvF,CAAC;AACN;;;;"}
@@ -0,0 +1,28 @@
1
+ import { type FC, type HTMLAttributes, type ReactNode, type Ref } from 'react';
2
+ import { type ExplicitRegionProps } from '@hh.ru/magritte-internal-accessible-region';
3
+ import { type ModalProps } from '@hh.ru/magritte-ui-modal/public/types';
4
+ export interface ModalInnerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ explicitAria: ExplicitRegionProps;
6
+ ariaModal: boolean | undefined;
7
+ refCallback: Ref<HTMLDivElement>;
8
+ size: NonNullable<ModalProps['size']>;
9
+ height: ModalProps['height'];
10
+ minHeight: ModalProps['minHeight'];
11
+ disableVerticalPaddings: boolean;
12
+ disableHorizontalPaddings: boolean;
13
+ showOverlay: boolean;
14
+ header: ReactNode;
15
+ footer: ReactNode;
16
+ children: ReactNode;
17
+ }
18
+ /**
19
+ * Внутренний контейнер модала — сам `role="dialog"`-узел. Рендерится **под** `AccessibleRegionProvider`
20
+ * и `ModalSlotsProvider`, что и делает его отдельным компонентом:
21
+ * `useAccessibleRegionProps` видит заголовок/подзаголовок, зарегистрированные потомками
22
+ * (`NavBar.TitleContainer` в проп `header`), и выставляет `aria-labelledby` / `aria-describedby`
23
+ * (явные `aria-*` пропы при этом всё перекрывают).
24
+ *
25
+ * Футер приходит пропом и рендерится здесь — вне скролл-зоны, прижатым к низу. От его наличия зависят
26
+ * классы паддингов и нижний divider контента (его прячем, когда у `ActionBar` футера есть progress bar).
27
+ */
28
+ export declare const ModalInner: FC<ModalInnerProps>;
@@ -0,0 +1,46 @@
1
+ import './../index.css';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { useState, useMemo } from 'react';
4
+ import classnames from 'classnames';
5
+ import { useAccessibleRegionProps } from '@hh.ru/magritte-internal-accessible-region';
6
+ import { ActionBarDefaultPropsContext } from '@hh.ru/magritte-ui-action-bar';
7
+ import { ModalBody } from './ModalBody.js';
8
+ import { NavBar } from '@hh.ru/magritte-ui-nav-bar';
9
+ import { s as styles } from '../modal-Bf_KXESS.js';
10
+ import '@hh.ru/magritte-common-use-multiple-refs';
11
+ import '@hh.ru/magritte-ui-content-overlay';
12
+ import './ModalSlotsContext.js';
13
+ import '@hh.ru/magritte-internal-slot-registry';
14
+ import './ModalStickyHeader.js';
15
+ import '@hh.ru/magritte-ui-scrollbar';
16
+
17
+ const ACTION_BAR_DEFAULT_PROPS = { type: 'auto', padding: 24, showDivider: 'with-scroll' };
18
+ /**
19
+ * Внутренний контейнер модала — сам `role="dialog"`-узел. Рендерится **под** `AccessibleRegionProvider`
20
+ * и `ModalSlotsProvider`, что и делает его отдельным компонентом:
21
+ * `useAccessibleRegionProps` видит заголовок/подзаголовок, зарегистрированные потомками
22
+ * (`NavBar.TitleContainer` в проп `header`), и выставляет `aria-labelledby` / `aria-describedby`
23
+ * (явные `aria-*` пропы при этом всё перекрывают).
24
+ *
25
+ * Футер приходит пропом и рендерится здесь — вне скролл-зоны, прижатым к низу. От его наличия зависят
26
+ * классы паддингов и нижний divider контента (его прячем, когда у `ActionBar` футера есть progress bar).
27
+ */
28
+ const ModalInner = ({ explicitAria, ariaModal, refCallback, size, height, minHeight, disableVerticalPaddings, disableHorizontalPaddings, showOverlay, header, footer, children, ...rest }) => {
29
+ const aria = useAccessibleRegionProps(explicitAria);
30
+ const [scrollContainer, setScrollContainer] = useState(null);
31
+ const actionBarDefaultProps = useMemo(() => ({ ...ACTION_BAR_DEFAULT_PROPS, scrollContainer }), [scrollContainer]);
32
+ return (jsxs("div", { ...rest, ...aria, "aria-modal": ariaModal, role: "dialog", ref: refCallback, tabIndex: -1, className: classnames(styles.modal, {
33
+ [styles.sizeSmall]: size === 'small',
34
+ [styles.sizeMedium]: size === 'medium',
35
+ [styles.modalFullHeight]: height === 'full-screen',
36
+ [styles.noVerticalPaddings]: disableVerticalPaddings,
37
+ [styles.noHorizontalPaddings]: disableHorizontalPaddings,
38
+ [styles.modalWithBorder]: !showOverlay,
39
+ }), style: {
40
+ height: typeof height === 'number' ? `${height}px` : undefined,
41
+ minHeight: typeof minHeight === 'number' ? `min(calc(100vh - 12px * 2), ${minHeight}px)` : undefined,
42
+ }, children: [jsx(NavBar.TriggerProvider, { children: jsx(ModalBody, { header: header, scrollContainerRef: setScrollContainer, children: children }) }), footer ? (jsx("div", { className: styles.modalFooter, "data-qa": "modal-footer", children: jsx(ActionBarDefaultPropsContext, { props: actionBarDefaultProps, children: footer }) })) : (jsx("div", { className: classnames(styles.modalFooter, styles.modalFooterEmpty) }))] }));
43
+ };
44
+
45
+ export { ModalInner };
46
+ //# sourceMappingURL=ModalInner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalInner.js","sources":["src/internal/ModalInner.tsx"],"sourcesContent":["import { type FC, type HTMLAttributes, type ReactNode, type Ref, useMemo, useState } from 'react';\nimport classnames from 'classnames';\n\nimport { useAccessibleRegionProps, type ExplicitRegionProps } from '@hh.ru/magritte-internal-accessible-region';\nimport { ActionBarDefaultPropsContext, type ActionBarProps } from '@hh.ru/magritte-ui-action-bar';\nimport { ModalBody } from '@hh.ru/magritte-ui-modal/internal/ModalBody';\nimport { type ModalProps } from '@hh.ru/magritte-ui-modal/public/types';\nimport { NavBar } from '@hh.ru/magritte-ui-nav-bar';\n\nimport styles from './modal.less';\n\nconst ACTION_BAR_DEFAULT_PROPS: Partial<ActionBarProps> = { type: 'auto', padding: 24, showDivider: 'with-scroll' };\n\nexport interface ModalInnerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n explicitAria: ExplicitRegionProps;\n ariaModal: boolean | undefined;\n refCallback: Ref<HTMLDivElement>;\n size: NonNullable<ModalProps['size']>;\n height: ModalProps['height'];\n minHeight: ModalProps['minHeight'];\n disableVerticalPaddings: boolean;\n disableHorizontalPaddings: boolean;\n showOverlay: boolean;\n header: ReactNode;\n footer: ReactNode;\n children: ReactNode;\n}\n\n/**\n * Внутренний контейнер модала — сам `role=\"dialog\"`-узел. Рендерится **под** `AccessibleRegionProvider`\n * и `ModalSlotsProvider`, что и делает его отдельным компонентом:\n * `useAccessibleRegionProps` видит заголовок/подзаголовок, зарегистрированные потомками\n * (`NavBar.TitleContainer` в проп `header`), и выставляет `aria-labelledby` / `aria-describedby`\n * (явные `aria-*` пропы при этом всё перекрывают).\n *\n * Футер приходит пропом и рендерится здесь — вне скролл-зоны, прижатым к низу. От его наличия зависят\n * классы паддингов и нижний divider контента (его прячем, когда у `ActionBar` футера есть progress bar).\n */\nexport const ModalInner: FC<ModalInnerProps> = ({\n explicitAria,\n ariaModal,\n refCallback,\n size,\n height,\n minHeight,\n disableVerticalPaddings,\n disableHorizontalPaddings,\n showOverlay,\n header,\n footer,\n children,\n ...rest\n}) => {\n const aria = useAccessibleRegionProps(explicitAria);\n\n const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(null);\n const actionBarDefaultProps = useMemo(() => ({ ...ACTION_BAR_DEFAULT_PROPS, scrollContainer }), [scrollContainer]);\n\n return (\n <div\n {...rest}\n {...aria}\n aria-modal={ariaModal}\n role=\"dialog\"\n ref={refCallback}\n tabIndex={-1}\n className={classnames(styles.modal, {\n [styles.sizeSmall]: size === 'small',\n [styles.sizeMedium]: size === 'medium',\n [styles.modalFullHeight]: height === 'full-screen',\n [styles.noVerticalPaddings]: disableVerticalPaddings,\n [styles.noHorizontalPaddings]: disableHorizontalPaddings,\n [styles.modalWithBorder]: !showOverlay,\n })}\n style={{\n height: typeof height === 'number' ? `${height}px` : undefined,\n minHeight: typeof minHeight === 'number' ? `min(calc(100vh - 12px * 2), ${minHeight}px)` : undefined,\n }}\n >\n <NavBar.TriggerProvider>\n <ModalBody header={header} scrollContainerRef={setScrollContainer}>\n {children}\n </ModalBody>\n </NavBar.TriggerProvider>\n {footer ? (\n <div className={styles.modalFooter} data-qa=\"modal-footer\">\n <ActionBarDefaultPropsContext props={actionBarDefaultProps}>{footer}</ActionBarDefaultPropsContext>\n </div>\n ) : (\n <div className={classnames(styles.modalFooter, styles.modalFooterEmpty)} />\n )}\n </div>\n );\n};\n"],"names":["_jsxs","_jsx"],"mappings":";;;;;;;;;;;;;;;AAWA,MAAM,wBAAwB,GAA4B,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;AAiBpH;;;;;;;;;AASG;AACI,MAAM,UAAU,GAAwB,CAAC,EAC5C,YAAY,EACZ,SAAS,EACT,WAAW,EACX,IAAI,EACJ,MAAM,EACN,SAAS,EACT,uBAAuB,EACvB,yBAAyB,EACzB,WAAW,EACX,MAAM,EACN,MAAM,EACN,QAAQ,EACR,GAAG,IAAI,EACV,KAAI;AACD,IAAA,MAAM,IAAI,GAAG,wBAAwB,CAAC,YAAY,CAAC,CAAC;IAEpD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAqB,IAAI,CAAC,CAAC;IACjF,MAAM,qBAAqB,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,wBAAwB,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;AAEnH,IAAA,QACIA,IAAA,CAAA,KAAA,EAAA,EAAA,GACQ,IAAI,EAAA,GACJ,IAAI,EAAA,YAAA,EACI,SAAS,EACrB,IAAI,EAAC,QAAQ,EACb,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,CAAC,CAAC,EACZ,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE;AAChC,YAAA,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,KAAK,OAAO;AACpC,YAAA,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,KAAK,QAAQ;AACtC,YAAA,CAAC,MAAM,CAAC,eAAe,GAAG,MAAM,KAAK,aAAa;AAClD,YAAA,CAAC,MAAM,CAAC,kBAAkB,GAAG,uBAAuB;AACpD,YAAA,CAAC,MAAM,CAAC,oBAAoB,GAAG,yBAAyB;AACxD,YAAA,CAAC,MAAM,CAAC,eAAe,GAAG,CAAC,WAAW;SACzC,CAAC,EACF,KAAK,EAAE;AACH,YAAA,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,GAAG,CAAA,EAAG,MAAM,CAAA,EAAA,CAAI,GAAG,SAAS;AAC9D,YAAA,SAAS,EAAE,OAAO,SAAS,KAAK,QAAQ,GAAG,CAAA,4BAAA,EAA+B,SAAS,CAAA,GAAA,CAAK,GAAG,SAAS;AACvG,SAAA,EAAA,QAAA,EAAA,CAEDC,GAAC,CAAA,MAAM,CAAC,eAAe,EACnB,EAAA,QAAA,EAAAA,GAAA,CAAC,SAAS,EAAA,EAAC,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,kBAAkB,EAC5D,QAAA,EAAA,QAAQ,EACD,CAAA,EAAA,CACS,EACxB,MAAM,IACHA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,WAAW,EAAU,SAAA,EAAA,cAAc,EACtD,QAAA,EAAAA,GAAA,CAAC,4BAA4B,EAAA,EAAC,KAAK,EAAE,qBAAqB,EAAA,QAAA,EAAG,MAAM,EAAA,CAAgC,EACjG,CAAA,KAENA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAA,CAAI,CAC9E,CAAA,EAAA,CACC,EACR;AACN;;;;"}
@@ -0,0 +1,46 @@
1
+ import { type RefCallback } from 'react';
2
+ /**
3
+ * Реестр контейнера слота `top` `ContentOverlay` модала. Это единственная точка композиции, которую
4
+ * нельзя выразить пропами: кандидаты (контейнер у нижнего края навбара, запасной `div` хедера, запасной
5
+ * `div` тела) рождаются глубоко в поддереве, их наличие/тип статически неизвестны, и побеждает
6
+ * смонтированный кандидат с наибольшим `priority` — см. `useResolvedTopOverlayElement`.
7
+ */
8
+ export interface TopOverlayCandidate {
9
+ priority: number;
10
+ element: HTMLElement;
11
+ }
12
+ export interface ModalSlots {
13
+ /**
14
+ * Кандидаты на контейнер слота `top` `ContentOverlay`. Одновременно живёт несколько регистрантов —
15
+ * побеждает кандидат с максимальным приоритетом.
16
+ */
17
+ topOverlayContainer: TopOverlayCandidate;
18
+ }
19
+ /**
20
+ * Приоритеты кандидатов на слот `top`. Чем больше число — тем выше приоритет: резолвер выбирает
21
+ * смонтированного кандидата с максимальным приоритетом.
22
+ */
23
+ export declare const TOP_OVERLAY_PRIORITY: {
24
+ /** Запасной контейнер в теле модала. Низший приоритет — на случай, когда хедера нет вовсе. */
25
+ readonly bodyFallback: 0;
26
+ /** Запасной контейнер внутри sticky-хедера, когда внутри хедера нет навбара. */
27
+ readonly headerFallback: 1;
28
+ /** Контейнер, прицепленный к нижнему краю навбара (`bottomAttachment`). Высший приоритет. */
29
+ readonly navBar: 2;
30
+ };
31
+ export declare const ModalSlotsProvider: import("react").FC<{
32
+ children?: import("react").ReactNode | undefined;
33
+ }>, useModalSlots: () => import("@hh.ru/magritte-internal-slot-registry").SlotRegistryStore<ModalSlots> | null;
34
+ /**
35
+ * Возвращает стабильный callback-реф, который регистрирует свою DOM-ноду как кандидата слота `top`
36
+ * с приоритетом `priority` напрямую в slot-registry (и снимает регистрацию, когда нода отвязывается).
37
+ * Не держит стейт и не ре-рендерит сам компонент-регистрант — перерисовывается только потребитель
38
+ * (`useResolvedTopOverlayElement` в `ModalBody`).
39
+ */
40
+ export declare const useRegisterTopOverlayCandidate: (priority: number) => RefCallback<HTMLElement>;
41
+ /**
42
+ * Реактивно отдаёт DOM-элемент кандидата с наибольшим приоритетом, зарегистрированного в слоте
43
+ * `topOverlayContainer`. Возвращает `null`, пока нет ни одного живого кандидата. Возвращаемое значение —
44
+ * сам элемент (стабильная ссылка), поэтому кэшировать снапшот не нужно: `useSyncExternalStore` не зациклит.
45
+ */
46
+ export declare const useResolvedTopOverlayElement: () => HTMLElement | null;
@@ -0,0 +1,66 @@
1
+ import './../index.css';
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+ import { createSlotRegistry } from '@hh.ru/magritte-internal-slot-registry';
4
+
5
+ /**
6
+ * Приоритеты кандидатов на слот `top`. Чем больше число — тем выше приоритет: резолвер выбирает
7
+ * смонтированного кандидата с максимальным приоритетом.
8
+ */
9
+ const TOP_OVERLAY_PRIORITY = {
10
+ /** Запасной контейнер в теле модала. Низший приоритет — на случай, когда хедера нет вовсе. */
11
+ bodyFallback: 0,
12
+ /** Запасной контейнер внутри sticky-хедера, когда внутри хедера нет навбара. */
13
+ headerFallback: 1,
14
+ /** Контейнер, прицепленный к нижнему краю навбара (`bottomAttachment`). Высший приоритет. */
15
+ navBar: 2,
16
+ };
17
+ const { SlotRegistryProvider: ModalSlotsProvider, useSlotRegistry: useModalSlots } = createSlotRegistry('ModalSlots');
18
+ const noop = () => undefined;
19
+ /**
20
+ * Стабильные токены регистранта по приоритету (индекс = `priority`).
21
+ * Это безопасно: в пределах одного модала на приоритет приходится не более одного кандидата
22
+ * (роли-синглтоны), а разные модалы пишут в разные сторы, где токен адресует свою запись независимо.
23
+ */
24
+ const TOP_OVERLAY_TOKENS = [{}, {}, {}];
25
+ /**
26
+ * Возвращает стабильный callback-реф, который регистрирует свою DOM-ноду как кандидата слота `top`
27
+ * с приоритетом `priority` напрямую в slot-registry (и снимает регистрацию, когда нода отвязывается).
28
+ * Не держит стейт и не ре-рендерит сам компонент-регистрант — перерисовывается только потребитель
29
+ * (`useResolvedTopOverlayElement` в `ModalBody`).
30
+ */
31
+ const useRegisterTopOverlayCandidate = (priority) => {
32
+ const store = useModalSlots();
33
+ const token = TOP_OVERLAY_TOKENS[priority];
34
+ return useCallback((element) => {
35
+ if (element) {
36
+ store?.register('topOverlayContainer', token, { priority, element });
37
+ }
38
+ else {
39
+ store?.unregister('topOverlayContainer', token);
40
+ }
41
+ }, [store, token, priority]);
42
+ };
43
+ /**
44
+ * Реактивно отдаёт DOM-элемент кандидата с наибольшим приоритетом, зарегистрированного в слоте
45
+ * `topOverlayContainer`. Возвращает `null`, пока нет ни одного живого кандидата. Возвращаемое значение —
46
+ * сам элемент (стабильная ссылка), поэтому кэшировать снапшот не нужно: `useSyncExternalStore` не зациклит.
47
+ */
48
+ const useResolvedTopOverlayElement = () => {
49
+ const store = useModalSlots();
50
+ const subscribe = useCallback((listener) => store?.subscribe(listener) ?? noop, [store]);
51
+ const getSnapshot = useCallback(() => {
52
+ let bestElement = null;
53
+ let bestPriority = -Infinity;
54
+ (store?.getAll('topOverlayContainer') ?? []).forEach(({ priority, element }) => {
55
+ if (priority > bestPriority) {
56
+ bestElement = element;
57
+ bestPriority = priority;
58
+ }
59
+ });
60
+ return bestElement;
61
+ }, [store]);
62
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
63
+ };
64
+
65
+ export { ModalSlotsProvider, TOP_OVERLAY_PRIORITY, useModalSlots, useRegisterTopOverlayCandidate, useResolvedTopOverlayElement };
66
+ //# sourceMappingURL=ModalSlotsContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalSlotsContext.js","sources":["src/internal/ModalSlotsContext.ts"],"sourcesContent":["import { useCallback, useSyncExternalStore, type RefCallback } from 'react';\n\nimport { createSlotRegistry } from '@hh.ru/magritte-internal-slot-registry';\n\n/**\n * Реестр контейнера слота `top` `ContentOverlay` модала. Это единственная точка композиции, которую\n * нельзя выразить пропами: кандидаты (контейнер у нижнего края навбара, запасной `div` хедера, запасной\n * `div` тела) рождаются глубоко в поддереве, их наличие/тип статически неизвестны, и побеждает\n * смонтированный кандидат с наибольшим `priority` — см. `useResolvedTopOverlayElement`.\n */\nexport interface TopOverlayCandidate {\n priority: number;\n element: HTMLElement;\n}\n\nexport interface ModalSlots {\n /**\n * Кандидаты на контейнер слота `top` `ContentOverlay`. Одновременно живёт несколько регистрантов —\n * побеждает кандидат с максимальным приоритетом.\n */\n topOverlayContainer: TopOverlayCandidate;\n}\n\n/**\n * Приоритеты кандидатов на слот `top`. Чем больше число — тем выше приоритет: резолвер выбирает\n * смонтированного кандидата с максимальным приоритетом.\n */\nexport const TOP_OVERLAY_PRIORITY = {\n /** Запасной контейнер в теле модала. Низший приоритет — на случай, когда хедера нет вовсе. */\n bodyFallback: 0,\n /** Запасной контейнер внутри sticky-хедера, когда внутри хедера нет навбара. */\n headerFallback: 1,\n /** Контейнер, прицепленный к нижнему краю навбара (`bottomAttachment`). Высший приоритет. */\n navBar: 2,\n} as const;\n\nexport const { SlotRegistryProvider: ModalSlotsProvider, useSlotRegistry: useModalSlots } =\n createSlotRegistry<ModalSlots>('ModalSlots');\n\nconst noop = (): void => undefined;\n\n/**\n * Стабильные токены регистранта по приоритету (индекс = `priority`).\n * Это безопасно: в пределах одного модала на приоритет приходится не более одного кандидата\n * (роли-синглтоны), а разные модалы пишут в разные сторы, где токен адресует свою запись независимо.\n */\nconst TOP_OVERLAY_TOKENS: readonly object[] = [{}, {}, {}];\n\n/**\n * Возвращает стабильный callback-реф, который регистрирует свою DOM-ноду как кандидата слота `top`\n * с приоритетом `priority` напрямую в slot-registry (и снимает регистрацию, когда нода отвязывается).\n * Не держит стейт и не ре-рендерит сам компонент-регистрант — перерисовывается только потребитель\n * (`useResolvedTopOverlayElement` в `ModalBody`).\n */\nexport const useRegisterTopOverlayCandidate = (priority: number): RefCallback<HTMLElement> => {\n const store = useModalSlots();\n const token = TOP_OVERLAY_TOKENS[priority];\n\n return useCallback(\n (element: HTMLElement | null) => {\n if (element) {\n store?.register('topOverlayContainer', token, { priority, element });\n } else {\n store?.unregister('topOverlayContainer', token);\n }\n },\n [store, token, priority]\n );\n};\n\n/**\n * Реактивно отдаёт DOM-элемент кандидата с наибольшим приоритетом, зарегистрированного в слоте\n * `topOverlayContainer`. Возвращает `null`, пока нет ни одного живого кандидата. Возвращаемое значение —\n * сам элемент (стабильная ссылка), поэтому кэшировать снапшот не нужно: `useSyncExternalStore` не зациклит.\n */\nexport const useResolvedTopOverlayElement = (): HTMLElement | null => {\n const store = useModalSlots();\n\n const subscribe = useCallback((listener: () => void) => store?.subscribe(listener) ?? noop, [store]);\n\n const getSnapshot = useCallback(() => {\n let bestElement: HTMLElement | null = null;\n let bestPriority = -Infinity;\n (store?.getAll('topOverlayContainer') ?? []).forEach(({ priority, element }) => {\n if (priority > bestPriority) {\n bestElement = element;\n bestPriority = priority;\n }\n });\n return bestElement;\n }, [store]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n"],"names":[],"mappings":";;;AAuBA;;;AAGG;AACU,MAAA,oBAAoB,GAAG;;AAEhC,IAAA,YAAY,EAAE,CAAC;;AAEf,IAAA,cAAc,EAAE,CAAC;;AAEjB,IAAA,MAAM,EAAE,CAAC;EACF;AAEE,MAAA,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,eAAe,EAAE,aAAa,EAAE,GACrF,kBAAkB,CAAa,YAAY,EAAE;AAEjD,MAAM,IAAI,GAAG,MAAY,SAAS,CAAC;AAEnC;;;;AAIG;AACH,MAAM,kBAAkB,GAAsB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAE3D;;;;;AAKG;AACU,MAAA,8BAA8B,GAAG,CAAC,QAAgB,KAA8B;AACzF,IAAA,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;AAC9B,IAAA,MAAM,KAAK,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;AAE3C,IAAA,OAAO,WAAW,CACd,CAAC,OAA2B,KAAI;QAC5B,IAAI,OAAO,EAAE;AACT,YAAA,KAAK,EAAE,QAAQ,CAAC,qBAAqB,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;SACxE;aAAM;AACH,YAAA,KAAK,EAAE,UAAU,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;SACnD;KACJ,EACD,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,CAC3B,CAAC;AACN,EAAE;AAEF;;;;AAIG;AACI,MAAM,4BAA4B,GAAG,MAAyB;AACjE,IAAA,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;IAE9B,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,QAAoB,KAAK,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AAErG,IAAA,MAAM,WAAW,GAAG,WAAW,CAAC,MAAK;QACjC,IAAI,WAAW,GAAuB,IAAI,CAAC;AAC3C,QAAA,IAAI,YAAY,GAAG,CAAC,QAAQ,CAAC;AAC7B,QAAA,CAAC,KAAK,EAAE,MAAM,CAAC,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAI;AAC3E,YAAA,IAAI,QAAQ,GAAG,YAAY,EAAE;gBACzB,WAAW,GAAG,OAAO,CAAC;gBACtB,YAAY,GAAG,QAAQ,CAAC;aAC3B;AACL,SAAC,CAAC,CAAC;AACH,QAAA,OAAO,WAAW,CAAC;AACvB,KAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;AACrE;;;;"}
@@ -0,0 +1,8 @@
1
+ import { type FC, PropsWithChildren } from 'react';
2
+ export type ModalStickyHeaderProps = PropsWithChildren;
3
+ /**
4
+ * Sticky-хедер модала. Нижний край хедера (для `offsetTop` скроллбара и контейнера top-overlay) берётся
5
+ * из `bottomAttachment` навбара / запасного `div` хедера — они позиционируются по визуальному низу хедера
6
+ * и переживают overlay-навбар (когда его обёртка схлопывается в 0), поэтому отдельный flow-маркер не нужен.
7
+ */
8
+ export declare const ModalStickyHeader: FC<ModalStickyHeaderProps>;
@@ -0,0 +1,40 @@
1
+ import './../index.css';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { useMemo } from 'react';
4
+ import classnames from 'classnames';
5
+ import { useRegisterTopOverlayCandidate, TOP_OVERLAY_PRIORITY } from './ModalSlotsContext.js';
6
+ import { NavBarDefaultPropsContext } from '@hh.ru/magritte-ui-nav-bar';
7
+ import { s as styles } from '../modal-Bf_KXESS.js';
8
+ import '@hh.ru/magritte-internal-slot-registry';
9
+
10
+ const NAV_BAR_ACTIONS_DEFAULT_PROPS = {
11
+ verticalAlign: 'top',
12
+ };
13
+ const NAV_BAR_ACTIONS_ICONS_DEFAULT_PROPS = {
14
+ borderRadius: 12,
15
+ padding: 8,
16
+ };
17
+ const NAV_BAR_ROOT_DEFAULT_PROPS = { showDivider: 'with-scroll', snapScroll: false };
18
+ /**
19
+ * Sticky-хедер модала. Нижний край хедера (для `offsetTop` скроллбара и контейнера top-overlay) берётся
20
+ * из `bottomAttachment` навбара / запасного `div` хедера — они позиционируются по визуальному низу хедера
21
+ * и переживают overlay-навбар (когда его обёртка схлопывается в 0), поэтому отдельный flow-маркер не нужен.
22
+ */
23
+ const ModalStickyHeader = ({ children }) => {
24
+ // Хедер предлагает в слот `top` двух кандидатов: контейнер у нижнего края навбара (если навбар внутри
25
+ // есть) и собственный запасной `div`. Каждый реф регистрирует свою ноду в общем реестре
26
+ // `topOverlayContainer` (победитель резолвится по приоритету в ModalBody): навбар важнее запасного
27
+ // `div` хедера, а тот — важнее запасного `div` тела. Незадействованный кандидат (например навбар, когда
28
+ // навбара нет) просто не монтируется → его реф не срабатывает → он не регистрируется и не участвует.
29
+ const navBarRef = useRegisterTopOverlayCandidate(TOP_OVERLAY_PRIORITY.navBar);
30
+ const headerFallbackRef = useRegisterTopOverlayCandidate(TOP_OVERLAY_PRIORITY.headerFallback);
31
+ const navBarRootProps = useMemo(() => ({
32
+ ...NAV_BAR_ROOT_DEFAULT_PROPS,
33
+ bottomAttachment: jsx("div", { ref: navBarRef, className: classnames(styles.contentOverlayTop, styles.navBar) }),
34
+ }), [navBarRef]);
35
+ return (jsx("div", { className: styles.stickyHeader, children: jsxs("div", { className: styles.stickyHeaderInner, children: [jsx(NavBarDefaultPropsContext, { rootProps: navBarRootProps, actionsProps: NAV_BAR_ACTIONS_DEFAULT_PROPS, actionsIconsProps: NAV_BAR_ACTIONS_ICONS_DEFAULT_PROPS, children: children }), jsx("div", { ref: headerFallbackRef, className: classnames(styles.contentOverlayTop, styles.header) })] }) }));
36
+ };
37
+ ModalStickyHeader.displayName = 'ModalStickyHeader';
38
+
39
+ export { ModalStickyHeader };
40
+ //# sourceMappingURL=ModalStickyHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalStickyHeader.js","sources":["src/internal/ModalStickyHeader.tsx"],"sourcesContent":["import { type FC, PropsWithChildren, useMemo } from 'react';\nimport classnames from 'classnames';\n\nimport { type IconProps } from '@hh.ru/magritte-ui-icon';\nimport {\n TOP_OVERLAY_PRIORITY,\n useRegisterTopOverlayCandidate,\n} from '@hh.ru/magritte-ui-modal/internal/ModalSlotsContext';\nimport { type NavBarProps, NavBarDefaultPropsContext, type ActionsProps } from '@hh.ru/magritte-ui-nav-bar';\n\nimport styles from './modal.less';\n\nconst NAV_BAR_ACTIONS_DEFAULT_PROPS: Partial<ActionsProps> = {\n verticalAlign: 'top',\n};\n\nconst NAV_BAR_ACTIONS_ICONS_DEFAULT_PROPS: Partial<IconProps> = {\n borderRadius: 12,\n padding: 8,\n};\n\nconst NAV_BAR_ROOT_DEFAULT_PROPS = { showDivider: 'with-scroll', snapScroll: false } as const;\n\nexport type ModalStickyHeaderProps = PropsWithChildren;\n\n/**\n * Sticky-хедер модала. Нижний край хедера (для `offsetTop` скроллбара и контейнера top-overlay) берётся\n * из `bottomAttachment` навбара / запасного `div` хедера — они позиционируются по визуальному низу хедера\n * и переживают overlay-навбар (когда его обёртка схлопывается в 0), поэтому отдельный flow-маркер не нужен.\n */\nexport const ModalStickyHeader: FC<ModalStickyHeaderProps> = ({ children }) => {\n // Хедер предлагает в слот `top` двух кандидатов: контейнер у нижнего края навбара (если навбар внутри\n // есть) и собственный запасной `div`. Каждый реф регистрирует свою ноду в общем реестре\n // `topOverlayContainer` (победитель резолвится по приоритету в ModalBody): навбар важнее запасного\n // `div` хедера, а тот — важнее запасного `div` тела. Незадействованный кандидат (например навбар, когда\n // навбара нет) просто не монтируется → его реф не срабатывает → он не регистрируется и не участвует.\n const navBarRef = useRegisterTopOverlayCandidate(TOP_OVERLAY_PRIORITY.navBar);\n const headerFallbackRef = useRegisterTopOverlayCandidate(TOP_OVERLAY_PRIORITY.headerFallback);\n\n const navBarRootProps = useMemo<Partial<NavBarProps>>(\n () => ({\n ...NAV_BAR_ROOT_DEFAULT_PROPS,\n bottomAttachment: <div ref={navBarRef} className={classnames(styles.contentOverlayTop, styles.navBar)} />,\n }),\n [navBarRef]\n );\n\n return (\n <div className={styles.stickyHeader}>\n {/*\n * Отрицательные марджины-«вылет» хедера висят на этом внутреннем статичном элементе, а не на\n * самом sticky-хедере. Причина — баг WebKit/Safari: для `position: sticky` отрицательные\n * марджины попадают в scrollable-overflow скролл-контейнера (для статичных элементов — нет),\n * из-за чего в Safari появляется фантомный горизонтальный скролл.\n */}\n <div className={styles.stickyHeaderInner}>\n <NavBarDefaultPropsContext\n rootProps={navBarRootProps}\n actionsProps={NAV_BAR_ACTIONS_DEFAULT_PROPS}\n actionsIconsProps={NAV_BAR_ACTIONS_ICONS_DEFAULT_PROPS}\n >\n {children}\n </NavBarDefaultPropsContext>\n <div ref={headerFallbackRef} className={classnames(styles.contentOverlayTop, styles.header)} />\n </div>\n </div>\n );\n};\n\nModalStickyHeader.displayName = 'ModalStickyHeader';\n"],"names":["_jsx","_jsxs"],"mappings":";;;;;;;;AAYA,MAAM,6BAA6B,GAA0B;AACzD,IAAA,aAAa,EAAE,KAAK;CACvB,CAAC;AAEF,MAAM,mCAAmC,GAAuB;AAC5D,IAAA,YAAY,EAAE,EAAE;AAChB,IAAA,OAAO,EAAE,CAAC;CACb,CAAC;AAEF,MAAM,0BAA0B,GAAG,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AAI9F;;;;AAIG;MACU,iBAAiB,GAA+B,CAAC,EAAE,QAAQ,EAAE,KAAI;;;;;;IAM1E,MAAM,SAAS,GAAG,8BAA8B,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC9E,MAAM,iBAAiB,GAAG,8BAA8B,CAAC,oBAAoB,CAAC,cAAc,CAAC,CAAC;AAE9F,IAAA,MAAM,eAAe,GAAG,OAAO,CAC3B,OAAO;AACH,QAAA,GAAG,0BAA0B;AAC7B,QAAA,gBAAgB,EAAEA,GAAK,CAAA,KAAA,EAAA,EAAA,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,EAAI,CAAA;AAC5G,KAAA,CAAC,EACF,CAAC,SAAS,CAAC,CACd,CAAC;IAEF,QACIA,GAAK,CAAA,KAAA,EAAA,EAAA,SAAS,EAAE,MAAM,CAAC,YAAY,EAAA,QAAA,EAO/BC,IAAK,CAAA,KAAA,EAAA,EAAA,SAAS,EAAE,MAAM,CAAC,iBAAiB,EAAA,QAAA,EAAA,CACpCD,GAAC,CAAA,yBAAyB,EACtB,EAAA,SAAS,EAAE,eAAe,EAC1B,YAAY,EAAE,6BAA6B,EAC3C,iBAAiB,EAAE,mCAAmC,EAAA,QAAA,EAErD,QAAQ,EAAA,CACe,EAC5BA,GAAA,CAAA,KAAA,EAAA,EAAK,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,EAAI,CAAA,CAAA,EAAA,CAC7F,EACJ,CAAA,EACR;AACN,EAAE;AAEF,iBAAiB,CAAC,WAAW,GAAG,mBAAmB;;;;"}
@@ -0,0 +1,38 @@
1
+ /** Отступ сверху под page-header (px) — дефолт для `position=left|right` без оверлея. */
2
+ export declare const DEFAULT_PAGE_HEADER_PADDING = 72;
3
+ export declare const noop: () => undefined;
4
+ /**
5
+ * Таймаут `CSSTransition` для анимации модала (мс).
6
+ * При `prefers-reduced-motion` анимация мгновенная (0) — согласовано с media-query в modal.less.
7
+ */
8
+ export declare const getAnimationTimeout: () => {
9
+ enter: number;
10
+ exit: number;
11
+ };
12
+ /** Классы фаз `CSSTransition` по позиции модала. */
13
+ export declare const CSS_CLASSES: {
14
+ center: {
15
+ appear: string;
16
+ appearActive: string;
17
+ enter: string;
18
+ enterActive: string;
19
+ exit: string;
20
+ exitActive: string;
21
+ };
22
+ left: {
23
+ appear: string;
24
+ appearActive: string;
25
+ enter: string;
26
+ enterActive: string;
27
+ exit: string;
28
+ exitActive: string;
29
+ };
30
+ right: {
31
+ appear: string;
32
+ appearActive: string;
33
+ enter: string;
34
+ enterActive: string;
35
+ exit: string;
36
+ exitActive: string;
37
+ };
38
+ };
@@ -0,0 +1,53 @@
1
+ import './../index.css';
2
+ import coreTokens from '@hh.ru/magritte-design-tokens/core-tokens';
3
+ import { s as styles } from '../modal-Bf_KXESS.js';
4
+
5
+ /** Отступ сверху под page-header (px) — дефолт для `position=left|right` без оверлея. */
6
+ const DEFAULT_PAGE_HEADER_PADDING = 72;
7
+ const noop = () => undefined;
8
+ /** Длительности анимации появления/исчезновения модала (мс) — из дизайн-токенов (см. modal.less). */
9
+ const ENTER_ANIMATION_DURATION = coreTokens['magritte-semantic-animation-ease-in-out-300'].duration;
10
+ const EXIT_ANIMATION_DURATION = coreTokens['magritte-semantic-animation-ease-in-out-200'].duration;
11
+ /**
12
+ * Таймаут `CSSTransition` для анимации модала (мс).
13
+ * При `prefers-reduced-motion` анимация мгновенная (0) — согласовано с media-query в modal.less.
14
+ */
15
+ const getAnimationTimeout = () => {
16
+ const prefersReducedMotion = typeof window !== 'undefined' &&
17
+ typeof window.matchMedia === 'function' &&
18
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
19
+ if (prefersReducedMotion) {
20
+ return { enter: 0, exit: 0 };
21
+ }
22
+ return { enter: ENTER_ANIMATION_DURATION, exit: EXIT_ANIMATION_DURATION };
23
+ };
24
+ /** Классы фаз `CSSTransition` по позиции модала. */
25
+ const CSS_CLASSES = {
26
+ center: {
27
+ appear: styles.animationEnterCenter,
28
+ appearActive: styles.animationEnterCenterActive,
29
+ enter: styles.animationEnterCenter,
30
+ enterActive: styles.animationEnterCenterActive,
31
+ exit: styles.animationExitCenter,
32
+ exitActive: styles.animationExitCenterActive,
33
+ },
34
+ left: {
35
+ appear: styles.animationEnterLeft,
36
+ appearActive: styles.animationEnterLeftActive,
37
+ enter: styles.animationEnterLeft,
38
+ enterActive: styles.animationEnterLeftActive,
39
+ exit: styles.animationExitLeft,
40
+ exitActive: styles.animationExitLeftActive,
41
+ },
42
+ right: {
43
+ appear: styles.animationEnterRight,
44
+ appearActive: styles.animationEnterRightActive,
45
+ enter: styles.animationEnterRight,
46
+ enterActive: styles.animationEnterRightActive,
47
+ exit: styles.animationExitRight,
48
+ exitActive: styles.animationExitRightActive,
49
+ },
50
+ };
51
+
52
+ export { CSS_CLASSES, DEFAULT_PAGE_HEADER_PADDING, getAnimationTimeout, noop };
53
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sources":["src/internal/constants.ts"],"sourcesContent":["import coreTokens from '@hh.ru/magritte-design-tokens/core-tokens';\n\nimport styles from './modal.less';\n\n/** Отступ сверху под page-header (px) — дефолт для `position=left|right` без оверлея. */\nexport const DEFAULT_PAGE_HEADER_PADDING = 72;\n\nexport const noop = (): undefined => undefined;\n\n/** Длительности анимации появления/исчезновения модала (мс) — из дизайн-токенов (см. modal.less). */\nconst ENTER_ANIMATION_DURATION = coreTokens['magritte-semantic-animation-ease-in-out-300'].duration;\nconst EXIT_ANIMATION_DURATION = coreTokens['magritte-semantic-animation-ease-in-out-200'].duration;\n\n/**\n * Таймаут `CSSTransition` для анимации модала (мс).\n * При `prefers-reduced-motion` анимация мгновенная (0) — согласовано с media-query в modal.less.\n */\nexport const getAnimationTimeout = (): { enter: number; exit: number } => {\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n if (prefersReducedMotion) {\n return { enter: 0, exit: 0 };\n }\n\n return { enter: ENTER_ANIMATION_DURATION, exit: EXIT_ANIMATION_DURATION };\n};\n\n/** Классы фаз `CSSTransition` по позиции модала. */\nexport const CSS_CLASSES = {\n center: {\n appear: styles.animationEnterCenter,\n appearActive: styles.animationEnterCenterActive,\n enter: styles.animationEnterCenter,\n enterActive: styles.animationEnterCenterActive,\n exit: styles.animationExitCenter,\n exitActive: styles.animationExitCenterActive,\n },\n left: {\n appear: styles.animationEnterLeft,\n appearActive: styles.animationEnterLeftActive,\n enter: styles.animationEnterLeft,\n enterActive: styles.animationEnterLeftActive,\n exit: styles.animationExitLeft,\n exitActive: styles.animationExitLeftActive,\n },\n right: {\n appear: styles.animationEnterRight,\n appearActive: styles.animationEnterRightActive,\n enter: styles.animationEnterRight,\n enterActive: styles.animationEnterRightActive,\n exit: styles.animationExitRight,\n exitActive: styles.animationExitRightActive,\n },\n};\n"],"names":[],"mappings":";;;AAIA;AACO,MAAM,2BAA2B,GAAG,GAAG;MAEjC,IAAI,GAAG,MAAiB,UAAU;AAE/C;AACA,MAAM,wBAAwB,GAAG,UAAU,CAAC,6CAA6C,CAAC,CAAC,QAAQ,CAAC;AACpG,MAAM,uBAAuB,GAAG,UAAU,CAAC,6CAA6C,CAAC,CAAC,QAAQ,CAAC;AAEnG;;;AAGG;AACI,MAAM,mBAAmB,GAAG,MAAsC;AACrE,IAAA,MAAM,oBAAoB,GACtB,OAAO,MAAM,KAAK,WAAW;AAC7B,QAAA,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;AACvC,QAAA,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO,CAAC;IAElE,IAAI,oBAAoB,EAAE;QACtB,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;KAChC;IAED,OAAO,EAAE,KAAK,EAAE,wBAAwB,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;AAC9E,EAAE;AAEF;AACa,MAAA,WAAW,GAAG;AACvB,IAAA,MAAM,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC,oBAAoB;QACnC,YAAY,EAAE,MAAM,CAAC,0BAA0B;QAC/C,KAAK,EAAE,MAAM,CAAC,oBAAoB;QAClC,WAAW,EAAE,MAAM,CAAC,0BAA0B;QAC9C,IAAI,EAAE,MAAM,CAAC,mBAAmB;QAChC,UAAU,EAAE,MAAM,CAAC,yBAAyB;AAC/C,KAAA;AACD,IAAA,IAAI,EAAE;QACF,MAAM,EAAE,MAAM,CAAC,kBAAkB;QACjC,YAAY,EAAE,MAAM,CAAC,wBAAwB;QAC7C,KAAK,EAAE,MAAM,CAAC,kBAAkB;QAChC,WAAW,EAAE,MAAM,CAAC,wBAAwB;QAC5C,IAAI,EAAE,MAAM,CAAC,iBAAiB;QAC9B,UAAU,EAAE,MAAM,CAAC,uBAAuB;AAC7C,KAAA;AACD,IAAA,KAAK,EAAE;QACH,MAAM,EAAE,MAAM,CAAC,mBAAmB;QAClC,YAAY,EAAE,MAAM,CAAC,yBAAyB;QAC9C,KAAK,EAAE,MAAM,CAAC,mBAAmB;QACjC,WAAW,EAAE,MAAM,CAAC,yBAAyB;QAC7C,IAAI,EAAE,MAAM,CAAC,kBAAkB;QAC/B,UAAU,EAAE,MAAM,CAAC,wBAAwB;AAC9C,KAAA;;;;;"}
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * `true`, если среди детей модала есть `TreeSelector`-обёртка. Единая точка детекции: её используют и
4
+ * `Modal` (форсит `height='full-screen'`), и `ModalContent` (рендерит особый контейнер) — проверка детей
5
+ * должна совпадать в обоих местах, иначе детекция разъедется.
6
+ */
7
+ export declare const isTreeSelectorChild: (children: ReactNode) => boolean;
@@ -0,0 +1,13 @@
1
+ import './../index.css';
2
+ import { Children } from 'react';
3
+ import { isValidTreeSelectorWrapper } from '@hh.ru/magritte-ui-tree-selector';
4
+
5
+ /**
6
+ * `true`, если среди детей модала есть `TreeSelector`-обёртка. Единая точка детекции: её используют и
7
+ * `Modal` (форсит `height='full-screen'`), и `ModalContent` (рендерит особый контейнер) — проверка детей
8
+ * должна совпадать в обоих местах, иначе детекция разъедется.
9
+ */
10
+ const isTreeSelectorChild = (children) => Children.toArray(children).some(isValidTreeSelectorWrapper);
11
+
12
+ export { isTreeSelectorChild };
13
+ //# sourceMappingURL=isTreeSelectorChild.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isTreeSelectorChild.js","sources":["src/internal/isTreeSelectorChild.ts"],"sourcesContent":["import { Children, type ReactNode } from 'react';\n\nimport { isValidTreeSelectorWrapper } from '@hh.ru/magritte-ui-tree-selector';\n\n/**\n * `true`, если среди детей модала есть `TreeSelector`-обёртка. Единая точка детекции: её используют и\n * `Modal` (форсит `height='full-screen'`), и `ModalContent` (рендерит особый контейнер) — проверка детей\n * должна совпадать в обоих местах, иначе детекция разъедется.\n */\nexport const isTreeSelectorChild = (children: ReactNode): boolean =>\n Children.toArray(children).some(isValidTreeSelectorWrapper);\n"],"names":[],"mappings":";;;AAIA;;;;AAIG;MACU,mBAAmB,GAAG,CAAC,QAAmB,KACnD,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,0BAA0B;;;;"}
@@ -1,4 +1,4 @@
1
- import './index.css';
1
+ import './../index.css';
2
2
  import { useState, useLayoutEffect, useEffect } from 'react';
3
3
  import { useDomStoredUniqueId, getValueFromDom, storeValueInDom } from '@hh.ru/magritte-common-dom-storage';
4
4
 
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useModalOrder.js","sources":["src/internal/useModalOrder.ts"],"sourcesContent":["import { RefObject, useEffect, useLayoutEffect, useState } from 'react';\n\nimport { storeValueInDom, getValueFromDom, useDomStoredUniqueId } from '@hh.ru/magritte-common-dom-storage';\n\ntype NotifyCallbackArg = { position: number; count: number };\n\ntype Subscribers = Record<string, (position: NotifyCallbackArg) => void>;\ntype VisibilityMap = Record<string, boolean>;\ntype PositionMap = Record<string, number>;\n\ntype ModalCounterState = {\n prevVisiblility: VisibilityMap;\n positions: PositionMap;\n subscribers: Subscribers;\n count: number;\n};\n\nconst STORAGE_NAME = '_MAGRITTE_MODAL_COUNTER_';\n\nconst getState = (): ModalCounterState => {\n const state = getValueFromDom<ModalCounterState>(STORAGE_NAME);\n if (state === undefined) {\n return {\n prevVisiblility: {},\n positions: {},\n subscribers: {},\n count: 0,\n };\n }\n return state;\n};\n\nconst storeState = (state: ModalCounterState): void => {\n storeValueInDom(STORAGE_NAME, state);\n};\n\nconst counterFactory = () => {\n return {\n updateVisiblility: (id: string, visible: boolean) => {\n // eslint-disable-next-line prefer-const\n const state = getState();\n const prevVisible = !!state.prevVisiblility[id];\n if (visible === prevVisible) {\n return;\n }\n state.prevVisiblility[id] = visible;\n if (visible) {\n state.count += 1;\n state.positions[id] = state.count;\n } else {\n state.count -= 1;\n const currentPosition = state.positions[id];\n state.positions[id] = 0;\n Object.entries(state.positions).forEach(([key, value]) => {\n state.positions[key] = value > currentPosition ? value - 1 : value;\n });\n }\n storeState(state);\n },\n subscribe: (id: string, callback: (position: NotifyCallbackArg) => void) => {\n const state = getState();\n state.subscribers[id] = callback;\n storeState(state);\n },\n unsubscribe: (id: string) => {\n const state = getState();\n delete state.subscribers[id];\n storeState(state);\n },\n notifyAll: () => {\n const { subscribers, count, positions } = getState();\n Object.entries(subscribers).forEach(([id, callback]) => callback({ position: positions[id], count }));\n },\n };\n};\n\nconst counter = counterFactory();\n\nexport const useModalOrder = (visible: boolean, modalRef: RefObject<HTMLDivElement>): [number, number] => {\n const [position, setPosition] = useState<{ position: number; count: number }>({ position: 0, count: 0 });\n const id = useDomStoredUniqueId();\n\n useLayoutEffect(() => {\n counter.updateVisiblility(id, visible);\n counter.notifyAll();\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n }, [visible]);\n\n useEffect(() => {\n counter.subscribe(id, setPosition);\n counter.updateVisiblility(id, visible);\n return () => {\n counter.unsubscribe(id);\n counter.updateVisiblility(id, false);\n counter.notifyAll();\n };\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n }, [id]);\n\n useLayoutEffect(() => {\n // Если это самый \"верхний\" модал передаем туда фокус\n if (\n position.position === position.count &&\n position.count > 0 &&\n !modalRef.current?.contains(document.activeElement)\n ) {\n modalRef.current?.focus();\n }\n }, [position, modalRef]);\n\n return [position.position, position.count];\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { type RefObject } from 'react';
2
+ interface UseModalTransitionParams {
3
+ visible: boolean | undefined;
4
+ isServerEnv: boolean;
5
+ showOverlay: boolean;
6
+ returnFocusOnClose: boolean | undefined;
7
+ onAppear?: VoidFunction;
8
+ onBeforeExit?: VoidFunction;
9
+ onAfterExit?: VoidFunction;
10
+ modalRef: RefObject<HTMLDivElement>;
11
+ /** Длительность анимации закрытия (мс) — с ней разблокируется скролл при размонтировании. */
12
+ animationTimeoutExit: number;
13
+ }
14
+ interface UseModalTransitionResult {
15
+ onEntering: VoidFunction;
16
+ onEntered: VoidFunction;
17
+ onExit: VoidFunction;
18
+ onExited: VoidFunction;
19
+ /** Текущий брейкпоинт входит в число тех, на которых модал не рендерится (XS/S). */
20
+ isHiddenForBreakpoint: boolean;
21
+ }
22
+ /**
23
+ * Жизненный цикл модала вокруг `CSSTransition`: блокировка скролла страницы на время показа, возврат
24
+ * фокуса на активатор после закрытия, колбэки `onAppear/onBeforeExit/onAfterExit`, регистрация в стеке
25
+ * модалов (`useModalOrder`) и обработка смены брейкпоинта (на XS/S модал скрывается). Возвращает
26
+ * хендлеры для `CSSTransition` и флаг «скрыт на текущем брейкпоинте».
27
+ */
28
+ export declare const useModalTransition: ({ visible, isServerEnv, showOverlay, returnFocusOnClose, onAppear, onBeforeExit, onAfterExit, modalRef, animationTimeoutExit, }: UseModalTransitionParams) => UseModalTransitionResult;
29
+ export {};