Skip to content

Commit 788c423

Browse files
committed
feat: support disabled prop
1 parent 8e1a491 commit 788c423

5 files changed

Lines changed: 237 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Then open `http://localhost:8000`.
7575
| blurDelay | Delay before hiding on blur, in seconds. | `number` | - |
7676
| builtinPlacements | Named placement presets. | `BuildInPlacements` | `{}` |
7777
| defaultPopupVisible | Initial uncontrolled visibility. | `boolean` | `false` |
78+
| disabled | Temporarily suppress popup visibility without resetting the current open state. | `boolean` | `false` |
7879
| focusDelay | Delay before showing on focus, in seconds. | `number` | - |
7980
| forceRender | Render popup before it is first shown. | `boolean` | `false` |
8081
| fresh | Keep popup content updated while closed. | `boolean` | - |

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ npm start
7575
| blurDelay | `blur` 后隐藏前的延迟,单位为秒。 | `number` | - |
7676
| builtinPlacements | 命名位置预设。 | `BuildInPlacements` | `{}` |
7777
| defaultPopupVisible | 非受控初始显示状态。 | `boolean` | `false` |
78+
| disabled | 临时隐藏弹层,但不主动重置当前打开状态。 | `boolean` | `false` |
7879
| focusDelay | `focus` 后显示前的延迟,单位为秒。 | `number` | - |
7980
| forceRender | 首次显示前渲染弹层。 | `boolean` | `false` |
8081
| fresh | 关闭时仍保持弹层内容更新。 | `boolean` | - |

src/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export interface TriggerProps {
6565
action?: ActionType | ActionType[];
6666
showAction?: ActionType[];
6767
hideAction?: ActionType[];
68+
/** Temporarily suppress popup visibility without resetting the current open state. */
69+
disabled?: boolean;
6870

6971
prefixCls?: string;
7072

@@ -159,6 +161,7 @@ export function generateTrigger(
159161
action = 'hover',
160162
showAction,
161163
hideAction,
164+
disabled = false,
162165

163166
// Open
164167
popupVisible,
@@ -319,7 +322,7 @@ export function generateTrigger(
319322
popupVisible,
320323
);
321324

322-
const mergedOpen = internalOpen || false;
325+
const mergedOpen = (internalOpen || false) && !disabled;
323326

324327
// ========================== Children ==========================
325328
const child = React.useMemo(() => {
@@ -374,10 +377,10 @@ export function generateTrigger(
374377
if (mergedOpen) {
375378
uniqueContext.show(getUniqueOptions(mouseEnterDelay), isOpen);
376379
} else {
377-
uniqueContext.hide(mouseLeaveDelay);
380+
uniqueContext.hide(disabled ? 0 : mouseLeaveDelay);
378381
}
379382
}
380-
}, [mergedOpen, targetEle]);
383+
}, [mergedOpen, targetEle, disabled]);
381384

382385
const openRef = React.useRef(mergedOpen);
383386
openRef.current = mergedOpen;

