@canlooks/can-ui 0.0.114 → 0.0.116

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.
@@ -13,10 +13,10 @@ exports.AnchorList = (0, react_1.memo)(({ anchors, renderAnchorItem, indent = 24
13
13
  * ------------------------------------------------------------------------
14
14
  * 初始化高亮与滚动位置
15
15
  */
16
- const initialized = (0, react_1.useRef)(false);
17
- (0, react_1.useEffect)(() => {
16
+ const mounted = (0, react_1.useRef)(false);
17
+ (0, utils_1.useExternalClass)(() => {
18
18
  if (routeMode === 'history') {
19
- initialized.current ||= scrollToId(location.hash.slice(1));
19
+ mounted.current ||= scrollToId(location.hash.slice(1));
20
20
  }
21
21
  });
22
22
  const scrollToId = (id) => {
@@ -28,7 +28,7 @@ exports.AnchorList = (0, react_1.memo)(({ anchors, renderAnchorItem, indent = 24
28
28
  scrollerEl.scrollTo({
29
29
  top: targetEl.offsetTop - offset,
30
30
  // 初始化之前无需平滑滚动
31
- behavior: initialized.current ? scrollBehavior : 'instant'
31
+ behavior: mounted.current ? scrollBehavior : 'instant'
32
32
  });
33
33
  return true;
34
34
  };
@@ -3,13 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Deferred = Deferred;
4
4
  const react_1 = require("react");
5
5
  function Deferred({ init, value, children }) {
6
- const [initialized, setInitialized] = (0, react_1.useState)(false);
7
- const deferredInitialized = (0, react_1.useDeferredValue)(initialized);
6
+ const [mounted, setMounted] = (0, react_1.useState)(false);
7
+ const deferredMounted = (0, react_1.useDeferredValue)(mounted);
8
8
  (0, react_1.useEffect)(() => {
9
- setInitialized(true);
9
+ setMounted(true);
10
10
  }, []);
11
11
  const deferredValue = (0, react_1.useDeferredValue)(value);
12
- if (!deferredInitialized) {
12
+ if (!deferredMounted) {
13
13
  return init;
14
14
  }
15
15
  return typeof children === 'function'
@@ -67,7 +67,7 @@ function Dialog({ icon, title, footer, prefix, suffix, width = 420, minWidth, ma
67
67
  if (scrollBehavior !== 'card') {
68
68
  return;
69
69
  }
70
- if (innerOpen.current) {
70
+ if (innerOpen.current && bodyRef.current && bodyWrapRef.current) {
71
71
  const resizeObserver = new ResizeObserver(onScroll);
72
72
  resizeObserver.observe(bodyRef.current);
73
73
  resizeObserver.observe(bodyWrapRef.current);
@@ -9,16 +9,15 @@ const progress_1 = require("../progress");
9
9
  const utils_1 = require("../../utils");
10
10
  const backdrop_1 = require("../backdrop");
11
11
  exports.LoadingMask = (0, react_1.memo)(({ open = false, text = '加载中...', progress, color, indicatorProps, progressProps, ...props }) => {
12
- const [visible, setVisible] = (0, utils_1.useDerivedState)((prevLoading) => {
13
- // 只有第一次需要原样返回loading,之后visible不会立即变为false,交由onTransitionEnd()处理
14
- return typeof prevLoading === 'undefined' ? open : true;
12
+ const [visible, setVisible] = (0, react_1.useState)(open);
13
+ (0, react_1.useMemo)(() => {
14
+ open && setVisible(true);
15
15
  }, [open]);
16
16
  const onExited = () => {
17
- // 动画结束后,loading为false才需要改变visible
18
17
  setVisible(false);
19
18
  };
20
19
  const showProgress = typeof progress === 'number';
21
- return open || visible.current
20
+ return open || visible
22
21
  ? (0, jsx_runtime_1.jsxs)(backdrop_1.Backdrop, { variant: "light", ...props, css: loadingMask_style_1.style, open: open, className: (0, utils_1.clsx)(loadingMask_style_1.classes.root, props.className), onExited: onExited, "data-show-progress": showProgress, children: [(0, jsx_runtime_1.jsxs)("div", { className: loadingMask_style_1.classes.indicator, children: [(0, jsx_runtime_1.jsx)(loadingIndicator_1.LoadingIndicator, { size: showProgress ? 14 : 30, borderWidth: showProgress ? 2 : 3, color: color, ...indicatorProps }), (0, jsx_runtime_1.jsx)("div", { className: loadingMask_style_1.classes.text, children: text })] }), showProgress &&
23
22
  (0, jsx_runtime_1.jsx)(progress_1.Progress, { className: loadingMask_style_1.classes.progress, value: progress, ...progressProps })] })
24
23
  : null;
@@ -57,12 +57,7 @@ function Popper({ ref, popperRef, anchorElement, container, effectContainer, con
57
57
  }
58
58
  };
59
59
  const { onChildrenOpenChange: tellParentOpenChange } = (0, popperContext_1.usePopperContext)();
60
- const initialized = (0, react_1.useRef)(false);
61
- (0, react_1.useEffect)(() => {
62
- if (!initialized.current) {
63
- initialized.current = true;
64
- return;
65
- }
60
+ (0, utils_1.useUpdateEffect)(() => {
66
61
  tellParentOpenChange(innerOpen.current);
67
62
  }, [innerOpen.current]);
68
63
  const onChildrenOpenChange = (childrenOpen) => {
@@ -4,6 +4,7 @@ exports.PopperContext = void 0;
4
4
  exports.usePopperContext = usePopperContext;
5
5
  exports.useScrollToTarget = useScrollToTarget;
6
6
  const react_1 = require("react");
7
+ const utils_1 = require("../../utils");
7
8
  exports.PopperContext = (0, react_1.createContext)({
8
9
  autoClose: false,
9
10
  open: false,
@@ -30,11 +31,10 @@ function useScrollToTarget(scrollerRef) {
30
31
  scrollerEl.scrollTop = targetEl.offsetTop + targetEl.clientHeight / 2 - scrollerEl.clientHeight / 2;
31
32
  }
32
33
  }, []);
33
- (0, react_1.useMemo)(() => {
34
+ (0, utils_1.useExternalClass)(() => {
34
35
  beforeOpenCallbacks.add(beforeOpen);
35
- }, []);
36
- (0, react_1.useEffect)(() => () => {
36
+ }, () => {
37
37
  beforeOpenCallbacks.delete(beforeOpen);
38
- }, []);
38
+ });
39
39
  return ref;
40
40
  }
@@ -79,15 +79,17 @@ exports.Tabs = (0, react_2.memo)(({ tabs, labelKey = 'label', primaryKey = 'valu
79
79
  const [shadowStart, setShadowStart] = (0, react_2.useState)(false);
80
80
  const [shadowEnd, setShadowEnd] = (0, react_2.useState)(false);
81
81
  const setShadow = () => {
82
- if (position === 'top' || position === 'bottom') {
83
- const { scrollLeft } = scrollRef.current;
84
- setShadowStart(scrollLeft > 0);
85
- setShadowEnd(scrollLeft < scrollRef.current.scrollWidth - scrollRef.current.clientWidth);
86
- }
87
- else {
88
- const { scrollTop } = scrollRef.current;
89
- setShadowStart(scrollTop > 0);
90
- setShadowEnd(scrollTop < scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
82
+ if (scrollRef.current) {
83
+ if (position === 'top' || position === 'bottom') {
84
+ const { scrollLeft } = scrollRef.current;
85
+ setShadowStart(scrollLeft > 0);
86
+ setShadowEnd(scrollLeft < scrollRef.current.scrollWidth - scrollRef.current.clientWidth);
87
+ }
88
+ else {
89
+ const { scrollTop } = scrollRef.current;
90
+ setShadowStart(scrollTop > 0);
91
+ setShadowEnd(scrollTop < scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
92
+ }
91
93
  }
92
94
  };
93
95
  (0, react_2.useEffect)(() => {
@@ -33,11 +33,11 @@ const Sweeping = ({ ref, in: _in = false, appear = true, orientation = 'vertical
33
33
  setSize(getCollapsedSize());
34
34
  });
35
35
  };
36
- const initialized = (0, react_1.useRef)(false);
37
- (0, react_1.useEffect)(() => {
38
- if (!initialized.current) {
36
+ const mounted = (0, react_1.useRef)(false);
37
+ (0, utils_1.useStrictEffect)(() => {
38
+ if (!mounted.current) {
39
39
  // 首次渲染
40
- initialized.current = true;
40
+ mounted.current = true;
41
41
  if (!appear) {
42
42
  // 若appear为false,则跳过首次动画
43
43
  return;
@@ -32,17 +32,10 @@ const Waterfall = ({ columnCount = { xs: 4 }, gap = 0, columnGap, rowGap, ...pro
32
32
  elements.current.forEach(el => resizeObserver.current.observe(el));
33
33
  });
34
34
  });
35
- const isInitialized = (0, react_1.useRef)(false);
36
- (0, react_1.useEffect)(() => {
37
- if (!isInitialized.current) {
38
- isInitialized.current = true;
39
- return;
40
- }
41
- computeItemOrder();
42
- }, [columnCountNum.current]);
43
- (0, react_1.useEffect)(() => () => {
35
+ (0, utils_1.useUpdateEffect)(computeItemOrder, [columnCountNum.current]);
36
+ (0, utils_1.useExternalClass)(() => void 0, () => {
44
37
  resizeObserver.current.disconnect();
45
- }, []);
38
+ });
46
39
  return ((0, jsx_runtime_1.jsx)("div", { ...props, ref: (0, utils_1.cloneRef)(containerRef, props.ref), css: (0, waterfall_style_1.useStyle)({ columnCount, columnGap, rowGap }), className: (0, utils_1.clsx)(waterfall_style_1.classes.root, props.className), children: react_1.Children.map(props.children, child => {
47
40
  if (!(0, react_1.isValidElement)(child)) {
48
41
  throw Error('Children of <Waterfall> must be element');
@@ -1,10 +1,5 @@
1
- import { RefObject, Dispatch, SetStateAction } from 'react';
1
+ import { RefObject, Dispatch, SetStateAction, EffectCallback, DependencyList } from 'react';
2
2
  import { DefineElement } from '../types';
3
- /**
4
- * 获取渲染前的值
5
- * @param value
6
- */
7
- export declare function usePrev<T>(value: T): T | null;
8
3
  /**
9
4
  * 将某个值使用ref同步,主要用于对付组件的闭包问题
10
5
  * @param value
@@ -27,6 +22,7 @@ export declare function useForceUpdate(): () => void;
27
22
  */
28
23
  export declare function useDerivedState<T>(referredState: (prevState: T | undefined) => T, deps: any[]): [RefObject<T>, Dispatch<SetStateAction<T>>];
29
24
  export declare function useDerivedState<T>(referredState: T, deps?: any[]): [RefObject<T>, Dispatch<SetStateAction<T>>];
25
+ export declare function useMounted(): void;
30
26
  /**
31
27
  * 组件卸载后得到{current: true}
32
28
  * @returns
@@ -60,3 +56,11 @@ export declare function useContainer<T extends HTMLElement | null>(container?: D
60
56
  * 使用外部类,该方法可避免`StrictMode`下,React渲染行为与外部类实例生命周期不同步的问题
61
57
  */
62
58
  export declare function useExternalClass<T>(setup: () => T, cleanup?: (instance: T) => void): T;
59
+ /**
60
+ * 用法同{@link useEffect},但StrictMode下不会执行两次
61
+ */
62
+ export declare function useStrictEffect(effect: EffectCallback, deps?: DependencyList): void;
63
+ /**
64
+ * 用法同{@link useEffect},但会排除首次渲染
65
+ */
66
+ export declare function useUpdateEffect(effect: EffectCallback, deps?: DependencyList): void;
@@ -1,28 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.usePrev = usePrev;
4
3
  exports.useSync = useSync;
5
4
  exports.useSyncState = useSyncState;
6
5
  exports.useForceUpdate = useForceUpdate;
7
6
  exports.useDerivedState = useDerivedState;
7
+ exports.useMounted = useMounted;
8
8
  exports.useUnmounted = useUnmounted;
9
9
  exports.useControlled = useControlled;
10
10
  exports.useLoading = useLoading;
11
11
  exports.useContainer = useContainer;
12
12
  exports.useExternalClass = useExternalClass;
13
+ exports.useStrictEffect = useStrictEffect;
14
+ exports.useUpdateEffect = useUpdateEffect;
13
15
  const react_1 = require("react");
14
16
  const utils_1 = require("./utils");
15
- /**
16
- * 获取渲染前的值
17
- * @param value
18
- */
19
- function usePrev(value) {
20
- const prev = (0, react_1.useRef)(null);
21
- (0, react_1.useEffect)(() => {
22
- prev.current = value;
23
- });
24
- return prev.current;
25
- }
26
17
  /**
27
18
  * 将某个值使用ref同步,主要用于对付组件的闭包问题
28
19
  * @param value
@@ -54,6 +45,10 @@ function useForceUpdate() {
54
45
  }
55
46
  function useDerivedState(referredState, deps) {
56
47
  const derivedState = (0, react_1.useRef)(void 0);
48
+ (0, react_1.useMemo)(() => {
49
+ // SSR模式下,derivedState会重置2次
50
+ derivedState.current = void 0;
51
+ }, []);
57
52
  const updateState = (state) => {
58
53
  const newState = typeof state === 'function' ? state(derivedState.current) : state;
59
54
  if (derivedState.current !== newState) {
@@ -74,15 +69,17 @@ function useDerivedState(referredState, deps) {
74
69
  }, [])
75
70
  ];
76
71
  }
72
+ function useMounted() {
73
+ }
77
74
  /**
78
75
  * 组件卸载后得到{current: true}
79
76
  * @returns
80
77
  */
81
78
  function useUnmounted() {
82
79
  const isUnmounted = (0, react_1.useRef)(false);
83
- (0, react_1.useEffect)(() => () => {
80
+ useExternalClass(() => void 0, () => {
84
81
  isUnmounted.current = true;
85
- }, []);
82
+ });
86
83
  return isUnmounted;
87
84
  }
88
85
  function useControlled(defaultValue, value, onChange) {
@@ -164,3 +161,29 @@ function useExternalClass(setup, cleanup) {
164
161
  }, []);
165
162
  return instance;
166
163
  }
164
+ /**
165
+ * 用法同{@link useEffect},但StrictMode下不会执行两次
166
+ */
167
+ function useStrictEffect(effect, deps) {
168
+ const prevDeps = (0, react_1.useRef)(deps);
169
+ (0, react_1.useEffect)(() => {
170
+ const isDepsChanged = prevDeps.current ? (0, utils_1.arrayShallowEqual)(prevDeps.current, deps) : true;
171
+ if (isDepsChanged) {
172
+ prevDeps.current = deps;
173
+ return effect();
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * 用法同{@link useEffect},但会排除首次渲染
179
+ */
180
+ function useUpdateEffect(effect, deps) {
181
+ const mounted = (0, react_1.useRef)(false);
182
+ useStrictEffect(() => {
183
+ if (!mounted.current) {
184
+ mounted.current = true;
185
+ return;
186
+ }
187
+ return effect();
188
+ }, [deps]);
189
+ }
@@ -47,6 +47,7 @@ export declare function mergeDeep<T extends {}, U, V, W, X>(target: T, source: U
47
47
  export declare function mergeDeep<T extends {}, U, V, W, X, Y>(target: T, source: U, source2: V, source3: W, source4: X, source5: Y): T & U & V & W & X & Y;
48
48
  export declare function mergeDeep<T extends {}, U, V, W, X, Y, Z>(target: T, source: U, source2: V, source3: W, source4: X, source5: Y, source6: Z): T & U & V & W & X & Y & Z;
49
49
  export declare function mergeDeep(target: object, ...sources: any[]): any;
50
+ export declare function arrayShallowEqual(a: any[], b: any[]): boolean;
50
51
  export type OverflowEdge = 'top' | 'bottom' | 'left' | 'right';
51
52
  /**
52
53
  * 判断元素是否完全在目标容器内,用于Popper组件
@@ -8,6 +8,7 @@ exports.toArray = toArray;
8
8
  exports.isSelected = isSelected;
9
9
  exports.cloneDeep = cloneDeep;
10
10
  exports.mergeDeep = mergeDeep;
11
+ exports.arrayShallowEqual = arrayShallowEqual;
11
12
  exports.isElementOverflowed = isElementOverflowed;
12
13
  exports.nextTick = nextTick;
13
14
  exports.cloneRef = cloneRef;
@@ -137,6 +138,22 @@ function mergeDeep(target, ...sources) {
137
138
  }
138
139
  return target;
139
140
  }
141
+ function arrayShallowEqual(a, b) {
142
+ if (a === b) {
143
+ return true;
144
+ }
145
+ if (a.length !== b.length) {
146
+ return false;
147
+ }
148
+ let isEqual = true;
149
+ for (let i = 0, { length } = a; i < length; i++) {
150
+ if (a[i] !== b[i]) {
151
+ isEqual = false;
152
+ break;
153
+ }
154
+ }
155
+ return isEqual;
156
+ }
140
157
  /**
141
158
  * 判断元素是否完全在目标容器内,用于Popper组件
142
159
  * @param target 目标元素
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
2
  import { classes, style } from './anchorList.style';
3
- import { clsx, listenAllPredecessorsScroll, useDerivedState, useSyncState } from '../../utils';
3
+ import { clsx, listenAllPredecessorsScroll, useDerivedState, useExternalClass, useSyncState } from '../../utils';
4
4
  import { memo, useEffect, useRef } from 'react';
5
5
  import { Flex } from '../flex';
6
6
  import { ActiveIndicator } from './activeIndicator';
@@ -10,10 +10,10 @@ export const AnchorList = memo(({ anchors, renderAnchorItem, indent = 24, scroll
10
10
  * ------------------------------------------------------------------------
11
11
  * 初始化高亮与滚动位置
12
12
  */
13
- const initialized = useRef(false);
14
- useEffect(() => {
13
+ const mounted = useRef(false);
14
+ useExternalClass(() => {
15
15
  if (routeMode === 'history') {
16
- initialized.current ||= scrollToId(location.hash.slice(1));
16
+ mounted.current ||= scrollToId(location.hash.slice(1));
17
17
  }
18
18
  });
19
19
  const scrollToId = (id) => {
@@ -25,7 +25,7 @@ export const AnchorList = memo(({ anchors, renderAnchorItem, indent = 24, scroll
25
25
  scrollerEl.scrollTo({
26
26
  top: targetEl.offsetTop - offset,
27
27
  // 初始化之前无需平滑滚动
28
- behavior: initialized.current ? scrollBehavior : 'instant'
28
+ behavior: mounted.current ? scrollBehavior : 'instant'
29
29
  });
30
30
  return true;
31
31
  };
@@ -1,12 +1,12 @@
1
1
  import { useDeferredValue, useEffect, useState } from 'react';
2
2
  export function Deferred({ init, value, children }) {
3
- const [initialized, setInitialized] = useState(false);
4
- const deferredInitialized = useDeferredValue(initialized);
3
+ const [mounted, setMounted] = useState(false);
4
+ const deferredMounted = useDeferredValue(mounted);
5
5
  useEffect(() => {
6
- setInitialized(true);
6
+ setMounted(true);
7
7
  }, []);
8
8
  const deferredValue = useDeferredValue(value);
9
- if (!deferredInitialized) {
9
+ if (!deferredMounted) {
10
10
  return init;
11
11
  }
12
12
  return typeof children === 'function'
@@ -64,7 +64,7 @@ export function Dialog({ icon, title, footer, prefix, suffix, width = 420, minWi
64
64
  if (scrollBehavior !== 'card') {
65
65
  return;
66
66
  }
67
- if (innerOpen.current) {
67
+ if (innerOpen.current && bodyRef.current && bodyWrapRef.current) {
68
68
  const resizeObserver = new ResizeObserver(onScroll);
69
69
  resizeObserver.observe(bodyRef.current);
70
70
  resizeObserver.observe(bodyWrapRef.current);
@@ -1,21 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
- import { memo } from 'react';
2
+ import { memo, useMemo, useState } from 'react';
3
3
  import { classes, style } from './loadingMask.style';
4
4
  import { LoadingIndicator } from '../loadingIndicator';
5
5
  import { Progress } from '../progress';
6
- import { clsx, useDerivedState } from '../../utils';
6
+ import { clsx } from '../../utils';
7
7
  import { Backdrop } from '../backdrop';
8
8
  export const LoadingMask = memo(({ open = false, text = '加载中...', progress, color, indicatorProps, progressProps, ...props }) => {
9
- const [visible, setVisible] = useDerivedState((prevLoading) => {
10
- // 只有第一次需要原样返回loading,之后visible不会立即变为false,交由onTransitionEnd()处理
11
- return typeof prevLoading === 'undefined' ? open : true;
9
+ const [visible, setVisible] = useState(open);
10
+ useMemo(() => {
11
+ open && setVisible(true);
12
12
  }, [open]);
13
13
  const onExited = () => {
14
- // 动画结束后,loading为false才需要改变visible
15
14
  setVisible(false);
16
15
  };
17
16
  const showProgress = typeof progress === 'number';
18
- return open || visible.current
17
+ return open || visible
19
18
  ? _jsxs(Backdrop, { variant: "light", ...props, css: style, open: open, className: clsx(classes.root, props.className), onExited: onExited, "data-show-progress": showProgress, children: [_jsxs("div", { className: classes.indicator, children: [_jsx(LoadingIndicator, { size: showProgress ? 14 : 30, borderWidth: showProgress ? 2 : 3, color: color, ...indicatorProps }), _jsx("div", { className: classes.text, children: text })] }), showProgress &&
20
19
  _jsx(Progress, { className: classes.progress, value: progress, ...progressProps })] })
21
20
  : null;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
2
  import { cloneElement, isValidElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { clsx, cloneRef, listenAllPredecessorsScroll, toArray, useControlled, useDerivedState, useForceUpdate, useSync, useSyncState, useUnmounted, useContainer, isElementOverflowed, isChildOf } from '../../utils';
4
+ import { clsx, cloneRef, listenAllPredecessorsScroll, toArray, useControlled, useDerivedState, useForceUpdate, useSync, useSyncState, useUnmounted, useContainer, isElementOverflowed, isChildOf, useUpdateEffect } from '../../utils';
5
5
  import { ClickAway } from '../clickAway';
6
6
  import { classes, style } from './popper.style';
7
7
  import { PopperContext, usePopperContext } from './popperContext';
@@ -54,12 +54,7 @@ export function Popper({ ref, popperRef, anchorElement, container, effectContain
54
54
  }
55
55
  };
56
56
  const { onChildrenOpenChange: tellParentOpenChange } = usePopperContext();
57
- const initialized = useRef(false);
58
- useEffect(() => {
59
- if (!initialized.current) {
60
- initialized.current = true;
61
- return;
62
- }
57
+ useUpdateEffect(() => {
63
58
  tellParentOpenChange(innerOpen.current);
64
59
  }, [innerOpen.current]);
65
60
  const onChildrenOpenChange = (childrenOpen) => {
@@ -1,4 +1,5 @@
1
- import { createContext, useContext, useRef, useEffect, useMemo, useCallback } from 'react';
1
+ import { createContext, useContext, useRef, useCallback } from 'react';
2
+ import { useExternalClass } from '../../utils';
2
3
  export const PopperContext = createContext({
3
4
  autoClose: false,
4
5
  open: false,
@@ -25,11 +26,10 @@ export function useScrollToTarget(scrollerRef) {
25
26
  scrollerEl.scrollTop = targetEl.offsetTop + targetEl.clientHeight / 2 - scrollerEl.clientHeight / 2;
26
27
  }
27
28
  }, []);
28
- useMemo(() => {
29
+ useExternalClass(() => {
29
30
  beforeOpenCallbacks.add(beforeOpen);
30
- }, []);
31
- useEffect(() => () => {
31
+ }, () => {
32
32
  beforeOpenCallbacks.delete(beforeOpen);
33
- }, []);
33
+ });
34
34
  return ref;
35
35
  }
@@ -75,15 +75,17 @@ export const Tabs = memo(({ tabs, labelKey = 'label', primaryKey = 'value', vari
75
75
  const [shadowStart, setShadowStart] = useState(false);
76
76
  const [shadowEnd, setShadowEnd] = useState(false);
77
77
  const setShadow = () => {
78
- if (position === 'top' || position === 'bottom') {
79
- const { scrollLeft } = scrollRef.current;
80
- setShadowStart(scrollLeft > 0);
81
- setShadowEnd(scrollLeft < scrollRef.current.scrollWidth - scrollRef.current.clientWidth);
82
- }
83
- else {
84
- const { scrollTop } = scrollRef.current;
85
- setShadowStart(scrollTop > 0);
86
- setShadowEnd(scrollTop < scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
78
+ if (scrollRef.current) {
79
+ if (position === 'top' || position === 'bottom') {
80
+ const { scrollLeft } = scrollRef.current;
81
+ setShadowStart(scrollLeft > 0);
82
+ setShadowEnd(scrollLeft < scrollRef.current.scrollWidth - scrollRef.current.clientWidth);
83
+ }
84
+ else {
85
+ const { scrollTop } = scrollRef.current;
86
+ setShadowStart(scrollTop > 0);
87
+ setShadowEnd(scrollTop < scrollRef.current.scrollHeight - scrollRef.current.clientHeight);
88
+ }
87
89
  }
88
90
  };
89
91
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "@emotion/react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useRef, useState } from 'react';
3
3
  import { TransitionBase } from './transitionBase';
4
- import { cloneRef } from '../../utils';
4
+ import { cloneRef, useStrictEffect } from '../../utils';
5
5
  export const Collapse = ({ transitionType = 'sweeping', ...props }) => {
6
6
  const Component = transitionType === 'sweeping' ? Sweeping : TransitionBase;
7
7
  return (_jsx(Component, { ...props, _mode: transitionType === 'sweeping' ? '_sweeping' : 'collapse' }));
@@ -29,11 +29,11 @@ const Sweeping = ({ ref, in: _in = false, appear = true, orientation = 'vertical
29
29
  setSize(getCollapsedSize());
30
30
  });
31
31
  };
32
- const initialized = useRef(false);
33
- useEffect(() => {
34
- if (!initialized.current) {
32
+ const mounted = useRef(false);
33
+ useStrictEffect(() => {
34
+ if (!mounted.current) {
35
35
  // 首次渲染
36
- initialized.current = true;
36
+ mounted.current = true;
37
37
  if (!appear) {
38
38
  // 若appear为false,则跳过首次动画
39
39
  return;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "@emotion/react/jsx-runtime";
2
- import { Children, isValidElement, useEffect, useLayoutEffect, useRef } from 'react';
2
+ import { Children, isValidElement, useLayoutEffect, useRef } from 'react';
3
3
  import { classes, useStyle } from './waterfall.style';
4
4
  import { WaterfallItem } from './waterfallItem';
5
- import { cloneRef, clsx, toResponsiveValue, useResponsiveValue } from '../../utils';
5
+ import { cloneRef, clsx, toResponsiveValue, useExternalClass, useResponsiveValue, useUpdateEffect } from '../../utils';
6
6
  export const Waterfall = ({ columnCount = { xs: 4 }, gap = 0, columnGap, rowGap, ...props }) => {
7
7
  columnCount = toResponsiveValue(columnCount);
8
8
  gap = toResponsiveValue(gap);
@@ -29,17 +29,10 @@ export const Waterfall = ({ columnCount = { xs: 4 }, gap = 0, columnGap, rowGap,
29
29
  elements.current.forEach(el => resizeObserver.current.observe(el));
30
30
  });
31
31
  });
32
- const isInitialized = useRef(false);
33
- useEffect(() => {
34
- if (!isInitialized.current) {
35
- isInitialized.current = true;
36
- return;
37
- }
38
- computeItemOrder();
39
- }, [columnCountNum.current]);
40
- useEffect(() => () => {
32
+ useUpdateEffect(computeItemOrder, [columnCountNum.current]);
33
+ useExternalClass(() => void 0, () => {
41
34
  resizeObserver.current.disconnect();
42
- }, []);
35
+ });
43
36
  return (_jsx("div", { ...props, ref: cloneRef(containerRef, props.ref), css: useStyle({ columnCount, columnGap, rowGap }), className: clsx(classes.root, props.className), children: Children.map(props.children, child => {
44
37
  if (!isValidElement(child)) {
45
38
  throw Error('Children of <Waterfall> must be element');
@@ -1,10 +1,5 @@
1
- import { RefObject, Dispatch, SetStateAction } from 'react';
1
+ import { RefObject, Dispatch, SetStateAction, EffectCallback, DependencyList } from 'react';
2
2
  import { DefineElement } from '../types';
3
- /**
4
- * 获取渲染前的值
5
- * @param value
6
- */
7
- export declare function usePrev<T>(value: T): T | null;
8
3
  /**
9
4
  * 将某个值使用ref同步,主要用于对付组件的闭包问题
10
5
  * @param value
@@ -27,6 +22,7 @@ export declare function useForceUpdate(): () => void;
27
22
  */
28
23
  export declare function useDerivedState<T>(referredState: (prevState: T | undefined) => T, deps: any[]): [RefObject<T>, Dispatch<SetStateAction<T>>];
29
24
  export declare function useDerivedState<T>(referredState: T, deps?: any[]): [RefObject<T>, Dispatch<SetStateAction<T>>];
25
+ export declare function useMounted(): void;
30
26
  /**
31
27
  * 组件卸载后得到{current: true}
32
28
  * @returns
@@ -60,3 +56,11 @@ export declare function useContainer<T extends HTMLElement | null>(container?: D
60
56
  * 使用外部类,该方法可避免`StrictMode`下,React渲染行为与外部类实例生命周期不同步的问题
61
57
  */
62
58
  export declare function useExternalClass<T>(setup: () => T, cleanup?: (instance: T) => void): T;
59
+ /**
60
+ * 用法同{@link useEffect},但StrictMode下不会执行两次
61
+ */
62
+ export declare function useStrictEffect(effect: EffectCallback, deps?: DependencyList): void;
63
+ /**
64
+ * 用法同{@link useEffect},但会排除首次渲染
65
+ */
66
+ export declare function useUpdateEffect(effect: EffectCallback, deps?: DependencyList): void;
@@ -1,16 +1,5 @@
1
1
  import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
2
- import { isPromise, getPromiseState } from './utils';
3
- /**
4
- * 获取渲染前的值
5
- * @param value
6
- */
7
- export function usePrev(value) {
8
- const prev = useRef(null);
9
- useEffect(() => {
10
- prev.current = value;
11
- });
12
- return prev.current;
13
- }
2
+ import { isPromise, getPromiseState, arrayShallowEqual } from './utils';
14
3
  /**
15
4
  * 将某个值使用ref同步,主要用于对付组件的闭包问题
16
5
  * @param value
@@ -42,6 +31,10 @@ export function useForceUpdate() {
42
31
  }
43
32
  export function useDerivedState(referredState, deps) {
44
33
  const derivedState = useRef(void 0);
34
+ useMemo(() => {
35
+ // SSR模式下,derivedState会重置2次
36
+ derivedState.current = void 0;
37
+ }, []);
45
38
  const updateState = (state) => {
46
39
  const newState = typeof state === 'function' ? state(derivedState.current) : state;
47
40
  if (derivedState.current !== newState) {
@@ -62,15 +55,17 @@ export function useDerivedState(referredState, deps) {
62
55
  }, [])
63
56
  ];
64
57
  }
58
+ export function useMounted() {
59
+ }
65
60
  /**
66
61
  * 组件卸载后得到{current: true}
67
62
  * @returns
68
63
  */
69
64
  export function useUnmounted() {
70
65
  const isUnmounted = useRef(false);
71
- useEffect(() => () => {
66
+ useExternalClass(() => void 0, () => {
72
67
  isUnmounted.current = true;
73
- }, []);
68
+ });
74
69
  return isUnmounted;
75
70
  }
76
71
  export function useControlled(defaultValue, value, onChange) {
@@ -152,3 +147,29 @@ export function useExternalClass(setup, cleanup) {
152
147
  }, []);
153
148
  return instance;
154
149
  }
150
+ /**
151
+ * 用法同{@link useEffect},但StrictMode下不会执行两次
152
+ */
153
+ export function useStrictEffect(effect, deps) {
154
+ const prevDeps = useRef(deps);
155
+ useEffect(() => {
156
+ const isDepsChanged = prevDeps.current ? arrayShallowEqual(prevDeps.current, deps) : true;
157
+ if (isDepsChanged) {
158
+ prevDeps.current = deps;
159
+ return effect();
160
+ }
161
+ });
162
+ }
163
+ /**
164
+ * 用法同{@link useEffect},但会排除首次渲染
165
+ */
166
+ export function useUpdateEffect(effect, deps) {
167
+ const mounted = useRef(false);
168
+ useStrictEffect(() => {
169
+ if (!mounted.current) {
170
+ mounted.current = true;
171
+ return;
172
+ }
173
+ return effect();
174
+ }, [deps]);
175
+ }
@@ -47,6 +47,7 @@ export declare function mergeDeep<T extends {}, U, V, W, X>(target: T, source: U
47
47
  export declare function mergeDeep<T extends {}, U, V, W, X, Y>(target: T, source: U, source2: V, source3: W, source4: X, source5: Y): T & U & V & W & X & Y;
48
48
  export declare function mergeDeep<T extends {}, U, V, W, X, Y, Z>(target: T, source: U, source2: V, source3: W, source4: X, source5: Y, source6: Z): T & U & V & W & X & Y & Z;
49
49
  export declare function mergeDeep(target: object, ...sources: any[]): any;
50
+ export declare function arrayShallowEqual(a: any[], b: any[]): boolean;
50
51
  export type OverflowEdge = 'top' | 'bottom' | 'left' | 'right';
51
52
  /**
52
53
  * 判断元素是否完全在目标容器内,用于Popper组件
@@ -111,6 +111,22 @@ export function mergeDeep(target, ...sources) {
111
111
  }
112
112
  return target;
113
113
  }
114
+ export function arrayShallowEqual(a, b) {
115
+ if (a === b) {
116
+ return true;
117
+ }
118
+ if (a.length !== b.length) {
119
+ return false;
120
+ }
121
+ let isEqual = true;
122
+ for (let i = 0, { length } = a; i < length; i++) {
123
+ if (a[i] !== b[i]) {
124
+ isEqual = false;
125
+ break;
126
+ }
127
+ }
128
+ return isEqual;
129
+ }
114
130
  /**
115
131
  * 判断元素是否完全在目标容器内,用于Popper组件
116
132
  * @param target 目标元素
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canlooks/can-ui",
3
- "version": "0.0.114",
3
+ "version": "0.0.116",
4
4
  "author": "C.CanLiang <canlooks@gmail.com>",
5
5
  "description": "My ui framework",
6
6
  "license": "MIT",
@@ -44,6 +44,7 @@
44
44
  "build:documentation": "vite build -c documentation/vite.config.mts && tsc -p documentation/tsconfig.bootstrap.json",
45
45
  "rebuild": "npm run clean && npm run build:core && npm run build:documentation",
46
46
  "dev": "vite -c test/vite.config.mts",
47
+ "dev:ssr": "next dev test",
47
48
  "dev:documentation": "vite -c documentation/vite.config.mts --host",
48
49
  "test:unit": "npx ts-node test/unit.ts"
49
50
  },
@@ -74,6 +75,7 @@
74
75
  "@types/react-syntax-highlighter": "^15.5.13",
75
76
  "@types/react-transition-group": "^4.4.12",
76
77
  "mime": "^4.1.0",
78
+ "next": "^16.0.10",
77
79
  "react": "^19.2.3",
78
80
  "react-dom": "^19.2.3",
79
81
  "react-markdown": "^10.1.0",