Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/demo/keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: keyboard
nav:
title: Demo
path: /demo
---

<code src="../examples/keyboard.tsx"></code>
52 changes: 52 additions & 0 deletions docs/examples/keyboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useRef } from 'react';
import Tour from '../../src/index';
import './basic.less';

const App = () => {
const firstBtnRef = useRef<HTMLButtonElement>(null);
const secondBtnRef = useRef<HTMLButtonElement>(null);
const thirdBtnRef = useRef<HTMLButtonElement>(null);
return (
<React.StrictMode>
<div style={{ margin: 20 }}>
<div>
<button
className="ant-target"
ref={firstBtnRef}
>
One
</button>
<button className="ant-target" ref={secondBtnRef}>
Two
</button>
<button className="ant-target" ref={thirdBtnRef}>
Three
</button>
</div>

<div style={{ height: 200 }} />

<Tour
defaultCurrent={0}
keyboard={true}
steps={[
{
title: 'One',
target: () => firstBtnRef.current,
},
{
title: 'Two',
target: () => secondBtnRef.current,
},
{
title: 'Three',
target: () => thirdBtnRef.current,
},
]}
/>
Comment on lines +29 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

示例缺少 Tour 可见性控制。

当前示例中 Tour 组件始终处于打开状态(依赖 defaultCurrent 但没有 open prop 控制)。这可能让用户难以理解如何在实际应用中初始打开和关闭 Tour。

建议添加状态控制和触发按钮来演示完整的使用场景:

+import React, { useRef, useState } from 'react';
-import React, { useRef } from 'react';
 import Tour from '../../src/index';
 import './basic.less';

 const App = () => {
+  const [open, setOpen] = useState(false);
   const firstBtnRef = useRef<HTMLButtonElement>(null);
   const secondBtnRef = useRef<HTMLButtonElement>(null);
   const thirdBtnRef = useRef<HTMLButtonElement>(null);
   return (
     <React.StrictMode>
     <div style={{ margin: 20 }}>
+      <button onClick={() => setOpen(true)}>开始导览</button>
       <div>
         <button
           className="ant-target"
           ref={firstBtnRef}
         >
           One
         </button>
         {/* ... */}
       </div>

       <Tour
+        open={open}
+        onClose={() => setOpen(false)}
         defaultCurrent={0}
         keyboard={true}
         steps={[
           {/* ... */}
         ]}
       />
     </div>
     </React.StrictMode>
   );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tour
defaultCurrent={0}
keyboard={true}
steps={[
{
title: 'One',
target: () => firstBtnRef.current,
},
{
title: 'Two',
target: () => secondBtnRef.current,
},
{
title: 'Three',
target: () => thirdBtnRef.current,
},
]}
/>
import React, { useRef, useState } from 'react';
import Tour from '../../src/index';
import './basic.less';
const App = () => {
const [open, setOpen] = useState(false);
const firstBtnRef = useRef<HTMLButtonElement>(null);
const secondBtnRef = useRef<HTMLButtonElement>(null);
const thirdBtnRef = useRef<HTMLButtonElement>(null);
return (
<React.StrictMode>
<div style={{ margin: 20 }}>
<button onClick={() => setOpen(true)}>开始导览</button>
<div>
<button
className="ant-target"
ref={firstBtnRef}
>
One
</button>
{/* ...other buttons... */}
</div>
<Tour
open={open}
onClose={() => setOpen(false)}
defaultCurrent={0}
keyboard={true}
steps={[
{
title: 'One',
target: () => firstBtnRef.current,
},
{
title: 'Two',
target: () => secondBtnRef.current,
},
{
title: 'Three',
target: () => thirdBtnRef.current,
},
]}
/>
</div>
</React.StrictMode>
);
};
export default App;
🤖 Prompt for AI Agents
In docs/examples/keyboard.tsx around lines 29 to 46, the Tour is always shown
because it only uses defaultCurrent and lacks open state control; add a boolean
state (e.g., const [open, setOpen] = useState(false)) and pass open={open} and
onClose={() => setOpen(false)} to the Tour, add a trigger button that calls
setOpen(true) to open the tour, and ensure any initial step control still uses
defaultCurrent or useCurrent as needed so the example demonstrates opening,
closing, and triggering the Tour in a real app.

</div>
</React.StrictMode>
);
};

export default App;
60 changes: 55 additions & 5 deletions src/Tour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const Tour: React.FC<TourProps> = props => {
className,
style,
getPopupContainer,
keyboard = false,
...restProps
} = props;

Expand Down Expand Up @@ -157,17 +158,66 @@ const Tour: React.FC<TourProps> = props => {
return getPlacements(arrowPointAtCenter);
}, [builtinPlacements, arrowPointAtCenter]);

