-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #165 from kobra-dev/kbar
Command Pallete
- Loading branch information
Showing
13 changed files
with
905 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
// This is copied from https://github.com/timc1/kbar/blob/main/src/InternalEvents.tsx | ||
// and is patched to remove `useDocumentLock` so we can use the MUI modal | ||
|
||
import { useKBar, VisualState } from "kbar"; | ||
import * as React from "react"; | ||
|
||
/* code copied from utils.ts, because it isn't exported from the kbar module */ | ||
const SSR = typeof window === "undefined"; | ||
const isMac = !SSR && window.navigator.platform === "MacIntel"; | ||
|
||
export function isModKey( | ||
event: KeyboardEvent | MouseEvent | React.KeyboardEvent | ||
) { | ||
return isMac ? event.metaKey : event.ctrlKey; | ||
} | ||
|
||
export function shouldRejectKeystrokes( | ||
{ | ||
ignoreWhenFocused | ||
}: { | ||
ignoreWhenFocused: string[]; | ||
} = { ignoreWhenFocused: [] } | ||
) { | ||
const inputs = ["input", "textarea", ...ignoreWhenFocused].map((el) => | ||
el.toLowerCase() | ||
); | ||
|
||
const activeElement = document.activeElement; | ||
const ignoreStrokes = | ||
activeElement && | ||
(inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1 || | ||
activeElement.attributes.getNamedItem("role")?.value === | ||
"textbox" || | ||
activeElement.attributes.getNamedItem("contenteditable")?.value === | ||
"true"); | ||
|
||
return ignoreStrokes; | ||
} | ||
/* end code copied from utils.ts */ | ||
|
||
type Timeout = ReturnType<typeof setTimeout>; | ||
|
||
export function InternalEvents() { | ||
useToggleHandler(); | ||
useShortcuts(); | ||
useFocusHandler(); | ||
return null; | ||
} | ||
|
||
/** | ||
* `useToggleHandler` handles the keyboard events for toggling kbar. | ||
*/ | ||
function useToggleHandler() { | ||
const { query, options, visualState, showing } = useKBar((state) => ({ | ||
visualState: state.visualState, | ||
showing: state.visualState !== VisualState.hidden | ||
})); | ||
|
||
React.useEffect(() => { | ||
function handleKeyDown(event: KeyboardEvent) { | ||
if ( | ||
isModKey(event) && | ||
event.key === "k" && | ||
event.defaultPrevented === false | ||
) { | ||
event.preventDefault(); | ||
query.toggle(); | ||
|
||
if (showing) { | ||
options.callbacks?.onClose?.(); | ||
} else { | ||
options.callbacks?.onOpen?.(); | ||
} | ||
} | ||
if (event.key === "Escape") { | ||
if (showing) { | ||
event.stopPropagation(); | ||
options.callbacks?.onClose?.(); | ||
} | ||
|
||
query.setVisualState((vs) => { | ||
if ( | ||
vs === VisualState.hidden || | ||
vs === VisualState.animatingOut | ||
) { | ||
return vs; | ||
} | ||
return VisualState.animatingOut; | ||
}); | ||
} | ||
} | ||
|
||
window.addEventListener("keydown", handleKeyDown); | ||
return () => window.removeEventListener("keydown", handleKeyDown); | ||
}, [options.callbacks, query, showing]); | ||
|
||
const timeoutRef = React.useRef<Timeout>(); | ||
const runAnimateTimer = React.useCallback( | ||
(vs: VisualState.animatingIn | VisualState.animatingOut) => { | ||
let ms = 0; | ||
if (vs === VisualState.animatingIn) { | ||
ms = options.animations?.enterMs || 0; | ||
} | ||
if (vs === VisualState.animatingOut) { | ||
ms = options.animations?.exitMs || 0; | ||
} | ||
|
||
clearTimeout(timeoutRef.current as Timeout); | ||
timeoutRef.current = setTimeout(() => { | ||
let backToRoot = false; | ||
|
||
// TODO: setVisualState argument should be a function or just a VisualState value. | ||
query.setVisualState(() => { | ||
const finalVs = | ||
vs === VisualState.animatingIn | ||
? VisualState.showing | ||
: VisualState.hidden; | ||
|
||
if (finalVs === VisualState.hidden) { | ||
backToRoot = true; | ||
} | ||
|
||
return finalVs; | ||
}); | ||
|
||
if (backToRoot) { | ||
query.setCurrentRootAction(null); | ||
} | ||
}, ms); | ||
}, | ||
[options.animations?.enterMs, options.animations?.exitMs, query] | ||
); | ||
|
||
React.useEffect(() => { | ||
switch (visualState) { | ||
case VisualState.animatingIn: | ||
case VisualState.animatingOut: | ||
runAnimateTimer(visualState); | ||
break; | ||
} | ||
}, [runAnimateTimer, visualState]); | ||
} | ||
|
||
/** | ||
* `useShortcuts` registers and listens to keyboard strokes and | ||
* performs actions for patterns that match the user defined `shortcut`. | ||
*/ | ||
function useShortcuts() { | ||
const { actions, query, options } = useKBar((state) => ({ | ||
actions: state.actions | ||
})); | ||
|
||
React.useEffect(() => { | ||
const actionsList = Object.keys(actions).map((key) => actions[key]); | ||
|
||
let buffer: string[] = []; | ||
let lastKeyStrokeTime = Date.now(); | ||
|
||
function handleKeyDown(event: KeyboardEvent) { | ||
const key = event.key?.toLowerCase(); | ||
|
||
if (shouldRejectKeystrokes() || event.metaKey || key === "shift") { | ||
return; | ||
} | ||
|
||
const currentTime = Date.now(); | ||
|
||
if (currentTime - lastKeyStrokeTime > 400) { | ||
buffer = []; | ||
} | ||
|
||
buffer.push(key); | ||
lastKeyStrokeTime = currentTime; | ||
const bufferString = buffer.join(""); | ||
|
||
for (let action of actionsList) { | ||
if (!action.shortcut) { | ||
continue; | ||
} | ||
if (action.shortcut.join("") === bufferString) { | ||
event.preventDefault(); | ||
if (action.children?.length) { | ||
query.setCurrentRootAction(action.id); | ||
query.toggle(); | ||
options.callbacks?.onOpen?.(); | ||
} else { | ||
action.command?.perform(); | ||
options.callbacks?.onSelectAction?.(action); | ||
} | ||
|
||
buffer = []; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
window.addEventListener("keydown", handleKeyDown); | ||
return () => window.removeEventListener("keydown", handleKeyDown); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [actions, 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) => ({ | ||
isShowing: | ||
state.visualState === VisualState.showing || | ||
state.visualState === VisualState.animatingIn | ||
})); | ||
|
||
const activeElementRef = React.useRef<HTMLElement | null>(null); | ||
|
||
React.useEffect(() => { | ||
if (isShowing) { | ||
activeElementRef.current = document.activeElement as HTMLElement; | ||
return; | ||
} | ||
|
||
// This fixes an issue on Safari where closing kbar causes the entire | ||
// page to scroll to the bottom. The reason this was happening was due | ||
// to the search input still in focus when we removed it from the dom. | ||
const currentActiveElement = document.activeElement as HTMLElement; | ||
if (currentActiveElement?.tagName.toLowerCase() === "input") { | ||
currentActiveElement.blur(); | ||
} | ||
|
||
const activeElement = activeElementRef.current; | ||
if (activeElement) { | ||
activeElement.focus(); | ||
} | ||
}, [isShowing]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// This is (mostly) copied from https://github.com/timc1/kbar/blob/main/src/KBarContextProvider.tsx | ||
// and is patched to use our custom InternalEvents_patched component | ||
|
||
import { useStore } from "./useStore_patched"; | ||
import * as React from "react"; | ||
import { InternalEvents } from "./InternalEvents_patched"; | ||
import { KBarContext, KBarProviderProps } from "kbar"; | ||
|
||
export const KBarProvider: React.FC<KBarProviderProps> = (props) => { | ||
const contextValue = useStore(props); | ||
|
||
return ( | ||
<KBarContext.Provider value={contextValue}> | ||
<InternalEvents /> | ||
{props.children} | ||
</KBarContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { forwardRef } from "react"; | ||
|
||
// This is a custom component using the styles from KBarPositioner that forwards the ref correctly | ||
type KBarPositionerFwdRefProps = { | ||
children: React.ReactNode; | ||
style?: React.CSSProperties; | ||
}; | ||
// eslint-disable-next-line react/display-name | ||
const KBarPositionerFwdRef = forwardRef< | ||
HTMLDivElement, | ||
KBarPositionerFwdRefProps | ||
>((props: KBarPositionerFwdRefProps, ref) => ( | ||
<div | ||
ref={ref} | ||
style={{ | ||
position: "fixed", | ||
display: "flex", | ||
alignItems: "flex-start", | ||
justifyContent: "center", | ||
width: "100%", | ||
inset: "0px", | ||
padding: "14vh 16px 16px", | ||
...(props.style ?? {}) | ||
}} | ||
> | ||
{props.children} | ||
</div> | ||
)); | ||
|
||
export default KBarPositionerFwdRef; |
Oops, something went wrong.