@rc-component/util 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/es/Dom/focus.d.ts CHANGED
@@ -1,11 +1,4 @@
1
1
  export declare function getFocusNodeList(node: HTMLElement, includePositive?: boolean): HTMLElement[];
2
- /** @deprecated Do not use since this may failed when used in async */
3
- export declare function saveLastFocusNode(): void;
4
- /** @deprecated Do not use since this may failed when used in async */
5
- export declare function clearLastFocusNode(): void;
6
- /** @deprecated Do not use since this may failed when used in async */
7
- export declare function backLastFocusNode(): void;
8
- export declare function limitTabRange(node: HTMLElement, e: KeyboardEvent): void;
9
2
  export interface InputFocusOptions extends FocusOptions {
10
3
  cursor?: 'start' | 'end' | 'all';
11
4
  }
@@ -13,3 +6,14 @@ export interface InputFocusOptions extends FocusOptions {
13
6
  * Focus element and set cursor position for input/textarea elements.
14
7
  */
15
8
  export declare function triggerFocus(element?: HTMLElement, option?: InputFocusOptions): void;
9
+ /**
10
+ * Lock focus in the element.
11
+ * It will force back to the first focusable element when focus leaves the element.
12
+ */
13
+ export declare function lockFocus(element: HTMLElement): VoidFunction;
14
+ /**
15
+ * Lock focus within an element.
16
+ * When locked, focus will be restricted to focusable elements within the specified element.
17
+ * If multiple elements are locked, only the last locked element will be effective.
18
+ */
19
+ export declare function useLockFocus(lock: boolean, getElement: () => HTMLElement | null): void;
package/es/Dom/focus.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { useEffect } from 'react';
1
2
  import isVisible from "./isVisible";
2
3
  function focusable(node, includePositive = false) {
3
4
  if (isVisible(node)) {
@@ -39,44 +40,6 @@ export function getFocusNodeList(node, includePositive = false) {
39
40
  }
40
41
  return res;
41
42
  }
42
- let lastFocusElement = null;
43
-
44
- /** @deprecated Do not use since this may failed when used in async */
45
- export function saveLastFocusNode() {
46
- lastFocusElement = document.activeElement;
47
- }
48
-
49
- /** @deprecated Do not use since this may failed when used in async */
50
- export function clearLastFocusNode() {
51
- lastFocusElement = null;
52
- }
53
-
54
- /** @deprecated Do not use since this may failed when used in async */
55
- export function backLastFocusNode() {
56
- if (lastFocusElement) {
57
- try {
58
- // 元素可能已经被移动了
59
- lastFocusElement.focus();
60
-
61
- /* eslint-disable no-empty */
62
- } catch (e) {
63
- // empty
64
- }
65
- /* eslint-enable no-empty */
66
- }
67
- }
68
- export function limitTabRange(node, e) {
69
- if (e.keyCode === 9) {
70
- const tabNodeList = getFocusNodeList(node);
71
- const lastTabNode = tabNodeList[e.shiftKey ? 0 : tabNodeList.length - 1];
72
- const leavingTab = lastTabNode === document.activeElement || node === document.activeElement;
73
- if (leavingTab) {
74
- const target = tabNodeList[e.shiftKey ? tabNodeList.length - 1 : 0];
75
- target.focus();
76
- e.preventDefault();
77
- }
78
- }
79
- }
80
43
  // Used for `rc-input` `rc-textarea` `rc-input-number`
81
44
  /**
82
45
  * Focus element and set cursor position for input/textarea elements.
@@ -102,4 +65,92 @@ export function triggerFocus(element, option) {
102
65
  element.setSelectionRange(0, len);
103
66
  }
104
67
  }
68
+ }
69
+
70
+ // ======================================================
71
+ // == Lock Focus ==
72
+ // ======================================================
73
+ let lastFocusElement = null;
74
+ let focusElements = [];
75
+ function getLastElement() {
76
+ return focusElements[focusElements.length - 1];
77
+ }
78
+ function hasFocus(element) {
79
+ const {
80
+ activeElement
81
+ } = document;
82
+ return element === activeElement || element.contains(activeElement);
83
+ }
84
+ function syncFocus() {
85
+ const lastElement = getLastElement();
86
+ const {
87
+ activeElement
88
+ } = document;
89
+ if (lastElement && !hasFocus(lastElement)) {
90
+ const focusableList = getFocusNodeList(lastElement);
91
+ const matchElement = focusableList.includes(lastFocusElement) ? lastFocusElement : focusableList[0];
92
+ matchElement?.focus();
93
+ } else {
94
+ lastFocusElement = activeElement;
95
+ }
96
+ }
97
+ function onWindowKeyDown(e) {
98
+ if (e.key === 'Tab') {
99
+ const {
100
+ activeElement
101
+ } = document;
102
+ const lastElement = getLastElement();
103
+ const focusableList = getFocusNodeList(lastElement);
104
+ const last = focusableList[focusableList.length - 1];
105
+ if (e.shiftKey && activeElement === focusableList[0]) {
106
+ // Tab backward on first focusable element
107
+ lastFocusElement = last;
108
+ } else if (!e.shiftKey && activeElement === last) {
109
+ // Tab forward on last focusable element
110
+ lastFocusElement = focusableList[0];
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Lock focus in the element.
117
+ * It will force back to the first focusable element when focus leaves the element.
118
+ */
119
+ export function lockFocus(element) {
120
+ if (element) {
121
+ // Refresh focus elements
122
+ focusElements = focusElements.filter(ele => ele !== element);
123
+ focusElements.push(element);
124
+
125
+ // Just add event since it will de-duplicate
126
+ window.addEventListener('focusin', syncFocus);
127
+ window.addEventListener('keydown', onWindowKeyDown, true);
128
+ syncFocus();
129
+ }
130
+
131
+ // Always return unregister function
132
+ return () => {
133
+ lastFocusElement = null;
134
+ focusElements = focusElements.filter(ele => ele !== element);
135
+ if (focusElements.length === 0) {
136
+ window.removeEventListener('focusin', syncFocus);
137
+ window.removeEventListener('keydown', onWindowKeyDown, true);
138
+ }
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Lock focus within an element.
144
+ * When locked, focus will be restricted to focusable elements within the specified element.
145
+ * If multiple elements are locked, only the last locked element will be effective.
146
+ */
147
+ export function useLockFocus(lock, getElement) {
148
+ useEffect(() => {
149
+ if (lock) {
150
+ const element = getElement();
151
+ if (element) {
152
+ return lockFocus(element);
153
+ }
154
+ }
155
+ }, [lock]);
105
156
  }
@@ -1,7 +1,5 @@
1
- type ScrollBarSize = {
1
+ export default function getScrollBarSize(fresh?: boolean): number;
2
+ export declare function getTargetScrollBarSize(target: HTMLElement): {
2
3
  width: number;
3
4
  height: number;
4
5
  };
5
- export default function getScrollBarSize(fresh?: boolean): number;
6
- export declare function getTargetScrollBarSize(target: HTMLElement): ScrollBarSize;
7
- export {};
package/es/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { default as useMergedState } from './hooks/useMergedState';
3
3
  export { default as useControlledState } from './hooks/useControlledState';
4
4
  export { supportNodeRef, supportRef, useComposeRef } from './ref';
5
5
  export { default as get } from './utils/get';
6
- export { default as set, merge } from './utils/set';
6
+ export { default as set, merge, mergeWith } from './utils/set';
7
7
  export { default as warning, noteOnce } from './warning';
8
8
  export { default as omit } from './omit';
9
9
  export { default as toArray } from './Children/toArray';
package/es/index.js CHANGED
@@ -3,7 +3,7 @@ export { default as useMergedState } from "./hooks/useMergedState";
3
3
  export { default as useControlledState } from "./hooks/useControlledState";
4
4
  export { supportNodeRef, supportRef, useComposeRef } from "./ref";
5
5
  export { default as get } from "./utils/get";
6
- export { default as set, merge } from "./utils/set";
6
+ export { default as set, merge, mergeWith } from "./utils/set";
7
7
  export { default as warning, noteOnce } from "./warning";
8
8
  export { default as omit } from "./omit";
9
9
  export { default as toArray } from "./Children/toArray";
package/es/utils/set.d.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  export type Path = (string | number | symbol)[];
2
2
  export default function set<Entity = any, Output = Entity, Value = any>(entity: Entity, paths: Path, value: Value, removeIfUndefined?: boolean): Output;
3
+ export type MergeFn = (current: any, next: any) => any;
3
4
  /**
4
- * Merge objects which will create
5
+ * Merge multiple objects. Support custom merge logic.
6
+ * @param sources object sources
7
+ * @param config.prepareArray Customize array prepare function.
8
+ * It will return empty [] by default.
9
+ * So when match array, it will auto be override with next array in sources.
10
+ */
11
+ export declare function mergeWith<T extends object>(sources: T[], config?: {
12
+ prepareArray?: MergeFn;
13
+ }): T;
14
+ /**
15
+ * Merge multiple objects into a new single object.
16
+ * Arrays will be replaced by default.
5
17
  */
6
18
  export declare function merge<T extends object>(...sources: T[]): T;
package/es/utils/set.js CHANGED
@@ -38,10 +38,20 @@ function createEmpty(source) {
38
38
  }
39
39
  const keys = typeof Reflect === 'undefined' ? Object.keys : Reflect.ownKeys;
40
40
 
41
+ // ================================ Merge ================================
42
+
41
43
  /**
42
- * Merge objects which will create
44
+ * Merge multiple objects. Support custom merge logic.
45
+ * @param sources object sources
46
+ * @param config.prepareArray Customize array prepare function.
47
+ * It will return empty [] by default.
48
+ * So when match array, it will auto be override with next array in sources.
43
49
  */
44
- export function merge(...sources) {
50
+ export function mergeWith(sources, config = {}) {
51
+ const {
52
+ prepareArray
53
+ } = config;
54
+ const finalPrepareArray = prepareArray || (() => []);
45
55
  let clone = createEmpty(sources[0]);
46
56
  sources.forEach(src => {
47
57
  function internalMerge(path, parentLoopSet) {
@@ -55,7 +65,7 @@ export function merge(...sources) {
55
65
  const originValue = get(clone, path);
56
66
  if (isArr) {
57
67
  // Array will always be override
58
- clone = set(clone, path, []);
68
+ clone = set(clone, path, finalPrepareArray(originValue, value));
59
69
  } else if (!originValue || typeof originValue !== 'object') {
60
70
  // Init container if not exist
61
71
  clone = set(clone, path, createEmpty(value));
@@ -71,4 +81,12 @@ export function merge(...sources) {
71
81
  internalMerge([]);
72
82
  });
73
83
  return clone;
84
+ }
85
+
86
+ /**
87
+ * Merge multiple objects into a new single object.
88
+ * Arrays will be replaced by default.
89
+ */
90
+ export function merge(...sources) {
91
+ return mergeWith(sources);
74
92
  }
@@ -1,11 +1,4 @@
1
1
  export declare function getFocusNodeList(node: HTMLElement, includePositive?: boolean): HTMLElement[];
2
- /** @deprecated Do not use since this may failed when used in async */
3
- export declare function saveLastFocusNode(): void;
4
- /** @deprecated Do not use since this may failed when used in async */
5
- export declare function clearLastFocusNode(): void;
6
- /** @deprecated Do not use since this may failed when used in async */
7
- export declare function backLastFocusNode(): void;
8
- export declare function limitTabRange(node: HTMLElement, e: KeyboardEvent): void;
9
2
  export interface InputFocusOptions extends FocusOptions {
10
3
  cursor?: 'start' | 'end' | 'all';
11
4
  }
@@ -13,3 +6,14 @@ export interface InputFocusOptions extends FocusOptions {
13
6
  * Focus element and set cursor position for input/textarea elements.
14
7
  */
15
8
  export declare function triggerFocus(element?: HTMLElement, option?: InputFocusOptions): void;
9
+ /**
10
+ * Lock focus in the element.
11
+ * It will force back to the first focusable element when focus leaves the element.
12
+ */
13
+ export declare function lockFocus(element: HTMLElement): VoidFunction;
14
+ /**
15
+ * Lock focus within an element.
16
+ * When locked, focus will be restricted to focusable elements within the specified element.
17
+ * If multiple elements are locked, only the last locked element will be effective.
18
+ */
19
+ export declare function useLockFocus(lock: boolean, getElement: () => HTMLElement | null): void;
package/lib/Dom/focus.js CHANGED
@@ -3,12 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.backLastFocusNode = backLastFocusNode;
7
- exports.clearLastFocusNode = clearLastFocusNode;
8
6
  exports.getFocusNodeList = getFocusNodeList;
9
- exports.limitTabRange = limitTabRange;
10
- exports.saveLastFocusNode = saveLastFocusNode;
7
+ exports.lockFocus = lockFocus;
11
8
  exports.triggerFocus = triggerFocus;
9
+ exports.useLockFocus = useLockFocus;
10
+ var _react = require("react");
12
11
  var _isVisible = _interopRequireDefault(require("./isVisible"));
13
12
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
13
  function focusable(node, includePositive = false) {
@@ -51,44 +50,6 @@ function getFocusNodeList(node, includePositive = false) {
51
50
  }
52
51
  return res;
53
52
  }
54
- let lastFocusElement = null;
55
-
56
- /** @deprecated Do not use since this may failed when used in async */
57
- function saveLastFocusNode() {
58
- lastFocusElement = document.activeElement;
59
- }
60
-
61
- /** @deprecated Do not use since this may failed when used in async */
62
- function clearLastFocusNode() {
63
- lastFocusElement = null;
64
- }
65
-
66
- /** @deprecated Do not use since this may failed when used in async */
67
- function backLastFocusNode() {
68
- if (lastFocusElement) {
69
- try {
70
- // 元素可能已经被移动了
71
- lastFocusElement.focus();
72
-
73
- /* eslint-disable no-empty */
74
- } catch (e) {
75
- // empty
76
- }
77
- /* eslint-enable no-empty */
78
- }
79
- }
80
- function limitTabRange(node, e) {
81
- if (e.keyCode === 9) {
82
- const tabNodeList = getFocusNodeList(node);
83
- const lastTabNode = tabNodeList[e.shiftKey ? 0 : tabNodeList.length - 1];
84
- const leavingTab = lastTabNode === document.activeElement || node === document.activeElement;
85
- if (leavingTab) {
86
- const target = tabNodeList[e.shiftKey ? tabNodeList.length - 1 : 0];
87
- target.focus();
88
- e.preventDefault();
89
- }
90
- }
91
- }
92
53
  // Used for `rc-input` `rc-textarea` `rc-input-number`
93
54
  /**
94
55
  * Focus element and set cursor position for input/textarea elements.
@@ -114,4 +75,92 @@ function triggerFocus(element, option) {
114
75
  element.setSelectionRange(0, len);
115
76
  }
116
77
  }
78
+ }
79
+
80
+ // ======================================================
81
+ // == Lock Focus ==
82
+ // ======================================================
83
+ let lastFocusElement = null;
84
+ let focusElements = [];
85
+ function getLastElement() {
86
+ return focusElements[focusElements.length - 1];
87
+ }
88
+ function hasFocus(element) {
89
+ const {
90
+ activeElement
91
+ } = document;
92
+ return element === activeElement || element.contains(activeElement);
93
+ }
94
+ function syncFocus() {
95
+ const lastElement = getLastElement();
96
+ const {
97
+ activeElement
98
+ } = document;
99
+ if (lastElement && !hasFocus(lastElement)) {
100
+ const focusableList = getFocusNodeList(lastElement);
101
+ const matchElement = focusableList.includes(lastFocusElement) ? lastFocusElement : focusableList[0];
102
+ matchElement?.focus();
103
+ } else {
104
+ lastFocusElement = activeElement;
105
+ }
106
+ }
107
+ function onWindowKeyDown(e) {
108
+ if (e.key === 'Tab') {
109
+ const {
110
+ activeElement
111
+ } = document;
112
+ const lastElement = getLastElement();
113
+ const focusableList = getFocusNodeList(lastElement);
114
+ const last = focusableList[focusableList.length - 1];
115
+ if (e.shiftKey && activeElement === focusableList[0]) {
116
+ // Tab backward on first focusable element
117
+ lastFocusElement = last;
118
+ } else if (!e.shiftKey && activeElement === last) {
119
+ // Tab forward on last focusable element
120
+ lastFocusElement = focusableList[0];
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Lock focus in the element.
127
+ * It will force back to the first focusable element when focus leaves the element.
128
+ */
129
+ function lockFocus(element) {
130
+ if (element) {
131
+ // Refresh focus elements
132
+ focusElements = focusElements.filter(ele => ele !== element);
133
+ focusElements.push(element);
134
+
135
+ // Just add event since it will de-duplicate
136
+ window.addEventListener('focusin', syncFocus);
137
+ window.addEventListener('keydown', onWindowKeyDown, true);
138
+ syncFocus();
139
+ }
140
+
141
+ // Always return unregister function
142
+ return () => {
143
+ lastFocusElement = null;
144
+ focusElements = focusElements.filter(ele => ele !== element);
145
+ if (focusElements.length === 0) {
146
+ window.removeEventListener('focusin', syncFocus);
147
+ window.removeEventListener('keydown', onWindowKeyDown, true);
148
+ }
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Lock focus within an element.
154
+ * When locked, focus will be restricted to focusable elements within the specified element.
155
+ * If multiple elements are locked, only the last locked element will be effective.
156
+ */
157
+ function useLockFocus(lock, getElement) {
158
+ (0, _react.useEffect)(() => {
159
+ if (lock) {
160
+ const element = getElement();
161
+ if (element) {
162
+ return lockFocus(element);
163
+ }
164
+ }
165
+ }, [lock]);
117
166
  }
@@ -1,7 +1,5 @@
1
- type ScrollBarSize = {
1
+ export default function getScrollBarSize(fresh?: boolean): number;
2
+ export declare function getTargetScrollBarSize(target: HTMLElement): {
2
3
  width: number;
3
4
  height: number;
4
5
  };
5
- export default function getScrollBarSize(fresh?: boolean): number;
6
- export declare function getTargetScrollBarSize(target: HTMLElement): ScrollBarSize;
7
- export {};
package/lib/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { default as useMergedState } from './hooks/useMergedState';
3
3
  export { default as useControlledState } from './hooks/useControlledState';
4
4
  export { supportNodeRef, supportRef, useComposeRef } from './ref';
5
5
  export { default as get } from './utils/get';
6
- export { default as set, merge } from './utils/set';
6
+ export { default as set, merge, mergeWith } from './utils/set';
7
7
  export { default as warning, noteOnce } from './warning';
8
8
  export { default as omit } from './omit';
9
9
  export { default as toArray } from './Children/toArray';
package/lib/index.js CHANGED
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "merge", {
15
15
  return _set.merge;
16
16
  }
17
17
  });
18
+ Object.defineProperty(exports, "mergeWith", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _set.mergeWith;
22
+ }
23
+ });
18
24
  Object.defineProperty(exports, "noteOnce", {
19
25
  enumerable: true,
20
26
  get: function () {
@@ -1,6 +1,18 @@
1
1
  export type Path = (string | number | symbol)[];
2
2
  export default function set<Entity = any, Output = Entity, Value = any>(entity: Entity, paths: Path, value: Value, removeIfUndefined?: boolean): Output;
3
+ export type MergeFn = (current: any, next: any) => any;
3
4
  /**
4
- * Merge objects which will create
5
+ * Merge multiple objects. Support custom merge logic.
6
+ * @param sources object sources
7
+ * @param config.prepareArray Customize array prepare function.
8
+ * It will return empty [] by default.
9
+ * So when match array, it will auto be override with next array in sources.
10
+ */
11
+ export declare function mergeWith<T extends object>(sources: T[], config?: {
12
+ prepareArray?: MergeFn;
13
+ }): T;
14
+ /**
15
+ * Merge multiple objects into a new single object.
16
+ * Arrays will be replaced by default.
5
17
  */
6
18
  export declare function merge<T extends object>(...sources: T[]): T;
package/lib/utils/set.js CHANGED
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = set;
7
7
  exports.merge = merge;
8
+ exports.mergeWith = mergeWith;
8
9
  var _get = _interopRequireDefault(require("./get"));
9
10
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
11
  function internalSet(entity, paths, value, removeIfUndefined) {
@@ -46,10 +47,20 @@ function createEmpty(source) {
46
47
  }
47
48
  const keys = typeof Reflect === 'undefined' ? Object.keys : Reflect.ownKeys;
48
49
 
50
+ // ================================ Merge ================================
51
+
49
52
  /**
50
- * Merge objects which will create
53
+ * Merge multiple objects. Support custom merge logic.
54
+ * @param sources object sources
55
+ * @param config.prepareArray Customize array prepare function.
56
+ * It will return empty [] by default.
57
+ * So when match array, it will auto be override with next array in sources.
51
58
  */
52
- function merge(...sources) {
59
+ function mergeWith(sources, config = {}) {
60
+ const {
61
+ prepareArray
62
+ } = config;
63
+ const finalPrepareArray = prepareArray || (() => []);
53
64
  let clone = createEmpty(sources[0]);
54
65
  sources.forEach(src => {
55
66
  function internalMerge(path, parentLoopSet) {
@@ -63,7 +74,7 @@ function merge(...sources) {
63
74
  const originValue = (0, _get.default)(clone, path);
64
75
  if (isArr) {
65
76
  // Array will always be override
66
- clone = set(clone, path, []);
77
+ clone = set(clone, path, finalPrepareArray(originValue, value));
67
78
  } else if (!originValue || typeof originValue !== 'object') {
68
79
  // Init container if not exist
69
80
  clone = set(clone, path, createEmpty(value));
@@ -79,4 +90,12 @@ function merge(...sources) {
79
90
  internalMerge([]);
80
91
  });
81
92
  return clone;
93
+ }
94
+
95
+ /**
96
+ * Merge multiple objects into a new single object.
97
+ * Arrays will be replaced by default.
98
+ */
99
+ function merge(...sources) {
100
+ return mergeWith(sources);
82
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rc-component/util",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Common Utils For React Component",
5
5
  "keywords": [
6
6
  "react",