@rainbow-oh/yee-c 0.15.2 → 0.15.3
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/dist/cjs/Dialog/dialog.js +1 -1
- package/dist/cjs/Drawer/drawer.js +1 -1
- package/dist/cjs/ImageViewer/popup.js +1 -1
- package/dist/cjs/hooks/useEsc.d.ts +1 -1
- package/dist/cjs/hooks/useEsc.js +15 -10
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Dialog/dialog.js +3 -2
- package/dist/esm/Drawer/drawer.js +4 -1
- package/dist/esm/ImageViewer/popup.js +4 -1
- package/dist/esm/hooks/useEsc.d.ts +1 -1
- package/dist/esm/hooks/useEsc.js +31 -9
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Dialog/dialog.tsx +3 -2
- package/src/Drawer/drawer.tsx +3 -1
- package/src/ImageViewer/popup.tsx +3 -1
- package/src/hooks/useEsc.test.tsx +90 -0
- package/src/hooks/useEsc.ts +23 -12
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Dialog/dialog.tsx
CHANGED
|
@@ -90,9 +90,10 @@ const Dialog = (baseprops: DialogProps) => {
|
|
|
90
90
|
// lock focus in the dialog
|
|
91
91
|
useLockFocus(contentElement, delayOpen);
|
|
92
92
|
|
|
93
|
-
// enable esc to close dialog
|
|
93
|
+
// enable esc to close dialog; gated on open so a mounted-but-closed
|
|
94
|
+
// dialog does not occupy the esc layer stack and swallow the top layer
|
|
94
95
|
useEsc({
|
|
95
|
-
enabled: keyboard,
|
|
96
|
+
enabled: keyboard && open,
|
|
96
97
|
onEsc: () => {
|
|
97
98
|
onCancel?.();
|
|
98
99
|
},
|
package/src/Drawer/drawer.tsx
CHANGED
|
@@ -87,8 +87,10 @@ const Drawer = (baseprops: DrawerProps) => {
|
|
|
87
87
|
|
|
88
88
|
useLockFocus(contentRef.current, delayOpen);
|
|
89
89
|
|
|
90
|
+
// gated on open so a mounted-but-closed drawer does not occupy the esc
|
|
91
|
+
// layer stack and swallow the top layer
|
|
90
92
|
useEsc({
|
|
91
|
-
enabled: keyboard,
|
|
93
|
+
enabled: keyboard && open,
|
|
92
94
|
onEsc: () => {
|
|
93
95
|
onClose?.();
|
|
94
96
|
},
|
|
@@ -48,8 +48,10 @@ const Popup: React.ForwardRefRenderFunction<
|
|
|
48
48
|
const internalRef = useRef<HTMLDivElement>(null);
|
|
49
49
|
const componentRef = (ref as React.RefObject<HTMLDivElement>) || internalRef;
|
|
50
50
|
|
|
51
|
+
// gated on open so a mounted-but-closed viewer does not occupy the esc
|
|
52
|
+
// layer stack and swallow the top layer
|
|
51
53
|
useEsc({
|
|
52
|
-
enabled: keyboard,
|
|
54
|
+
enabled: keyboard && open,
|
|
53
55
|
onEsc: () => {
|
|
54
56
|
onClose?.();
|
|
55
57
|
},
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { cleanup, fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import Dialog from '../Dialog';
|
|
6
|
+
import Popover from '../Popover';
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
cleanup();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// jsdom has no ResizeObserver; @rc-component/trigger uses it for popup align.
|
|
13
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
14
|
+
globalThis.ResizeObserver = class {
|
|
15
|
+
observe() {}
|
|
16
|
+
unobserve() {}
|
|
17
|
+
disconnect() {}
|
|
18
|
+
} as unknown as typeof ResizeObserver;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// useEsc 层级栈:同一 document keydown 会到达所有已启用监听,
|
|
22
|
+
// Esc 必须只关最上层(后启用者),再按才逐层关闭。
|
|
23
|
+
describe('useEsc layering', () => {
|
|
24
|
+
it('closes only the topmost layer (popover) on Escape, dialog on the next', () => {
|
|
25
|
+
const onPopoverChange = vi.fn();
|
|
26
|
+
const onDialogCancel = vi.fn();
|
|
27
|
+
|
|
28
|
+
function LayeredApp() {
|
|
29
|
+
const [popoverOpen, setPopoverOpen] = useState(true);
|
|
30
|
+
return (
|
|
31
|
+
<Dialog open onCancel={onDialogCancel} keyboard title="dialog">
|
|
32
|
+
<Popover
|
|
33
|
+
trigger="click"
|
|
34
|
+
open={popoverOpen}
|
|
35
|
+
onOpenChange={(open: boolean) => {
|
|
36
|
+
setPopoverOpen(open);
|
|
37
|
+
onPopoverChange(open);
|
|
38
|
+
}}
|
|
39
|
+
content={<div>filter panel</div>}
|
|
40
|
+
>
|
|
41
|
+
<button type="button">trigger</button>
|
|
42
|
+
</Popover>
|
|
43
|
+
</Dialog>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
render(<LayeredApp />);
|
|
48
|
+
|
|
49
|
+
// Dialog 先开、Popover 后开 → Popover 是最上层
|
|
50
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
51
|
+
expect(onPopoverChange).toHaveBeenCalledWith(false);
|
|
52
|
+
expect(onDialogCancel).not.toHaveBeenCalled();
|
|
53
|
+
|
|
54
|
+
// Popover 已关,Esc 落到 Dialog 层
|
|
55
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
56
|
+
expect(onDialogCancel).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('closes the dialog when it is the only enabled layer', () => {
|
|
60
|
+
const onDialogCancel = vi.fn();
|
|
61
|
+
render(
|
|
62
|
+
<Dialog open onCancel={onDialogCancel} keyboard title="dialog">
|
|
63
|
+
<div>body</div>
|
|
64
|
+
</Dialog>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
68
|
+
expect(onDialogCancel).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('ignores Esc for mounted-but-closed layers so they do not swallow the open one', () => {
|
|
72
|
+
const onOpenCancel = vi.fn();
|
|
73
|
+
const onClosedCancel = vi.fn();
|
|
74
|
+
// 关闭态的 Dialog 后挂载:门控缺失时它会占据 esc 栈顶,吃掉打开层的 Esc
|
|
75
|
+
render(
|
|
76
|
+
<>
|
|
77
|
+
<Dialog open onCancel={onOpenCancel} keyboard title="open">
|
|
78
|
+
<div>body</div>
|
|
79
|
+
</Dialog>
|
|
80
|
+
<Dialog open={false} onCancel={onClosedCancel} keyboard title="closed">
|
|
81
|
+
<div>body</div>
|
|
82
|
+
</Dialog>
|
|
83
|
+
</>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
87
|
+
expect(onOpenCancel).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(onClosedCancel).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/hooks/useEsc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
2
|
import useEvent from './useEvent';
|
|
3
3
|
|
|
4
4
|
export interface UseEscProps {
|
|
@@ -13,8 +13,14 @@ export interface UseEscProps {
|
|
|
13
13
|
enabled?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// 层级栈:所有 useEsc 监听都挂在 document 上,同一 keydown 会触发全部 handler。
|
|
17
|
+
// 按启用顺序压栈,Esc 只响应最后启用(最上层)的那一个 —— 弹层内再开
|
|
18
|
+
// 筛选/下拉/抽屉时,第一次 Esc 只关最上层,再按才逐层关闭。
|
|
19
|
+
let escSeq = 0;
|
|
20
|
+
const escLayers: number[] = [];
|
|
21
|
+
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
23
|
+
* Hook for listening to ESC key press
|
|
18
24
|
*
|
|
19
25
|
* @example
|
|
20
26
|
* ```tsx
|
|
@@ -29,24 +35,29 @@ export interface UseEscProps {
|
|
|
29
35
|
export default function useEsc({ onEsc, enabled = true }: UseEscProps = {}) {
|
|
30
36
|
const handleEsc = useEvent(onEsc || (() => {}));
|
|
31
37
|
|
|
32
|
-
const
|
|
33
|
-
(event: KeyboardEvent) => {
|
|
34
|
-
if (event.key === 'Escape') {
|
|
35
|
-
event.preventDefault();
|
|
36
|
-
handleEsc(event);
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
[handleEsc],
|
|
40
|
-
);
|
|
38
|
+
const [id] = useState(() => ++escSeq);
|
|
41
39
|
|
|
42
40
|
useEffect(() => {
|
|
43
41
|
if (!enabled) {
|
|
44
42
|
return;
|
|
45
43
|
}
|
|
46
44
|
|
|
45
|
+
escLayers.push(id);
|
|
46
|
+
|
|
47
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
48
|
+
if (event.key === 'Escape' && escLayers[escLayers.length - 1] === id) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
handleEsc(event);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
47
54
|
document.addEventListener('keydown', handleKeyDown);
|
|
48
55
|
return () => {
|
|
49
56
|
document.removeEventListener('keydown', handleKeyDown);
|
|
57
|
+
const index = escLayers.lastIndexOf(id);
|
|
58
|
+
if (index !== -1) {
|
|
59
|
+
escLayers.splice(index, 1);
|
|
60
|
+
}
|
|
50
61
|
};
|
|
51
|
-
}, [enabled,
|
|
62
|
+
}, [enabled, handleEsc, id]);
|
|
52
63
|
}
|