From 62246099b66c11cc004764b4f8d52725d914f2f5 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Fri, 5 Aug 2022 17:39:31 -0500 Subject: [PATCH] Improved type documentation --- README.md | 33 +-- package.json | 1 + src/Command.tsx | 7 - src/CommandMenu.tsx | 82 ++----- src/{menuProvider.tsx => MenuProvider.tsx} | 17 +- src/hooks/useBodyScrollable.ts | 21 +- src/hooks/useClickOutside.ts | 5 - src/hooks/useCommands.ts | 28 +-- src/hooks/useInView.ts | 16 +- src/hooks/useKmenu.ts | 16 +- src/hooks/useShortcut.ts | 24 +- src/index.tsx | 2 +- src/styles/menu.module.css | 1 - src/types.ts | 243 ++++++++++++--------- src/typings.d.ts | 13 +- src/utils/parse.ts | 16 +- src/utils/run.ts | 7 +- yarn.lock | 5 + 18 files changed, 250 insertions(+), 287 deletions(-) rename src/{menuProvider.tsx => MenuProvider.tsx} (55%) diff --git a/README.md b/README.md index 9e68f12..8da8250 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index ded0ca7..dfd66f7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Command.tsx b/src/Command.tsx index a5a4922..ee77a74 100644 --- a/src/Command.tsx +++ b/src/Command.tsx @@ -13,17 +13,13 @@ const Command: FC<{ setOpen: Dispatch> config?: Partial }> = ({ onMouseEnter, isSelected, command, setOpen, config }) => { - /* Refs for the top and bottom of the span for scroll navigation */ const topRef = useRef(null) const bottomRef = useRef(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 @@ -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]) diff --git a/src/CommandMenu.tsx b/src/CommandMenu.tsx index 6446ba2..192310e 100644 --- a/src/CommandMenu.tsx +++ b/src/CommandMenu.tsx @@ -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, @@ -25,56 +25,49 @@ 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} + * @returns {React.ReactElement} the menu provider + */ export const CommandMenu: FC = ({ index, commands, main }) => { - /* Ref for handling the search bar */ const input = useRef(null) - /* Contains filtered results when the user searches for commands */ const [results, setResults] = useState(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, @@ -82,35 +75,22 @@ export const CommandMenu: FC = ({ index, commands, main }) => { }) }, [query, setQuery]) - /* Reducer for the keyboard navigation */ const reducer: Reducer = (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 @@ -118,26 +98,18 @@ export const CommandMenu: FC = ({ index, commands, main }) => { } } - /* Ref for controlling the dialog */ const menuRef = useRef(null) - /* Ref for controlling the div wheere all the commands are located */ const parentRef = useRef(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` @@ -147,36 +119,27 @@ export const CommandMenu: FC = ({ 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() @@ -188,23 +151,17 @@ export const CommandMenu: FC = ({ 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) @@ -212,15 +169,10 @@ export const CommandMenu: FC = ({ index, commands, main }) => { }, [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 }) ) @@ -228,7 +180,6 @@ export const CommandMenu: FC = ({ index, commands, main }) => { }) }) - /* Clean up the event listeners on component unmount */ return () => { commands.commands.forEach((row) => { row.commands.forEach((command) => { @@ -268,7 +219,8 @@ export const CommandMenu: FC = ({ 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' }} > = ({ 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` }} /> diff --git a/src/menuProvider.tsx b/src/MenuProvider.tsx similarity index 55% rename from src/menuProvider.tsx rename to src/MenuProvider.tsx index e0ba30f..e4eec6d 100644 --- a/src/menuProvider.tsx +++ b/src/MenuProvider.tsx @@ -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({} as MenuContextType) -/* Create the wrapper for the Provider and other hooks */ -export const MenuProvider: FC = ({ +/** + * 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} + * @returns {React.ReactElement} the menu provider + */ +export const MenuProvider: FC = ({ 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 ( { - /* 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 } diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts index f565fd3..f90e5bf 100644 --- a/src/hooks/useClickOutside.ts +++ b/src/hooks/useClickOutside.ts @@ -1,21 +1,16 @@ import { useEffect } from 'react' import { UseClickOutsideProps } from '../types' -/* Hook which checks if the user has clicked outside a div boundary */ const useClickOutside = ({ ref, handler }: UseClickOutsideProps) => { useEffect(() => { const listener = (event: MouseEvent) => { - /* Do nothing if you're clicking the elements inside the div */ if (!ref.current || ref.current.contains(event.target as Node)) return - /* Run the handler if you're not clicking the elements inside the div */ handler() } - /* Add event listeners for mobile and PC to detect clicks/touches */ document.addEventListener('mousedown', listener) document.addEventListener('touchstart', listener) - /* Clean up the event listeners on unmount */ return () => { document.removeEventListener('mousedown', listener) document.removeEventListener('touchstart', listener) diff --git a/src/hooks/useCommands.ts b/src/hooks/useCommands.ts index cd5d93b..b8aa460 100644 --- a/src/hooks/useCommands.ts +++ b/src/hooks/useCommands.ts @@ -1,5 +1,5 @@ import { useCallback, useContext, useState } from 'react' -import { MenuContext } from '../menuProvider' +import { MenuContext } from '../MenuProvider' import { Command, CommandWithIndex, @@ -8,42 +8,37 @@ import { UseCommandsProps } from '../types' +/** + * A hook that allows you to sort and dynamically add menu commands + * + * @param {Command[]} initialCommands - The initial set of commands + * @returns {CommandWithIndex} An object with arrays of sorted commands + * @returns {() => void} A function which you can use to dynamically add commands + */ export const useCommands = ( initialCommands: UseCommandsProps -): /* Return the sorted commands, and the setCommands function */ [ - CommandWithIndex, - (commands: Command[]) => void -] => { - /* Hook for the initial height of the command menu */ +): [CommandWithIndex, (commands: Command[]) => void] => { const [height, setHeight] = useState() - /* Hook for the globalIndex of the command menu */ const [index, setIndex] = useState() - /* Hook containing all of the sorted commands in the command menu */ const [commands, setCommands] = useState(() => { - /* Variables used for counting current categories, height, and the index */ let currentCategories = 0 let height = 0 let index = 0 - /* Empty array that'll contain all of the sorted commands */ const sorted: SortedCommands[] = [] const { dimensions } = useContext(MenuContext) - /* Loop through each category in the initial commands */ // eslint-disable-next-line no-unused-expressions initialCommands?.forEach((category) => { currentCategories++ - /* Map each command onto the new array with a global index */ const indexedCommands: GlobalCommand[] = category.commands.map( (command) => { index++ - /* Adjust the height of the menu accordingly with the current index and the current categories */ if (index <= 5) height = currentCategories * (dimensions?.sectionHeight || 31) + index * (dimensions?.commandHeight || 54) - /* Return the command with a global index */ return { ...command, globalIndex: index - 1 @@ -51,25 +46,20 @@ export const useCommands = ( } ) - /* Push the array onto the sorted array */ sorted.push({ category: category.category, commands: indexedCommands }) }) - /* Assign the appropriate values to the hooks */ setHeight(height) setIndex(index) - /* Return the array of sorted commands onto the setCommands hook */ return sorted }) - /* Return the commands with a global index and an initial height */ return [ { index: index!, commands: commands, initialHeight: height! }, - /* Basically the same thing as above, just within another function */ useCallback( (cmds: Command[]) => { let currentCategories = 0 diff --git a/src/hooks/useInView.ts b/src/hooks/useInView.ts index e6486ed..f8ef66c 100644 --- a/src/hooks/useInView.ts +++ b/src/hooks/useInView.ts @@ -1,26 +1,24 @@ import { useEffect, useState } from 'react' import { UseInViewProps } from '../types' -/* The hook to determine whether or not a given element is in view using the Intersection Observer API */ +/** + * A hook to check if a given element is in view inside a div. + * + * @param {React.RefObject} ref - The ref of the element we are checking + * @returns {boolean} Whether or not the given element is in view + */ const useInView = ({ ref }: UseInViewProps) => { - /* State for managing if the element is in view or not */ const [isIntersecting, setIntersecting] = useState(false) - /* Create a new Intersection Observer */ const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting) ) useEffect(() => { - /* Configure the callback to get notified of the intersection */ observer.observe(ref.current!) - /* Disconnect the observer on unmount */ - return () => { - observer.disconnect() - } + return () => observer.disconnect() }, []) - /* Return the state of the observer */ return isIntersecting } diff --git a/src/hooks/useKmenu.ts b/src/hooks/useKmenu.ts index 0752d77..2dfb4f4 100644 --- a/src/hooks/useKmenu.ts +++ b/src/hooks/useKmenu.ts @@ -1,22 +1,26 @@ import { useCallback, useContext } from 'react' -import { MenuContext } from '../menuProvider' +import { MenuContext } from '../MenuProvider' import { UseKmenuReturnType } from '../types' -/* Utilities for using kmenu */ +/** + * Utilities for using kmenu. + * + * @returns {string} The current input in the active command menu + * @returns {Dispatch>} The setter function for setting the text in the command menu + * @returns {number} The index of the currently active command menu + * @returns {Dispatch>} The setter function for setting the open menu index + * @returns {() => void} A function for toggling the state of the palette + */ export const useKmenu = (): UseKmenuReturnType => { - /* Pull the information out of our Provider */ const context = useContext(MenuContext) - /* Throw an error if the Provider doesn't exist, or if this is called outside the provider */ if (!context) throw new Error('useKmenu must be called inside the MenuProvider') - /* Function for toggling the menu */ const toggle = useCallback(() => { context.setOpen((open: number) => (open === 0 ? 1 : 0)) }, []) - /* Return the query, the index of the open menu, the toggle function and the setOpen hook */ return [ context.query, context.setQuery, diff --git a/src/hooks/useShortcut.ts b/src/hooks/useShortcut.ts index 2f83605..b0c248a 100644 --- a/src/hooks/useShortcut.ts +++ b/src/hooks/useShortcut.ts @@ -2,26 +2,27 @@ import { useCallback, useEffect, useState } from 'react' import { UseShortcutProps } from '../types' -/* Hook for creating new shortcuts */ +/** + * Hook for creating new shortcuts. + * + * @param {string} targetKey - The key this shortcut is listening for + * @param {enum} modifier - Modifier that will be used in conjuntion with the target key + * @param {() => void} handler - The function that will run when this shortcut is pressed + * + * @returns {boolean} Whether or not this shortcut was pressed + */ export const useShortcut = ({ targetKey, modifier, handler }: UseShortcutProps): boolean => { - /* Boolean to see if the shortcut is active or not */ const [keyPressed, setKeyPressed] = useState(false) - /* Function that runs whenever the user presses a key */ const downHandler = useCallback((event: KeyboardEvent) => { - /* Firstly, check if the key the user has pressed is the target key */ if (event.key === targetKey) { - /* If yes, then check if it also has a modifier and ALSO check that the user has passed in a modifier option to this hook */ if (modifier === 'shift' && event.shiftKey) { - /* Prevent the default event for this shortcut */ event.preventDefault() - /* Toggle the hook */ setKeyPressed(true) - /* Run the handler if it's not undefined */ handler?.() } else if (modifier === 'ctrl' && event.ctrlKey) { event.preventDefault() @@ -35,7 +36,7 @@ export const useShortcut = ({ event.preventDefault() setKeyPressed(true) handler?.() - } /* If they haven't passed in any modifiers, just run the function */ else { + } else { event.preventDefault() setKeyPressed(true) handler?.() @@ -43,24 +44,19 @@ export const useShortcut = ({ } }, []) - /* Function that runs whenever the user releases a key */ const upHandler = useCallback((event: KeyboardEvent) => { - /* Check if the key is the target key -- if it is, then just set the hook to false */ if (event.key === targetKey) setKeyPressed(false) }, []) useEffect(() => { - /* Run a handler on the keyboard event accordingly */ window.addEventListener('keydown', downHandler) window.addEventListener('keyup', upHandler) - /* Clean up the handlers */ return () => { window.removeEventListener('keydown', downHandler) window.removeEventListener('keyup', upHandler) } }, []) - /* Return a boolean value on whether or not the key was pressed */ return keyPressed } diff --git a/src/index.tsx b/src/index.tsx index 5390f11..f53dc55 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,4 +14,4 @@ export { useCommands } from './hooks/useCommands' /* The hook with utilities for using the menu */ export { useKmenu } from './hooks/useKmenu' /* The MenuProvider which the menu must be wrapped under */ -export { MenuProvider } from './menuProvider' +export { MenuProvider } from './MenuProvider' diff --git a/src/styles/menu.module.css b/src/styles/menu.module.css index 261cd0b..eb41e79 100644 --- a/src/styles/menu.module.css +++ b/src/styles/menu.module.css @@ -20,7 +20,6 @@ .dialog { padding: 0.5rem; width: 640px; - border-radius: 10px; transition: 0.1s ease 0s; will-change: height; position: relative; diff --git a/src/types.ts b/src/types.ts index 218c5f0..a6817c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,39 +1,42 @@ -import { - Dispatch, - ReactElement, - ReactNode, - RefObject, - SetStateAction -} from 'react' - -/* The props passed from the MenuProvider onto the index */ +import { Dispatch, ReactElement, RefObject, SetStateAction } from 'react' + export type MenuContext = { - /* Hooks that handle the open/close state of the component */ + /** + * The index of which element is currently open + */ open: number + /** + * The setter to update the open state + */ setOpen: Dispatch> - /* Hooks that handle the search bar */ + /** + * The text that is currently in the serch bar + */ query: string + /** + * The setter to update the text in the search bar + */ setQuery: Dispatch> - /* Menu configuration options to be passed onto all menus */ + /** + * The menu configuration options to be passed down onto all menus + */ config?: Partial - /* Dimensions of the individual elements */ + /** + * Dimensions of individual elements in the menu + */ dimensions?: Partial } -/* Props passed onto the MenuProvider */ -export type MenuProviderProps = { - children: ReactNode - /* Provide the config here to pass down onto all menus */ - config?: Partial - /* Dimensions of the individual elements */ - dimensions?: Partial -} +export type MenuProviderProps = Pick -/* Props that contain the dimensions of the commands */ type Dimensions = { - /* Height of each section/category in px */ + /** + * Height of each section/category (px) + */ sectionHeight: number - /* Height of each command in px */ + /** + * Height of each command (px) + */ commandHeight: number } @@ -50,161 +53,207 @@ export type MenuProps = { export type GlobalCommand = CategoryCommand & { globalIndex: number } export type Command = { - /* The category this command will display under */ + /** + * The category this command will display under + */ category: string - /* The commands this category will have */ + /** + * The commands this category will have + */ commands: CategoryCommand[] } -/* This type is used for commands */ export type CommandWithIndex = { - /* The index of all commands inside of the menu */ index: number - /* Initial height of the command menu */ initialHeight: number - /* The sorted commands in the menu */ commands: SortedCommands[] } -/* Type used for commands AFTER they've been sorted with a global index */ export type SortedCommands = { - /* The category the commands belong to */ category: string - /* An array of sorted commands, all with an index */ commands: GlobalCommand[] } export type CategoryCommand = { - /* Icon to be displayed next to the command text */ + /** + * Icon to be displayed next to the command text + */ icon?: ReactElement - /* The text displayed on the command */ + /** + * The text displayed on the command + */ text: string - /* The action to be performed */ + /** + * The action to be performed + */ perform?: () => void - /* The URL to be opened */ + /** + * The URL to be opened + */ href?: string - /* Whether or not that URL should open in a new tab */ + /** + * Whether or not that URL should open in a new tab + */ newTab?: boolean - /* Keywords for the command that will make it show up when the user searches something */ + /** + * Keywords for the command that will make it show up when the user searches something + */ keywords?: string - /* Keyboard shortcuts which can trigger the command OUTSIDE the command menu */ + /** + * Keyboard shortcuts which can trigger the command OUTSIDE the command menu + * + */ shortcuts?: Shortcut - /* Whether or not to close this menu when the functino is called */ + /** + * Whether or not to close this menu when the functino is called + */ closeOnComplete?: boolean } export type Shortcut = { - /* Key that will be used in conjunction with the shortcut */ - /* NOTE: Some operating systems don't recognise the meta key. On macOS it's the 'cmd' key */ + /** + * Key that will be used in conjunction with the shortcut + */ modifier?: 'shift' | 'ctrl' | 'alt' | 'meta' - /* The key(s) which triggers the function */ + /** + * The key(s) that will trigger the function + */ keys: [string, string?] } -/* Configuration options for the menu */ export type Config = { - /* The colour of the backdrop */ - /* Default: #FFFFFF20 */ + /** + * The colour of the backdrop (include opacity) + * + * @default #FFFFFF20 + */ backdropColor: string - /* The blurring behind the backdrop element */ - /* Default: 2px */ + /** + * The backround blur of the backdrop (px) + * + * @default 2px + */ backdropBlur: number - /* - * The background colour of the modal + /** + * The background colour of the menu * * @default #FFFFFF */ backgroundColor: string - /* Border width on the dialog */ - /* Default: 0px */ + /** + * Width of the border surrounding the menu + * + * @default 1px + */ borderWidth: number - /* The colour of the border on the dialog */ - /* Default: transparent */ + /** + * The colour of the border surrounding the menu + * + * @default #3F3F3F + */ borderColor: string - /* The border radius of the dialog */ - /* Default: 12px */ + /** + * The colour of the border surrounding the menu + * + * @default 10px + */ borderRadius: number - /* The shadow on the dialog */ - /* Default: 0px 0px 60px 10px #00000020 */ + /** + * The shadow of the menu + * + * @default 0px 0px 60px 10px #00000020 + */ boxShadow: string - /* The colour of the text in the search bar */ - /* Default: #000000 */ + /** + * The colour of the text in the search bar + * + * @default #000000 + */ inputColor: string - /* The colour of the border below the input text */ - /* Default: #E9CEF */ + /** + * The colour of the border below the search bar + * + * @default #E9ECEF + */ inputBorder: string - /* The placeholder text on the search bar */ - /* Default: 'What do you need?' */ + /** + * The placeholder input text in the search bar + * + * @default 'What do you need?' + */ placeholderText: string - /* The colour of the category headings on the menu */ - /* Default: #828282 */ + /** + * The colour of the command category headings + * + * @default #777777 + */ headingColor: string - /* The colour of the command icon and text when the command is inactive */ - /* Default: #828282 */ + /** + * The colour of the icon and text when the command is inactive + * + * @default #828282 + */ commandInactive: string - /* The colour of the command icon and text when the command is active */ - /* Default: #343434 */ + /** + * The colour of the icon and text when the command is active + * + * @default #343434 + */ commandActive: string - /* The colour of the bar which hovers the selected item */ - /* Default: #82828220 */ + /** + * The background colour of the active bar (include opacity) + * + * @default #FFFFFF20 + */ barBackground: string - /* The colour of background of the keyboard shortcut next to the element */ - /* Default: #82828220 */ + /** + * The background colour of the keyboard shortcut + * + * @default #82828220 + */ shortcutBackground: string } -/* Props for the parse function to parse command shortcuts */ export type ParseProps = { - /* The command which defined this shortcut */ command: CategoryCommand - /* Keyboard event object to get the user interaction with the keyboard */ event: KeyboardEvent - /* A character map containing the characters the user has pressed for double key commands */ map: string[] } -/* Types for the useClickOutside hook */ export type UseClickOutsideProps = { - /* The click boundary */ ref: RefObject - /* What happens when the user clicks outside */ handler: () => void } -/* Types for the useCommands hook */ export type UseCommandsProps = Command[] -/* Types for the useInView hook */ export type UseInViewProps = { - /* The ref that we'll be checking */ ref: RefObject } -/* Return type of the useKmenu hook */ export type UseKmenuReturnType = [ - /* The input text */ string, - /* The setter function for the input text */ Dispatch>, - /* The index of the menu that's currently open */ number, - /* The setter function for opening different menus */ Dispatch>, - /* The toggle function */ () => void ] -/* Types for the useShortcut hook */ export type UseShortcutProps = { - /* The target key this hook is listening for */ + /** + * The key this shortcut is listening for + */ targetKey: string - /* Modifier that will be used in conjuntion with the target key */ + /** + * Modifier that will be used in conjuntion with the target key + */ modifier?: 'shift' | 'ctrl' | 'alt' | 'meta' - /* Function to call when this is actually pressed */ + /** + * Function to call when this is actually pressed + */ handler?: () => void } -/* Type of actions one can perform on the menu */ export enum ActionType { INCREASE = 'INCREASE', DECREASE = 'DECREASE', @@ -212,13 +261,11 @@ export enum ActionType { CUSTOM = 'CUSTOM' } -/* Action prop passed onto the useReducer for handling keyboard navigation */ export type Action = { type: ActionType custom: number } -/* State prop passed onto the useReducer for handling keyboard navigation */ export type State = { selected: number } diff --git a/src/typings.d.ts b/src/typings.d.ts index cd16102..9f89ebb 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -3,15 +3,16 @@ * will be overridden with file-specific definitions by rollup */ declare module '*.css' { - const content: { [className: string]: string }; - export default content; + const content: { [className: string]: string } + export default content } -interface SvgrComponent extends React.StatelessComponent> {} +interface SvgrComponent + extends React.StatelessComponent> {} declare module '*.svg' { - const svgUrl: string; - const svgComponent: SvgrComponent; - export default svgUrl; + const svgUrl: string + const svgComponent: SvgrComponent + export default svgUrl export { svgComponent as ReactComponent } } diff --git a/src/utils/parse.ts b/src/utils/parse.ts index bab82cf..25d2cd6 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -1,24 +1,17 @@ import { ParseProps } from '../types' import run from './run' -/* Function used for parsing a shortcut */ const parse = ({ command, event, map }: ParseProps) => { - /* Push the key pressed onto the map array */ map.push(event.key) - /* Clear the map array every one second or thousand miliseconds */ setTimeout(() => map.splice(0, map.length), 1000) - /* Check if the command shortcuts have a modifier */ if (typeof command.shortcuts?.modifier === 'string') { - /* Add checks for all the different modifiers which check if the user had specified them and if they're actually being pressed along with the target key */ if ( command.shortcuts.modifier === 'ctrl' && event.ctrlKey && event.key === command.shortcuts.keys[0] ) { - /* Prevent the default action */ event.preventDefault() - /* Throw the command in the run function and run it */ run(command) } else if ( command.shortcuts.modifier === 'alt' && @@ -41,19 +34,14 @@ const parse = ({ command, event, map }: ParseProps) => { event.preventDefault() return run(command) } - } /* Check if the shortcut is a two-key shortcut */ else if ( - command.shortcuts?.keys.length === 2 - ) { - /* Get the last two keys from the keymap array */ + } else if (command.shortcuts?.keys.length === 2) { const last = map.slice(-2) - /* Check if the two keys are the same as the ones from the shortcut */ if ( last[0] === command.shortcuts.keys[0] && last[1] === command.shortcuts.keys[1] ) - /* If they are, run the command */ return run(command) - } /* Check if the shortcut is just a single key. If it is, just run the command directly without any other checks */ else if ( + } else if ( command.shortcuts?.keys.length === 1 && event.key === command.shortcuts.keys[0] ) diff --git a/src/utils/run.ts b/src/utils/run.ts index d101db2..8cec494 100644 --- a/src/utils/run.ts +++ b/src/utils/run.ts @@ -1,13 +1,8 @@ import { CategoryCommand } from '../types' -/* Function used for running the command */ const run = (command: CategoryCommand) => { - /* If the command's perform is not undefined, run the command */ if (typeof command.perform !== 'undefined') return command.perform?.() - /* If the command's href is not undefined, then open a new window */ else if ( - typeof command.href !== 'undefined' - ) - /* Ternary, just checking whether or not the command should open in a new tab or not */ + else if (typeof command.href !== 'undefined') window.open(command.href, command.newTab ? '_blank' : '_self') } diff --git a/yarn.lock b/yarn.lock index d3e6f97..698d39c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1965,6 +1965,11 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/resize-observer-browser@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" + integrity sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg== + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"