Skip to content

Commit 1ad1832

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

7 files changed

Lines changed: 382 additions & 23 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/UniqueProvider/index.tsx

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,34 +55,87 @@ const UniqueProvider = ({
5555
});
5656

5757
// ========================== Register ==========================
58-
// Store the isOpen function from the latest show call
59-
const isOpenRef = React.useRef<(() => boolean) | null>(null);
58+
// Track the current trigger owner by its stable isOpen callback.
59+
const activeOpenRef = React.useRef<(() => boolean) | null>(null);
60+
const disabledOpenRef = React.useRef<(() => boolean) | null>(null);
6061

6162
const delayInvoke = useDelay();
6263

6364
const show = useEvent(
6465
(showOptions: UniqueShowOptions, isOpen: () => boolean) => {
65-
// Store the isOpen function for later use in hide
66-
isOpenRef.current = isOpen;
66+
activeOpenRef.current = isOpen;
67+
disabledOpenRef.current = null;
6768

6869
delayInvoke(() => {
69-
trigger(showOptions);
70+
if (activeOpenRef.current === isOpen && isOpen()) {
71+
trigger(showOptions);
72+
}
7073
}, showOptions.delay);
7174
},
7275
);
7376

74-
const hide = (delay: number) => {
77+
const hide = useEvent((delay: number, isOpen: () => boolean) => {
78+
if (activeOpenRef.current !== isOpen) {
79+
return;
80+
}
81+
7582
delayInvoke(() => {
76-
// Check if we should still hide by calling the isOpen function
77-
// If isOpen returns true, it means another trigger wants to keep it open
78-
if (isOpenRef.current?.()) {
79-
return; // Don't hide if something else wants it open
83+
if (activeOpenRef.current !== isOpen) {
84+
return;
85+
}
86+
87+
if (isOpen()) {
88+
return;
8089
}
8190

91+
activeOpenRef.current = null;
92+
disabledOpenRef.current = null;
8293
trigger(false);
8394
// Don't clear target, currentNode, options immediately, wait until animation completes
8495
}, delay);
85-
};
96+
});
97+
98+
const setDisabled = useEvent(
99+
(
100+
disabled: boolean,
101+
showOptions: UniqueShowOptions,
102+
isOpen: () => boolean,
103+
restoreWhenUnowned: boolean,
104+
) => {
105+
if (disabled) {
106+
if (activeOpenRef.current !== isOpen) {
107+
return;
108+
}
109+
110+
delayInvoke(() => {
111+
if (activeOpenRef.current !== isOpen) {
112+
return;
113+
}
114+
115+
if (open) {
116+
disabledOpenRef.current = isOpen;
117+
} else {
118+
activeOpenRef.current = null;
119+
disabledOpenRef.current = null;
120+
}
121+
trigger(false);
122+
}, 0);
123+
} else if (
124+
disabledOpenRef.current === isOpen ||
125+
(restoreWhenUnowned && activeOpenRef.current === null)
126+
) {
127+
if (!isOpen()) {
128+
return;
129+
}
130+
131+
activeOpenRef.current = isOpen;
132+
disabledOpenRef.current = null;
133+
delayInvoke(() => {
134+
trigger(showOptions);
135+
}, 0);
136+
}
137+
},
138+
);
86139

87140
// Callback after animation completes
88141
const onVisibleChanged = useEvent((visible: boolean) => {
@@ -142,8 +195,9 @@ const UniqueProvider = ({
142195
() => ({
143196
show,
144197
hide,
198+
setDisabled,
145199
}),
146-
[],
200+
[hide, setDisabled, show],
147201
);
148202

149203
// =========================== Align ============================

src/context.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ export interface UniqueShowOptions {
4040

4141
export interface UniqueContextProps {
4242
show: (options: UniqueShowOptions, isOpen: () => boolean) => void;
43-
hide: (delay: number) => void;
43+
hide: (delay: number, isOpen: () => boolean) => void;
44+
setDisabled: (
45+
disabled: boolean,
46+
options: UniqueShowOptions,
47+
isOpen: () => boolean,
48+
restoreWhenUnowned: boolean,
49+
) => void;
4450
}
4551

4652
export const UniqueContext = React.createContext<UniqueContextProps | null>(

src/index.tsx

Lines changed: 63 additions & 10 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,10 @@ export function generateTrigger(
319322
popupVisible,
320323
);
321324

322-
const mergedOpen = internalOpen || false;
325+
const rawOpen = internalOpen || false;
326+
const mergedOpen = rawOpen && !disabled;
327+
328+
const uniqueOpenRef = React.useRef(false);
323329

324330
// ========================== Children ==========================
325331
const child = React.useMemo(() => {
@@ -333,7 +339,11 @@ export function generateTrigger(
333339
const originChildProps = child?.props || {};
334340

335341
// Support ref
336-
const isOpen = useEvent(() => mergedOpen);
342+
const isOpen = useEvent(() =>
343+
uniqueContext && unique && openUncontrolled && !parentContext
344+
? uniqueOpenRef.current && !disabled
345+
: mergedOpen,
346+
);
337347

338348
// Extract common options for UniqueProvider
339349
const getUniqueOptions = useEvent((delay: number = 0) => ({
@@ -371,20 +381,24 @@ export function generateTrigger(
371381
!openUncontrolled &&
372382
!parentContext
373383
) {
374-
if (mergedOpen) {
384+
if (rawOpen && !disabled) {
375385
uniqueContext.show(getUniqueOptions(mouseEnterDelay), isOpen);
376386
} else {
377-
uniqueContext.hide(mouseLeaveDelay);
387+
uniqueContext.hide(disabled ? 0 : mouseLeaveDelay, isOpen);
378388
}
379389
}
380-
}, [mergedOpen, targetEle]);
390+
}, [rawOpen, targetEle]);
381391

382-
const openRef = React.useRef(mergedOpen);
383-
openRef.current = mergedOpen;
392+
const openRef = React.useRef(rawOpen);
393+
openRef.current = rawOpen;
384394

385395
const internalTriggerOpen = useEvent((nextOpen: boolean) => {
396+
if (disabled && nextOpen) {
397+
return;
398+
}
399+
386400
flushSync(() => {
387-
if (mergedOpen !== nextOpen) {
401+
if (rawOpen !== nextOpen) {
388402
setInternalOpen(nextOpen);
389403
onOpenChange?.(nextOpen);
390404
onPopupVisibleChange?.(nextOpen);
@@ -396,6 +410,10 @@ export function generateTrigger(
396410
const delayInvoke = useDelay();
397411

398412
const triggerOpen = (nextOpen: boolean, delay = 0) => {
413+
if (disabled && nextOpen) {
414+
return;
415+
}
416+
399417
// If it's controlled mode, always use internal trigger logic
400418
// UniqueProvider will be synced through useLayoutEffect
401419
if (popupVisible !== undefined) {
@@ -408,10 +426,12 @@ export function generateTrigger(
408426
// If UniqueContext exists and not controlled, pass delay to Provider instead of handling it internally
409427
// If there is a parentContext, don't call uniqueContext methods
410428
if (uniqueContext && unique && openUncontrolled && !parentContext) {
429+
uniqueOpenRef.current = nextOpen;
430+
411431
if (nextOpen) {
412432
uniqueContext.show(getUniqueOptions(delay), isOpen);
413433
} else {
414-
uniqueContext.hide(delay);
434+
uniqueContext.hide(delay, isOpen);
415435
}
416436
return;
417437
}
@@ -421,6 +441,39 @@ export function generateTrigger(
421441
}, delay);
422442
};
423443

444+
useLayoutEffect(
445+
(firstMount) => {
446+
const uniqueRawOpen = openUncontrolled
447+
? uniqueOpenRef.current
448+
: rawOpen;
449+
450+
if (
451+
!firstMount &&
452+
uniqueContext &&
453+
unique &&
454+
(disabled || uniqueRawOpen) &&
455+
!parentContext
456+
) {
457+
uniqueContext.setDisabled(
458+
disabled,
459+
getUniqueOptions(),
460+
isOpen,
461+
!openUncontrolled,
462+
);
463+
}
464+
},
465+
[
466+
disabled,
467+
getUniqueOptions,
468+
isOpen,
469+
openUncontrolled,
470+
parentContext,
471+
rawOpen,
472+
unique,
473+
uniqueContext,
474+
],
475+
);
476+
424477
function onEsc({ top }: Parameters<PortalProps['onEsc']>[0]) {
425478
if (top) {
426479
triggerOpen(false);
@@ -645,7 +698,7 @@ export function generateTrigger(
645698

646699
// Click to hide is special action since click popup element should not hide
647700
const onPopupPointerDown = useWinClick(
648-
mergedOpen,
701+
rawOpen,
649702
clickToHide || touchToHide,
650703
targetEle,
651704
popupEle,

0 commit comments

Comments
 (0)