Skip to content

Commit

Permalink
Improved type documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
haaarshsingh committed Aug 5, 2022
1 parent 64ece83 commit 6224609
Show file tree
Hide file tree
Showing 18 changed files with 250 additions and 287 deletions.
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,22 +229,23 @@ You can easily customise the colours on your command menu as well. Here's a list

_NOTE: ALL PROPERTIES ARE **OPTIONAL**_

| Parameter | Description | Type | Default |
|--------------------|--------------------------------------------------------------|--------------|---------------------|
| backdropColor | The colour of the backdrop (include opacity) | string | #FFFFFF20 |
| backdropBlur | The backround blur of the backdrop (px) | number | 2px |
| backgroundColor | The background colour of the menu | string | #FFFFFF |
| borderWidth | Width of the border surrounding the menu | number | 1px |
| borderColor | The colour of the border surrounding the menu | string | #3F3F3F |
| borderRadius | The radius of the menu (px) | number | 16px |
| inputBorder | The colour of the border below the search bar | string | #FFFFFF |
| inputColor | The colour of the text in the search bar | string | #FFFFFF |
| placeholderText | The placeholder input text in the search bar | string | 'What do you need?' |
| headingColor | The colour of the command category headings | string | #777777 |
| commandInactive | The colour of the icon and text when the command is inactive | string | #777777 |
| commandActive | The colour of the icon and text when the command is active | string | #FFFFFF |
| barBackground | The background colour of the active bar (include opacity) | string | #FFFFFF20 |
| shortcutBackground | The background colour of the keyboard shortcut | string | #82828220 |
| Parameter | Description | Type | Default |
|--------------------|--------------------------------------------------------------|--------------|-----------------------------|
| backdropColor | The colour of the backdrop (include opacity) | string | #FFFFFF20 |
| backdropBlur | The backround blur of the backdrop (px) | number | 2px |
| backgroundColor | The background colour of the menu | string | #FFFFFF |
| borderWidth | Width of the border surrounding the menu | number | 1px |
| borderColor | The colour of the border surrounding the menu | string | #3F3F3F |
| borderRadius | The radius of the menu (px) | number | 10px |
| boxShadow | The shadow of the menu | string | 0px 0px 60px 10px #00000020 |
| inputBorder | The colour of the border below the search bar | string | #E9ECEF |
| inputColor | The colour of the text in the search bar | string | #000000 |
| placeholderText | The placeholder input text in the search bar | string | 'What do you need?' |
| headingColor | The colour of the command category headings | string | #777777 |
| commandInactive | The colour of the icon and text when the command is inactive | string | #828282 |
| commandActive | The colour of the icon and text when the command is active | string | #343434 |
| barBackground | The background colour of the active bar (include opacity) | string | #FFFFFF20 |
| shortcutBackground | The background colour of the keyboard shortcut | string | #82828220 |

