Open suggest box with Ctrl + Space
This adds a keyboard shortcut to open the suggest box when it's closed. This matches the behavior in VS Code itself.
This commit is contained in:
@@ -18,6 +18,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react";
|
||||
import type { Option } from "./options";
|
||||
import { findMatchingOptions } from "./options";
|
||||
import { SuggestBoxItem } from "./SuggestBoxItem";
|
||||
import { useOpenKey } from "./useOpenKey";
|
||||
|
||||
const Input = styled(VSCodeTextField)`
|
||||
width: 430px;
|
||||
@@ -125,6 +126,7 @@ export const SuggestBox = <T extends Option<T>>({
|
||||
const focus = useFocus(context);
|
||||
const role = useRole(context, { role: "listbox" });
|
||||
const dismiss = useDismiss(context);
|
||||
const openKey = useOpenKey(context);
|
||||
const listNav = useListNavigation(context, {
|
||||
listRef,
|
||||
activeIndex,
|
||||
@@ -134,7 +136,7 @@ export const SuggestBox = <T extends Option<T>>({
|
||||
});
|
||||
|
||||
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
||||
[focus, role, dismiss, listNav],
|
||||
[focus, role, dismiss, openKey, listNav],
|
||||
);
|
||||
|
||||
const handleInput = useCallback(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { useEffectEvent } from "../useEffectEvent";
|
||||
|
||||
describe("useEffectEvent", () => {
|
||||
it("does not change reference when changing the callback function", () => {
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(callback) => useEffectEvent(callback),
|
||||
{
|
||||
initialProps: callback1,
|
||||
},
|
||||
);
|
||||
|
||||
const callbackResult = result.current;
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current).toBe(callbackResult);
|
||||
|
||||
rerender(callback2);
|
||||
|
||||
expect(result.current).toBe(callbackResult);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import type { KeyboardEvent } from "react";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import type { FloatingContext } from "@floating-ui/react";
|
||||
import { mockedObject } from "../../../../../test/vscode-tests/utils/mocking.helpers";
|
||||
import { useOpenKey } from "../useOpenKey";
|
||||
|
||||
describe("useOpenKey", () => {
|
||||
const onOpenChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onOpenChange.mockReset();
|
||||
});
|
||||
|
||||
const render = ({ open }: { open: boolean }) => {
|
||||
const context = mockedObject<FloatingContext>({
|
||||
open,
|
||||
onOpenChange,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useOpenKey(context));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
reference: {
|
||||
onKeyDown: expect.any(Function),
|
||||
},
|
||||
});
|
||||
|
||||
const onKeyDown = result.current.reference?.onKeyDown;
|
||||
if (!onKeyDown) {
|
||||
throw new Error("onKeyDown is undefined");
|
||||
}
|
||||
|
||||
return {
|
||||
onKeyDown,
|
||||
};
|
||||
};
|
||||
|
||||
const mockKeyboardEvent = ({
|
||||
key = "",
|
||||
altKey = false,
|
||||
ctrlKey = false,
|
||||
metaKey = false,
|
||||
shiftKey = false,
|
||||
preventDefault = jest.fn(),
|
||||
}: Partial<KeyboardEvent>) =>
|
||||
mockedObject<KeyboardEvent>({
|
||||
key,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
preventDefault,
|
||||
});
|
||||
|
||||
const pressKey = (event: Parameters<typeof mockKeyboardEvent>[0]) => {
|
||||
const { onKeyDown } = render({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const keyboardEvent = mockKeyboardEvent(event);
|
||||
|
||||
onKeyDown(keyboardEvent);
|
||||
|
||||
return {
|
||||
onKeyDown,
|
||||
keyboardEvent,
|
||||
};
|
||||
};
|
||||
|
||||
it("opens when pressing Ctrl + Space and it is closed", () => {
|
||||
const { keyboardEvent } = pressKey({
|
||||
key: " ",
|
||||
ctrlKey: true,
|
||||
});
|
||||
|
||||
expect(keyboardEvent.preventDefault).toHaveBeenCalledTimes(1);
|
||||
expect(onOpenChange).toHaveBeenCalledTimes(1);
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, keyboardEvent);
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + Space and it is open", () => {
|
||||
const { onKeyDown } = render({
|
||||
open: true,
|
||||
});
|
||||
|
||||
// Do not mock any properties to ensure that none of them are used.
|
||||
const keyboardEvent = mockedObject<KeyboardEvent>({});
|
||||
|
||||
onKeyDown(keyboardEvent);
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Cmd + Space", () => {
|
||||
pressKey({
|
||||
key: " ",
|
||||
metaKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + Shift + Space", () => {
|
||||
pressKey({
|
||||
key: " ",
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + Alt + Space", () => {
|
||||
pressKey({
|
||||
key: " ",
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + Cmd + Space", () => {
|
||||
pressKey({
|
||||
key: " ",
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + Shift + Alt + Space", () => {
|
||||
pressKey({
|
||||
key: " ",
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Space", () => {
|
||||
pressKey({
|
||||
key: " ",
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + Tab", () => {
|
||||
pressKey({
|
||||
key: "Tab",
|
||||
ctrlKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open when pressing Ctrl + a letter", () => {
|
||||
pressKey({
|
||||
key: "a",
|
||||
ctrlKey: true,
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not change reference when the context changes", () => {
|
||||
const context = mockedObject<FloatingContext>({
|
||||
open: false,
|
||||
onOpenChange,
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook((context) => useOpenKey(context), {
|
||||
initialProps: context,
|
||||
});
|
||||
|
||||
const firstOnKeyDown = result.current.reference?.onKeyDown;
|
||||
expect(firstOnKeyDown).toBeDefined();
|
||||
|
||||
rerender(
|
||||
mockedObject<FloatingContext>({
|
||||
open: true,
|
||||
onOpenChange: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const secondOnKeyDown = result.current.reference?.onKeyDown;
|
||||
// test that useEffectEvent is used correctly and the reference doesn't change
|
||||
expect(secondOnKeyDown).toBe(firstOnKeyDown);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback, useInsertionEffect, useRef } from "react";
|
||||
|
||||
// Copy of https://github.com/floating-ui/floating-ui/blob/5d025db1167e0bc13e7d386d7df2498b9edf2f8a/packages/react/src/hooks/utils/useEffectEvent.ts
|
||||
// since it's not exported
|
||||
|
||||
/**
|
||||
* Creates a reference to a callback that will never change in value. This will ensure that when a callback gets changed,
|
||||
* no new reference to the callback will be created and thus no unnecessary re-renders will be triggered.
|
||||
*
|
||||
* @param callback The callback to call when the event is triggered.
|
||||
*/
|
||||
export function useEffectEvent<T extends (...args: any[]) => any>(callback: T) {
|
||||
const ref = useRef<T>(callback);
|
||||
|
||||
useInsertionEffect(() => {
|
||||
ref.current = callback;
|
||||
});
|
||||
|
||||
return useCallback<(...args: Parameters<T>) => ReturnType<T>>(
|
||||
(...args) => ref.current(...args),
|
||||
[],
|
||||
) as T;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { KeyboardEvent } from "react";
|
||||
import { useMemo } from "react";
|
||||
import type {
|
||||
ElementProps,
|
||||
FloatingContext,
|
||||
ReferenceType,
|
||||
} from "@floating-ui/react";
|
||||
import { isReactEvent } from "@floating-ui/react/utils";
|
||||
import { useEffectEvent } from "./useEffectEvent";
|
||||
|
||||
/**
|
||||
* Open the floating element when Ctrl+Space is pressed.
|
||||
*/
|
||||
export const useOpenKey = <RT extends ReferenceType = ReferenceType>(
|
||||
context: FloatingContext<RT>,
|
||||
): ElementProps => {
|
||||
const { open, onOpenChange } = context;
|
||||
|
||||
const openOnOpenKey = useEffectEvent(
|
||||
(event: KeyboardEvent<Element> | KeyboardEvent) => {
|
||||
if (open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === " " &&
|
||||
event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.metaKey &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
onOpenChange(true, isReactEvent(event) ? event.nativeEvent : event);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return useMemo((): ElementProps => {
|
||||
return {
|
||||
reference: {
|
||||
onKeyDown: openOnOpenKey,
|
||||
},
|
||||
};
|
||||
}, [openOnOpenKey]);
|
||||
};
|
||||
Reference in New Issue
Block a user