@hsu-react/ui 2.4.2 → 2.4.4
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/Icon/index.d.ts +7 -1
- package/es/components/Icon/index.js +23 -5
- package/es/layout/NavTabBar/_hooks/useTabPath.js +45 -8
- package/lib/components/Icon/index.d.ts +7 -1
- package/lib/components/Icon/index.js +22 -4
- package/lib/layout/NavTabBar/_hooks/useTabPath.js +42 -7
- package/package.json +1 -1
- package/src/components/Icon/index.tsx +29 -5
- package/src/layout/NavTabBar/_hooks/useTabPath.ts +43 -10
|
@@ -6,5 +6,11 @@ interface IconProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpa
|
|
|
6
6
|
onRef?: (ref?: React.RefObject<HTMLDivElement>) => void;
|
|
7
7
|
fontSize?: number | string;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `forwardRef` 不是可选的:`Tooltip` / `Dropdown` / `Popover` 都要往触发元素上挂 ref
|
|
11
|
+
* 才能定位浮层。作为普通函数组件时,把 Icon 直接塞进它们里面会让 React 报
|
|
12
|
+
* “Function components cannot be given refs”,浮层的定位也失去了锚点。
|
|
13
|
+
* 图标是这几个组件最常见的触发元素,所以这个坑几乎人人会踩。
|
|
14
|
+
*/
|
|
15
|
+
declare const Icon: React.ForwardRefExoticComponent<Omit<IconProps, "ref"> & React.RefAttributes<HTMLSpanElement>>;
|
|
10
16
|
export default Icon;
|
|
@@ -9,7 +9,7 @@ function _objectWithoutProperties(source, excluded) { if (source == null) return
|
|
|
9
9
|
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
10
10
|
import { Icon as Iconify } from "@iconify/react";
|
|
11
11
|
import * as AntdIcons from "@ant-design/icons";
|
|
12
|
-
import React, { useEffect, useRef } from "react";
|
|
12
|
+
import React, { useCallback, useEffect, useRef } from "react";
|
|
13
13
|
import classNames from "classnames";
|
|
14
14
|
import styles from "./index.module.scss";
|
|
15
15
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
@@ -19,7 +19,13 @@ var getAntdIcon = function getAntdIcon(icon) {
|
|
|
19
19
|
if (!/(?:Outlined|Filled|TwoTone)$/.test(icon)) return undefined;
|
|
20
20
|
return AntdIcons[icon];
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* `forwardRef` 不是可选的:`Tooltip` / `Dropdown` / `Popover` 都要往触发元素上挂 ref
|
|
24
|
+
* 才能定位浮层。作为普通函数组件时,把 Icon 直接塞进它们里面会让 React 报
|
|
25
|
+
* “Function components cannot be given refs”,浮层的定位也失去了锚点。
|
|
26
|
+
* 图标是这几个组件最常见的触发元素,所以这个坑几乎人人会踩。
|
|
27
|
+
*/
|
|
28
|
+
var Icon = /*#__PURE__*/React.forwardRef(function (props, forwardedRef) {
|
|
23
29
|
var iconProps = props.iconProps,
|
|
24
30
|
icon = props.icon,
|
|
25
31
|
className = props.className,
|
|
@@ -29,6 +35,17 @@ var Icon = function Icon(props) {
|
|
|
29
35
|
fontSize = props.fontSize,
|
|
30
36
|
iconConfig = _objectWithoutProperties(props, _excluded);
|
|
31
37
|
var ref = useRef(null);
|
|
38
|
+
|
|
39
|
+
// 内外两个 ref 都要喂:内部这个是 `onRef` 回调的载体(老接口,不能动),
|
|
40
|
+
// 外部那个是浮层组件用来定位的
|
|
41
|
+
var setRef = useCallback(function (node) {
|
|
42
|
+
ref.current = node;
|
|
43
|
+
if (typeof forwardedRef === "function") {
|
|
44
|
+
forwardedRef(node);
|
|
45
|
+
} else if (forwardedRef) {
|
|
46
|
+
forwardedRef.current = node;
|
|
47
|
+
}
|
|
48
|
+
}, [forwardedRef]);
|
|
32
49
|
useEffect(function () {
|
|
33
50
|
onRef === null || onRef === void 0 || onRef(ref);
|
|
34
51
|
}, [onRef, ref]);
|
|
@@ -43,7 +60,7 @@ var Icon = function Icon(props) {
|
|
|
43
60
|
return /*#__PURE__*/_jsx(AntdNamedIcon, _objectSpread(_objectSpread({}, iconConfig), {}, {
|
|
44
61
|
className: classNames([styles.icon, className]),
|
|
45
62
|
style: mergedStyle,
|
|
46
|
-
ref:
|
|
63
|
+
ref: setRef
|
|
47
64
|
}));
|
|
48
65
|
}
|
|
49
66
|
|
|
@@ -51,7 +68,7 @@ var Icon = function Icon(props) {
|
|
|
51
68
|
// the antd Icon's svg (whose viewBox would scale the inner svg down so much the icon becomes invisible).
|
|
52
69
|
return /*#__PURE__*/_jsx("span", _objectSpread(_objectSpread({}, iconConfig), {}, {
|
|
53
70
|
role: "img",
|
|
54
|
-
ref:
|
|
71
|
+
ref: setRef,
|
|
55
72
|
className: classNames(["anticon", styles.icon, className]),
|
|
56
73
|
style: mergedStyle,
|
|
57
74
|
children: /*#__PURE__*/_jsx(Iconify, _objectSpread(_objectSpread({}, iconProps), {}, {
|
|
@@ -60,5 +77,6 @@ var Icon = function Icon(props) {
|
|
|
60
77
|
height: "1em"
|
|
61
78
|
}))
|
|
62
79
|
}));
|
|
63
|
-
};
|
|
80
|
+
});
|
|
81
|
+
Icon.displayName = "Icon";
|
|
64
82
|
export default Icon;
|
|
@@ -14,7 +14,7 @@ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o =
|
|
|
14
14
|
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
15
15
|
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
16
16
|
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
17
|
-
import { useCallback, useEffect, useState } from "react";
|
|
17
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
18
18
|
import { useLocation } from "react-router";
|
|
19
19
|
import { checkTabPathMatch } from "../_utils/pathMatch";
|
|
20
20
|
import { array_is_includes } from "hsu-utils";
|
|
@@ -25,6 +25,19 @@ export var useTabPath = function useTabPath(_ref) {
|
|
|
25
25
|
var items = _ref.items,
|
|
26
26
|
affixRouter = _ref.affixRouter;
|
|
27
27
|
var location = useLocation();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keyed on the *contents* of `affixRouter`, not on the array itself.
|
|
31
|
+
*
|
|
32
|
+
* Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
|
|
33
|
+
* component a fresh `[]` default on every render — either way the array identity changes each
|
|
34
|
+
* time. Depending on it directly re-runs the effect below on every render, and that effect
|
|
35
|
+
* calls setState, so the whole thing becomes a render loop.
|
|
36
|
+
*/
|
|
37
|
+
var affixKey = affixRouter.join("\x01");
|
|
38
|
+
var affixSet = useMemo(function () {
|
|
39
|
+
return new Set(affixKey ? affixKey.split("\x01") : []);
|
|
40
|
+
}, [affixKey]);
|
|
28
41
|
var _useState = useState(""),
|
|
29
42
|
_useState2 = _slicedToArray(_useState, 2),
|
|
30
43
|
tabKey = _useState2[0],
|
|
@@ -43,7 +56,7 @@ export var useTabPath = function useTabPath(_ref) {
|
|
|
43
56
|
_checkAffix(item.children);
|
|
44
57
|
} else {
|
|
45
58
|
// Check whether it is an affixed route
|
|
46
|
-
var isAffix =
|
|
59
|
+
var isAffix = affixSet.has(item.key) || item.affix;
|
|
47
60
|
if (isAffix) {
|
|
48
61
|
setOpenkeys(function (prev) {
|
|
49
62
|
var find = prev.find(function (i) {
|
|
@@ -57,7 +70,7 @@ export var useTabPath = function useTabPath(_ref) {
|
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
});
|
|
60
|
-
}, [
|
|
73
|
+
}, [affixSet]);
|
|
61
74
|
var _checkPath = useCallback(function (items, parents) {
|
|
62
75
|
var pathname = decodeURI(location.pathname);
|
|
63
76
|
var search = location.search;
|
|
@@ -79,36 +92,48 @@ export var useTabPath = function useTabPath(_ref) {
|
|
|
79
92
|
// Handle param routes
|
|
80
93
|
setTabKey("".concat(pathname).concat(search));
|
|
81
94
|
setOpenkeys(function (prev) {
|
|
95
|
+
var nextKey = "".concat(pathname).concat(search);
|
|
82
96
|
var find = prev.find(function (i) {
|
|
83
97
|
return i.key.split("?")[0] === pathname;
|
|
84
98
|
});
|
|
85
99
|
if (find) {
|
|
100
|
+
// Already on this key: return `prev` untouched. `map` builds a new array
|
|
101
|
+
// every time, and React treats a new reference as a state change — which is
|
|
102
|
+
// the other half of the render loop above.
|
|
103
|
+
if (find.key === nextKey) {
|
|
104
|
+
return prev;
|
|
105
|
+
}
|
|
86
106
|
return prev.map(function (i) {
|
|
87
107
|
return i.key.split("?")[0] === pathname ? _objectSpread(_objectSpread({}, i), {}, {
|
|
88
|
-
key:
|
|
108
|
+
key: nextKey
|
|
89
109
|
}) : i;
|
|
90
110
|
});
|
|
91
111
|
}
|
|
92
112
|
return [].concat(_toConsumableArray(prev), [_objectSpread(_objectSpread({}, item), {}, {
|
|
93
|
-
key:
|
|
113
|
+
key: nextKey
|
|
94
114
|
})]);
|
|
95
115
|
});
|
|
96
116
|
} else {
|
|
97
117
|
// Handle plain routes
|
|
98
118
|
setTabKey("".concat(item.key).concat(search));
|
|
99
119
|
setOpenkeys(function (prev) {
|
|
120
|
+
var nextKey = "".concat(item.key).concat(search);
|
|
100
121
|
var find = prev.find(function (i) {
|
|
101
122
|
return i.key.split("?")[0] === item.key;
|
|
102
123
|
});
|
|
103
124
|
if (find) {
|
|
125
|
+
// Same as above: an unchanged tab must not produce a new reference
|
|
126
|
+
if (find.key === nextKey) {
|
|
127
|
+
return prev;
|
|
128
|
+
}
|
|
104
129
|
return prev.map(function (i) {
|
|
105
130
|
return i.key.split("?")[0] === item.key ? _objectSpread(_objectSpread({}, i), {}, {
|
|
106
|
-
key:
|
|
131
|
+
key: nextKey
|
|
107
132
|
}) : i;
|
|
108
133
|
});
|
|
109
134
|
}
|
|
110
135
|
return [].concat(_toConsumableArray(prev), [_objectSpread(_objectSpread({}, item), {}, {
|
|
111
|
-
key:
|
|
136
|
+
key: nextKey
|
|
112
137
|
})]);
|
|
113
138
|
});
|
|
114
139
|
}
|
|
@@ -116,12 +141,24 @@ export var useTabPath = function useTabPath(_ref) {
|
|
|
116
141
|
}
|
|
117
142
|
});
|
|
118
143
|
}, [location.pathname, location.search]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
|
|
147
|
+
* location never changes, so path matching would not re-run and the bar would be left empty
|
|
148
|
+
* while its page is still on screen. Going empty has to re-trigger the match on its own.
|
|
149
|
+
*
|
|
150
|
+
* This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
|
|
151
|
+
* runs the effect once more, and that pass finds the tab already present and returns the same
|
|
152
|
+
* state object — so React stops there. (Only true because the setters below bail out when
|
|
153
|
+
* nothing changed; without that this would spin.)
|
|
154
|
+
*/
|
|
155
|
+
var isEmpty = openKeys.length === 0;
|
|
119
156
|
useEffect(function () {
|
|
120
157
|
// Handle affixed routes first to populate openKeys
|
|
121
158
|
_checkAffix(items);
|
|
122
159
|
// Then run path matching
|
|
123
160
|
_checkPath(items);
|
|
124
|
-
}, [_checkAffix, _checkPath, items]);
|
|
161
|
+
}, [_checkAffix, _checkPath, items, isEmpty]);
|
|
125
162
|
return {
|
|
126
163
|
tabKey: tabKey,
|
|
127
164
|
setTabKey: setTabKey,
|
|
@@ -6,5 +6,11 @@ interface IconProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpa
|
|
|
6
6
|
onRef?: (ref?: React.RefObject<HTMLDivElement>) => void;
|
|
7
7
|
fontSize?: number | string;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `forwardRef` 不是可选的:`Tooltip` / `Dropdown` / `Popover` 都要往触发元素上挂 ref
|
|
11
|
+
* 才能定位浮层。作为普通函数组件时,把 Icon 直接塞进它们里面会让 React 报
|
|
12
|
+
* “Function components cannot be given refs”,浮层的定位也失去了锚点。
|
|
13
|
+
* 图标是这几个组件最常见的触发元素,所以这个坑几乎人人会踩。
|
|
14
|
+
*/
|
|
15
|
+
declare const Icon: React.ForwardRefExoticComponent<Omit<IconProps, "ref"> & React.RefAttributes<HTMLSpanElement>>;
|
|
10
16
|
export default Icon;
|
|
@@ -19,7 +19,13 @@ const getAntdIcon = icon => {
|
|
|
19
19
|
if (!/(?:Outlined|Filled|TwoTone)$/.test(icon)) return undefined;
|
|
20
20
|
return AntdIcons[icon];
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* `forwardRef` 不是可选的:`Tooltip` / `Dropdown` / `Popover` 都要往触发元素上挂 ref
|
|
24
|
+
* 才能定位浮层。作为普通函数组件时,把 Icon 直接塞进它们里面会让 React 报
|
|
25
|
+
* “Function components cannot be given refs”,浮层的定位也失去了锚点。
|
|
26
|
+
* 图标是这几个组件最常见的触发元素,所以这个坑几乎人人会踩。
|
|
27
|
+
*/
|
|
28
|
+
const Icon = /*#__PURE__*/_react2.default.forwardRef((props, forwardedRef) => {
|
|
23
29
|
const {
|
|
24
30
|
iconProps,
|
|
25
31
|
icon,
|
|
@@ -31,6 +37,17 @@ const Icon = props => {
|
|
|
31
37
|
...iconConfig
|
|
32
38
|
} = props;
|
|
33
39
|
const ref = (0, _react2.useRef)(null);
|
|
40
|
+
|
|
41
|
+
// 内外两个 ref 都要喂:内部这个是 `onRef` 回调的载体(老接口,不能动),
|
|
42
|
+
// 外部那个是浮层组件用来定位的
|
|
43
|
+
const setRef = (0, _react2.useCallback)(node => {
|
|
44
|
+
ref.current = node;
|
|
45
|
+
if (typeof forwardedRef === "function") {
|
|
46
|
+
forwardedRef(node);
|
|
47
|
+
} else if (forwardedRef) {
|
|
48
|
+
forwardedRef.current = node;
|
|
49
|
+
}
|
|
50
|
+
}, [forwardedRef]);
|
|
34
51
|
(0, _react2.useEffect)(() => {
|
|
35
52
|
onRef?.(ref);
|
|
36
53
|
}, [onRef, ref]);
|
|
@@ -47,7 +64,7 @@ const Icon = props => {
|
|
|
47
64
|
...iconConfig,
|
|
48
65
|
className: (0, _classnames.default)([_indexModule.default.icon, className]),
|
|
49
66
|
style: mergedStyle,
|
|
50
|
-
ref:
|
|
67
|
+
ref: setRef
|
|
51
68
|
});
|
|
52
69
|
}
|
|
53
70
|
|
|
@@ -56,7 +73,7 @@ const Icon = props => {
|
|
|
56
73
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
57
74
|
...iconConfig,
|
|
58
75
|
role: "img",
|
|
59
|
-
ref:
|
|
76
|
+
ref: setRef,
|
|
60
77
|
className: (0, _classnames.default)(["anticon", _indexModule.default.icon, className]),
|
|
61
78
|
style: mergedStyle,
|
|
62
79
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_react.Icon, {
|
|
@@ -66,5 +83,6 @@ const Icon = props => {
|
|
|
66
83
|
height: "1em"
|
|
67
84
|
})
|
|
68
85
|
});
|
|
69
|
-
};
|
|
86
|
+
});
|
|
87
|
+
Icon.displayName = "Icon";
|
|
70
88
|
var _default = exports.default = Icon;
|
|
@@ -16,6 +16,17 @@ const useTabPath = ({
|
|
|
16
16
|
affixRouter
|
|
17
17
|
}) => {
|
|
18
18
|
const location = (0, _reactRouter.useLocation)();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Keyed on the *contents* of `affixRouter`, not on the array itself.
|
|
22
|
+
*
|
|
23
|
+
* Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
|
|
24
|
+
* component a fresh `[]` default on every render — either way the array identity changes each
|
|
25
|
+
* time. Depending on it directly re-runs the effect below on every render, and that effect
|
|
26
|
+
* calls setState, so the whole thing becomes a render loop.
|
|
27
|
+
*/
|
|
28
|
+
const affixKey = affixRouter.join("\u0001");
|
|
29
|
+
const affixSet = (0, _react.useMemo)(() => new Set(affixKey ? affixKey.split("\u0001") : []), [affixKey]);
|
|
19
30
|
const [tabKey, setTabKey] = (0, _react.useState)("");
|
|
20
31
|
const [openKeys, setOpenkeys] = (0, _react.useState)([]);
|
|
21
32
|
|
|
@@ -28,7 +39,7 @@ const useTabPath = ({
|
|
|
28
39
|
_checkAffix(item.children);
|
|
29
40
|
} else {
|
|
30
41
|
// Check whether it is an affixed route
|
|
31
|
-
const isAffix =
|
|
42
|
+
const isAffix = affixSet.has(item.key) || item.affix;
|
|
32
43
|
if (isAffix) {
|
|
33
44
|
setOpenkeys(prev => {
|
|
34
45
|
const find = prev.find(i => i.key === item.key);
|
|
@@ -40,7 +51,7 @@ const useTabPath = ({
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
});
|
|
43
|
-
}, [
|
|
54
|
+
}, [affixSet]);
|
|
44
55
|
const _checkPath = (0, _react.useCallback)((items, parents) => {
|
|
45
56
|
const pathname = decodeURI(location.pathname);
|
|
46
57
|
const search = location.search;
|
|
@@ -58,32 +69,44 @@ const useTabPath = ({
|
|
|
58
69
|
// Handle param routes
|
|
59
70
|
setTabKey(`${pathname}${search}`);
|
|
60
71
|
setOpenkeys(prev => {
|
|
72
|
+
const nextKey = `${pathname}${search}`;
|
|
61
73
|
const find = prev.find(i => i.key.split("?")[0] === pathname);
|
|
62
74
|
if (find) {
|
|
75
|
+
// Already on this key: return `prev` untouched. `map` builds a new array
|
|
76
|
+
// every time, and React treats a new reference as a state change — which is
|
|
77
|
+
// the other half of the render loop above.
|
|
78
|
+
if (find.key === nextKey) {
|
|
79
|
+
return prev;
|
|
80
|
+
}
|
|
63
81
|
return prev.map(i => i.key.split("?")[0] === pathname ? {
|
|
64
82
|
...i,
|
|
65
|
-
key:
|
|
83
|
+
key: nextKey
|
|
66
84
|
} : i);
|
|
67
85
|
}
|
|
68
86
|
return [...prev, {
|
|
69
87
|
...item,
|
|
70
|
-
key:
|
|
88
|
+
key: nextKey
|
|
71
89
|
}];
|
|
72
90
|
});
|
|
73
91
|
} else {
|
|
74
92
|
// Handle plain routes
|
|
75
93
|
setTabKey(`${item.key}${search}`);
|
|
76
94
|
setOpenkeys(prev => {
|
|
95
|
+
const nextKey = `${item.key}${search}`;
|
|
77
96
|
const find = prev.find(i => i.key.split("?")[0] === item.key);
|
|
78
97
|
if (find) {
|
|
98
|
+
// Same as above: an unchanged tab must not produce a new reference
|
|
99
|
+
if (find.key === nextKey) {
|
|
100
|
+
return prev;
|
|
101
|
+
}
|
|
79
102
|
return prev.map(i => i.key.split("?")[0] === item.key ? {
|
|
80
103
|
...i,
|
|
81
|
-
key:
|
|
104
|
+
key: nextKey
|
|
82
105
|
} : i);
|
|
83
106
|
}
|
|
84
107
|
return [...prev, {
|
|
85
108
|
...item,
|
|
86
|
-
key:
|
|
109
|
+
key: nextKey
|
|
87
110
|
}];
|
|
88
111
|
});
|
|
89
112
|
}
|
|
@@ -91,12 +114,24 @@ const useTabPath = ({
|
|
|
91
114
|
}
|
|
92
115
|
});
|
|
93
116
|
}, [location.pathname, location.search]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
|
|
120
|
+
* location never changes, so path matching would not re-run and the bar would be left empty
|
|
121
|
+
* while its page is still on screen. Going empty has to re-trigger the match on its own.
|
|
122
|
+
*
|
|
123
|
+
* This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
|
|
124
|
+
* runs the effect once more, and that pass finds the tab already present and returns the same
|
|
125
|
+
* state object — so React stops there. (Only true because the setters below bail out when
|
|
126
|
+
* nothing changed; without that this would spin.)
|
|
127
|
+
*/
|
|
128
|
+
const isEmpty = openKeys.length === 0;
|
|
94
129
|
(0, _react.useEffect)(() => {
|
|
95
130
|
// Handle affixed routes first to populate openKeys
|
|
96
131
|
_checkAffix(items);
|
|
97
132
|
// Then run path matching
|
|
98
133
|
_checkPath(items);
|
|
99
|
-
}, [_checkAffix, _checkPath, items]);
|
|
134
|
+
}, [_checkAffix, _checkPath, items, isEmpty]);
|
|
100
135
|
return {
|
|
101
136
|
tabKey,
|
|
102
137
|
setTabKey,
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@iconify/react";
|
|
6
6
|
|
|
7
7
|
import * as AntdIcons from "@ant-design/icons";
|
|
8
|
-
import React, { useEffect, useRef } from "react";
|
|
8
|
+
import React, { useCallback, useEffect, useRef } from "react";
|
|
9
9
|
import classNames from "classnames";
|
|
10
10
|
import styles from "./index.module.scss";
|
|
11
11
|
|
|
@@ -38,7 +38,13 @@ interface IconProps
|
|
|
38
38
|
fontSize?: number | string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* `forwardRef` 不是可选的:`Tooltip` / `Dropdown` / `Popover` 都要往触发元素上挂 ref
|
|
43
|
+
* 才能定位浮层。作为普通函数组件时,把 Icon 直接塞进它们里面会让 React 报
|
|
44
|
+
* “Function components cannot be given refs”,浮层的定位也失去了锚点。
|
|
45
|
+
* 图标是这几个组件最常见的触发元素,所以这个坑几乎人人会踩。
|
|
46
|
+
*/
|
|
47
|
+
const Icon = React.forwardRef<HTMLSpanElement, IconProps>((props, forwardedRef) => {
|
|
42
48
|
const {
|
|
43
49
|
iconProps,
|
|
44
50
|
icon,
|
|
@@ -51,6 +57,22 @@ const Icon: React.FC<IconProps> = (props) => {
|
|
|
51
57
|
} = props;
|
|
52
58
|
const ref = useRef<HTMLDivElement>(null);
|
|
53
59
|
|
|
60
|
+
// 内外两个 ref 都要喂:内部这个是 `onRef` 回调的载体(老接口,不能动),
|
|
61
|
+
// 外部那个是浮层组件用来定位的
|
|
62
|
+
const setRef = useCallback(
|
|
63
|
+
(node: HTMLSpanElement | null) => {
|
|
64
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current =
|
|
65
|
+
node as unknown as HTMLDivElement | null;
|
|
66
|
+
|
|
67
|
+
if (typeof forwardedRef === "function") {
|
|
68
|
+
forwardedRef(node);
|
|
69
|
+
} else if (forwardedRef) {
|
|
70
|
+
forwardedRef.current = node;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[forwardedRef]
|
|
74
|
+
);
|
|
75
|
+
|
|
54
76
|
useEffect(() => {
|
|
55
77
|
onRef?.(ref);
|
|
56
78
|
}, [onRef, ref]);
|
|
@@ -69,7 +91,7 @@ const Icon: React.FC<IconProps> = (props) => {
|
|
|
69
91
|
{...iconConfig}
|
|
70
92
|
className={classNames([styles.icon, className])}
|
|
71
93
|
style={mergedStyle}
|
|
72
|
-
ref={
|
|
94
|
+
ref={setRef}
|
|
73
95
|
/>
|
|
74
96
|
);
|
|
75
97
|
}
|
|
@@ -80,13 +102,15 @@ const Icon: React.FC<IconProps> = (props) => {
|
|
|
80
102
|
<span
|
|
81
103
|
{...iconConfig}
|
|
82
104
|
role="img"
|
|
83
|
-
ref={
|
|
105
|
+
ref={setRef}
|
|
84
106
|
className={classNames(["anticon", styles.icon, className])}
|
|
85
107
|
style={mergedStyle}
|
|
86
108
|
>
|
|
87
109
|
<Iconify {...iconProps} icon={icon} width="1em" height="1em" />
|
|
88
110
|
</span>
|
|
89
111
|
);
|
|
90
|
-
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
Icon.displayName = "Icon";
|
|
91
115
|
|
|
92
116
|
export default Icon;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { useLocation } from "react-router";
|
|
3
3
|
import { TabType } from "..";
|
|
4
4
|
import { checkTabPathMatch } from "../_utils/pathMatch";
|
|
@@ -14,6 +14,17 @@ interface UseTabPathOptions {
|
|
|
14
14
|
*/
|
|
15
15
|
export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
|
|
16
16
|
const location = useLocation();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Keyed on the *contents* of `affixRouter`, not on the array itself.
|
|
20
|
+
*
|
|
21
|
+
* Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
|
|
22
|
+
* component a fresh `[]` default on every render — either way the array identity changes each
|
|
23
|
+
* time. Depending on it directly re-runs the effect below on every render, and that effect
|
|
24
|
+
* calls setState, so the whole thing becomes a render loop.
|
|
25
|
+
*/
|
|
26
|
+
const affixKey = affixRouter.join("\u0001");
|
|
27
|
+
const affixSet = useMemo(() => new Set(affixKey ? affixKey.split("\u0001") : []), [affixKey]);
|
|
17
28
|
const [tabKey, setTabKey] = useState<string>("");
|
|
18
29
|
const [openKeys, setOpenkeys] = useState<TabType[]>([]);
|
|
19
30
|
|
|
@@ -27,7 +38,7 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
|
|
|
27
38
|
_checkAffix(item.children);
|
|
28
39
|
} else {
|
|
29
40
|
// Check whether it is an affixed route
|
|
30
|
-
const isAffix =
|
|
41
|
+
const isAffix = affixSet.has(item.key) || item.affix;
|
|
31
42
|
if (isAffix) {
|
|
32
43
|
setOpenkeys((prev) => {
|
|
33
44
|
const find = prev.find((i) => i.key === item.key);
|
|
@@ -40,7 +51,7 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
|
|
|
40
51
|
}
|
|
41
52
|
});
|
|
42
53
|
},
|
|
43
|
-
[
|
|
54
|
+
[affixSet]
|
|
44
55
|
);
|
|
45
56
|
|
|
46
57
|
const _checkPath = useCallback(
|
|
@@ -72,32 +83,42 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
|
|
|
72
83
|
setTabKey(`${pathname}${search}`);
|
|
73
84
|
|
|
74
85
|
setOpenkeys((prev) => {
|
|
86
|
+
const nextKey = `${pathname}${search}`;
|
|
75
87
|
const find = prev.find((i) => i.key.split("?")[0] === pathname);
|
|
76
88
|
if (find) {
|
|
89
|
+
// Already on this key: return `prev` untouched. `map` builds a new array
|
|
90
|
+
// every time, and React treats a new reference as a state change — which is
|
|
91
|
+
// the other half of the render loop above.
|
|
92
|
+
if (find.key === nextKey) {
|
|
93
|
+
return prev;
|
|
94
|
+
}
|
|
77
95
|
return prev.map((i) =>
|
|
78
|
-
i.key.split("?")[0] === pathname
|
|
79
|
-
? { ...i, key: `${pathname}${search}` }
|
|
80
|
-
: i
|
|
96
|
+
i.key.split("?")[0] === pathname ? { ...i, key: nextKey } : i
|
|
81
97
|
);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
return [...prev, { ...item, key:
|
|
100
|
+
return [...prev, { ...item, key: nextKey }];
|
|
85
101
|
});
|
|
86
102
|
} else {
|
|
87
103
|
// Handle plain routes
|
|
88
104
|
setTabKey(`${item.key}${search}`);
|
|
89
105
|
|
|
90
106
|
setOpenkeys((prev) => {
|
|
107
|
+
const nextKey = `${item.key}${search}`;
|
|
91
108
|
const find = prev.find((i) => i.key.split("?")[0] === item.key);
|
|
92
109
|
if (find) {
|
|
110
|
+
// Same as above: an unchanged tab must not produce a new reference
|
|
111
|
+
if (find.key === nextKey) {
|
|
112
|
+
return prev;
|
|
113
|
+
}
|
|
93
114
|
return prev.map((i) =>
|
|
94
115
|
i.key.split("?")[0] === item.key
|
|
95
|
-
? { ...i, key:
|
|
116
|
+
? { ...i, key: nextKey }
|
|
96
117
|
: i
|
|
97
118
|
);
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
return [...prev, { ...item, key:
|
|
121
|
+
return [...prev, { ...item, key: nextKey }];
|
|
101
122
|
});
|
|
102
123
|
}
|
|
103
124
|
}
|
|
@@ -107,12 +128,24 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
|
|
|
107
128
|
[location.pathname, location.search]
|
|
108
129
|
);
|
|
109
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
|
|
133
|
+
* location never changes, so path matching would not re-run and the bar would be left empty
|
|
134
|
+
* while its page is still on screen. Going empty has to re-trigger the match on its own.
|
|
135
|
+
*
|
|
136
|
+
* This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
|
|
137
|
+
* runs the effect once more, and that pass finds the tab already present and returns the same
|
|
138
|
+
* state object — so React stops there. (Only true because the setters below bail out when
|
|
139
|
+
* nothing changed; without that this would spin.)
|
|
140
|
+
*/
|
|
141
|
+
const isEmpty = openKeys.length === 0;
|
|
142
|
+
|
|
110
143
|
useEffect(() => {
|
|
111
144
|
// Handle affixed routes first to populate openKeys
|
|
112
145
|
_checkAffix(items);
|
|
113
146
|
// Then run path matching
|
|
114
147
|
_checkPath(items);
|
|
115
|
-
}, [_checkAffix, _checkPath, items]);
|
|
148
|
+
}, [_checkAffix, _checkPath, items, isEmpty]);
|
|
116
149
|
|
|
117
150
|
return { tabKey, setTabKey, openKeys, setOpenkeys };
|
|
118
151
|
};
|