diff --git a/docs/demo/keyboard.md b/docs/demo/keyboard.md new file mode 100644 index 0000000..6dee400 --- /dev/null +++ b/docs/demo/keyboard.md @@ -0,0 +1,8 @@ +--- +title: keyboard +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/keyboard.tsx b/docs/examples/keyboard.tsx new file mode 100644 index 0000000..2dc8a8c --- /dev/null +++ b/docs/examples/keyboard.tsx @@ -0,0 +1,52 @@ +import React, { useRef } from 'react'; +import Tour from '../../src/index'; +import './basic.less'; + +const App = () => { + const firstBtnRef = useRef(null); + const secondBtnRef = useRef(null); + const thirdBtnRef = useRef(null); + return ( + +
+
+ + + +
+ +
+ + firstBtnRef.current, + }, + { + title: 'Two', + target: () => secondBtnRef.current, + }, + { + title: 'Three', + target: () => thirdBtnRef.current, + }, + ]} + /> +
+ + ); +}; + +export default App; diff --git a/src/Tour.tsx b/src/Tour.tsx index 7c4afeb..b8ece97 100644 --- a/src/Tour.tsx +++ b/src/Tour.tsx @@ -58,6 +58,7 @@ const Tour: React.FC = props => { className, style, getPopupContainer, + keyboard = false, ...restProps } = props; @@ -157,17 +158,66 @@ const Tour: React.FC = 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]); + + 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 = () => ( { arrowPointAtCenter?: boolean; }) => TriggerProps['builtinPlacements']); disabledInteraction?: boolean; + keyboard?: boolean; }