Repository navigation
fix: close unique popup when trigger unmounted #650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,10 +60,14 @@ const UniqueProvider = ({ | |
|
|
||
| const delayInvoke = useDelay(); | ||
|
|
||
| // Store the target of the latest show call, used to detect removed targets | ||
| const targetRef = React.useRef<HTMLElement>(null); | ||
|
|
||
| const show = useEvent( | ||
| (showOptions: UniqueShowOptions, isOpen: () => boolean) => { | ||
| // Store the isOpen function for later use in hide | ||
| isOpenRef.current = isOpen; | ||
| targetRef.current = showOptions.target; | ||
|
|
||
| delayInvoke(() => { | ||
| trigger(showOptions); | ||
|
|
@@ -75,7 +79,9 @@ const UniqueProvider = ({ | |
| delayInvoke(() => { | ||
| // Check if we should still hide by calling the isOpen function | ||
| // If isOpen returns true, it means another trigger wants to keep it open | ||
| if (isOpenRef.current?.()) { | ||
| // Skip the check when the target is removed from the DOM (e.g. trigger unmounted), | ||
| // since its isOpen function is frozen and would keep returning true | ||
| if (isOpenRef.current?.() && targetRef.current?.isConnected) { | ||
| return; // Don't hide if something else wants it open | ||
| } | ||
|
|
||
|
|
@@ -138,10 +144,18 @@ const UniqueProvider = ({ | |
| mergedOptions?.prefixCls, | ||
| ]); | ||
|
|
||
| const release = useEvent((target: HTMLElement) => { | ||
| // Close the popup only when it is currently showing for the removed target | ||
| if (open && targetRef.current === target) { | ||
| trigger(false); | ||
|
Comment on lines
+149
to
+150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# 检查 useDelay 对待执行回调的取消条件。
fd -i '^useDelay\.(ts|tsx)$' src | while IFS= read -r file; do
ast-grep outline "$file"
rg -n -C 8 'setTimeout|clearTimeout|useEffect|delayInvoke' "$file"
doneRepository: react-component/trigger Length of output: 971 🏁 Script executed: set -e
printf '%s\n' '--- useDelay references ---'
rg -n -F --glob '*.{ts,tsx}' -e 'useDelay' -e 'delayInvoke' src || true
printf '%s\n' '--- Trigger relevant source ---'
rg -n -F --glob 'Trigger/**/*.{ts,tsx}' -e 'unique' -e 'delayInvoke' -e 'useDelay' -e 'useEffect' src || true
printf '%s\n' '--- UniqueProvider source ---'
nl -ba src/UniqueProvider/index.tsx | sed -n '1,190p'
printf '%s\n' '--- Trigger source candidates ---'
rg --files src | rg '(^|/)(Trigger|index)\.(ts|tsx)$' | sortRepository: react-component/trigger Length of output: 7933 🏁 Script executed: set -e
printf '%s\n' '--- Trigger component delay and cleanup ---'
nl -ba src/index.tsx | sed -n '380,465p'
printf '%s\n' '--- release declarations and references ---'
rg -n -F --glob '*.{ts,tsx}' -e 'release(' -e 'release:' -e 'UniqueContext' src || true
printf '%s\n' '--- context declarations ---'
nl -ba src/context.tsx | sed -n '1,180p'Repository: react-component/trigger Length of output: 4405 在
请让 建议修复diff --git a/src/UniqueProvider/index.tsx b/src/UniqueProvider/index.tsx
@@
const release = useEvent((target: HTMLElement) => {
// Close the popup only when it is currently showing for the removed target
- if (open && targetRef.current === target) {
- trigger(false);
+ if (targetRef.current === target) {
+ delayInvoke.cancel();
+ if (open) {
+ trigger(false);
+ }
}
});使 🤖 Prompt for AI Agents |
||
| } | ||
| }); | ||
|
|
||
| const contextValue = React.useMemo<UniqueContextProps>( | ||
| () => ({ | ||
| show, | ||
| hide, | ||
| release, | ||
| }), | ||
| [], | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -265,6 +265,10 @@ export function generateTrigger( | |
| // Use state to control here since `useRef` update not trigger render | ||
| const [targetEle, setTargetEle] = React.useState<HTMLElement>(null); | ||
|
|
||
| // Keep the latest target for unmount checking | ||
| const targetEleRef = React.useRef<HTMLElement>(null); | ||
| targetEleRef.current = targetEle; | ||
|
|
||
| // Used for forwardRef target. Not use internal | ||
| const externalForwardRef = React.useRef<HTMLElement>(null); | ||
|
|
||
|
|
@@ -386,6 +390,17 @@ export function generateTrigger( | |
| const openRef = React.useRef(mergedOpen); | ||
| openRef.current = mergedOpen; | ||
|
|
||
| // Close the shared popup when the trigger is unmounted | ||
| React.useEffect(() => { | ||
| if (uniqueContext && unique && !parentContext) { | ||
| return () => { | ||
| if (targetEleRef.current) { | ||
| uniqueContext.release(targetEleRef.current); | ||
| } | ||
| }; | ||
| } | ||
| }, []); | ||
|
Comment on lines
+395
to
+402
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '255,290p' src/index.tsx
sed -n '380,410p' src/index.tsxRepository: react-component/trigger Length of output: 2147 🏁 Script executed: set -o pipefail
printf '%s\n' '--- src/index.tsx imports and Trigger declarations ---'
sed -n '1,180p' src/index.tsx
printf '%s\n' '--- src/index.tsx unique-related logic ---'
rg -n -F -- 'unique' src/index.tsx src
printf '%s\n' '--- UniqueContext declarations and consumers ---'
rg -n -F -- 'UniqueContext' src
rg -n -F -e 'release(' -e 'show(' -e 'hide(' src/context src || true
printf '%s\n' '--- package, lint, and documentation references ---'
rg -n -F -e 'exhaustive-deps' -e 'eslint-plugin-react-hooks' -e 'unique' package.json .eslintrc .eslintrc.js .eslintrc.json eslint.config.js docs README.md tests src 2>/dev/null || true
printf '%s\n' '--- Trigger usage sites ---'
rg -n -F -e '<Trigger' -e 'unique:' -- . ':!node_modules' ':!dist' ':!build' 2>/dev/null || trueRepository: react-component/trigger Length of output: 25118 🏁 Script executed: set -o pipefail
printf '%s\n' '--- src/context.ts ---'
nl -ba src/context.ts | sed -n '1,90p'
printf '%s\n' '--- UniqueProvider context value and release implementation ---'
nl -ba src/UniqueProvider/index.tsx | sed -n '1,285p'
printf '%s\n' '--- exact Trigger control flow ---'
nl -ba src/index.tsx | sed -n '180,265p'
nl -ba src/index.tsx | sed -n '340,445p'
nl -ba src/index.tsx | sed -n '810,845p'
printf '%s\n' '--- lint scripts and configuration ---'
cat package.json
rg -n -F -e 'react-hooks' -e 'exhaustive-deps' -e 'plugins:' -e 'rules:' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' .
printf '%s\n' '--- hook/effect patterns near cleanup logic ---'
rg -n -F -e 'React.useEffect' -e 'React.useLayoutEffect' -e 'useLayoutEffect' src tests | head -120
printf '%s\n' '--- unique transition and unmount tests ---'
nl -ba tests/unique.test.tsx | sed -n '1,180p'
nl -ba tests/unique.test.tsx | sed -n '330,475p'Repository: react-component/trigger Length of output: 37137 让卸载清理读取最新的
不要直接把 建议修改@@
const uniqueContext = React.useContext(UniqueContext);
+ const uniqueContextRef = React.useRef(uniqueContext);
+ uniqueContextRef.current = uniqueContext;
+
+ const uniqueRef = React.useRef(unique);
+ uniqueRef.current = unique;
+
+ const parentContextRef = React.useRef(parentContext);
+ parentContextRef.current = parentContext;
@@
// Close the shared popup when the trigger is unmounted
React.useEffect(() => {
- if (uniqueContext && unique && !parentContext) {
- return () => {
- if (targetEleRef.current) {
- uniqueContext.release(targetEleRef.current);
- }
- };
- }
+ return () => {
+ const currentUniqueContext = uniqueContextRef.current;
+ if (
+ currentUniqueContext &&
+ uniqueRef.current &&
+ !parentContextRef.current &&
+ targetEleRef.current
+ ) {
+ currentUniqueContext.release(targetEleRef.current);
+ }
+ };
}, []);🤖 Prompt for AI Agents |
||
|
|
||
| const internalTriggerOpen = useEvent((nextOpen: boolean) => { | ||
| flushSync(() => { | ||
| if (rawOpen !== nextOpen) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
区分已显示目标与待显示目标。
当 A 的弹层已打开,而 B 调用
show并等待延迟时,targetRef.current立即变成 B,但弹层仍属于 A。如果此时卸载 B,release(B)会因open为 true 而关闭 A 的弹层。即使 A 是保持popupVisible={true}的受控触发器,这次卸载也会错误地隐藏 A。请在实际调用trigger(showOptions)时记录已显示目标,并单独跟踪待显示目标。🤖 Prompt for AI Agents