Skip to content

Commit

Permalink
chore: ensure input focus when kbar is open (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
timc1 authored Nov 4, 2022
1 parent b4ac01e commit 30657cd
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 8 deletions.
26 changes: 23 additions & 3 deletions src/InternalEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,14 @@ function wrap(handler: (event: KeyboardEvent) => void) {
* performs actions for patterns that match the user defined `shortcut`.
*/
function useShortcuts() {
const { actions, query, options } = useKBar((state) => ({
const { actions, query, open, options } = useKBar((state) => ({
actions: state.actions,
open: state.visualState === VisualState.showing,
}));

React.useEffect(() => {
if (open) return;

const actionsList = Object.keys(actions).map((key) => actions[key]);

let actionsWithShortcuts: ActionImpl[] = [];
Expand Down Expand Up @@ -214,15 +217,15 @@ function useShortcuts() {
return () => {
unsubscribe();
};
}, [actions, options.callbacks, query]);
}, [actions, open, options.callbacks, query]);
}

/**
* `useFocusHandler` ensures that focus is set back on the element which was
* in focus prior to kbar being triggered.
*/
function useFocusHandler() {
const { isShowing } = useKBar((state) => ({
const { isShowing, query } = useKBar((state) => ({
isShowing:
state.visualState === VisualState.showing ||
state.visualState === VisualState.animatingIn,
Expand All @@ -249,4 +252,21 @@ function useFocusHandler() {
activeElement.focus();
}
}, [isShowing]);

// When focus is blurred from the search input while kbar is still
// open, any keystroke should set focus back to the search input.
React.useEffect(() => {
function handler(event: KeyboardEvent) {
const input = query.getInput();
if (event.target !== input) {
input.focus();
}
}
if (isShowing) {
window.addEventListener("keydown", handler);
return () => {
window.removeEventListener("keydown", handler);
};
}
}, [isShowing, query]);
}
6 changes: 2 additions & 4 deletions src/KBarSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@ export function KBarSearch(
showing: state.visualState === VisualState.showing,
}));

const ownRef = React.useRef<HTMLInputElement>(null);

const { defaultPlaceholder, ...rest } = props;

React.useEffect(() => {
query.setSearch("");
ownRef.current!.focus();
query.getInput().focus();
return () => query.setSearch("");
}, [currentRootActionId, query]);

Expand All @@ -46,7 +44,7 @@ export function KBarSearch(
return (
<input
{...rest}
ref={ownRef}
ref={query.inputRefSetter}
autoFocus
autoComplete="off"
role="combobox"
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface KBarQuery {
registerActions: (actions: Action[]) => () => void;
toggle: () => void;
setActiveIndex: (cb: number | ((currIndex: number) => number)) => void;
inputRefSetter: (el: HTMLInputElement) => void;
getInput: () => HTMLInputElement;
}

export interface IKBarContext {
Expand Down
1 change: 0 additions & 1 deletion src/useKBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,3 @@ export function useKBar<C = null>(

return render;
}

13 changes: 13 additions & 0 deletions src/useStore.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { deepEqual } from "fast-equals";
import * as React from "react";
import invariant from "tiny-invariant";
import { ActionInterface } from "./action/ActionInterface";
import { history } from "./action/HistoryImpl";
import type {
Expand Down Expand Up @@ -72,6 +73,8 @@ export function useStore(props: useStoreProps) {
[actionsInterface]
);

const inputRef = React.useRef<HTMLInputElement | null>(null);

return React.useMemo(() => {
return {
getState,
Expand Down Expand Up @@ -109,6 +112,16 @@ export function useStore(props: useStoreProps) {
...state,
activeIndex: typeof cb === "number" ? cb : cb(state.activeIndex),
})),
inputRefSetter: (el: HTMLInputElement) => {
inputRef.current = el;
},
getInput: () => {
invariant(
inputRef.current,
"Input is undefined, make sure you apple `query.inputRefSetter` to your search input."
);
return inputRef.current;
},
},
options: optionsRef.current,
subscribe: (collector, cb) => publisher.subscribe(collector, cb),
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export function shouldRejectKeystrokes(
);

const activeElement = document.activeElement;

const ignoreStrokes =
activeElement &&
(inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1 ||
Expand Down

1 comment on commit 30657cd

@vercel
Copy link

@vercel vercel bot commented on 30657cd Nov 4, 2022

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

kbar – ./

kbar-timc.vercel.app
kbar-git-main-timc.vercel.app
kbar.vercel.app

Please sign in to comment.