tests/basic.test.jsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,81 @@ describe('Trigger.Basic', () => {
179179
trigger(document, '.rc-trigger-popup', 'pointerEnter');
180180
expect(isPopupHidden()).toBeFalsy();
181181
});
182+
183+
it('temporarily hides while disabled and restores without mouse leave', () => {
184+
const onOpenChange = jest.fn();
185+
const Demo = ({ disabled = false }) => (
186+
<Trigger
187+
action={['hover']}
188+
disabled={disabled}
189+
onOpenChange={onOpenChange}
190+
popup={<strong>trigger</strong>}
191+
>
192+
<div className="target">hover</div>
193+
</Trigger>
194+
);
195+
196+
const { container, rerender } = render(<Demo />);
197+
198+
trigger(container, '.target', 'mouseEnter');
199+
expect(isPopupHidden()).toBeFalsy();
200+
expect(onOpenChange).toHaveBeenCalledWith(true);
201+
onOpenChange.mockReset();
202+
203+
rerender(<Demo disabled />);
204+
expect(isPopupHidden()).toBeTruthy();
205+
expect(onOpenChange).not.toHaveBeenCalled();
206+
207+
rerender(<Demo />);
208+
expect(isPopupHidden()).toBeFalsy();
209+
expect(onOpenChange).not.toHaveBeenCalled();
210+
});
211+
212+
it('does not restore after mouse leave while disabled', () => {
213+
const Demo = ({ disabled = false }) => (
214+
<Trigger
215+
action={['hover']}
216+
disabled={disabled}
217+
popup={<strong>trigger</strong>}
218+
>
219+
<div className="target">hover</div>
220+
</Trigger>
221+
);
222+
223+
const { container, rerender } = render(<Demo />);
224+
225+
trigger(container, '.target', 'mouseEnter');
226+
rerender(<Demo disabled />);
227+
trigger(container, '.target', 'mouseLeave');
228+
rerender(<Demo />);
229+
230+
expect(isPopupHidden()).toBeTruthy();
231+
});
232+
233+
it('ignores pending and direct show actions while disabled', () => {
234+
const Demo = ({ disabled = false }) => (
235+
<Trigger
236+
action="hover"
237+
disabled={disabled}
238+
mouseEnterDelay={1}
239+
popup={<strong>trigger</strong>}
240+
>
241+
<div className="target">hover</div>
242+
</Trigger>
243+
);
244+
245+
const { container, rerender } = render(<Demo />);
246+
const target = container.querySelector('.target');
247+
248+
fireEvent.mouseEnter(target);
249+
rerender(<Demo disabled />);
250+
act(() => jest.runAllTimers());
251+
expect(document.querySelector('.rc-trigger-popup')).toBeFalsy();
252+
253+
fireEvent.mouseEnter(target);
254+
act(() => jest.runAllTimers());
255+
expect(document.querySelector('.rc-trigger-popup')).toBeFalsy();
256+
});
182257
});
183258