### Setting up the menu

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@types/node": "^12.12.38",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "^0.1.7",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"babel-eslint": "^10.0.3",
Expand Down
7 changes: 0 additions & 7 deletions src/Command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ const Command: FC<{
setOpen: Dispatch<SetStateAction<number>>
config?: Partial<Config>
}> = ({ onMouseEnter, isSelected, command, setOpen, config }) => {
/* Refs for the top and bottom of the span for scroll navigation */
const topRef = useRef<HTMLSpanElement>(null)
const bottomRef = useRef<HTMLSpanElement>(null)
/* Check if the user presses the enter key to run the command */
const enter = useShortcut({ targetKey: 'Enter' })

/* Use a custom hook that uses the IntersectionObserver API to check if the command is in view from the top and the bottom */
const inViewTop = useInView({ ref: topRef })
const inViewBottom = useInView({ ref: bottomRef })

/* Function determining whether or not to scroll the div and when to run commands */
useEffect(() => {
if (isSelected && (!inViewTop || !inViewBottom))
// eslint-disable-next-line
Expand All @@ -32,11 +28,8 @@ const Command: FC<{
block: 'end'
})

/* If the user presses the enter key, then run the command */
if (enter && isSelected) {
/* Close the menu on select if the closeOnComplete value isn't set to true */
if (!command.closeOnComplete) setOpen(0)
/* Pass the entire command object in the run function */
run(command)
}
}, [isSelected, enter])
Expand Down
82 changes: 17 additions & 65 deletions src/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React, {
import { AnimatePresence, AnimateSharedLayout, motion } from 'framer-motion'
import useClickOutside from './hooks/useClickOutside'
import { useShortcut } from './hooks/useShortcut'
import { MenuContext } from './menuProvider'
import { MenuContext } from './MenuProvider'
import {
Action,
ActionType,
Expand All @@ -25,119 +25,91 @@ import Command from './Command'
import useBodyScrollable from './hooks/useBodyScrollable'
import useScrollbarSize from 'react-scrollbar-size'

/* The initial state of our keyboard selection */
const initialState = { selected: 0 }

/**
* The main command menu component.
*
* @param {number} index - The hierarchial index of this palette
* @param {CommandWithIndex[]} commands - The SORTED commands which will be displayed in this palette
* @param {boolean} main - Whether this is the first command menu the user will see on toggle
* @type {React.FC<MenuProps>}
* @returns {React.ReactElement} the menu provider
*/
export const CommandMenu: FC<MenuProps> = ({ index, commands, main }) => {
/* Ref for handling the search bar */
const input = useRef<HTMLInputElement>(null)
/* Contains filtered results when the user searches for commands */
const [results, setResults] = useState<CommandWithIndex | null>(null)
/* Get important data out of our Provider */
const { open, setOpen, config, query, setQuery, dimensions } =
useContext(MenuContext)

useEffect(() => {
/* Set the keyboard selected command back to zero */
state.selected = 0
let index = 0

/* If there is no query, then set to results back to the original commands */
if (!query) return setResults(commands)

/* Array which includes all of the sorted commands before they're passed onto the hook */
const sorted: SortedCommands[] = []
/* Loop through each category of commands */
// eslint-disable-next-line no-unused-expressions
commands.commands.forEach((row) => {
/* Object which contains the filtered results to be pushed back */
const results: SortedCommands = {
category: row.category,
commands: []
}

/* Loop through each command inside of the category */
row.commands.forEach((command) => {
/* Create a text object including the title and keywords */
const text =
command.text.toLowerCase() + command.keywords?.toLowerCase()
/* Check if the query in the input bar includes any keywords or the title */
if (text.includes(query.toLowerCase())) {
/* If so, push the command with the appropriate global index onto the array */
results.commands.push({ ...command, globalIndex: index })
/* Increase the index for the next component */
index++
}
})

/* Push the category into the array ONLY if they have commands */
if (results.commands.length > 0) sorted.push(results)
})

/* Set the results to the filtered results in our array */
setResults({
index: index,
commands: sorted,
initialHeight: commands.initialHeight
})
}, [query, setQuery])

/* Reducer for the keyboard navigation */
const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
/* Increase action when the user presses the down or tab key */
case ActionType.INCREASE:
/* Check if the current selected element is the last one */
return state.selected === results!.index - 1
? /* If it is, the set it to the first one */
{ ...state, selected: 0 }
: /* If it's not, then just increase the index */
{ ...state, selected: state.selected + 1 }
/* Decrease action when the user presses up or shift+tab */
? { ...state, selected: 0 }
: { ...state, selected: state.selected + 1 }
case ActionType.DECREASE:
/* Check if the selected element is the first one */
return state.selected === 0
? /* If it is, then set the selected element to the last one */
{ ...state, selected: results!.index - 1 }
: /* If it's not, then just decrease the index */
{ ...state, selected: state.selected - 1 }
/* Custom action when the user hovers their mouse over an element */
? { ...state, selected: results!.index - 1 }
: { ...state, selected: state.selected - 1 }
case ActionType.CUSTOM:
/* Just set the value into whatever is inputted into the reducer function */
return {
...state,
selected: action.custom
}
/* Action to reset the menu when the menu is closed and re-opened, or when the user searches for something */
case ActionType.RESET:
/* Just set the selected to the first element on the list */
return {
...state,
selected: 0
}
}
}

/* Ref for controlling the dialog */
const menuRef = useRef<HTMLDivElement>(null)
/* Ref for controlling the div wheere all the commands are located */
const parentRef = useRef<HTMLDivElement>(null)
/* useReducer hook to manage the keyboard state */
const [state, dispatch] = useReducer(reducer, initialState)

/* Use our hooks we've defined for checking if the body is scrollable and for getting the width of the scrollbar */
const scrollable = useBodyScrollable()
// @ts-ignore
const { height, width } = useScrollbarSize()

/* Reset the menu whenever the open state is changed */
useEffect(() => {
/* Reset the component whenever the menu is toggled */
dispatch({ type: ActionType.RESET, custom: 0 })
/* Set the query to blank */
setQuery('')

/* Toggle scrollbars base upon whether or not the bar is open or not to prevent background scrolling */
if (open && scrollable) {
document.body.style.overflow = 'hidden'
document.body.style.paddingRight = `${width}px`
Expand All @@ -147,36 +119,27 @@ export const CommandMenu: FC<MenuProps> = ({ index, commands, main }) => {
}
}, [open, setOpen])

/* Shortcuts for navigating up and down the menu */
const up = useShortcut({ targetKey: 'ArrowUp' })
const down = useShortcut({ targetKey: 'ArrowDown' })

/* Hook for detecting clicks outside the menu area */
useClickOutside({ ref: menuRef, handler: () => setOpen(0) })

/* Handle keyboard shortcuts for navigating the menu */
useEffect(() => {
/* First check if this instance of the menu is actually open or not */
if (open === index) {
/* Move up or down depending on what key the user has pressed */
if (up) dispatch({ type: ActionType.DECREASE, custom: 0 })
else if (down) dispatch({ type: ActionType.INCREASE, custom: 0 })
}
}, [up, down])

/* Function for toggling the menu along with using the tab key to navigate the menu */
const navigation = (event: KeyboardEvent) => {
/* Toggle the menu if the user presses ctrl/cmdk+k */
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
if (main) setOpen((open: number) => (open === index ? 0 : index))
else if (!main && open === index) setOpen(0)
}

/* Close the menu if the user presses escape */
if (event.key === 'Escape') setOpen(0)

/* If the menu is open, then check if the user presses tab/shift+tab and navigate accordingly */
if (open === index) {
if (event.key === 'Tab' && !event.shiftKey) {
event.preventDefault()
Expand All @@ -188,47 +151,35 @@ export const CommandMenu: FC<MenuProps> = ({ index, commands, main }) => {
}
}

/* Function to toggle the menu on mobile */
const mobileToggle = (event: TouchEvent) => {
/* Check if the user has pressed their screen with two fingers */
if (event.touches.length >= 2) {
/* Prevent the default action */
event.preventDefault()
/* Adjust the menu depending upon whether or not it's the main menu and if it's currently open/active */
if (main) setOpen((open: number) => (open === index ? 0 : index))
else if (!main && open === index) setOpen(0)
}
}

/* Adding and removing event listeners for keyboard and mobile navigation */
useEffect(() => {
window.addEventListener('keydown', navigation)
window.addEventListener('touchstart', mobileToggle)
/* Remove event listeners during the cleanup phase */
return () => {
window.removeEventListener('keydown', navigation)
window.removeEventListener('touchstart', mobileToggle)
}
}, [open, setOpen])

useEffect(() => {
/* Loop through each category inside the component array */
commands.commands.forEach((row) => {
/* Loop through each command inside of the individual categories */
row.commands.forEach((command) => {
/* Check if they have shortcuts */
if (command.shortcuts) {
/* Create a map array to keep track of the shortcuts */
const map: string[] = []
/* Add a keydown event to listen for the shortcuts, and use the parse function to parse the shortcut */
window.addEventListener('keydown', (event) =>
parse({ command: command, event: event, map: map })
)
}
})
})

/* Clean up the event listeners on component unmount */
return () => {
commands.commands.forEach((row) => {
row.commands.forEach((command) => {
Expand Down Expand Up @@ -268,7 +219,8 @@ export const CommandMenu: FC<MenuProps> = ({ index, commands, main }) => {
backgroundColor: config?.backgroundColor || '#FFFFFF',
borderColor: config?.borderColor || 'transparent',
borderWidth: config?.borderWidth || 0,
boxShadow: config?.boxShadow || '0px 0px 59px 10px #00000020'
borderRadius: config?.borderRadius || '10px',
boxShadow: config?.boxShadow || '0px 0px 60px 10px #00000020'
}}
>
<input
Expand All @@ -283,7 +235,7 @@ export const CommandMenu: FC<MenuProps> = ({ index, commands, main }) => {
ref={input}
onChange={() => setQuery(input.current?.value!)}
style={{
color: config?.inputColor || '#000',
color: config?.inputColor || '#000000',
borderBottom: `${config?.inputBorder || '#e9ecef'} 1px solid`
}}
/>
Expand Down
17 changes: 10 additions & 7 deletions src/menuProvider.tsx → src/MenuProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React, { createContext, FC, useMemo, useState } from 'react'
import React, { createContext, FC, ReactNode, useState } from 'react'
import { MenuContext as MenuContextType, MenuProviderProps } from './types'

/* Create the Menu context object */
export const MenuContext = createContext<MenuContextType>({} as MenuContextType)

/* Create the wrapper for the Provider and other hooks */
export const MenuProvider: FC<MenuProviderProps> = ({
/**
* The provider component for kmenu.
*
* @param {Config} config - the configuration file to be passed down to all palettes
* @param {Dimensions} dimensions - the dimensions
* @type {React.FC<MenuProviderProps & { children: ReactNode }>}
* @returns {React.ReactElement} the menu provider
*/
export const MenuProvider: FC<MenuProviderProps & { children: ReactNode }> = ({
children,
dimensions,
config
}) => {
/* Hook for toggling the open/close state of the menu */
const [open, setOpen] = useState(0)
/* Hook for managing the search queries */
const [query, setQuery] = useState('')

/* Pass down the provider and the children below this component */
return (
<MenuContext.Provider
value={{
Expand Down
21 changes: 8 additions & 13 deletions src/hooks/useBodyScrollable.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import { useEffect, useState } from 'react'

/* Hook for checking if the body of our element is actually scrollable using the ResizeObserver API */
/**
* A hook to check if the HTML body element is scrollable vertically.
*
* @returns {boolean} Whether or not the given element is in view
*/
const useBodyScrollable = () => {
/* Hook for managing the state, set to true if the body's scroll height is GREATER than the window's inner height */
const [bodyScrollable, setBodyScrollable] = useState(true)

useEffect(() => {
/* Check for resizes on the screen using the ResizeObserver API */
/* Resize observer does not properly work for some reason? */
const resizeObserver = new (window as any).ResizeObserver(() =>
/* Set the hook to true if the body's height is still greater than the window's inner height */
const resizeObserver = new ResizeObserver(() =>
setBodyScrollable(document.body.scrollHeight > window.innerHeight)
)
/* Add the observer onto the document's body */
resizeObserver.observe(document.body)

/* Clean up on component unmount */
return () => {
resizeObserver.unobserve(document.body)
}
resizeObserver.observe(document.body)
return () => resizeObserver.unobserve(document.body)
}, [])

/* Return the state */
return bodyScrollable
}

Expand Down
Loading

0 comments on commit 6224609

Please sign in to comment.