@rc-component/portal 2.1.2 → 2.2.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.
@@ -1,3 +1,9 @@
1
1
  import { type EscCallback } from './Portal';
2
- export declare let stack: string[];
2
+ export declare const _test: () => {
3
+ stack: {
4
+ id: string;
5
+ onEsc?: EscCallback;
6
+ }[];
7
+ reset: () => void;
8
+ };
3
9
  export default function useEscKeyDown(open: boolean, onEsc?: EscCallback): void;
@@ -1,34 +1,79 @@
1
- import { useEffect, useMemo } from 'react';
2
- import useId from "@rc-component/util/es/hooks/useId";
3
1
  import { useEvent } from '@rc-component/util';
4
- export let stack = []; // export for testing
2
+ import useId from "@rc-component/util/es/hooks/useId";
3
+ import { useEffect, useMemo } from 'react';
4
+ let stack = [];
5
+ const IME_LOCK_DURATION = 200;
6
+ let lastCompositionEndTime = 0;
7
+
8
+ // Export for testing
9
+ export const _test = process.env.NODE_ENV === 'test' ? () => ({
10
+ stack,
11
+ reset: () => {
12
+ // Not reset stack to ensure effect will clean up correctly
13
+ lastCompositionEndTime = 0;
14
+ }
15
+ }) : null;
5
16
 