184259
it('contextMenu works', () => {
@@ -1009,7 +1084,62 @@ describe('Trigger.Basic', () => {
10091084
expect(document.querySelector('.rc-trigger-popup')).toBeTruthy();
10101085
});
10111086

1087+
it('temporarily hides a controlled popup without changing its open state', () => {
1088+
const onOpenChange = jest.fn();
1089+
const Demo = ({ disabled = false }) => (
1090+
<Trigger
1091+
disabled={disabled}
1092+
onOpenChange={onOpenChange}
1093+
popup={<strong>trigger</strong>}
1094+
popupVisible
1095+
>
1096+
{({ open }) => <div data-open={open} />}
1097+
</Trigger>
1098+
);
1099+
1100+
const { container, rerender } = render(<Demo />);
1101+
1102+
expect(container.firstChild).toHaveAttribute('data-open', 'true');
1103+
expect(isPopupHidden()).toBeFalsy();
1104+
1105+
rerender(<Demo disabled />);
1106+
expect(container.firstChild).toHaveAttribute('data-open', 'false');
1107+
expect(isPopupHidden()).toBeTruthy();
1108+
1109+
rerender(<Demo />);
1110+
expect(container.firstChild).toHaveAttribute('data-open', 'true');
1111+
expect(isPopupHidden()).toBeFalsy();
1112+
expect(onOpenChange).not.toHaveBeenCalled();
1113+
});
1114+
10121115
describe('click window to hide', () => {
1116+
it('clears the open state while disabled', async () => {
1117+
const Demo = ({ disabled = false }) => (
1118+
<Trigger
1119+
action="click"
1120+
disabled={disabled}
1121+
popup={<strong>trigger</strong>}
1122+
>
1123+
<div className="target" />
1124+
</Trigger>
1125+
);
1126+
1127+
const { container, rerender } = render(<Demo />);
1128+
1129+
fireEvent.click(container.querySelector('.target'));
1130+
await awaitFakeTimer();
1131+
expect(isPopupHidden()).toBeFalsy();
1132+
1133+
rerender(<Demo disabled />);
1134+
expect(isPopupHidden()).toBeTruthy();
1135+
1136+
fireEvent.mouseDown(document.body);
1137+
await awaitFakeTimer();
1138+
rerender(<Demo />);
1139+
1140+
expect(isPopupHidden()).toBeTruthy();
1141+
});
1142+
10131143
it('should hide', async () => {
10141144
const onOpenChange = jest.fn();
10151145
const onPopupVisibleChange = jest.fn();

tests/unique.test.tsx

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,104 @@ describe('Trigger.Unique', () => {
6969
jest.restoreAllMocks();
7070
});
7171

72+
it('restores a disabled hovered popup without mouse leave', async () => {
73+
const Demo = ({ disabled = false }) => (
74+
<UniqueProvider>
75+
<Trigger
76+
action="hover"
77+
disabled={disabled}
78+
popup={<strong>trigger</strong>}
79+
unique
80+
>
81+
<div className="target">hover</div>
82+
</Trigger>
83+
</UniqueProvider>
84+
);
85+
86+
const { container, rerender } = render(<Demo />);
87+
88+
fireEvent.mouseEnter(container.querySelector('.target'));
89+
await awaitFakeTimer();
90+
expect(document.querySelector('.rc-trigger-popup')).not.toHaveClass(
91+
'rc-trigger-popup-hidden',
92+
);
93+
94+
rerender(<Demo disabled />);
95+
await awaitFakeTimer();
96+
expect(document.querySelector('.rc-trigger-popup')).toHaveClass(
97+
'rc-trigger-popup-hidden',
98+
);
99+
100+
rerender(<Demo />);
101+
await awaitFakeTimer();
102+
expect(document.querySelector('.rc-trigger-popup')).not.toHaveClass(
103+
'rc-trigger-popup-hidden',
104+
);
105+
});
106+
107+
it('ignores leave delay when controlled popup is disabled', async () => {
108+
const Demo = ({ disabled = false }) => (
109+
<UniqueProvider>
110+
<Trigger
111+
disabled={disabled}
112+
mouseLeaveDelay={10}
113+
popup={<strong>trigger</strong>}
114+
popupVisible
115+
unique
116+
>
117+
<div className="target" />
118+
</Trigger>
119+
</UniqueProvider>
120+
);
121+
122+
const { rerender } = render(<Demo />);
123+
await awaitFakeTimer();
124+
expect(document.querySelector('.rc-trigger-popup')).not.toHaveClass(
125+
'rc-trigger-popup-hidden',
126+
);
127+
128+
rerender(<Demo disabled />);
129+
expect(document.querySelector('.rc-trigger-popup')).toHaveClass(
130+
'rc-trigger-popup-hidden',
131+
);
132+
133+
rerender(<Demo />);
134+
expect(document.querySelector('.rc-trigger-popup')).not.toHaveClass(
135+
'rc-trigger-popup-hidden',
136+
);
137+
});
138+
139+
it('closes immediately when disabled during leave delay', async () => {
140+
const Demo = ({ disabled = false }) => (
141+
<UniqueProvider>
142+
<Trigger
143+
action="hover"
144+
disabled={disabled}
145+
mouseLeaveDelay={10}
146+
popup={<strong>trigger</strong>}
147+
unique
148+
>
149+
<div className="target" />
150+
</Trigger>
151+
</UniqueProvider>
152+
);
153+
154+
const { container, rerender } = render(<Demo />);
155+
156+
fireEvent.mouseEnter(container.querySelector('.target'));
157+
await awaitFakeTimer();
158+
expect(document.querySelector('.rc-trigger-popup')).not.toHaveClass(
159+
'rc-trigger-popup-hidden',
160+
);
161+
162+
fireEvent.mouseLeave(container.querySelector('.target'));
163+
rerender(<Demo disabled />);
164+
165+
expect(document.querySelector('.rc-trigger-popup')).toHaveClass(
166+
'rc-trigger-popup-hidden',
167+
);
168+
});
169+
72170
it('moving will not hide the popup', async () => {
73171
const { container } = render(
74172
<UniqueProvider>
@@ -376,7 +474,7 @@ describe('Trigger.Unique', () => {
376474
});
377475

378476
it('esc should close unique popup', async () => {
379-
const { container,baseElement } = render(
477+
const { container, baseElement } = render(
380478
<UniqueProvider>
381479
<Trigger action={['click']} popup={<div>Popup</div>} unique>
382480
<div className="target" />

0 commit comments

Comments
 (0)