From 649257b4eff12eabdeaf5ecb365b7977919f554d Mon Sep 17 00:00:00 2001 From: Charis Lam <26616127+charislam@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:01:19 -0400 Subject: [PATCH] feat: finish command menu base --- package-lock.json | 2 + .../ui-patterns/CommandMenu/api/Badges.tsx | 17 +++ .../CommandMenu/api/CommandInput.tsx | 36 ++++++ .../CommandMenu/api/CommandList.tsx | 60 +++++++++ .../CommandMenu/api/CommandMenu.tsx | 115 ++++++++++++++++++ .../CommandMenu/api/CommandProvider.tsx | 26 +++- .../CommandMenu/api/TextHighlighter.tsx | 42 +++++++ .../CommandMenu/api/hooks/commandsHooks.ts | 2 + .../CommandMenu/api/hooks/pagesHooks.ts | 2 + .../api/hooks/useCrossCompatRouter.ts | 67 ++++++++++ .../CommandMenu/api/hooks/useHistoryKeys.ts | 46 +++++++ .../CommandMenu/api/hooks/viewHooks.ts | 20 +-- packages/ui-patterns/CommandMenu/api/utils.ts | 5 + packages/ui-patterns/CommandMenu/index.tsx | 14 +++ .../CommandMenu/internal/Command.tsx | 93 +++++++++++++- .../CommandMenu/internal/CommandEmpty.tsx | 46 +++++++ .../CommandMenu/internal/CommandGroup.tsx | 24 ++++ .../CommandMenu/internal/Context.tsx | 2 + .../CommandMenu/internal/state/viewState.ts | 4 - packages/ui-patterns/jest.config.js | 4 + packages/ui-patterns/jest.setup.js | 1 + packages/ui-patterns/package.json | 2 + packages/ui-patterns/stubs/cssStub.js | 5 + 23 files changed, 612 insertions(+), 23 deletions(-) create mode 100644 packages/ui-patterns/CommandMenu/api/Badges.tsx create mode 100644 packages/ui-patterns/CommandMenu/api/CommandInput.tsx create mode 100644 packages/ui-patterns/CommandMenu/api/CommandList.tsx create mode 100644 packages/ui-patterns/CommandMenu/api/CommandMenu.tsx create mode 100644 packages/ui-patterns/CommandMenu/api/TextHighlighter.tsx create mode 100644 packages/ui-patterns/CommandMenu/api/hooks/useCrossCompatRouter.ts create mode 100644 packages/ui-patterns/CommandMenu/api/hooks/useHistoryKeys.ts create mode 100644 packages/ui-patterns/CommandMenu/index.tsx create mode 100644 packages/ui-patterns/CommandMenu/internal/CommandEmpty.tsx create mode 100644 packages/ui-patterns/CommandMenu/internal/CommandGroup.tsx create mode 100644 packages/ui-patterns/jest.setup.js create mode 100644 packages/ui-patterns/stubs/cssStub.js diff --git a/package-lock.json b/package-lock.json index c151fa40fbf..c92751dce61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42943,6 +42943,7 @@ "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "react-tooltip": "*", + "react-use": "^17.5.0", "reactflow": "*", "remark-gfm": "^4.0.0", "sql-formatter": "^15.3.1", @@ -42962,6 +42963,7 @@ "api-types": "*", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "next-router-mock": "^0.9.13", "ts-jest": "^29.1.2" }, "peerDependencies": { diff --git a/packages/ui-patterns/CommandMenu/api/Badges.tsx b/packages/ui-patterns/CommandMenu/api/Badges.tsx new file mode 100644 index 00000000000..1534561d3a2 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/Badges.tsx @@ -0,0 +1,17 @@ +import { Microscope } from 'lucide-react' + +import { Badge } from 'ui' + +interface BadgeExperimentalProps { + className?: string +} + +function BadgeExperimental({ className }: BadgeExperimentalProps) { + return ( + + Experimental + + ) +} + +export { BadgeExperimental } diff --git a/packages/ui-patterns/CommandMenu/api/CommandInput.tsx b/packages/ui-patterns/CommandMenu/api/CommandInput.tsx new file mode 100644 index 00000000000..c19942ecae6 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/CommandInput.tsx @@ -0,0 +1,36 @@ +'use client' + +import { forwardRef } from 'react' + +import { CommandInput_Shadcn_, cn } from 'ui' + +import { useQuery, useSetQuery } from './hooks/queryHooks' + +const CommandInput = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const query = useQuery() + const setQuery = useSetQuery() + + return ( + + ) +}) + +CommandInput.displayName = CommandInput_Shadcn_.displayName + +export { CommandInput } diff --git a/packages/ui-patterns/CommandMenu/api/CommandList.tsx b/packages/ui-patterns/CommandMenu/api/CommandList.tsx new file mode 100644 index 00000000000..12c11f0bac8 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/CommandList.tsx @@ -0,0 +1,60 @@ +'use client' + +import { forwardRef, useRef } from 'react' + +import { CommandList_Shadcn_, cn } from 'ui' + +import { CommandItem, type ICommand } from '../internal/Command' +import { CommandEmpty } from '../internal/CommandEmpty' +import { CommandGroup } from '../internal/CommandGroup' +import { useCommands } from './hooks/commandsHooks' +import { useQuery } from './hooks/queryHooks' +import { TextHighlighter } from './TextHighlighter' + +const CommandList = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const commandSections = useCommands() + const query = useQuery() + + const innerRef = useRef(undefined) + const setInnerRef = (elem: HTMLDivElement) => (innerRef.current = elem) + + const setRef = (elem: HTMLDivElement) => { + if (ref) typeof ref === 'function' ? ref(elem) : (ref.current = elem) + setInnerRef(elem) + } + + return ( + + No results found. + {commandSections.map((section) => { + if (section.commands.every((command) => command.defaultHidden) && !query) return null + + return ( + + {section.commands + .filter((command) => !command.defaultHidden || query) + .map((_command) => { + const command = _command as ICommand // strip the readonly applied from the proxy + + return ( + + {command.name} + + ) + })} + + ) + })} + + ) +}) +CommandList.displayName = CommandList_Shadcn_.displayName + +export { CommandList } diff --git a/packages/ui-patterns/CommandMenu/api/CommandMenu.tsx b/packages/ui-patterns/CommandMenu/api/CommandMenu.tsx new file mode 100644 index 00000000000..d93041ced0c --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/CommandMenu.tsx @@ -0,0 +1,115 @@ +'use client' + +import { AlertTriangle, ArrowLeft } from 'lucide-react' +import { type PropsWithChildren, forwardRef, useEffect, useState } from 'react' +import { ErrorBoundary } from 'react-error-boundary' + +import { Button, Command_Shadcn_, Dialog, DialogContent, cn } from 'ui' + +import { useCurrentPage, usePageComponent, usePopPage } from './hooks/pagesHooks' +import { useQuery, useSetQuery } from './hooks/queryHooks' +import { useCommandMenuSize, useCommandMenuOpen, useSetCommandMenuOpen } from './hooks/viewHooks' +import { PageType } from './utils' + +function Breadcrumb() { + const currPage = useCurrentPage() + const popPage = usePopPage() + + if (!currPage) return + + return ( +
+ + {currPage.name} +
+ ) +} + +const CommandWrapper = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, className, ...props }, ref) => { + const currPage = useCurrentPage() + + return ( + + {currPage?.type === PageType.Commands && } + {children} + + ) +}) +CommandWrapper.displayName = Command_Shadcn_.displayName + +function CommandError({ resetErrorBoundary }: { resetErrorBoundary: () => void }) { + return ( +
+
+ +

+ Sorry, looks like we're having some issues with the command menu! +

+

Please try again in a bit.

+ +
+
+ ) +} + +function PageSwitch({ children }: PropsWithChildren) { + const PageComponent = usePageComponent() + + return PageComponent ? : {children} +} + +function CommandMenu({ children }: PropsWithChildren) { + const open = useCommandMenuOpen() + const setOpen = useSetCommandMenuOpen() + + const size = useCommandMenuSize() + + const page = useCurrentPage() + const popPage = usePopPage() + + const query = useQuery() + const setQuery = useSetQuery() + + return ( + + setOpen(false)} + onEscapeKeyDown={(e) => { + e.preventDefault() + return query ? setQuery('') : page ? popPage() : setOpen(false) + }} + size={size} + className={cn('place-self-start mx-auto top-24')} + > + + {children} + + + + ) +} + +export { CommandMenu, CommandWrapper } diff --git a/packages/ui-patterns/CommandMenu/api/CommandProvider.tsx b/packages/ui-patterns/CommandMenu/api/CommandProvider.tsx index 06bc2393a09..32123f692fe 100644 --- a/packages/ui-patterns/CommandMenu/api/CommandProvider.tsx +++ b/packages/ui-patterns/CommandMenu/api/CommandProvider.tsx @@ -1,12 +1,15 @@ +'use client' + import { useConstant } from 'common' -import { useEffect, type PropsWithChildren } from 'react' +import { type PropsWithChildren, useEffect, useMemo, useCallback } from 'react' -import { useToggleCommandMenu } from './hooks/viewHooks' import { CommandContext } from '../internal/Context' import { initCommandsState } from '../internal/state/commandsState' import { initPagesState } from '../internal/state/pagesState' import { initQueryState } from '../internal/state/queryState' import { initViewState } from '../internal/state/viewState' +import { useCrossCompatRouter } from './hooks/useCrossCompatRouter' +import { useSetCommandMenuOpen, useToggleCommandMenu } from './hooks/viewHooks' const CommandProviderInternal = ({ children }: PropsWithChildren) => { const combinedState = useConstant(() => ({ @@ -19,6 +22,7 @@ const CommandProviderInternal = ({ children }: PropsWithChildren) => { return {children} } +// This is a component not a hook so it can access the wrapping context. const CommandShortcut = () => { const toggleOpen = useToggleCommandMenu() @@ -38,9 +42,27 @@ const CommandShortcut = () => { return null } +// This is a component not a hook so it can access the wrapping context. +function CloseOnNavigation() { + const setIsOpen = useSetCommandMenuOpen() + const router = useCrossCompatRouter() + + const completeNavigation = useCallback(() => { + setIsOpen(false) + }, [setIsOpen]) + + useEffect(() => { + router.events.onRouteChangeComplete(completeNavigation) + return () => router.events.offRouteChangeComplete(completeNavigation) + }, [router]) + + return null +} + const CommandProvider = ({ children }: PropsWithChildren) => ( + {children} ) diff --git a/packages/ui-patterns/CommandMenu/api/TextHighlighter.tsx b/packages/ui-patterns/CommandMenu/api/TextHighlighter.tsx new file mode 100644 index 00000000000..aa8ecddd79d --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/TextHighlighter.tsx @@ -0,0 +1,42 @@ +'use client' + +import { + Children, + type DetailedHTMLProps, + type HTMLAttributes, + type PropsWithChildren, +} from 'react' + +import { useQuery } from './hooks/queryHooks' + +interface TextHighlighterBaseProps + extends PropsWithChildren, HTMLSpanElement>> { + query: string + text?: string +} + +function TextHighlighterBase({ children, text, query, ...props }: TextHighlighterBaseProps) { + const child = text ?? Children.toArray(children)[0] + if (typeof child !== 'string') return child + + const idx = child.toLowerCase().indexOf(query.toLowerCase()) + if (idx === -1) { + return {child} + } + + return ( + + {child.substring(0, idx)} + {child.substring(idx, idx + query.length)} + {child.substring(idx + query.length)} + + ) +} + +function TextHighlighter(props: Omit) { + const query = useQuery() + + return +} + +export { TextHighlighter, TextHighlighterBase } diff --git a/packages/ui-patterns/CommandMenu/api/hooks/commandsHooks.ts b/packages/ui-patterns/CommandMenu/api/hooks/commandsHooks.ts index d67b483e7c4..c58d7dcfd34 100644 --- a/packages/ui-patterns/CommandMenu/api/hooks/commandsHooks.ts +++ b/packages/ui-patterns/CommandMenu/api/hooks/commandsHooks.ts @@ -1,3 +1,5 @@ +'use client' + import { isEqual } from 'lodash' import { useEffect, useMemo, useRef } from 'react' import { useSnapshot } from 'valtio' diff --git a/packages/ui-patterns/CommandMenu/api/hooks/pagesHooks.ts b/packages/ui-patterns/CommandMenu/api/hooks/pagesHooks.ts index 9136ef3d474..b1b5728e995 100644 --- a/packages/ui-patterns/CommandMenu/api/hooks/pagesHooks.ts +++ b/packages/ui-patterns/CommandMenu/api/hooks/pagesHooks.ts @@ -1,3 +1,5 @@ +'use client' + import { isEqual } from 'lodash' import { useEffect, useMemo, useRef } from 'react' import { useSnapshot } from 'valtio' diff --git a/packages/ui-patterns/CommandMenu/api/hooks/useCrossCompatRouter.ts b/packages/ui-patterns/CommandMenu/api/hooks/useCrossCompatRouter.ts new file mode 100644 index 00000000000..dec64423056 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/hooks/useCrossCompatRouter.ts @@ -0,0 +1,67 @@ +'use client' + +import { useRouter as useLegacyRouter } from 'next/compat/router' +import { useRouter as useNextRouter } from 'next/navigation' +import { useEffect, useMemo, useRef, useTransition } from 'react' +import { usePrevious } from 'react-use' + +type Handler = (...evts: any[]) => void + +function useHasChanged(value: T, { onlyTo }: { onlyTo?: T } = {}) { + const prev = usePrevious(value) + + if (prev === value) return false + if (onlyTo && value !== onlyTo) return false + + return true +} + +function useCrossCompatRouter() { + const legacyRouter = useLegacyRouter() + const newRouter = useNextRouter() + const isUsingLegacyRouting = !!legacyRouter + + const [isPending, startTransition] = useTransition() + const hasPendingEnded = useHasChanged(isPending, { onlyTo: false }) + const onPendingEnd = useRef(new Set<() => void>()) + useEffect(() => { + if (!isUsingLegacyRouting && hasPendingEnded) { + onPendingEnd.current.forEach((fn) => fn()) + } + }, [hasPendingEnded, isUsingLegacyRouting]) + + const api = useMemo( + () => ({ + events: { + onRouteChangeComplete: (fn: Handler) => { + if (isUsingLegacyRouting) { + legacyRouter.events.on('routeChangeComplete', fn) + } else { + onPendingEnd.current.add(fn) + } + }, + offRouteChangeComplete: (fn: Handler) => { + if (isUsingLegacyRouting) { + legacyRouter.events.off('routeChangeComplete', fn) + } else { + onPendingEnd.current.delete(fn) + } + }, + }, + push: (path: string) => { + if (isUsingLegacyRouting) { + legacyRouter.push(path) + } else { + startTransition(() => { + newRouter.push(path) + }) + } + }, + }), + [] + ) + + return api +} + +export { useCrossCompatRouter } diff --git a/packages/ui-patterns/CommandMenu/api/hooks/useHistoryKeys.ts b/packages/ui-patterns/CommandMenu/api/hooks/useHistoryKeys.ts new file mode 100644 index 00000000000..0a4673a9853 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/api/hooks/useHistoryKeys.ts @@ -0,0 +1,46 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' + +import { useSetQuery } from './queryHooks' + +interface UseHistoryKeysOptions { + stack: string[] + enable?: boolean +} + +/** + * Enables shell-style message history when hitting up/down on the keyboard + */ +function useHistoryKeys({ stack, enable = true }: UseHistoryKeysOptions) { + const [idx, setIdx] = useState(0) + const setQuery = useSetQuery() + + const navigateIdx = useCallback((newIdx: number) => { + if (!enable) return + + setQuery(stack[newIdx] ?? '') + setIdx(newIdx) + }, []) + + useEffect(() => { + function onKeyDown(e: KeyboardEvent) { + switch (e.key) { + case 'ArrowUp': + return navigateIdx(Math.max(idx - 1, 0)) + case 'ArrowDown': + return navigateIdx(Math.min(idx + 1, stack.length - 1)) + default: + return + } + } + + window.addEventListener('keydown', onKeyDown) + + return () => { + window.removeEventListener('keydown', onKeyDown) + } + }, [navigateIdx, stack]) +} + +export { useHistoryKeys } diff --git a/packages/ui-patterns/CommandMenu/api/hooks/viewHooks.ts b/packages/ui-patterns/CommandMenu/api/hooks/viewHooks.ts index 85e0c9560d4..864a05eb8a9 100644 --- a/packages/ui-patterns/CommandMenu/api/hooks/viewHooks.ts +++ b/packages/ui-patterns/CommandMenu/api/hooks/viewHooks.ts @@ -1,3 +1,5 @@ +'use client' + import { useLayoutEffect, useRef } from 'react' import { useSnapshot } from 'valtio' @@ -10,7 +12,7 @@ const useCommandMenuInitiated = () => { return initiated } -const useCommandMenuVisible = () => { +const useCommandMenuOpen = () => { const { viewState } = useCommandContext() const { open } = useSnapshot(viewState) return open @@ -28,18 +30,6 @@ const useToggleCommandMenu = () => { return toggleOpen } -const useIsCommandNavigating = () => { - const { viewState } = useCommandContext() - const { isNavigating } = useSnapshot(viewState) - return isNavigating -} - -const useSetIsCommandNavigating = () => { - const { viewState } = useCommandContext() - const { setIsNavigating } = useSnapshot(viewState) - return setIsNavigating -} - const useCommandMenuSize = () => { const { viewState } = useCommandContext() const { size } = useSnapshot(viewState) @@ -60,11 +50,9 @@ const useSetCommandMenuSize = (newSize: DialogSize) => { export { useCommandMenuInitiated, - useCommandMenuVisible, + useCommandMenuOpen, useSetCommandMenuOpen, useToggleCommandMenu, - useIsCommandNavigating, - useSetIsCommandNavigating, useCommandMenuSize, useSetCommandMenuSize, } diff --git a/packages/ui-patterns/CommandMenu/api/utils.ts b/packages/ui-patterns/CommandMenu/api/utils.ts index 491708a8436..efbdbcb2d72 100644 --- a/packages/ui-patterns/CommandMenu/api/utils.ts +++ b/packages/ui-patterns/CommandMenu/api/utils.ts @@ -1 +1,6 @@ +const escapeAttributeSelector = (str: string) => encodeURIComponent(str) + +export { escapeAttributeSelector } +export { generateCommandClassNames } from '../internal/Command' +export { orderSectionFirst } from '../internal/state/commandsState' export { PageType } from '../internal/state/pagesState' diff --git a/packages/ui-patterns/CommandMenu/index.tsx b/packages/ui-patterns/CommandMenu/index.tsx new file mode 100644 index 00000000000..f746101f700 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/index.tsx @@ -0,0 +1,14 @@ +export * from './api/Badges' +export { CommandInput } from './api/CommandInput' +export { CommandList } from './api/CommandList' +export { CommandMenu, CommandWrapper } from './api/CommandMenu' +export { CommandProvider } from './api/CommandProvider' +export { TextHighlighter, TextHighlighterBase } from './api/TextHighlighter' +export * from './api/hooks/commandsHooks' +export * from './api/hooks/pagesHooks' +export * from './api/hooks/queryHooks' +export { useHistoryKeys } from './api/hooks/useHistoryKeys' +export * from './api/hooks/viewHooks' +export * from './api/utils' + +export * from './api/types' diff --git a/packages/ui-patterns/CommandMenu/internal/Command.tsx b/packages/ui-patterns/CommandMenu/internal/Command.tsx index 7d09e407635..f5ffc7d408a 100644 --- a/packages/ui-patterns/CommandMenu/internal/Command.tsx +++ b/packages/ui-patterns/CommandMenu/internal/Command.tsx @@ -1,4 +1,10 @@ -import { type ReactNode } from 'react' +'use client' + +import { type PropsWithChildren, type ReactNode, forwardRef } from 'react' + +import { CommandItem_Shadcn_, cn } from 'ui' +import { useCrossCompatRouter } from '../api/hooks/useCrossCompatRouter' +import { useSetCommandMenuOpen } from '../api/hooks/viewHooks' type ICommand = IActionCommand | IRouteCommand @@ -32,4 +38,89 @@ interface IRouteCommand extends IBaseCommand { route: `/${string}` | `http${string}` } +const isActionCommand = (command: ICommand): command is IActionCommand => 'action' in command +const isRouteCommand = (command: ICommand): command is IRouteCommand => 'route' in command + +const generateCommandClassNames = (isLink: boolean) => + cn( + 'cursor-default', + 'select-none', + 'items-center gap-2', + 'rounded-md', + 'text-sm', + 'group', + 'py-3', + 'text-foreground-light', + 'relative', + 'flex', + isLink + ? ` +bg-transparent +px-2 +transition-all +outline-none +aria-selected:border-overlay +aria-selected:bg-selection/90 +aria-selected:shadow-sm +aria-selected:scale-[100.3%] +data-[disabled]:pointer-events-none data-[disabled]:opacity-50` + : ` +px-2 +aria-selected:bg-selection/80 +aria-selected:backdrop-filter +aria-selected:backdrop-blur-md +data-[disabled]:pointer-events-none +data-[disabled]:opacity-50 +` + ) + +interface CommandItemProps extends React.ComponentPropsWithoutRef { + command: ICommand +} + +const CommandItem = forwardRef< + React.ElementRef, + PropsWithChildren +>(({ children, className, command: _command, ...props }, ref) => { + const router = useCrossCompatRouter() + const setIsOpen = useSetCommandMenuOpen() + + const command = _command as ICommand // strip the readonly applied from the proxy + + return ( + { + command.route.startsWith('http') + ? (setIsOpen(false), window.open(command.route, '_blank', 'noreferrer,noopener')) + : router.push(command.route) + } + : () => {} + } + value={command.value} + className={cn( + generateCommandClassNames(isRouteCommand(command)), + className, + command.className + )} + {...props} + > +
+
+ {command.icon?.()} + {children} +
+ {command.badge?.()} +
+
+ ) +}) +CommandItem.displayName = CommandItem_Shadcn_.displayName + +export { CommandItem, generateCommandClassNames, isActionCommand, isRouteCommand } export type { ICommand } diff --git a/packages/ui-patterns/CommandMenu/internal/CommandEmpty.tsx b/packages/ui-patterns/CommandMenu/internal/CommandEmpty.tsx new file mode 100644 index 00000000000..2128bbba340 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/internal/CommandEmpty.tsx @@ -0,0 +1,46 @@ +'use client' + +import { type PropsWithChildren, type RefObject, useMemo, useState } from 'react' + +import { cn } from 'ui' + +import { useQuery } from '../api/hooks/queryHooks' + +const CommandEmpty = ({ + children, + className, + listRef, + ...props +}: PropsWithChildren<{ + className?: string + /** + * Reference to the div that contains the command item list in the DOM. + * + * Hacking around a bug in cmdk where the empty state will show even when + * there are force-mounted items. + */ + listRef: RefObject +}>) => { + const query = useQuery() + + const [render, setRender] = useState(false) + useMemo(() => { + if (!query) setRender(false) + /** + * In a timeout so the DOM update happens before we query. + */ + setTimeout(() => { + setRender(!listRef?.current?.querySelector('[cmdk-item]')) + }) + }, [query]) + + return ( + render && ( +
+ {children} +
+ ) + ) +} + +export { CommandEmpty } diff --git a/packages/ui-patterns/CommandMenu/internal/CommandGroup.tsx b/packages/ui-patterns/CommandMenu/internal/CommandGroup.tsx new file mode 100644 index 00000000000..fc2cc297995 --- /dev/null +++ b/packages/ui-patterns/CommandMenu/internal/CommandGroup.tsx @@ -0,0 +1,24 @@ +'use client' + +import { forwardRef } from 'react' + +import { CommandGroup_Shadcn_, cn } from 'ui' + +const CommandGroup = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +CommandGroup.displayName = CommandGroup_Shadcn_.displayName + +export { CommandGroup } diff --git a/packages/ui-patterns/CommandMenu/internal/Context.tsx b/packages/ui-patterns/CommandMenu/internal/Context.tsx index 4c28b7dc527..e8f67e1b834 100644 --- a/packages/ui-patterns/CommandMenu/internal/Context.tsx +++ b/packages/ui-patterns/CommandMenu/internal/Context.tsx @@ -1,3 +1,5 @@ +'use client' + import { createContext, useContext } from 'react' import { type ICommandsState } from '../internal/state/commandsState' diff --git a/packages/ui-patterns/CommandMenu/internal/state/viewState.ts b/packages/ui-patterns/CommandMenu/internal/state/viewState.ts index 93ac8bdc30b..aee98a4e5c1 100644 --- a/packages/ui-patterns/CommandMenu/internal/state/viewState.ts +++ b/packages/ui-patterns/CommandMenu/internal/state/viewState.ts @@ -8,8 +8,6 @@ type IViewState = { open: boolean setOpen: (open: boolean) => void toggleOpen: () => void - isNavigating: boolean - setIsNavigating: (isNavigating: boolean) => void size: DialogSize setSize: (size: DialogSize) => void } @@ -27,8 +25,6 @@ const initViewState = () => { state.init() state.open = !state.open }, - isNavigating: false, - setIsNavigating: (isNavigating) => (state.isNavigating = isNavigating), size: 'large', setSize: (size) => (state.size = size), }) diff --git a/packages/ui-patterns/jest.config.js b/packages/ui-patterns/jest.config.js index 8bb37208d7a..304afee0989 100644 --- a/packages/ui-patterns/jest.config.js +++ b/packages/ui-patterns/jest.config.js @@ -2,4 +2,8 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^.+\\.s?css$': '/stubs/cssStub.js', + }, } diff --git a/packages/ui-patterns/jest.setup.js b/packages/ui-patterns/jest.setup.js new file mode 100644 index 00000000000..fde9e7c9b63 --- /dev/null +++ b/packages/ui-patterns/jest.setup.js @@ -0,0 +1 @@ +jest.mock('next/navigation', () => require('next-router-mock')) diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index ee62b3c0aa1..5770dcbc41f 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -24,6 +24,7 @@ "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "react-tooltip": "*", + "react-use": "^17.5.0", "reactflow": "*", "remark-gfm": "^4.0.0", "sql-formatter": "^15.3.1", @@ -43,6 +44,7 @@ "api-types": "*", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "next-router-mock": "^0.9.13", "ts-jest": "^29.1.2" }, "peerDependencies": { diff --git a/packages/ui-patterns/stubs/cssStub.js b/packages/ui-patterns/stubs/cssStub.js new file mode 100644 index 00000000000..3258ed64ca3 --- /dev/null +++ b/packages/ui-patterns/stubs/cssStub.js @@ -0,0 +1,5 @@ +/** + * A testing stub for CSS imports. + */ + +module.exports = {}