17
+ // Global event handlers
18
+ const onGlobalKeyDown = event => {
19
+ if (event.key === 'Escape' && !event.isComposing) {
20
+ const now = Date.now();
21
+ if (now - lastCompositionEndTime < IME_LOCK_DURATION) {
22
+ return;
23
+ }
24
+ const len = stack.length;
25
+ for (let i = len - 1; i >= 0; i -= 1) {
26
+ stack[i].onEsc({
27
+ top: i === len - 1,
28
+ event
29
+ });
30
+ }
31
+ }
32
+ };
33
+ const onGlobalCompositionEnd = () => {
34
+ lastCompositionEndTime = Date.now();
35
+ };
36
+ function attachGlobalEventListeners() {
37
+ window.addEventListener('keydown', onGlobalKeyDown);
38
+ window.addEventListener('compositionend', onGlobalCompositionEnd);
39
+ }
40
+ function detachGlobalEventListeners() {
41
+ if (stack.length === 0) {
42
+ window.removeEventListener('keydown', onGlobalKeyDown);
43
+ window.removeEventListener('compositionend', onGlobalCompositionEnd);
44
+ }
45
+ }
6
46
  export default function useEscKeyDown(open, onEsc) {
7
47
  const id = useId();
8
- const handleEscKeyDown = useEvent(event => {
9
- if (event.key === 'Escape' && !event.isComposing) {
10
- const top = stack[stack.length - 1] === id;
11
- onEsc?.({
12
- top,
13
- event
48
+ const onEventEsc = useEvent(onEsc);
49
+ const ensure = () => {
50
+ if (!stack.find(item => item.id === id)) {
51
+ stack.push({
52
+ id,
53
+ onEsc: onEventEsc
14
54
  });
15
55
  }
16
- });
56
+ };
57
+ const clear = () => {
58
+ stack = stack.filter(item => item.id !== id);
59
+ };
17
60
  useMemo(() => {
18
- if (open && !stack.includes(id)) {
19
- stack.push(id);
61
+ if (open) {
62
+ ensure();
20
63
  } else if (!open) {
21
- stack = stack.filter(item => item !== id);
64
+ clear();
22
65
  }
23
- }, [open, id]);
66
+ }, [open]);
24
67
  useEffect(() => {
25
- if (!open) {
26
- return;
68
+ if (open) {
69
+ ensure();
70
+ // Attach global event listeners
71
+ attachGlobalEventListeners();
72
+ return () => {
73
+ clear();
74
+ // Remove global event listeners if instances is empty
75
+ detachGlobalEventListeners();
76
+ };
27
77
  }
28
- window.addEventListener('keydown', handleEscKeyDown);
29
- return () => {
30
- stack = stack.filter(item => item !== id);
31
- window.removeEventListener('keydown', handleEscKeyDown);
32
- };
33
- }, [open, id]);
78
+ }, [open]);
34
79
  }
@@ -1,3 +1,9 @@
1
1
  import { type EscCallback } from './Portal';
2
- export declare let stack: string[];
2
+ export declare const _test: () => {
3
+ stack: {
4
+ id: string;
5
+ onEsc?: EscCallback;
6
+ }[];
7
+ reset: () => void;
8
+ };
3
9
  export default function useEscKeyDown(open: boolean, onEsc?: EscCallback): void;
@@ -3,40 +3,85 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports._test = void 0;
6
7
  exports.default = useEscKeyDown;
7
- exports.stack = void 0;
8
- var _react = require("react");
9
- var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
10
8
  var _util = require("@rc-component/util");
9
+ var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
10
+ var _react = require("react");
11
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
- let stack = exports.stack = []; // export for testing
12
+ let stack = [];
13
+ const IME_LOCK_DURATION = 200;
14
+ let lastCompositionEndTime = 0;
15
+
16
+ // Export for testing
17
+ const _test = exports._test = process.env.NODE_ENV === 'test' ? () => ({
18
+ stack,
19
+ reset: () => {
20
+ // Not reset stack to ensure effect will clean up correctly
21
+ lastCompositionEndTime = 0;
22
+ }
23
+ }) : null;
13
24
 
25
+ // Global event handlers
26
+ const onGlobalKeyDown = event => {
27
+ if (event.key === 'Escape' && !event.isComposing) {
28
+ const now = Date.now();
29
+ if (now - lastCompositionEndTime < IME_LOCK_DURATION) {
30
+ return;
31
+ }
32
+ const len = stack.length;
33
+ for (let i = len - 1; i >= 0; i -= 1) {
34
+ stack[i].onEsc({
35
+ top: i === len - 1,
36
+ event
37
+ });
38
+ }
39
+ }
40
+ };
41
+ const onGlobalCompositionEnd = () => {
42
+ lastCompositionEndTime = Date.now();
43
+ };
44
+ function attachGlobalEventListeners() {
45
+ window.addEventListener('keydown', onGlobalKeyDown);
46
+ window.addEventListener('compositionend', onGlobalCompositionEnd);
47
+ }
48
+ function detachGlobalEventListeners() {
49
+ if (stack.length === 0) {
50
+ window.removeEventListener('keydown', onGlobalKeyDown);
51
+ window.removeEventListener('compositionend', onGlobalCompositionEnd);
52
+ }
53
+ }
14
54
  function useEscKeyDown(open, onEsc) {
15
55
  const id = (0, _useId.default)();
16
- const handleEscKeyDown = (0, _util.useEvent)(event => {
17
- if (event.key === 'Escape' && !event.isComposing) {
18
- const top = stack[stack.length - 1] === id;
19
- onEsc?.({
20
- top,
21
- event
56
+ const onEventEsc = (0, _util.useEvent)(onEsc);
57
+ const ensure = () => {
58
+ if (!stack.find(item => item.id === id)) {
59
+ stack.push({
60
+ id,
61
+ onEsc: onEventEsc
22
62
  });
23
63
  }
24
- });
64
+ };
65
+ const clear = () => {
66
+ stack = stack.filter(item => item.id !== id);
67
+ };
25
68
  (0, _react.useMemo)(() => {
26
- if (open && !stack.includes(id)) {
27
- stack.push(id);
69
+ if (open) {
70
+ ensure();
28
71
  } else if (!open) {
29
- exports.stack = stack = stack.filter(item => item !== id);
72
+ clear();
30
73
  }
31
- }, [open, id]);
74
+ }, [open]);
32
75
  (0, _react.useEffect)(() => {
33
- if (!open) {
34
- return;
76
+ if (open) {
77
+ ensure();
78
+ // Attach global event listeners
79
+ attachGlobalEventListeners();
80
+ return () => {
81
+ clear();
82
+ // Remove global event listeners if instances is empty
83
+ detachGlobalEventListeners();
84
+ };
35
85
  }
36
- window.addEventListener('keydown', handleEscKeyDown);
37
- return () => {
38
- exports.stack = stack = stack.filter(item => item !== id);
39
- window.removeEventListener('keydown', handleEscKeyDown);
40
- };
41
- }, [open, id]);
86
+ }, [open]);
42
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rc-component/portal",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "React Portal Component",
5
5
  "keywords": [
6
6
  "react",