// ================= close ========================
const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent);
};

// ================= Keyboard Navigation ==============
const keyboardController = React.useRef(new AbortController());
const handleKeyDown = React.useCallback((e: KeyboardEvent) => {
if (!mergedOpen) {
return;
}
if (e.key === 'ArrowLeft') {
if (mergedCurrent <= 0) {
return;
}
e.preventDefault();
e.stopPropagation();
onInternalChange(mergedCurrent - 1);
return;
}
if (e.key === 'ArrowRight') {
if (mergedCurrent >= steps.length - 1) {
return;
}
e.preventDefault();
e.stopPropagation();
onInternalChange(mergedCurrent + 1);
return;
}
if (e.key === 'Escape') {
if (!mergedClosable) {
return;
}
e.preventDefault();
e.stopPropagation();
handleClose();
return;
}
}, [mergedCurrent, mergedOpen, steps.length, handleClose, onInternalChange]);
Comment on lines +169 to +200
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

缺少 mergedClosable 依赖导致闭包陈旧。

handleKeyDown 在第 192 行使用了 mergedClosable,但该变量未包含在依赖数组中。这会导致回调捕获过时的 mergedClosable 值,当可关闭状态变化时,Escape 键行为不会更新。

应用此差异修复依赖数组:

-  }, [mergedCurrent, mergedOpen, steps.length, handleClose, onInternalChange]);
+  }, [mergedCurrent, mergedOpen, steps.length, handleClose, onInternalChange, mergedClosable]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleKeyDown = React.useCallback((e: KeyboardEvent) => {
if (!mergedOpen) {
return;
}
if (e.key === 'ArrowLeft') {
if (mergedCurrent <= 0) {
return;
}
e.preventDefault();
e.stopPropagation();
onInternalChange(mergedCurrent - 1);
return;
}
if (e.key === 'ArrowRight') {
if (mergedCurrent >= steps.length - 1) {
return;
}
e.preventDefault();
e.stopPropagation();
onInternalChange(mergedCurrent + 1);
return;
}
if (e.key === 'Escape') {
if (!mergedClosable) {
return;
}
e.preventDefault();
e.stopPropagation();
handleClose();
return;
}
}, [mergedCurrent, mergedOpen, steps.length, handleClose, onInternalChange]);
const handleKeyDown = React.useCallback((e: KeyboardEvent) => {
if (!mergedOpen) {
return;
}
if (e.key === 'ArrowLeft') {
if (mergedCurrent <= 0) {
return;
}
e.preventDefault();
e.stopPropagation();
onInternalChange(mergedCurrent - 1);
return;
}
if (e.key === 'ArrowRight') {
if (mergedCurrent >= steps.length - 1) {
return;
}
e.preventDefault();
e.stopPropagation();
onInternalChange(mergedCurrent + 1);
return;
}
if (e.key === 'Escape') {
if (!mergedClosable) {
return;
}
e.preventDefault();
e.stopPropagation();
handleClose();
return;
}
}, [mergedCurrent, mergedOpen, steps.length, handleClose, onInternalChange, mergedClosable]);
🤖 Prompt for AI Agents
In src/Tour.tsx around lines 169 to 200, the handleKeyDown callback uses
mergedClosable (line ~192) but it is missing from the dependency array, causing
a stale-closure bug; update the useCallback dependency list to include
mergedClosable so the callback is recreated when closable state changes,
ensuring Escape key behavior reflects the latest mergedClosable value.


React.useEffect(() => {
keyboardController.current.abort();
keyboardController.current = new AbortController();
if (keyboard) {
document.addEventListener('keydown', handleKeyDown, {
signal: keyboardController.current.signal,
});
}
return () => {
keyboardController.current.abort();
};
}, [handleKeyDown, keyboard]);

// ========================= Render =========================
// Skip if not init yet
if (targetElement === undefined || !hasOpened) {
return null;
}

const handleClose = () => {
setMergedOpen(false);
onClose?.(mergedCurrent);
};

const getPopupElement = () => (
<TourStep
styles={styles}
Expand Down
1 change: 1 addition & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ export interface TourProps extends Pick<TriggerProps, 'onPopupAlign'> {
arrowPointAtCenter?: boolean;
}) => TriggerProps['builtinPlacements']);
disabledInteraction?: boolean;
keyboard?: boolean;
}