@zat-design/sisyphus-react 4.5.8-beta.3 → 4.5.8-beta.5

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.
@@ -0,0 +1 @@
1
+ export const DEFAULT_FIXED_TOP = 'var(--pro-layout-content-sticky-top, 64px)';
@@ -1,11 +1,17 @@
1
1
  import _debounce from "lodash/debounce";
2
- import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
2
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import { useSetState, useLocalStorageState } from 'ahooks';
4
4
  import { handleScroll, handleScrollError } from "./utils";
5
5
  import Step from "./components/Step";
6
6
  import Item from "./components/Item";
7
7
  import Listener from "./components/Listener";
8
+ import { DEFAULT_FIXED_TOP } from "./constants";
8
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ const DEFAULT_ANCHOR_GAP = '16px';
11
+ const ANCHOR_SCROLL_DELAY = 50;
12
+ const STICKY_HEADER_SELECTOR = '[data-pro-step-sticky-header="true"]';
13
+ const toCssLength = value => typeof value === 'number' ? `${value}px` : value;
14
+
9
15
  // 创建上下文并提供类型
10
16
  export const ProStepContext = /*#__PURE__*/createContext(null);
11
17
  export const useStep = () => {
@@ -23,13 +29,24 @@ const ProStep = ({
23
29
  });
24
30
  // 注册子节点id与title映射的集合
25
31
  const registerMap = useRef({});
32
+ const wrapperRef = useRef(null);
33
+ const stickyHeaderRef = useRef(null);
34
+ const autoAnchorEnabledRef = useRef(false);
35
+ const scrollTaskRef = useRef(null);
26
36
  const [, setLocalData] = useLocalStorageState('cache-pro-step');
27
37
  const ids = Object.keys(registerMap.current);
28
38
  const {
29
39
  collapse = false,
30
40
  lazyLoad = false,
31
- targetOffset
41
+ targetOffset,
42
+ fixedTop
32
43
  } = resetProps;
44
+ const hasExplicitTargetOffset = targetOffset !== undefined && targetOffset !== null;
45
+ const anchorBase = fixedTop === undefined || fixedTop === null ? DEFAULT_FIXED_TOP : toCssLength(fixedTop);
46
+ const autoAnchorOffset = `calc(${anchorBase} + var(--pro-step-sticky-header-height, 0px) + ${DEFAULT_ANCHOR_GAP})`;
47
+ const wrapperStyle = hasExplicitTargetOffset ? {
48
+ '--pro-step-anchor-offset': toCssLength(targetOffset)
49
+ } : undefined;
33
50
  const dataSource = useMemo(() => {
34
51
  if (resetProps?.dataSource) {
35
52
  return resetProps?.dataSource;
@@ -56,12 +73,14 @@ const ProStep = ({
56
73
  order,
57
74
  disabled,
58
75
  lazyLoad,
76
+ element,
59
77
  ...rest
60
78
  }) => {
61
79
  const record = {};
62
80
  record.id = id;
63
81
  record.title = title;
64
82
  record.order = order;
83
+ record.element = element;
65
84
  if (!registerMap.current[id]) {
66
85
  debounceReRender();
67
86
  }
@@ -100,6 +119,80 @@ const ProStep = ({
100
119
  });
101
120
  }
102
121
  };
122
+ const measureStickyHeader = useCallback((detect = false) => {
123
+ if (hasExplicitTargetOffset) {
124
+ return false;
125
+ }
126
+ if (!detect && !autoAnchorEnabledRef.current) {
127
+ return false;
128
+ }
129
+ const wrapper = wrapperRef.current;
130
+ if (!wrapper) {
131
+ return false;
132
+ }
133
+ const currentHeader = stickyHeaderRef.current;
134
+ const header = currentHeader && currentHeader.parentElement === wrapper ? currentHeader : Array.from(wrapper.children).find(element => element.matches(STICKY_HEADER_SELECTOR));
135
+ if (!header) {
136
+ autoAnchorEnabledRef.current = false;
137
+ stickyHeaderRef.current = null;
138
+ wrapper.style.removeProperty('--pro-step-sticky-header-height');
139
+ wrapper.style.removeProperty('--pro-step-anchor-offset');
140
+ return false;
141
+ }
142
+ autoAnchorEnabledRef.current = true;
143
+ stickyHeaderRef.current = header;
144
+ wrapper.style.setProperty('--pro-step-sticky-header-height', `${header.getBoundingClientRect().height}px`);
145
+ wrapper.style.setProperty('--pro-step-anchor-offset', autoAnchorOffset);
146
+ return true;
147
+ }, [autoAnchorOffset, hasExplicitTargetOffset]);
148
+ useLayoutEffect(() => {
149
+ autoAnchorEnabledRef.current = false;
150
+ if (hasExplicitTargetOffset || !measureStickyHeader(true)) {
151
+ return undefined;
152
+ }
153
+ const header = stickyHeaderRef.current;
154
+ if (!header || typeof ResizeObserver === 'undefined') {
155
+ return undefined;
156
+ }
157
+ const observer = new ResizeObserver(() => {
158
+ measureStickyHeader();
159
+ });
160
+ observer.observe(header);
161
+ return () => {
162
+ observer.disconnect();
163
+ autoAnchorEnabledRef.current = false;
164
+ stickyHeaderRef.current = null;
165
+ };
166
+ }, [hasExplicitTargetOffset, measureStickyHeader]);
167
+ useEffect(() => {
168
+ return () => {
169
+ if (scrollTaskRef.current !== null) {
170
+ window.clearTimeout(scrollTaskRef.current);
171
+ }
172
+ };
173
+ }, []);
174
+ const scrollToStep = useCallback((id, options) => {
175
+ if (autoAnchorEnabledRef.current) {
176
+ measureStickyHeader();
177
+ }
178
+ const root = wrapperRef.current;
179
+ if (!root) {
180
+ return;
181
+ }
182
+ const registeredElement = registerMap.current[id]?.element;
183
+ const target = registeredElement && root.contains(registeredElement) ? registeredElement : Array.from(root.querySelectorAll('.pro-step-item')).find(item => item.id === id);
184
+ if (scrollTaskRef.current !== null) {
185
+ window.clearTimeout(scrollTaskRef.current);
186
+ }
187
+ scrollTaskRef.current = window.setTimeout(() => {
188
+ scrollTaskRef.current = null;
189
+ handleScroll(id, {
190
+ ...options,
191
+ root,
192
+ target
193
+ });
194
+ }, ANCHOR_SCROLL_DELAY);
195
+ }, [measureStickyHeader]);
103
196
  const notify = async params => {
104
197
  const {
105
198
  excludes
@@ -121,7 +214,7 @@ const ProStep = ({
121
214
  setState({
122
215
  errorCollection: nextErrorCollection
123
216
  });
124
- handleScrollError();
217
+ handleScrollError(undefined, wrapperRef.current || document);
125
218
  return res;
126
219
  };
127
220
  const triggerTo = async keys => {
@@ -139,7 +232,7 @@ const ProStep = ({
139
232
  setState({
140
233
  errorCollection: nextErrorCollection
141
234
  });
142
- handleScrollError();
235
+ handleScrollError(undefined, wrapperRef.current || document);
143
236
  return result;
144
237
  };
145
238
  return /*#__PURE__*/_jsx(ProStepContext.Provider, {
@@ -149,19 +242,22 @@ const ProStep = ({
149
242
  register,
150
243
  notify,
151
244
  triggerTo,
152
- handleScroll,
245
+ handleScroll: scrollToStep,
153
246
  lazyLoad,
154
247
  targetOffset,
155
248
  unNotify,
156
249
  source: 'ProStep'
157
250
  },
158
251
  children: /*#__PURE__*/_jsxs("div", {
252
+ ref: wrapperRef,
159
253
  className: "pro-step-wrapper",
160
254
  id: "pro-step",
255
+ style: wrapperStyle,
161
256
  children: [children, /*#__PURE__*/_jsx(Step, {
162
257
  ...resetProps,
163
258
  dataSource: dataSource,
164
- errorCollection: errorCollection
259
+ errorCollection: errorCollection,
260
+ onAnchorClick: scrollToStep
165
261
  })]
166
262
  })
167
263
  });
@@ -45,8 +45,8 @@ export interface ProStepPropsType {
45
45
  children?: ReactNode;
46
46
  /** 组件自定义样式 */
47
47
  style?: CSSProperties;
48
- /** 滚动偏移量(单位:像素) */
49
- targetOffset?: number;
48
+ /** 完整滚动偏移量,显式设置时覆盖 ProHeader 自动偏移 */
49
+ targetOffset?: string | number;
50
50
  /** 是否使用折叠标题 */
51
51
  collapse?: boolean;
52
52
  /** 是否自动滚动到第一个报错的步骤 */
@@ -102,8 +102,8 @@ export interface ProStepContextType {
102
102
  handleScroll: (id: string, options?: ScrollOptions) => void;
103
103
  /** 懒加载配置 */
104
104
  lazyLoad?: boolean | any;
105
- /** 滚动偏移量 */
106
- targetOffset?: number;
105
+ /** 完整滚动偏移量 */
106
+ targetOffset?: string | number;
107
107
  /** 取消注册函数 */
108
108
  unNotify: (keys: string | string[]) => void;
109
109
  /** 来源 */
@@ -126,6 +126,8 @@ export interface RegisterParams {
126
126
  order: number;
127
127
  /** 是否禁用 */
128
128
  disabled?: boolean;
129
+ /** 当前步骤项 DOM,仅供 ProStep 实例内定位使用 */
130
+ element?: HTMLElement | null;
129
131
  /** 懒加载配置 */
130
132
  lazyLoad?: boolean | any;
131
133
  /** 允许其他任意属性 */
@@ -144,6 +146,8 @@ export interface RegisterMapItem {
144
146
  order: number;
145
147
  /** 提交事件 */
146
148
  subEvent?: () => Promise<any>;
149
+ /** 当前步骤项 DOM,仅供 ProStep 实例内定位使用 */
150
+ element?: HTMLElement | null;
147
151
  /** 允许其他任意属性 */
148
152
  [key: string]: any;
149
153
  }
@@ -161,7 +165,7 @@ export interface NotifyParams {
161
165
  */
162
166
  export interface ScrollOptions {
163
167
  /** 目标偏移量 */
164
- targetOffset?: number;
168
+ targetOffset?: string | number;
165
169
  /** 是否滚动到错误位置 */
166
170
  scrollToError?: boolean;
167
171
  }
@@ -213,9 +217,11 @@ export interface MenuItemProps {
213
217
  /** 当前数量 */
214
218
  currentNum?: number;
215
219
  /** 目标偏移量 */
216
- targetOffset?: number;
220
+ targetOffset?: string | number;
217
221
  /** 是否滚动到错误位置 */
218
222
  scrollToError?: boolean;
223
+ /** 当前 ProStep 实例的锚点处理器 */
224
+ onAnchorClick?: (id: string, options?: ScrollOptions) => void;
219
225
  }
220
226
  /**
221
227
  * ProStep 组件基础类型
@@ -1,17 +1,17 @@
1
1
  import React from 'react';
2
- import type { LoadedMapType } from '../propsType';
2
+ import type { LoadedMapType, ScrollOptions } from '../propsType';
3
3
  /**
4
4
  * 滚动到错误位置, 延迟200ms解决ProForm错误还未生成
5
5
  */
6
- export declare const handleScrollError: (dom?: HTMLElement) => void;
6
+ export declare const handleScrollError: (dom?: HTMLElement, root?: ParentNode) => void;
7
7
  /**
8
8
  * 处理滚动到指定元素位置, 如发现有错误, 则优先滚动到错误位置
9
9
  * @param id 目标元素的ID
10
10
  * @param options 滚动选项
11
11
  */
12
- export declare const handleScroll: (id: string, options?: {
13
- targetOffset?: number;
14
- scrollToError?: boolean;
12
+ export declare const handleScroll: (id: string, options?: ScrollOptions & {
13
+ root?: ParentNode;
14
+ target?: HTMLElement | null;
15
15
  }) => void;
16
16
  /**
17
17
  * 获取加载模块数据信息
@@ -1,12 +1,13 @@
1
1
  import _pick from "lodash/pick";
2
- import _debounce from "lodash/debounce";
3
2
  import scrollIntoView from 'scroll-into-view-if-needed';
3
+ const toCssLength = value => typeof value === 'number' ? `${value}px` : value;
4
+
4
5
  /**
5
6
  * 滚动到错误位置, 延迟200ms解决ProForm错误还未生成
6
7
  */
7
- export const handleScrollError = dom => {
8
+ export const handleScrollError = (dom, root = document) => {
8
9
  setTimeout(() => {
9
- const errorDom = dom || document.querySelector('[class*="form-item-has-error"]');
10
+ const errorDom = dom || root.querySelector('[class*="form-item-has-error"]');
10
11
  if (errorDom) {
11
12
  scrollIntoView(errorDom, {
12
13
  behavior: 'smooth',
@@ -24,10 +25,12 @@ export const handleScrollError = dom => {
24
25
  */
25
26
  export const handleScroll = (id, options) => {
26
27
  const {
27
- targetOffset = 0,
28
- scrollToError
28
+ targetOffset,
29
+ scrollToError,
30
+ root = document,
31
+ target
29
32
  } = options || {};
30
- const dom = document.querySelector(`#${id}`);
33
+ const dom = target || Array.from(root.querySelectorAll('.pro-step-item')).find(item => item.id === id);
31
34
  if (dom) {
32
35
  // 查找指定id下的错误表单项
33
36
  const errorDom = dom.querySelector('[class*="form-item-has-error"]');
@@ -35,14 +38,35 @@ export const handleScroll = (id, options) => {
35
38
  // 如果发现错误表单项,执行handleScrollError函数
36
39
  handleScrollError(errorDom);
37
40
  } else {
38
- // 如果没有错误表单项,则滚动到指定元素位置
39
- setTimeout(_debounce(() => {
41
+ // 目录点击必须重新对齐到 sticky 区域下方,不能因“当前可见”而跳过。
42
+ const previousScrollMarginTop = dom.style.scrollMarginTop;
43
+ if (targetOffset !== undefined && targetOffset !== null) {
44
+ dom.style.scrollMarginTop = toCssLength(targetOffset);
45
+ }
46
+ try {
40
47
  scrollIntoView(dom, {
41
- behavior: 'smooth',
48
+ // compute-scroll-into-view 已计算 scroll-margin;custom behavior 避免外层库再次扣减。
49
+ behavior: actions => {
50
+ actions.forEach(({
51
+ el,
52
+ top,
53
+ left
54
+ }) => {
55
+ el.scroll({
56
+ top,
57
+ left,
58
+ behavior: 'smooth'
59
+ });
60
+ });
61
+ },
42
62
  block: 'start',
43
- scrollMode: 'if-needed'
63
+ scrollMode: 'always'
44
64
  });
45
- }, 100), 0);
65
+ } finally {
66
+ if (targetOffset !== undefined && targetOffset !== null) {
67
+ dom.style.scrollMarginTop = previousScrollMarginTop;
68
+ }
69
+ }
46
70
  }
47
71
  }
48
72
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-react",
3
- "version": "4.5.8-beta.3",
3
+ "version": "4.5.8-beta.5",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",