@koslibs/components 1.3.0 → 1.4.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.
- package/CHANGELOG.md +30 -0
- package/dist/components/Button/button.js +1 -0
- package/dist/components/Modal/colors.module.js +5 -0
- package/dist/components/Modal/colors_module.css +10 -0
- package/dist/components/Modal/components/content/index.d.ts +15 -0
- package/dist/components/Modal/components/content/index.js +18 -0
- package/dist/components/Modal/components/content/index.module.js +5 -0
- package/dist/components/Modal/components/content/index_module.css +12 -0
- package/dist/components/Modal/components/controls/index.d.ts +56 -0
- package/dist/components/Modal/components/controls/index.js +23 -0
- package/dist/components/Modal/components/controls/index.module.js +19 -0
- package/dist/components/Modal/components/controls/index_module.css +52 -0
- package/dist/components/Modal/components/footer/index.d.ts +15 -0
- package/dist/components/Modal/components/footer/index.js +18 -0
- package/dist/components/Modal/components/footer/index.module.js +5 -0
- package/dist/components/Modal/components/footer/index_module.css +9 -0
- package/dist/components/Modal/components/header/index.d.ts +43 -0
- package/dist/components/Modal/components/header/index.js +53 -0
- package/dist/components/Modal/components/header/index.module.js +8 -0
- package/dist/components/Modal/components/header/index_module.css +31 -0
- package/dist/components/Modal/components/modal-surface/index.d.ts +4 -0
- package/dist/components/Modal/components/modal-surface/index.js +81 -0
- package/dist/components/Modal/context.d.ts +6 -0
- package/dist/components/Modal/context.js +3 -0
- package/dist/components/Modal/index.d.ts +10 -0
- package/dist/components/Modal/index.js +5 -0
- package/dist/components/Modal/index.module.js +15 -0
- package/dist/components/Modal/index_module.css +46 -0
- package/dist/components/Modal/modal.d.ts +10 -0
- package/dist/components/Modal/modal.js +13 -0
- package/dist/components/Modal/types.d.ts +41 -0
- package/dist/components/Modal/types.js +0 -0
- package/dist/components/Modal/utils/lock-scroll.d.ts +1 -0
- package/dist/components/Modal/utils/lock-scroll.js +31 -0
- package/dist/components/Modal/vars.css +9 -0
- package/dist/components/Popover/popover.js +9 -7
- package/dist/components/Portal/index.d.ts +2 -0
- package/dist/components/Portal/index.js +1 -0
- package/dist/components/Portal/portal.d.ts +2 -0
- package/dist/components/Portal/portal.js +19 -0
- package/dist/components/Portal/types.d.ts +18 -0
- package/dist/components/Portal/types.js +0 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @koslibs/components
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#5](https://github.com/koslibs/components/pull/5) [`68d3460`](https://github.com/koslibs/components/commit/68d34608d599582acf4115d31f64fb0168213edb) Thanks [@holypower777](https://github.com/holypower777)! - Перенесён Modal из GullEye core-components с Header, Content, Footer и Controls.
|
|
8
|
+
Добавлены прямой импорт `@koslibs/components/Modal`, размеры 480/600/720/960/fullscreen,
|
|
9
|
+
управляемое закрытие, блокировка прокрутки и поддержка вложенных окон.
|
|
10
|
+
Docs содержат вкладки документации, Props, CSS-переменных и истории изменений,
|
|
11
|
+
а также примеры длинного контента и Popover внутри нативного dialog.
|
|
12
|
+
Блокировка прокрутки учитывает `scrollbar-gutter: stable`, чтобы контент страницы
|
|
13
|
+
не смещался из-за двойной компенсации места под скроллбар.
|
|
14
|
+
|
|
15
|
+
Modal синхронизирован с документацией Figma: типографика Header 20 px semibold /
|
|
16
|
+
14 px regular, крестик 18 px в области 48 × 32 px, Header без нижнего и Footer
|
|
17
|
+
без верхнего отступа. В строковых Controls две Button с block=true делят ширину
|
|
18
|
+
одной строки поровну с учётом gap; column растягивает кнопки по ширине вертикально.
|
|
19
|
+
|
|
20
|
+
Убран `Header.onClose`: крестик вызывает общий `Modal.onClose` через внутренний
|
|
21
|
+
React Context. Вложенный Header использует обработчик ближайшего Modal;
|
|
22
|
+
обработчик больше не нужно передавать отдельно в заголовок.
|
|
23
|
+
|
|
24
|
+
Header, Content, Footer и Controls наследуют `Modal.dataTestId` с суффиксами
|
|
25
|
+
`-header`, `-content`, `-footer`, `-controls`. Явный dataTestId части имеет приоритет;
|
|
26
|
+
вложенные окна используют только свой базовый идентификатор.
|
|
27
|
+
|
|
28
|
+
Добавлен отдельный Portal с `getPortalContainer`, `immediateMount`, ref на контейнер
|
|
29
|
+
и немедленным монтированием по умолчанию (`immediateMount=true`). Значение false
|
|
30
|
+
позволяет дождаться DOM-контейнера из ref. Modal и Popover используют
|
|
31
|
+
общий Portal; прямой импорт доступен через `@koslibs/components/Portal`.
|
|
32
|
+
|
|
3
33
|
## 1.3.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
|
@@ -76,6 +76,7 @@ const Button = /*#__PURE__*/ forwardRef(({ view = 'primary', shape = 'rectangula
|
|
|
76
76
|
[colors_module.loading]: showLoader
|
|
77
77
|
}, className),
|
|
78
78
|
"data-test-id": dataTestId,
|
|
79
|
+
"data-block": block || void 0,
|
|
79
80
|
type: type,
|
|
80
81
|
disabled: disabled || showLoader,
|
|
81
82
|
"aria-busy": showLoader,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type ContentProps = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
/**
|
|
4
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -content.
|
|
5
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
6
|
+
*/
|
|
7
|
+
dataTestId?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const Content: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
|
|
10
|
+
/**
|
|
11
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -content.
|
|
12
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
13
|
+
*/
|
|
14
|
+
dataTestId?: string;
|
|
15
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { forwardRef, useContext } from "react";
|
|
4
|
+
import { ModalContext } from "../../context.js";
|
|
5
|
+
import index_module from "./index.module.js";
|
|
6
|
+
const Content = /*#__PURE__*/ forwardRef(({ children, className, dataTestId, ...restProps }, ref)=>{
|
|
7
|
+
const { dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
8
|
+
const testId = dataTestId ?? modalDataTestId;
|
|
9
|
+
return /*#__PURE__*/ jsx("div", {
|
|
10
|
+
...restProps,
|
|
11
|
+
ref: ref,
|
|
12
|
+
className: classnames(index_module.component, className),
|
|
13
|
+
"data-test-id": testId ? `${testId}-content` : void 0,
|
|
14
|
+
children: children
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
Content.displayName = 'ModalContent';
|
|
18
|
+
export { Content };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export type ControlsProps = HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
/**
|
|
5
|
+
* Основное действие, отображается после дополнительного.
|
|
6
|
+
*/
|
|
7
|
+
primary?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Дополнительное действие.
|
|
10
|
+
*/
|
|
11
|
+
secondary?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Выравнивание действий.
|
|
14
|
+
* В строковых раскладках block-кнопки делят свободную ширину поровну.
|
|
15
|
+
* В column кнопки занимают всю ширину и располагаются друг под другом.
|
|
16
|
+
* @default 'end'
|
|
17
|
+
*/
|
|
18
|
+
layout?: 'start' | 'end' | 'center' | 'space-between' | 'column';
|
|
19
|
+
/**
|
|
20
|
+
* Расстояние между действиями.
|
|
21
|
+
* @default 12
|
|
22
|
+
*/
|
|
23
|
+
gap?: 8 | 12 | 16 | 24;
|
|
24
|
+
/**
|
|
25
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -controls.
|
|
26
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
27
|
+
*/
|
|
28
|
+
dataTestId?: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const Controls: React.ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
|
|
31
|
+
/**
|
|
32
|
+
* Основное действие, отображается после дополнительного.
|
|
33
|
+
*/
|
|
34
|
+
primary?: ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* Дополнительное действие.
|
|
37
|
+
*/
|
|
38
|
+
secondary?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Выравнивание действий.
|
|
41
|
+
* В строковых раскладках block-кнопки делят свободную ширину поровну.
|
|
42
|
+
* В column кнопки занимают всю ширину и располагаются друг под другом.
|
|
43
|
+
* @default 'end'
|
|
44
|
+
*/
|
|
45
|
+
layout?: "start" | "end" | "center" | "space-between" | "column";
|
|
46
|
+
/**
|
|
47
|
+
* Расстояние между действиями.
|
|
48
|
+
* @default 12
|
|
49
|
+
*/
|
|
50
|
+
gap?: 8 | 12 | 16 | 24;
|
|
51
|
+
/**
|
|
52
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -controls.
|
|
53
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
54
|
+
*/
|
|
55
|
+
dataTestId?: string;
|
|
56
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import react, { forwardRef, useContext } from "react";
|
|
4
|
+
import { ModalContext } from "../../context.js";
|
|
5
|
+
import index_module from "./index.module.js";
|
|
6
|
+
const Controls = /*#__PURE__*/ forwardRef(({ primary, secondary, layout = 'end', gap = 12, className, children, dataTestId, ...restProps }, ref)=>{
|
|
7
|
+
const { dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
8
|
+
const testId = dataTestId ?? modalDataTestId;
|
|
9
|
+
return /*#__PURE__*/ jsx("div", {
|
|
10
|
+
...restProps,
|
|
11
|
+
ref: ref,
|
|
12
|
+
className: classnames(index_module.component, index_module[layout], index_module[`gap-${gap}`], className),
|
|
13
|
+
"data-test-id": testId ? `${testId}-controls` : void 0,
|
|
14
|
+
children: children ?? /*#__PURE__*/ jsxs(react.Fragment, {
|
|
15
|
+
children: [
|
|
16
|
+
secondary,
|
|
17
|
+
primary
|
|
18
|
+
]
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
Controls.displayName = 'ModalControls';
|
|
23
|
+
export { Controls };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "./index_module.css";
|
|
2
|
+
const index_module = {
|
|
3
|
+
component: "component-IPNCFT",
|
|
4
|
+
column: "column-dBeQXP",
|
|
5
|
+
start: "start-ouHlcU",
|
|
6
|
+
end: "end-TYAvEX",
|
|
7
|
+
center: "center-I4ALjs",
|
|
8
|
+
"space-between": "space-between-OxpVhA",
|
|
9
|
+
spaceBetween: "space-between-OxpVhA",
|
|
10
|
+
"gap-8": "gap-8-xwhlts",
|
|
11
|
+
gap8: "gap-8-xwhlts",
|
|
12
|
+
"gap-12": "gap-12-IFyQQK",
|
|
13
|
+
gap12: "gap-12-IFyQQK",
|
|
14
|
+
"gap-16": "gap-16-iZpxeN",
|
|
15
|
+
gap16: "gap-16-iZpxeN",
|
|
16
|
+
"gap-24": "gap-24-eEhuId",
|
|
17
|
+
gap24: "gap-24-eEhuId"
|
|
18
|
+
};
|
|
19
|
+
export default index_module;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
.component-IPNCFT {
|
|
2
|
+
flex-wrap: wrap;
|
|
3
|
+
align-items: center;
|
|
4
|
+
width: 100%;
|
|
5
|
+
display: flex;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.component-IPNCFT:not(.column-dBeQXP) > button[data-block="true"] {
|
|
9
|
+
flex: 1 1 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.start-ouHlcU {
|
|
13
|
+
justify-content: flex-start;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.end-TYAvEX {
|
|
17
|
+
justify-content: flex-end;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.center-I4ALjs {
|
|
21
|
+
justify-content: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.space-between-OxpVhA {
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.column-dBeQXP {
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
align-items: stretch;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.column-dBeQXP > button {
|
|
34
|
+
width: 100%;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.gap-8-xwhlts {
|
|
38
|
+
gap: var(--size-8, 8px);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.gap-12-IFyQQK {
|
|
42
|
+
gap: var(--size-12, 12px);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.gap-16-iZpxeN {
|
|
46
|
+
gap: var(--size-16, 16px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.gap-24-eEhuId {
|
|
50
|
+
gap: var(--size-24, 24px);
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type FooterProps = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
/**
|
|
4
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -footer.
|
|
5
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
6
|
+
*/
|
|
7
|
+
dataTestId?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const Footer: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
|
|
10
|
+
/**
|
|
11
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -footer.
|
|
12
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
13
|
+
*/
|
|
14
|
+
dataTestId?: string;
|
|
15
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { forwardRef, useContext } from "react";
|
|
4
|
+
import { ModalContext } from "../../context.js";
|
|
5
|
+
import index_module from "./index.module.js";
|
|
6
|
+
const Footer = /*#__PURE__*/ forwardRef(({ children, className, dataTestId, ...restProps }, ref)=>{
|
|
7
|
+
const { dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
8
|
+
const testId = dataTestId ?? modalDataTestId;
|
|
9
|
+
return /*#__PURE__*/ jsx("div", {
|
|
10
|
+
...restProps,
|
|
11
|
+
ref: ref,
|
|
12
|
+
className: classnames(index_module.component, className),
|
|
13
|
+
"data-test-id": testId ? `${testId}-footer` : void 0,
|
|
14
|
+
children: children
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
Footer.displayName = 'ModalFooter';
|
|
18
|
+
export { Footer };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type HeaderProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'onClose' | 'children'> & {
|
|
3
|
+
/**
|
|
4
|
+
* Заголовок окна.
|
|
5
|
+
*/
|
|
6
|
+
title?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Описание под заголовком.
|
|
9
|
+
*/
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Показывать кнопку закрытия.
|
|
13
|
+
* Кнопка вызывает onClose ближайшего Modal.
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
hasCloser?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -header.
|
|
19
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
20
|
+
*/
|
|
21
|
+
dataTestId?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const Header: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "title" | "children" | "onClose"> & {
|
|
24
|
+
/**
|
|
25
|
+
* Заголовок окна.
|
|
26
|
+
*/
|
|
27
|
+
title?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Описание под заголовком.
|
|
30
|
+
*/
|
|
31
|
+
subtitle?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Показывать кнопку закрытия.
|
|
34
|
+
* Кнопка вызывает onClose ближайшего Modal.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
hasCloser?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Базовый идентификатор для автоматизированных тестов с суффиксом -header.
|
|
40
|
+
* По умолчанию наследуется от ближайшего Modal; явно заданный имеет приоритет.
|
|
41
|
+
*/
|
|
42
|
+
dataTestId?: string;
|
|
43
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CrossHeavyMIcon } from "@koslibs/icons/CrossHeavyMIcon";
|
|
3
|
+
import classnames from "classnames";
|
|
4
|
+
import { forwardRef, useContext } from "react";
|
|
5
|
+
import { Button } from "../../../Button/index.js";
|
|
6
|
+
import { TypographyText } from "../../../Typography/index.js";
|
|
7
|
+
import { ModalContext } from "../../context.js";
|
|
8
|
+
import index_module from "./index.module.js";
|
|
9
|
+
const Header = /*#__PURE__*/ forwardRef(({ title, subtitle, hasCloser = true, className, dataTestId, ...restProps }, ref)=>{
|
|
10
|
+
const { onClose, dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
11
|
+
const testId = dataTestId ?? modalDataTestId;
|
|
12
|
+
const handleClick = ()=>{
|
|
13
|
+
onClose?.();
|
|
14
|
+
};
|
|
15
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
16
|
+
...restProps,
|
|
17
|
+
ref: ref,
|
|
18
|
+
className: classnames(index_module.component, className),
|
|
19
|
+
"data-test-id": testId ? `${testId}-header` : void 0,
|
|
20
|
+
children: [
|
|
21
|
+
/*#__PURE__*/ jsxs("div", {
|
|
22
|
+
className: index_module.content,
|
|
23
|
+
children: [
|
|
24
|
+
Boolean(title) && /*#__PURE__*/ jsx(TypographyText, {
|
|
25
|
+
size: 20,
|
|
26
|
+
weight: "semibold",
|
|
27
|
+
children: title
|
|
28
|
+
}),
|
|
29
|
+
Boolean(subtitle) && /*#__PURE__*/ jsx(TypographyText, {
|
|
30
|
+
size: 14,
|
|
31
|
+
color: "secondary",
|
|
32
|
+
children: subtitle
|
|
33
|
+
})
|
|
34
|
+
]
|
|
35
|
+
}),
|
|
36
|
+
hasCloser && /*#__PURE__*/ jsx(Button, {
|
|
37
|
+
size: 32,
|
|
38
|
+
view: "transparent",
|
|
39
|
+
className: index_module.closer,
|
|
40
|
+
labelClassName: index_module.closerLabel,
|
|
41
|
+
"aria-label": "Закрыть окно",
|
|
42
|
+
onClick: handleClick,
|
|
43
|
+
children: /*#__PURE__*/ jsx(CrossHeavyMIcon, {
|
|
44
|
+
width: 18,
|
|
45
|
+
height: 18,
|
|
46
|
+
"aria-hidden": "true"
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
Header.displayName = 'ModalHeader';
|
|
53
|
+
export { Header };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.component-sqEuYd {
|
|
2
|
+
justify-content: space-between;
|
|
3
|
+
align-items: flex-start;
|
|
4
|
+
gap: var(--size-24, 24px);
|
|
5
|
+
padding: var(--modal-padding) var(--modal-padding) 0;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
display: flex;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.content-cnwKDB {
|
|
11
|
+
gap: var(--size-4, 4px);
|
|
12
|
+
overflow-wrap: anywhere;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
flex: 1;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
display: flex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.component-sqEuYd > .closer-odHjP5 {
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
width: 48px;
|
|
22
|
+
min-width: 48px;
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.closerLabel-r61y3R {
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
display: flex;
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ModalProps } from '../../types.js';
|
|
2
|
+
type SurfaceProps = Omit<ModalProps, 'open' | 'getPortalContainer'>;
|
|
3
|
+
export declare const ModalSurface: import("react").ForwardRefExoticComponent<SurfaceProps & import("react").RefAttributes<HTMLDialogElement>>;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { ModalContext } from "../../context.js";
|
|
5
|
+
import { lockScroll } from "../../utils/lock-scroll.js";
|
|
6
|
+
import colors_module from "../../colors.module.js";
|
|
7
|
+
import index_module from "../../index.module.js";
|
|
8
|
+
const ModalSurface = /*#__PURE__*/ forwardRef(({ onClose, size = 720, escapeKeyDown = true, backdropClick = true, lockScroll: shouldLockScroll = true, className, dataTestId, children, onPointerDown, onClick, ...restProps }, ref)=>{
|
|
9
|
+
const dialogRef = useRef(null);
|
|
10
|
+
const backdropPointerDown = useRef(false);
|
|
11
|
+
const mounted = useRef(false);
|
|
12
|
+
const contextValue = useMemo(()=>({
|
|
13
|
+
onClose,
|
|
14
|
+
dataTestId
|
|
15
|
+
}), [
|
|
16
|
+
onClose,
|
|
17
|
+
dataTestId
|
|
18
|
+
]);
|
|
19
|
+
const mergedRef = useCallback((element)=>{
|
|
20
|
+
dialogRef.current = element;
|
|
21
|
+
if ('function' == typeof ref) ref(element);
|
|
22
|
+
else if (ref) ref.current = element;
|
|
23
|
+
}, [
|
|
24
|
+
ref
|
|
25
|
+
]);
|
|
26
|
+
useLayoutEffect(()=>{
|
|
27
|
+
const dialog = dialogRef.current;
|
|
28
|
+
if (!dialog) return;
|
|
29
|
+
mounted.current = true;
|
|
30
|
+
if (!dialog.open) dialog.showModal();
|
|
31
|
+
return ()=>{
|
|
32
|
+
mounted.current = false;
|
|
33
|
+
dialog.close();
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
useLayoutEffect(()=>{
|
|
37
|
+
const dialog = dialogRef.current;
|
|
38
|
+
if (dialog && shouldLockScroll) return lockScroll(dialog.ownerDocument);
|
|
39
|
+
}, [
|
|
40
|
+
shouldLockScroll
|
|
41
|
+
]);
|
|
42
|
+
const isBackdrop = (event)=>{
|
|
43
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
44
|
+
return event.target === event.currentTarget && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom);
|
|
45
|
+
};
|
|
46
|
+
const handleCancel = (event)=>{
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
if (escapeKeyDown) onClose?.();
|
|
50
|
+
};
|
|
51
|
+
const handleClose = (event)=>{
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
if (mounted.current && !event.currentTarget.open) onClose?.();
|
|
54
|
+
};
|
|
55
|
+
const handlePointerDown = (event)=>{
|
|
56
|
+
onPointerDown?.(event);
|
|
57
|
+
backdropPointerDown.current = !event.defaultPrevented && isBackdrop(event);
|
|
58
|
+
};
|
|
59
|
+
const handleClick = (event)=>{
|
|
60
|
+
onClick?.(event);
|
|
61
|
+
const close = backdropPointerDown.current && isBackdrop(event);
|
|
62
|
+
backdropPointerDown.current = false;
|
|
63
|
+
if (backdropClick && close && !event.defaultPrevented) onClose?.();
|
|
64
|
+
};
|
|
65
|
+
return /*#__PURE__*/ jsx("dialog", {
|
|
66
|
+
...restProps,
|
|
67
|
+
ref: mergedRef,
|
|
68
|
+
className: classnames(index_module.component, colors_module.component, index_module[`size-${size}`], className),
|
|
69
|
+
"data-test-id": dataTestId,
|
|
70
|
+
onCancel: handleCancel,
|
|
71
|
+
onClose: handleClose,
|
|
72
|
+
onPointerDown: handlePointerDown,
|
|
73
|
+
onClick: handleClick,
|
|
74
|
+
children: /*#__PURE__*/ jsx(ModalContext.Provider, {
|
|
75
|
+
value: contextValue,
|
|
76
|
+
children: children
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
ModalSurface.displayName = 'ModalSurface';
|
|
81
|
+
export { ModalSurface };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Modal, Modal as ModalComponent } from './modal.js';
|
|
2
|
+
export type { ModalProps } from './types.js';
|
|
3
|
+
export { Header, Header as ModalHeader } from './components/header/index.js';
|
|
4
|
+
export type { HeaderProps } from './components/header/index.js';
|
|
5
|
+
export { Content, Content as ModalContent } from './components/content/index.js';
|
|
6
|
+
export type { ContentProps } from './components/content/index.js';
|
|
7
|
+
export { Footer, Footer as ModalFooter } from './components/footer/index.js';
|
|
8
|
+
export type { FooterProps } from './components/footer/index.js';
|
|
9
|
+
export { Controls, Controls as ModalControls } from './components/controls/index.js';
|
|
10
|
+
export type { ControlsProps } from './components/controls/index.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Modal, Modal as ModalComponent } from "./modal.js";
|
|
2
|
+
export { Header, Header as ModalHeader } from "./components/header/index.js";
|
|
3
|
+
export { Content, Content as ModalContent } from "./components/content/index.js";
|
|
4
|
+
export { Footer, Footer as ModalFooter } from "./components/footer/index.js";
|
|
5
|
+
export { Controls, Controls as ModalControls } from "./components/controls/index.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "./index_module.css";
|
|
2
|
+
const index_module = {
|
|
3
|
+
component: "component-kCkvk3",
|
|
4
|
+
"size-480": "size-480-fXx4Vl",
|
|
5
|
+
size480: "size-480-fXx4Vl",
|
|
6
|
+
"size-600": "size-600-X9nvTO",
|
|
7
|
+
size600: "size-600-X9nvTO",
|
|
8
|
+
"size-720": "size-720-_guzbs",
|
|
9
|
+
size720: "size-720-_guzbs",
|
|
10
|
+
"size-960": "size-960-I8vXpb",
|
|
11
|
+
size960: "size-960-I8vXpb",
|
|
12
|
+
"size-fullscreen": "size-fullscreen-exf2cb",
|
|
13
|
+
sizeFullscreen: "size-fullscreen-exf2cb"
|
|
14
|
+
};
|
|
15
|
+
export default index_module;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@import "../../styles/theme.css";
|
|
2
|
+
@import "./vars.css";
|
|
3
|
+
|
|
4
|
+
.component-kCkvk3 {
|
|
5
|
+
border-radius: var(--border-radius-12, 12px);
|
|
6
|
+
max-width: calc(100vw - var(--size-32, 32px));
|
|
7
|
+
max-height: calc(100dvh - var(--size-32, 32px));
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
font-family: var(--font-primary);
|
|
11
|
+
box-shadow: var(--modal-shadow);
|
|
12
|
+
border: none;
|
|
13
|
+
margin: auto;
|
|
14
|
+
padding: 0;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
|
|
17
|
+
&[open] {
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
display: flex;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.size-480-fXx4Vl {
|
|
24
|
+
width: 480px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.size-600-X9nvTO {
|
|
28
|
+
width: 600px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.size-720-_guzbs {
|
|
32
|
+
width: 720px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.size-960-I8vXpb {
|
|
36
|
+
width: 960px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.size-fullscreen-exf2cb {
|
|
40
|
+
border-radius: 0;
|
|
41
|
+
width: 100vw;
|
|
42
|
+
max-width: none;
|
|
43
|
+
height: 100dvh;
|
|
44
|
+
max-height: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const Modal: import("react").ForwardRefExoticComponent<Omit<import("react").DialogHTMLAttributes<HTMLDialogElement>, "open" | "onClose" | "onCancel"> & {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onClose?: VoidFunction;
|
|
4
|
+
size?: 480 | 600 | 720 | 960 | "fullscreen";
|
|
5
|
+
escapeKeyDown?: boolean;
|
|
6
|
+
backdropClick?: boolean;
|
|
7
|
+
lockScroll?: boolean;
|
|
8
|
+
getPortalContainer?: () => Element | null;
|
|
9
|
+
dataTestId?: string;
|
|
10
|
+
} & import("react").RefAttributes<HTMLDialogElement>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Portal } from "../Portal/index.js";
|
|
4
|
+
import { ModalSurface } from "./components/modal-surface/index.js";
|
|
5
|
+
const Modal = /*#__PURE__*/ forwardRef(({ open, getPortalContainer, ...restProps }, ref)=>open ? /*#__PURE__*/ jsx(Portal, {
|
|
6
|
+
getPortalContainer: getPortalContainer,
|
|
7
|
+
children: /*#__PURE__*/ jsx(ModalSurface, {
|
|
8
|
+
...restProps,
|
|
9
|
+
ref: ref
|
|
10
|
+
})
|
|
11
|
+
}) : null);
|
|
12
|
+
Modal.displayName = 'Modal';
|
|
13
|
+
export { Modal };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DialogHTMLAttributes } from 'react';
|
|
2
|
+
export type ModalProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose' | 'onCancel'> & {
|
|
3
|
+
/**
|
|
4
|
+
* Видимость окна. Закрытие запрашивается через onClose.
|
|
5
|
+
*/
|
|
6
|
+
open: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Обработчик запроса закрытия. Обновите open в вызывающем компоненте.
|
|
9
|
+
* Вызывается в том числе кнопкой закрытия Header.
|
|
10
|
+
*/
|
|
11
|
+
onClose?: VoidFunction;
|
|
12
|
+
/**
|
|
13
|
+
* Ширина в пикселях или окно на весь экран.
|
|
14
|
+
* @default 720
|
|
15
|
+
*/
|
|
16
|
+
size?: 480 | 600 | 720 | 960 | 'fullscreen';
|
|
17
|
+
/**
|
|
18
|
+
* Разрешить закрытие по Escape.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
escapeKeyDown?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Разрешить закрытие по подложке.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
backdropClick?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Блокировать прокрутку страницы.
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
lockScroll?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Контейнер портала. По умолчанию используется body текущего документа.
|
|
34
|
+
*/
|
|
35
|
+
getPortalContainer?: () => Element | null;
|
|
36
|
+
/**
|
|
37
|
+
* Идентификатор для автоматизированных тестов.
|
|
38
|
+
* Части наследуют его с суффиксами -header, -content, -footer и -controls.
|
|
39
|
+
*/
|
|
40
|
+
dataTestId?: string;
|
|
41
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lockScroll(document: Document): () => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const locks = new WeakMap();
|
|
2
|
+
function lockScroll(document) {
|
|
3
|
+
const { body, documentElement, defaultView } = document;
|
|
4
|
+
const existing = locks.get(document);
|
|
5
|
+
if (existing) existing.count += 1;
|
|
6
|
+
else {
|
|
7
|
+
locks.set(document, {
|
|
8
|
+
count: 1,
|
|
9
|
+
overflow: body.style.overflow,
|
|
10
|
+
paddingRight: body.style.paddingRight
|
|
11
|
+
});
|
|
12
|
+
const scrollbarWidth = (defaultView?.innerWidth ?? 0) - documentElement.clientWidth;
|
|
13
|
+
const hasStableGutter = defaultView?.getComputedStyle(documentElement).scrollbarGutter?.split(/\s+/).includes('stable');
|
|
14
|
+
if (scrollbarWidth > 0 && defaultView && !hasStableGutter) {
|
|
15
|
+
const paddingRight = parseFloat(defaultView.getComputedStyle(body).paddingRight) || 0;
|
|
16
|
+
body.style.paddingRight = `${paddingRight + scrollbarWidth}px`;
|
|
17
|
+
}
|
|
18
|
+
body.style.overflow = 'hidden';
|
|
19
|
+
}
|
|
20
|
+
return ()=>{
|
|
21
|
+
const lock = locks.get(document);
|
|
22
|
+
if (!lock) return;
|
|
23
|
+
lock.count -= 1;
|
|
24
|
+
if (0 === lock.count) {
|
|
25
|
+
body.style.overflow = lock.overflow;
|
|
26
|
+
body.style.paddingRight = lock.paddingRight;
|
|
27
|
+
locks.delete(document);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export { lockScroll };
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { Portal } from "../Portal/index.js";
|
|
4
4
|
import { PopoverSurface } from "./components/popover-surface/index.js";
|
|
5
5
|
const Popover = /*#__PURE__*/ forwardRef(({ open, anchorElement, getPortalContainer, ...restProps }, ref)=>{
|
|
6
6
|
if (!open || !anchorElement) return null;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
return /*#__PURE__*/ jsx(Portal, {
|
|
8
|
+
getPortalContainer: getPortalContainer ?? (()=>anchorElement.ownerDocument.body),
|
|
9
|
+
children: /*#__PURE__*/ jsx(PopoverSurface, {
|
|
10
|
+
...restProps,
|
|
11
|
+
anchorElement: anchorElement,
|
|
12
|
+
ref: ref
|
|
13
|
+
})
|
|
14
|
+
});
|
|
13
15
|
});
|
|
14
16
|
Popover.displayName = 'Popover';
|
|
15
17
|
export { Popover };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Portal } from "./portal.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
function getBody() {
|
|
4
|
+
return "u" < typeof document ? null : document.body;
|
|
5
|
+
}
|
|
6
|
+
const Portal = /*#__PURE__*/ forwardRef(({ children, getPortalContainer = getBody, immediateMount = true }, ref)=>{
|
|
7
|
+
const [deferredContainer, setDeferredContainer] = useState(null);
|
|
8
|
+
let container = deferredContainer;
|
|
9
|
+
if (immediateMount) container = "u" < typeof document ? null : getPortalContainer() ?? null;
|
|
10
|
+
useEffect(()=>{
|
|
11
|
+
if (!immediateMount) setDeferredContainer(getPortalContainer() ?? null);
|
|
12
|
+
});
|
|
13
|
+
useImperativeHandle(ref, ()=>container, [
|
|
14
|
+
container
|
|
15
|
+
]);
|
|
16
|
+
return container ? /*#__PURE__*/ createPortal(children, container) : null;
|
|
17
|
+
});
|
|
18
|
+
Portal.displayName = 'Portal';
|
|
19
|
+
export { Portal };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type PortalProps = {
|
|
3
|
+
/**
|
|
4
|
+
* Содержимое, которое будет перенесено в контейнер портала без дополнительной обёртки.
|
|
5
|
+
*/
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Контейнер портала. По умолчанию используется body текущего документа.
|
|
9
|
+
* Если функция возвращает null или undefined, содержимое не рендерится.
|
|
10
|
+
*/
|
|
11
|
+
getPortalContainer?: () => Element | null | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Рендерить содержимое сразу, если контейнер уже существует.
|
|
14
|
+
* При false контейнер определяется после монтирования, чтобы дождаться DOM-контейнера из ref.
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
immediateMount?: boolean;
|
|
18
|
+
};
|
|
File without changes
|
package/dist/index.d.ts
CHANGED
|
@@ -8,3 +8,7 @@ export { Popover } from './components/Popover/index.js';
|
|
|
8
8
|
export type { PopoverProps, PopoverPosition } from './components/Popover/index.js';
|
|
9
9
|
export { Collapse } from './components/Collapse/index.js';
|
|
10
10
|
export type { CollapseProps } from './components/Collapse/index.js';
|
|
11
|
+
export { Modal, ModalComponent, ModalHeader, ModalContent, ModalFooter, ModalControls, } from './components/Modal/index.js';
|
|
12
|
+
export { Portal } from './components/Portal/index.js';
|
|
13
|
+
export type { PortalProps } from './components/Portal/index.js';
|
|
14
|
+
export type { ModalProps, HeaderProps as ModalHeaderProps, ContentProps as ModalContentProps, FooterProps as ModalFooterProps, ControlsProps as ModalControlsProps, } from './components/Modal/index.js';
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,5 @@ export { Spinner } from "./components/Spinner/index.js";
|
|
|
3
3
|
export { Text, Title, Typography, TypographyText, TypographyTitle } from "./components/Typography/index.js";
|
|
4
4
|
export { Popover } from "./components/Popover/index.js";
|
|
5
5
|
export { Collapse } from "./components/Collapse/index.js";
|
|
6
|
+
export { Modal, ModalComponent, ModalContent, ModalControls, ModalFooter, ModalHeader } from "./components/Modal/index.js";
|
|
7
|
+
export { Portal } from "./components/Portal/index.js";
|