fix: let Enter press the tour's Skip and Back buttons, and mirror its arrow keys in right-to-left languages

This commit is contained in:
Linus Rath
2026-10-28 03:20:43 +01:00
parent 2c4e259ddc
commit b099c8a2db
2 changed files with 52 additions and 4 deletions
+40
View File
@@ -88,3 +88,43 @@ describe('tour targets', () => {
expect(ids({ supportsContacts: false })).not.toContain('nav-contacts');
});
});
describe('tour keyboard', () => {
function showStep() {
anchors('<a data-tour="one" data-size></a>');
render(<TourOverlay />);
act(() => { vi.advanceTimersByTime(400); });
}
it('lets Enter press the focused Skip button instead of going to the next step', () => {
showStep();
const skip = screen.getByRole('button', { name: 'tour.skip' });
skip.focus();
act(() => { fireEvent.keyDown(skip, { key: 'Enter' }); });
expect(tour.nextStep).not.toHaveBeenCalled();
});
it('still goes on with Enter when no button has the focus', () => {
showStep();
act(() => { fireEvent.keyDown(document.body, { key: 'Enter' }); });
expect(tour.nextStep).toHaveBeenCalledTimes(1);
});
it('leaves Enter and the arrows to a field being typed in', () => {
showStep();
const field = document.createElement('input');
hosts[0].appendChild(field);
act(() => { fireEvent.keyDown(field, { key: 'Enter' }); });
act(() => { fireEvent.keyDown(field, { key: 'ArrowRight' }); });
expect(tour.nextStep).not.toHaveBeenCalled();
});
it('mirrors the arrows in right-to-left languages', () => {
document.documentElement.setAttribute('dir', 'rtl');
showStep();
act(() => { fireEvent.keyDown(document.body, { key: 'ArrowLeft' }); });
expect(tour.nextStep).toHaveBeenCalledTimes(1);
act(() => { fireEvent.keyDown(document.body, { key: 'ArrowRight' }); });
expect(tour.prevStep).toHaveBeenCalledTimes(1);
});
});
+12 -4
View File
@@ -6,6 +6,8 @@ import { useTranslations } from "next-intl";
import { useTour } from "./tour-provider";
import { cn } from "@/lib/utils";
import { useFocusTrap } from "@/hooks/use-focus-trap";
import { isEditableEventTarget } from "@/lib/keyboard";
import { isDocumentRTL } from "@/i18n/direction";
interface Rect {
top: number;
@@ -211,7 +213,7 @@ export function TourOverlay() {
pendingTimerRef.current = null;
}
};
}, [step, currentStep]); // eslint-disable-line react-hooks/exhaustive-deps
}, [step, currentStep]);
// Recalculate on resize/scroll (debounced)
useEffect(() => {
@@ -232,14 +234,20 @@ export function TourOverlay() {
};
}, [visible, updatePosition]);
// Keyboard navigation
// Keyboard navigation. Enter on a focused button (Skip, Back) and keys
// typed into a field (the search step) are theirs, not the tour's.
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "ArrowRight" || e.key === "Enter") {
const onControl = e.target instanceof HTMLElement && !!e.target.closest("button, a[href]");
const typing = isEditableEventTarget(e);
// The arrow pointing the way you read goes on: → in LTR, ← in RTL.
const forward = isDocumentRTL() ? "ArrowLeft" : "ArrowRight";
const back = isDocumentRTL() ? "ArrowRight" : "ArrowLeft";
if ((e.key === forward && !typing) || (e.key === "Enter" && !onControl && !typing)) {
e.preventDefault();
e.stopPropagation();
goNext();
} else if (e.key === "ArrowLeft") {
} else if (e.key === back && !typing) {
e.preventDefault();
e.stopPropagation();
goPrev();