@hsu-react/ui 2.5.7 → 2.5.8
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/es/components/Modal/index.d.ts +12 -0
- package/es/components/Modal/index.js +22 -1
- package/es/components/Modal/index.module.scss +7 -0
- package/es/layout/Menu/index.module.scss +32 -0
- package/es/styles/tokens.d.ts +10 -0
- package/es/styles/tokens.js +11 -0
- package/es/styles/tokens.json +1 -0
- package/lib/components/Modal/index.d.ts +12 -0
- package/lib/components/Modal/index.js +22 -0
- package/lib/components/Modal/index.module.scss +7 -0
- package/lib/layout/Menu/index.module.scss +32 -0
- package/lib/styles/tokens.d.ts +10 -0
- package/lib/styles/tokens.js +12 -1
- package/lib/styles/tokens.json +1 -0
- package/package.json +1 -1
- package/src/components/Modal/index.md +1 -0
- package/src/components/Modal/index.module.scss +7 -0
- package/src/components/Modal/index.tsx +40 -0
- package/src/components/Modal/minHeight.test.tsx +105 -0
- package/src/layout/Menu/index.module.scss +32 -0
- package/src/layout/Menu/scrollGutter.test.ts +50 -0
- package/src/styles/tokens.json +1 -0
- package/src/styles/tokens.ts +11 -0
|
@@ -6,6 +6,18 @@ export interface ModalProps extends AntdModalProps {
|
|
|
6
6
|
edgeDetection?: boolean;
|
|
7
7
|
full?: boolean;
|
|
8
8
|
titleButtonGroup?: ButtonProps[];
|
|
9
|
+
/**
|
|
10
|
+
* 高度**下界**。给「内容要等接口回来才画得出」的弹窗用:不给下界的话,
|
|
11
|
+
* 打开那一刻只有标题栏那么高,数据回来再撑开 —— 视觉上跳一下。
|
|
12
|
+
*
|
|
13
|
+
* - `true`:用标准档(`modalMinHeight`,700px),后台详情 / 列表 / 记录类弹窗都用它
|
|
14
|
+
* - 数字:px;字符串:原样当 CSS 长度
|
|
15
|
+
* - 不传:维持内容自适应 —— **短表单不要传**,否则平白留出一大片空白
|
|
16
|
+
*
|
|
17
|
+
* 它只是下界:内容更高照常撑开、照常滚动;并且在样式里与 `90vh` 取小,
|
|
18
|
+
* 矮窗口下不会被顶出屏幕。
|
|
19
|
+
*/
|
|
20
|
+
minHeight?: number | string | boolean;
|
|
9
21
|
}
|
|
10
22
|
interface ModalFC extends React.FC<ModalProps> {
|
|
11
23
|
confirm: typeof AntdModal.confirm;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
2
|
-
var _excluded = ["moveable", "className", "classNames", "open", "onCancel", "onOk", "afterClose", "edgeDetection", "full", "destroyOnClose", "destroyOnHidden", "footer", "title", "titleButtonGroup"];
|
|
2
|
+
var _excluded = ["moveable", "className", "classNames", "styles", "minHeight", "open", "onCancel", "onOk", "afterClose", "edgeDetection", "full", "destroyOnClose", "destroyOnHidden", "footer", "title", "titleButtonGroup"];
|
|
3
3
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
4
4
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
5
5
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
@@ -14,14 +14,23 @@ import { useModalElements, useModalDrag } from "./_hooks";
|
|
|
14
14
|
import Button from "../Button";
|
|
15
15
|
import { mergeSemantic } from "../../utils/semantic";
|
|
16
16
|
import { modalFuncs } from "../../feedback";
|
|
17
|
+
import { modalMinHeight } from "../../styles/tokens";
|
|
17
18
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
19
|
import { Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
20
|
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
21
|
+
/** `minHeight` prop → CSS 长度。`true` 走标准档,`false` / 不传等于不设下界 */
|
|
22
|
+
var resolveMinHeight = function resolveMinHeight(minHeight) {
|
|
23
|
+
if (minHeight === undefined || minHeight === false) return undefined;
|
|
24
|
+
if (minHeight === true) return "".concat(modalMinHeight, "px");
|
|
25
|
+
return typeof minHeight === "number" ? "".concat(minHeight, "px") : minHeight;
|
|
26
|
+
};
|
|
20
27
|
var Modal = function Modal(props) {
|
|
21
28
|
var _props$moveable = props.moveable,
|
|
22
29
|
moveable = _props$moveable === void 0 ? true : _props$moveable,
|
|
23
30
|
className = props.className,
|
|
24
31
|
classNames = props.classNames,
|
|
32
|
+
outerStyles = props.styles,
|
|
33
|
+
minHeight = props.minHeight,
|
|
25
34
|
open = props.open,
|
|
26
35
|
onCancel = props.onCancel,
|
|
27
36
|
onOk = props.onOk,
|
|
@@ -65,6 +74,7 @@ var Modal = function Modal(props) {
|
|
|
65
74
|
var handleOk = function handleOk(e) {
|
|
66
75
|
onOk === null || onOk === void 0 || onOk(e);
|
|
67
76
|
};
|
|
77
|
+
var resolvedMinHeight = resolveMinHeight(minHeight);
|
|
68
78
|
return /*#__PURE__*/_jsx(AntdModal, _objectSpread(_objectSpread({
|
|
69
79
|
centered: true
|
|
70
80
|
}, moadlConfig), {}, {
|
|
@@ -90,6 +100,17 @@ var Modal = function Modal(props) {
|
|
|
90
100
|
body: "".concat(styles.body, " ").concat((_outer$body = outer.body) !== null && _outer$body !== void 0 ? _outer$body : ""),
|
|
91
101
|
footer: "".concat(styles.footer, " ").concat((_outer$footer = outer.footer) !== null && _outer$footer !== void 0 ? _outer$footer : "", " ").concat(footer === false ? styles.noFooter : "")
|
|
92
102
|
});
|
|
103
|
+
})
|
|
104
|
+
/* 下界通过 CSS 变量交给样式表,而不是直接写 `minHeight` 内联样式:
|
|
105
|
+
clamp(与 90vh 取小)必须在 CSS 里做——`min-height` 在 CSS 里赢过
|
|
106
|
+
`max-height`,内联写死的话矮窗口会被顶出屏幕 */,
|
|
107
|
+
styles: mergeSemantic(outerStyles, function (outer) {
|
|
108
|
+
var _outer$container2;
|
|
109
|
+
return _objectSpread(_objectSpread({}, outer), {}, {
|
|
110
|
+
container: _objectSpread(_objectSpread({}, (_outer$container2 = outer.container) !== null && _outer$container2 !== void 0 ? _outer$container2 : {}), resolvedMinHeight ? {
|
|
111
|
+
"--vita-modal-min-height": resolvedMinHeight
|
|
112
|
+
} : {})
|
|
113
|
+
});
|
|
93
114
|
}),
|
|
94
115
|
footer: footer,
|
|
95
116
|
afterClose: function afterClose() {
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
width: 100%;
|
|
32
32
|
height: 100%;
|
|
33
33
|
max-height: 90vh;
|
|
34
|
+
// 高度下界,由 `<Modal minHeight>` 写入 `--vita-modal-min-height`(不传就是 0)。
|
|
35
|
+
// 内容异步加载的弹窗不给下界的话,打开那一刻只有标题栏那么高,数据回来再撑开。
|
|
36
|
+
//
|
|
37
|
+
// 必须在这里与 90vh 取小:CSS 里 `min-height` 的优先级高于 `max-height`,
|
|
38
|
+
// 直接写 `min-height: 700px` 会让上面那行 `max-height: 90vh` 在矮窗口上失效,
|
|
39
|
+
// 弹窗被顶出屏幕。
|
|
40
|
+
min-height: min(var(--vita-modal-min-height, 0px), 90vh);
|
|
34
41
|
padding: 0px 0px 15px;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -28,6 +28,38 @@
|
|
|
28
28
|
overflow-y: auto;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* ===== 滚动条占位不许吃掉菜单项的内缩 =====
|
|
32
|
+
上面那条规则把「谁滚」定在了菜单根上,但没有把几何契约收完:滚动条一旦
|
|
33
|
+
**实占宽度**(消费方常见做法:槽宽恒定、只切 thumb 颜色,这样悬浮时不抖),
|
|
34
|
+
它就从菜单根的内容盒里切走一条 —— 而菜单项的左右外边距量的正是这个内容盒。
|
|
35
|
+
于是展开子菜单、菜单一超高,右边多出一条空白,左边纹丝不动:
|
|
36
|
+
|
|
37
|
+
展开前 菜单项 8..220(左留白 8 / 右留白 10)
|
|
38
|
+
展开后 菜单项 8..206(左留白 8 / 右留白 24)← 展开的那一项看着「右边距变大了」
|
|
39
|
+
|
|
40
|
+
`scrollbar-gutter: stable both-edges` 把这条槽**两侧对称保留**,且不论当下滚不滚
|
|
41
|
+
都保留:左右留白从此相等,展开 / 收起也不再有宽度变化(连带把「展开那一瞬内容
|
|
42
|
+
重排」一并去掉)。槽宽由浏览器按该元素实际的滚动条算,**不是写死的 14px** ——
|
|
43
|
+
消费方把滚动条改窄改宽,留白跟着变。
|
|
44
|
+
|
|
45
|
+
配套把菜单项自己的左右外边距清零:留白改由这条槽提供,不再叠加。不清的话
|
|
46
|
+
留白变成 `8 + 槽宽`(实测 22/24),230px 的侧栏里「MCP 连接器」直接被截断。
|
|
47
|
+
纵向节奏与嵌套缩进不受影响 —— antd 的行内缩进走的是 padding,不是 margin。
|
|
48
|
+
|
|
49
|
+
`@supports` 兜底:不认这个属性的浏览器维持原样,不会两头落空。 */
|
|
50
|
+
@supports (scrollbar-gutter: stable both-edges) {
|
|
51
|
+
&:global(.ant-menu-inline) {
|
|
52
|
+
scrollbar-gutter: stable both-edges;
|
|
53
|
+
|
|
54
|
+
:global(.ant-menu-item),
|
|
55
|
+
:global(.ant-menu-submenu-title) {
|
|
56
|
+
width: 100%;
|
|
57
|
+
|
|
58
|
+
margin-inline: 0px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
/* 子菜单在展开 / 收起动画期间被 rc-motion 逐帧改高度。antd 的
|
|
32
64
|
`.ant-motion-collapse` 自带 overflow hidden,这里补的是**动画结束、类名摘掉
|
|
33
65
|
之后**那一帧:此时 ul 的高度刚回到 auto,若父级恰好差几像素,浏览器会先画出
|
package/es/styles/tokens.d.ts
CHANGED
|
@@ -93,6 +93,16 @@ export declare const modalWidth: {
|
|
|
93
93
|
md: number;
|
|
94
94
|
lg: number;
|
|
95
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* 内容异步加载的弹窗的**高度下界**(`<Modal minHeight>` 的默认档)。
|
|
98
|
+
*
|
|
99
|
+
* 取 700 不是拍的:`Panel.List.Modal` 从一开始就把自己钉在 `height: 700px`,那是
|
|
100
|
+
* 「搜索行 + 工具条 + 表头 + 十行左右 + 分页」放得下的高度。详情 / 消息 / 记录这类
|
|
101
|
+
* 弹窗与它同处一套后台,用同一个下界,纵向节奏才是一套的。
|
|
102
|
+
*
|
|
103
|
+
* 它只是**下界**:内容更高照常撑开,并且在 CSS 里与 `90vh` 取小,矮窗口不会被顶出屏幕。
|
|
104
|
+
*/
|
|
105
|
+
export declare const modalMinHeight: number;
|
|
96
106
|
/**
|
|
97
107
|
* 断点。与 `styles/_responsive.scss` 的 mixin 同源,所以 JS 判断不会和 CSS 媒体查询错开。
|
|
98
108
|
*/
|
package/es/styles/tokens.js
CHANGED
|
@@ -63,6 +63,17 @@ export var controlTokens = raw.control;
|
|
|
63
63
|
*/
|
|
64
64
|
export var modalWidth = raw.modalWidth;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 内容异步加载的弹窗的**高度下界**(`<Modal minHeight>` 的默认档)。
|
|
68
|
+
*
|
|
69
|
+
* 取 700 不是拍的:`Panel.List.Modal` 从一开始就把自己钉在 `height: 700px`,那是
|
|
70
|
+
* 「搜索行 + 工具条 + 表头 + 十行左右 + 分页」放得下的高度。详情 / 消息 / 记录这类
|
|
71
|
+
* 弹窗与它同处一套后台,用同一个下界,纵向节奏才是一套的。
|
|
72
|
+
*
|
|
73
|
+
* 它只是**下界**:内容更高照常撑开,并且在 CSS 里与 `90vh` 取小,矮窗口不会被顶出屏幕。
|
|
74
|
+
*/
|
|
75
|
+
export var modalMinHeight = raw.modalMinHeight;
|
|
76
|
+
|
|
66
77
|
/**
|
|
67
78
|
* 断点。与 `styles/_responsive.scss` 的 mixin 同源,所以 JS 判断不会和 CSS 媒体查询错开。
|
|
68
79
|
*/
|
package/es/styles/tokens.json
CHANGED
|
@@ -6,6 +6,18 @@ export interface ModalProps extends AntdModalProps {
|
|
|
6
6
|
edgeDetection?: boolean;
|
|
7
7
|
full?: boolean;
|
|
8
8
|
titleButtonGroup?: ButtonProps[];
|
|
9
|
+
/**
|
|
10
|
+
* 高度**下界**。给「内容要等接口回来才画得出」的弹窗用:不给下界的话,
|
|
11
|
+
* 打开那一刻只有标题栏那么高,数据回来再撑开 —— 视觉上跳一下。
|
|
12
|
+
*
|
|
13
|
+
* - `true`:用标准档(`modalMinHeight`,700px),后台详情 / 列表 / 记录类弹窗都用它
|
|
14
|
+
* - 数字:px;字符串:原样当 CSS 长度
|
|
15
|
+
* - 不传:维持内容自适应 —— **短表单不要传**,否则平白留出一大片空白
|
|
16
|
+
*
|
|
17
|
+
* 它只是下界:内容更高照常撑开、照常滚动;并且在样式里与 `90vh` 取小,
|
|
18
|
+
* 矮窗口下不会被顶出屏幕。
|
|
19
|
+
*/
|
|
20
|
+
minHeight?: number | string | boolean;
|
|
9
21
|
}
|
|
10
22
|
interface ModalFC extends React.FC<ModalProps> {
|
|
11
23
|
confirm: typeof AntdModal.confirm;
|
|
@@ -11,13 +11,22 @@ var _hooks = require("./_hooks");
|
|
|
11
11
|
var _Button = _interopRequireDefault(require("../Button"));
|
|
12
12
|
var _semantic = require("../../utils/semantic");
|
|
13
13
|
var _feedback = require("../../feedback");
|
|
14
|
+
var _tokens = require("../../styles/tokens");
|
|
14
15
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
15
16
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
+
/** `minHeight` prop → CSS 长度。`true` 走标准档,`false` / 不传等于不设下界 */
|
|
18
|
+
const resolveMinHeight = minHeight => {
|
|
19
|
+
if (minHeight === undefined || minHeight === false) return undefined;
|
|
20
|
+
if (minHeight === true) return `${_tokens.modalMinHeight}px`;
|
|
21
|
+
return typeof minHeight === "number" ? `${minHeight}px` : minHeight;
|
|
22
|
+
};
|
|
16
23
|
const Modal = props => {
|
|
17
24
|
const {
|
|
18
25
|
moveable = true,
|
|
19
26
|
className,
|
|
20
27
|
classNames,
|
|
28
|
+
styles: outerStyles,
|
|
29
|
+
minHeight,
|
|
21
30
|
open,
|
|
22
31
|
onCancel,
|
|
23
32
|
onOk,
|
|
@@ -62,6 +71,7 @@ const Modal = props => {
|
|
|
62
71
|
const handleOk = e => {
|
|
63
72
|
onOk?.(e);
|
|
64
73
|
};
|
|
74
|
+
const resolvedMinHeight = resolveMinHeight(minHeight);
|
|
65
75
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.Modal, {
|
|
66
76
|
centered: true,
|
|
67
77
|
...moadlConfig,
|
|
@@ -85,6 +95,18 @@ const Modal = props => {
|
|
|
85
95
|
container: `${_indexModule.default.content} ${outer.container ?? ""}`,
|
|
86
96
|
body: `${_indexModule.default.body} ${outer.body ?? ""}`,
|
|
87
97
|
footer: `${_indexModule.default.footer} ${outer.footer ?? ""} ${footer === false ? _indexModule.default.noFooter : ""}`
|
|
98
|
+
}))
|
|
99
|
+
/* 下界通过 CSS 变量交给样式表,而不是直接写 `minHeight` 内联样式:
|
|
100
|
+
clamp(与 90vh 取小)必须在 CSS 里做——`min-height` 在 CSS 里赢过
|
|
101
|
+
`max-height`,内联写死的话矮窗口会被顶出屏幕 */,
|
|
102
|
+
styles: (0, _semantic.mergeSemantic)(outerStyles, outer => ({
|
|
103
|
+
...outer,
|
|
104
|
+
container: {
|
|
105
|
+
...(outer.container ?? {}),
|
|
106
|
+
...(resolvedMinHeight ? {
|
|
107
|
+
"--vita-modal-min-height": resolvedMinHeight
|
|
108
|
+
} : {})
|
|
109
|
+
}
|
|
88
110
|
})),
|
|
89
111
|
footer: footer,
|
|
90
112
|
afterClose: () => {
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
width: 100%;
|
|
32
32
|
height: 100%;
|
|
33
33
|
max-height: 90vh;
|
|
34
|
+
// 高度下界,由 `<Modal minHeight>` 写入 `--vita-modal-min-height`(不传就是 0)。
|
|
35
|
+
// 内容异步加载的弹窗不给下界的话,打开那一刻只有标题栏那么高,数据回来再撑开。
|
|
36
|
+
//
|
|
37
|
+
// 必须在这里与 90vh 取小:CSS 里 `min-height` 的优先级高于 `max-height`,
|
|
38
|
+
// 直接写 `min-height: 700px` 会让上面那行 `max-height: 90vh` 在矮窗口上失效,
|
|
39
|
+
// 弹窗被顶出屏幕。
|
|
40
|
+
min-height: min(var(--vita-modal-min-height, 0px), 90vh);
|
|
34
41
|
padding: 0px 0px 15px;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -28,6 +28,38 @@
|
|
|
28
28
|
overflow-y: auto;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* ===== 滚动条占位不许吃掉菜单项的内缩 =====
|
|
32
|
+
上面那条规则把「谁滚」定在了菜单根上,但没有把几何契约收完:滚动条一旦
|
|
33
|
+
**实占宽度**(消费方常见做法:槽宽恒定、只切 thumb 颜色,这样悬浮时不抖),
|
|
34
|
+
它就从菜单根的内容盒里切走一条 —— 而菜单项的左右外边距量的正是这个内容盒。
|
|
35
|
+
于是展开子菜单、菜单一超高,右边多出一条空白,左边纹丝不动:
|
|
36
|
+
|
|
37
|
+
展开前 菜单项 8..220(左留白 8 / 右留白 10)
|
|
38
|
+
展开后 菜单项 8..206(左留白 8 / 右留白 24)← 展开的那一项看着「右边距变大了」
|
|
39
|
+
|
|
40
|
+
`scrollbar-gutter: stable both-edges` 把这条槽**两侧对称保留**,且不论当下滚不滚
|
|
41
|
+
都保留:左右留白从此相等,展开 / 收起也不再有宽度变化(连带把「展开那一瞬内容
|
|
42
|
+
重排」一并去掉)。槽宽由浏览器按该元素实际的滚动条算,**不是写死的 14px** ——
|
|
43
|
+
消费方把滚动条改窄改宽,留白跟着变。
|
|
44
|
+
|
|
45
|
+
配套把菜单项自己的左右外边距清零:留白改由这条槽提供,不再叠加。不清的话
|
|
46
|
+
留白变成 `8 + 槽宽`(实测 22/24),230px 的侧栏里「MCP 连接器」直接被截断。
|
|
47
|
+
纵向节奏与嵌套缩进不受影响 —— antd 的行内缩进走的是 padding,不是 margin。
|
|
48
|
+
|
|
49
|
+
`@supports` 兜底:不认这个属性的浏览器维持原样,不会两头落空。 */
|
|
50
|
+
@supports (scrollbar-gutter: stable both-edges) {
|
|
51
|
+
&:global(.ant-menu-inline) {
|
|
52
|
+
scrollbar-gutter: stable both-edges;
|
|
53
|
+
|
|
54
|
+
:global(.ant-menu-item),
|
|
55
|
+
:global(.ant-menu-submenu-title) {
|
|
56
|
+
width: 100%;
|
|
57
|
+
|
|
58
|
+
margin-inline: 0px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
/* 子菜单在展开 / 收起动画期间被 rc-motion 逐帧改高度。antd 的
|
|
32
64
|
`.ant-motion-collapse` 自带 overflow hidden,这里补的是**动画结束、类名摘掉
|
|
33
65
|
之后**那一帧:此时 ul 的高度刚回到 auto,若父级恰好差几像素,浏览器会先画出
|
package/lib/styles/tokens.d.ts
CHANGED
|
@@ -93,6 +93,16 @@ export declare const modalWidth: {
|
|
|
93
93
|
md: number;
|
|
94
94
|
lg: number;
|
|
95
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* 内容异步加载的弹窗的**高度下界**(`<Modal minHeight>` 的默认档)。
|
|
98
|
+
*
|
|
99
|
+
* 取 700 不是拍的:`Panel.List.Modal` 从一开始就把自己钉在 `height: 700px`,那是
|
|
100
|
+
* 「搜索行 + 工具条 + 表头 + 十行左右 + 分页」放得下的高度。详情 / 消息 / 记录这类
|
|
101
|
+
* 弹窗与它同处一套后台,用同一个下界,纵向节奏才是一套的。
|
|
102
|
+
*
|
|
103
|
+
* 它只是**下界**:内容更高照常撑开,并且在 CSS 里与 `90vh` 取小,矮窗口不会被顶出屏幕。
|
|
104
|
+
*/
|
|
105
|
+
export declare const modalMinHeight: number;
|
|
96
106
|
/**
|
|
97
107
|
* 断点。与 `styles/_responsive.scss` 的 mixin 同源,所以 JS 判断不会和 CSS 媒体查询错开。
|
|
98
108
|
*/
|
package/lib/styles/tokens.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.withAlpha = exports.toAntdTheme = exports.toAntdComponents = exports.radius = exports.modalWidth = exports.lightTokens = exports.fontTokens = exports.defaultPrimaryColor = exports.darkTokens = exports.controlTokens = exports.breakpoints = void 0;
|
|
6
|
+
exports.withAlpha = exports.toAntdTheme = exports.toAntdComponents = exports.radius = exports.modalWidth = exports.modalMinHeight = exports.lightTokens = exports.fontTokens = exports.defaultPrimaryColor = exports.darkTokens = exports.controlTokens = exports.breakpoints = void 0;
|
|
7
7
|
var _tokens = _interopRequireDefault(require("./tokens.json"));
|
|
8
8
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
9
9
|
/**
|
|
@@ -56,6 +56,17 @@ const controlTokens = exports.controlTokens = _tokens.default.control;
|
|
|
56
56
|
*/
|
|
57
57
|
const modalWidth = exports.modalWidth = _tokens.default.modalWidth;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 内容异步加载的弹窗的**高度下界**(`<Modal minHeight>` 的默认档)。
|
|
61
|
+
*
|
|
62
|
+
* 取 700 不是拍的:`Panel.List.Modal` 从一开始就把自己钉在 `height: 700px`,那是
|
|
63
|
+
* 「搜索行 + 工具条 + 表头 + 十行左右 + 分页」放得下的高度。详情 / 消息 / 记录这类
|
|
64
|
+
* 弹窗与它同处一套后台,用同一个下界,纵向节奏才是一套的。
|
|
65
|
+
*
|
|
66
|
+
* 它只是**下界**:内容更高照常撑开,并且在 CSS 里与 `90vh` 取小,矮窗口不会被顶出屏幕。
|
|
67
|
+
*/
|
|
68
|
+
const modalMinHeight = exports.modalMinHeight = _tokens.default.modalMinHeight;
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* 断点。与 `styles/_responsive.scss` 的 mixin 同源,所以 JS 判断不会和 CSS 媒体查询错开。
|
|
61
72
|
*/
|
package/lib/styles/tokens.json
CHANGED
package/package.json
CHANGED
|
@@ -74,6 +74,7 @@ Modal.config
|
|
|
74
74
|
| edgeDetection | 拖拽时是否进行边缘检测,防止拖出视口 | `boolean` | `true` |
|
|
75
75
|
| full | 是否以全屏方式展示 | `boolean` | `false` |
|
|
76
76
|
| titleButtonGroup | 标题区右侧的按钮组配置 | `ButtonProps[]` | - |
|
|
77
|
+
| minHeight | 高度**下界**,给内容要等接口回来才画得出的弹窗用(详情 / 消息 / 记录)。`true` 走标准档 700px;数字按 px;字符串原样当 CSS 长度。只是下界,内容更高照常撑开与滚动,并且与 `90vh` 取小,矮窗口不会被顶出屏幕。**短表单不要传**,否则平白留一片空白 | `number \| string \| boolean` | - |
|
|
77
78
|
|
|
78
79
|
> 其余属性(`open`、`onOk`、`onCancel`、`footer`、`width` 等)与 antd `Modal` 一致。
|
|
79
80
|
>
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
width: 100%;
|
|
32
32
|
height: 100%;
|
|
33
33
|
max-height: 90vh;
|
|
34
|
+
// 高度下界,由 `<Modal minHeight>` 写入 `--vita-modal-min-height`(不传就是 0)。
|
|
35
|
+
// 内容异步加载的弹窗不给下界的话,打开那一刻只有标题栏那么高,数据回来再撑开。
|
|
36
|
+
//
|
|
37
|
+
// 必须在这里与 90vh 取小:CSS 里 `min-height` 的优先级高于 `max-height`,
|
|
38
|
+
// 直接写 `min-height: 700px` 会让上面那行 `max-height: 90vh` 在矮窗口上失效,
|
|
39
|
+
// 弹窗被顶出屏幕。
|
|
40
|
+
min-height: min(var(--vita-modal-min-height, 0px), 90vh);
|
|
34
41
|
padding: 0px 0px 15px;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -5,14 +5,36 @@ import { useModalElements, useModalDrag } from "./_hooks";
|
|
|
5
5
|
import Button, { ButtonProps } from "../Button";
|
|
6
6
|
import { mergeSemantic } from "../../utils/semantic";
|
|
7
7
|
import { modalFuncs } from "../../feedback";
|
|
8
|
+
import { modalMinHeight } from "../../styles/tokens";
|
|
8
9
|
|
|
9
10
|
export interface ModalProps extends AntdModalProps {
|
|
10
11
|
moveable?: boolean;
|
|
11
12
|
edgeDetection?: boolean;
|
|
12
13
|
full?: boolean;
|
|
13
14
|
titleButtonGroup?: ButtonProps[];
|
|
15
|
+
/**
|
|
16
|
+
* 高度**下界**。给「内容要等接口回来才画得出」的弹窗用:不给下界的话,
|
|
17
|
+
* 打开那一刻只有标题栏那么高,数据回来再撑开 —— 视觉上跳一下。
|
|
18
|
+
*
|
|
19
|
+
* - `true`:用标准档(`modalMinHeight`,700px),后台详情 / 列表 / 记录类弹窗都用它
|
|
20
|
+
* - 数字:px;字符串:原样当 CSS 长度
|
|
21
|
+
* - 不传:维持内容自适应 —— **短表单不要传**,否则平白留出一大片空白
|
|
22
|
+
*
|
|
23
|
+
* 它只是下界:内容更高照常撑开、照常滚动;并且在样式里与 `90vh` 取小,
|
|
24
|
+
* 矮窗口下不会被顶出屏幕。
|
|
25
|
+
*/
|
|
26
|
+
minHeight?: number | string | boolean;
|
|
14
27
|
}
|
|
15
28
|
|
|
29
|
+
/** `minHeight` prop → CSS 长度。`true` 走标准档,`false` / 不传等于不设下界 */
|
|
30
|
+
const resolveMinHeight = (
|
|
31
|
+
minHeight: ModalProps["minHeight"]
|
|
32
|
+
): string | undefined => {
|
|
33
|
+
if (minHeight === undefined || minHeight === false) return undefined;
|
|
34
|
+
if (minHeight === true) return `${modalMinHeight}px`;
|
|
35
|
+
return typeof minHeight === "number" ? `${minHeight}px` : minHeight;
|
|
36
|
+
};
|
|
37
|
+
|
|
16
38
|
interface ModalFC extends React.FC<ModalProps> {
|
|
17
39
|
confirm: typeof AntdModal.confirm;
|
|
18
40
|
info: typeof AntdModal.info;
|
|
@@ -29,6 +51,8 @@ const Modal = ((props: ModalProps) => {
|
|
|
29
51
|
moveable = true,
|
|
30
52
|
className,
|
|
31
53
|
classNames,
|
|
54
|
+
styles: outerStyles,
|
|
55
|
+
minHeight,
|
|
32
56
|
open,
|
|
33
57
|
onCancel,
|
|
34
58
|
onOk,
|
|
@@ -79,6 +103,8 @@ const Modal = ((props: ModalProps) => {
|
|
|
79
103
|
onOk?.(e);
|
|
80
104
|
};
|
|
81
105
|
|
|
106
|
+
const resolvedMinHeight = resolveMinHeight(minHeight);
|
|
107
|
+
|
|
82
108
|
return (
|
|
83
109
|
<AntdModal
|
|
84
110
|
centered
|
|
@@ -114,6 +140,20 @@ const Modal = ((props: ModalProps) => {
|
|
|
114
140
|
footer === false ? styles.noFooter : ""
|
|
115
141
|
}`,
|
|
116
142
|
}))}
|
|
143
|
+
/* 下界通过 CSS 变量交给样式表,而不是直接写 `minHeight` 内联样式:
|
|
144
|
+
clamp(与 90vh 取小)必须在 CSS 里做——`min-height` 在 CSS 里赢过
|
|
145
|
+
`max-height`,内联写死的话矮窗口会被顶出屏幕 */
|
|
146
|
+
styles={mergeSemantic(outerStyles, (outer) => ({
|
|
147
|
+
...outer,
|
|
148
|
+
container: {
|
|
149
|
+
...(outer.container ?? {}),
|
|
150
|
+
...(resolvedMinHeight
|
|
151
|
+
? ({
|
|
152
|
+
"--vita-modal-min-height": resolvedMinHeight,
|
|
153
|
+
} as React.CSSProperties)
|
|
154
|
+
: {}),
|
|
155
|
+
},
|
|
156
|
+
}))}
|
|
117
157
|
footer={footer}
|
|
118
158
|
afterClose={() => {
|
|
119
159
|
afterClose?.();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
import Modal from ".";
|
|
8
|
+
import { modalMinHeight } from "../../styles/tokens";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 「内容异步加载的弹窗要有高度下界」这条能力的守卫。
|
|
12
|
+
*
|
|
13
|
+
* 缺陷长什么样:详情 / 消息 / 执行记录这类弹窗打开那一刻只有标题栏那么高,
|
|
14
|
+
* 接口回来再撑开 —— 视觉上跳一下。给下界就不跳。
|
|
15
|
+
*
|
|
16
|
+
* 两条底线一起盯:
|
|
17
|
+
* 1. **不传 `minHeight` 的弹窗不许变高** —— 否则两个输入框的短表单会平白留一片空白;
|
|
18
|
+
* 2. 下界必须在 CSS 里与 `90vh` 取小 —— `min-height` 在 CSS 里赢过 `max-height`,
|
|
19
|
+
* 直接落成内联 `min-height` 的话矮窗口会被顶出屏幕。所以组件只写 CSS 变量,
|
|
20
|
+
* clamp 留在样式表里,这里连样式表那一行也一并守住。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** antd 的 container 语义槽就是承载 `.content`(flex 列 + max-height: 90vh)的那一层 */
|
|
24
|
+
const container = () =>
|
|
25
|
+
document.querySelector(".ant-modal-content, .ant-modal-container") as
|
|
26
|
+
| HTMLElement
|
|
27
|
+
| null;
|
|
28
|
+
|
|
29
|
+
const minHeightVar = () =>
|
|
30
|
+
container()?.style.getPropertyValue("--vita-modal-min-height") ?? "";
|
|
31
|
+
|
|
32
|
+
describe("Modal minHeight", () => {
|
|
33
|
+
it("不传时不写下界 —— 短表单不会平白变高", () => {
|
|
34
|
+
render(
|
|
35
|
+
<Modal open getContainer={false}>
|
|
36
|
+
x
|
|
37
|
+
</Modal>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(container()).toBeTruthy();
|
|
41
|
+
expect(minHeightVar()).toBe("");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("minHeight 传 true 用标准档(与 Panel.List.Modal 同一个 700)", () => {
|
|
45
|
+
render(
|
|
46
|
+
<Modal open minHeight getContainer={false}>
|
|
47
|
+
x
|
|
48
|
+
</Modal>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(minHeightVar()).toBe(`${modalMinHeight}px`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("数字按 px、字符串原样、false 等于不设", () => {
|
|
55
|
+
const { unmount } = render(
|
|
56
|
+
<Modal open minHeight={520} getContainer={false}>
|
|
57
|
+
x
|
|
58
|
+
</Modal>
|
|
59
|
+
);
|
|
60
|
+
expect(minHeightVar()).toBe("520px");
|
|
61
|
+
unmount();
|
|
62
|
+
|
|
63
|
+
const second = render(
|
|
64
|
+
<Modal open minHeight="60vh" getContainer={false}>
|
|
65
|
+
x
|
|
66
|
+
</Modal>
|
|
67
|
+
);
|
|
68
|
+
expect(minHeightVar()).toBe("60vh");
|
|
69
|
+
second.unmount();
|
|
70
|
+
|
|
71
|
+
render(
|
|
72
|
+
<Modal open minHeight={false} getContainer={false}>
|
|
73
|
+
x
|
|
74
|
+
</Modal>
|
|
75
|
+
);
|
|
76
|
+
expect(minHeightVar()).toBe("");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("调用方自己的 styles.container 不会被吃掉", () => {
|
|
80
|
+
render(
|
|
81
|
+
<Modal
|
|
82
|
+
open
|
|
83
|
+
minHeight
|
|
84
|
+
styles={{ container: { borderRadius: "4px" } }}
|
|
85
|
+
getContainer={false}
|
|
86
|
+
>
|
|
87
|
+
x
|
|
88
|
+
</Modal>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(container()?.style.borderRadius).toBe("4px");
|
|
92
|
+
expect(minHeightVar()).toBe(`${modalMinHeight}px`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("样式表里 clamp 到 90vh —— 矮窗口不会被顶出屏幕", () => {
|
|
96
|
+
const scss = fs.readFileSync(
|
|
97
|
+
path.resolve(__dirname, "index.module.scss"),
|
|
98
|
+
"utf8"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(scss).toMatch(
|
|
102
|
+
/min-height:\s*min\(var\(--vita-modal-min-height,\s*0px\),\s*90vh\)/
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -28,6 +28,38 @@
|
|
|
28
28
|
overflow-y: auto;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* ===== 滚动条占位不许吃掉菜单项的内缩 =====
|
|
32
|
+
上面那条规则把「谁滚」定在了菜单根上,但没有把几何契约收完:滚动条一旦
|
|
33
|
+
**实占宽度**(消费方常见做法:槽宽恒定、只切 thumb 颜色,这样悬浮时不抖),
|
|
34
|
+
它就从菜单根的内容盒里切走一条 —— 而菜单项的左右外边距量的正是这个内容盒。
|
|
35
|
+
于是展开子菜单、菜单一超高,右边多出一条空白,左边纹丝不动:
|
|
36
|
+
|
|
37
|
+
展开前 菜单项 8..220(左留白 8 / 右留白 10)
|
|
38
|
+
展开后 菜单项 8..206(左留白 8 / 右留白 24)← 展开的那一项看着「右边距变大了」
|
|
39
|
+
|
|
40
|
+
`scrollbar-gutter: stable both-edges` 把这条槽**两侧对称保留**,且不论当下滚不滚
|
|
41
|
+
都保留:左右留白从此相等,展开 / 收起也不再有宽度变化(连带把「展开那一瞬内容
|
|
42
|
+
重排」一并去掉)。槽宽由浏览器按该元素实际的滚动条算,**不是写死的 14px** ——
|
|
43
|
+
消费方把滚动条改窄改宽,留白跟着变。
|
|
44
|
+
|
|
45
|
+
配套把菜单项自己的左右外边距清零:留白改由这条槽提供,不再叠加。不清的话
|
|
46
|
+
留白变成 `8 + 槽宽`(实测 22/24),230px 的侧栏里「MCP 连接器」直接被截断。
|
|
47
|
+
纵向节奏与嵌套缩进不受影响 —— antd 的行内缩进走的是 padding,不是 margin。
|
|
48
|
+
|
|
49
|
+
`@supports` 兜底:不认这个属性的浏览器维持原样,不会两头落空。 */
|
|
50
|
+
@supports (scrollbar-gutter: stable both-edges) {
|
|
51
|
+
&:global(.ant-menu-inline) {
|
|
52
|
+
scrollbar-gutter: stable both-edges;
|
|
53
|
+
|
|
54
|
+
:global(.ant-menu-item),
|
|
55
|
+
:global(.ant-menu-submenu-title) {
|
|
56
|
+
width: 100%;
|
|
57
|
+
|
|
58
|
+
margin-inline: 0px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
/* 子菜单在展开 / 收起动画期间被 rc-motion 逐帧改高度。antd 的
|
|
32
64
|
`.ant-motion-collapse` 自带 overflow hidden,这里补的是**动画结束、类名摘掉
|
|
33
65
|
之后**那一帧:此时 ul 的高度刚回到 auto,若父级恰好差几像素,浏览器会先画出
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 防回归守卫:**滚动条占位不许吃掉侧栏菜单项的内缩**。
|
|
7
|
+
*
|
|
8
|
+
* 缺陷长什么样:菜单根自己是滚动容器,滚动条一实占宽度就从它的内容盒里切走一条,
|
|
9
|
+
* 而菜单项的左右外边距量的正是这个内容盒 —— 展开子菜单让菜单超高、滚动条出现,
|
|
10
|
+
* 右留白从 10px 跳到 24px、左留白仍是 8px,看着就是「展开的那一项右边距变大了」。
|
|
11
|
+
*
|
|
12
|
+
* 正解是 `scrollbar-gutter: stable both-edges`(两侧对称、恒定保留,槽宽由浏览器按
|
|
13
|
+
* 实际滚动条算),并把菜单项自己的左右外边距清零,让留白只由这条槽提供。
|
|
14
|
+
*
|
|
15
|
+
* 这条守卫只能扫源码:jsdom 不做布局,量不出滚动条占位。所以顺带把两种「硬拉回来」
|
|
16
|
+
* 的写法也钉死 —— 写死滚动条宽度、或者用负 margin 把内容顶回去,换台机器就错开。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const SCSS = fs.readFileSync(
|
|
20
|
+
path.resolve(__dirname, "index.module.scss"),
|
|
21
|
+
"utf8"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
describe("侧栏菜单的滚动条占位", () => {
|
|
25
|
+
it("inline 菜单两侧对称保留滚动条槽", () => {
|
|
26
|
+
// 要的是**声明**本身,不是 @supports 条件里那份同名文本
|
|
27
|
+
expect(SCSS).toMatch(/scrollbar-gutter:\s*stable both-edges\s*;/);
|
|
28
|
+
// 槽由 @supports 兜底,不认这个属性的浏览器维持原样
|
|
29
|
+
expect(SCSS).toMatch(/@supports \(scrollbar-gutter: stable both-edges\)/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("菜单项不再自己留左右外边距 —— 留白只由那条槽提供,不叠加", () => {
|
|
33
|
+
expect(SCSS).toMatch(/margin-inline:\s*0px/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("不许写死滚动条宽度,也不许用负 margin 硬拉回来", () => {
|
|
37
|
+
// 滚动条宽度按平台 / 用户设置变,写死会在别的机器上错开
|
|
38
|
+
expect(SCSS).not.toMatch(/(?:width|margin|padding)[^;{}]*:\s*-?1[45]px/);
|
|
39
|
+
expect(SCSS).not.toMatch(/margin[a-z-]*:\s*[^;{}]*-\d/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("横向一律不滚、子菜单自己不滚 —— 上一轮修「闪滚动条」的口径没被冲掉", () => {
|
|
43
|
+
expect(SCSS).toMatch(/overflow-x:\s*hidden/);
|
|
44
|
+
expect(SCSS).toMatch(/\.ant-menu-sub\.ant-menu-inline\)\s*\{\s*overflow:\s*hidden/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("不许在库里写 scrollbar-width / scrollbar-color —— Chrome 会整套忽略消费方的 ::-webkit-scrollbar", () => {
|
|
48
|
+
expect(SCSS).not.toMatch(/^\s*scrollbar-(?:width|color)\s*:/m);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/styles/tokens.json
CHANGED
package/src/styles/tokens.ts
CHANGED
|
@@ -94,6 +94,17 @@ export const controlTokens = raw.control;
|
|
|
94
94
|
*/
|
|
95
95
|
export const modalWidth = raw.modalWidth;
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 内容异步加载的弹窗的**高度下界**(`<Modal minHeight>` 的默认档)。
|
|
99
|
+
*
|
|
100
|
+
* 取 700 不是拍的:`Panel.List.Modal` 从一开始就把自己钉在 `height: 700px`,那是
|
|
101
|
+
* 「搜索行 + 工具条 + 表头 + 十行左右 + 分页」放得下的高度。详情 / 消息 / 记录这类
|
|
102
|
+
* 弹窗与它同处一套后台,用同一个下界,纵向节奏才是一套的。
|
|
103
|
+
*
|
|
104
|
+
* 它只是**下界**:内容更高照常撑开,并且在 CSS 里与 `90vh` 取小,矮窗口不会被顶出屏幕。
|
|
105
|
+
*/
|
|
106
|
+
export const modalMinHeight = raw.modalMinHeight;
|
|
107
|
+
|
|
97
108
|
/**
|
|
98
109
|
* 断点。与 `styles/_responsive.scss` 的 mixin 同源,所以 JS 判断不会和 CSS 媒体查询错开。
|
|
99
110
|
*/
|