Files
supabase/packages/ui-patterns/src/CommandMenu/api/CommandMenuInput.tsx
T
Anthony Lio e7f0d3c04f fix(ui): popover component (#50657)
## What kind of change does this PR introduce?

fixture on the popover + command components

## What is the current behavior?

currently there is a misalignment in the command component <> searchbar
icon and items + the popover menu animation in is slightly scattered

## What is the new behavior?

- fixes icon alignment in command component
- updates popover animation in 

`command`
| state | preview |
| -------|------|
| before | <img width="881" height="542" alt="image"
src="https://github.com/user-attachments/assets/e38aa22f-5eea-4b08-8a24-254e26bf90fe"
/> |
| after | <img width="881" height="542" alt="image"
src="https://github.com/user-attachments/assets/85608e11-8415-4d47-945f-cc13772e4ad1"
/> |

`popover`
| state | preview |
| -------|------|
| before | <video
src="https://github.com/user-attachments/assets/cbb2da1a-6f73-4a8d-87d6-21e8b636c110"
/> |
| after | <video
src="https://github.com/user-attachments/assets/1b0832f5-e6ac-4e93-ae78-9e0aee31205c"
/> |

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Style**
  - Adjusted command input spacing for improved visual alignment.
- Updated popover transitions with state-based fade and zoom animations,
and refined their visual origin.
  - Set a consistent size for the AI docs command icon.
- **New Features**
- Added an option to customize the command menu input wrapper’s styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 18:17:45 +03:00

142 lines
4.2 KiB
TypeScript

'use client'
import { useBreakpoint, useDebounce } from 'common'
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
import type React from 'react'
import { cn, CommandInput } from 'ui'
import { useQuery, useSetQuery } from './hooks/queryHooks'
import { useCommandMenuTelemetryContext } from './hooks/useCommandMenuTelemetryContext'
const INPUT_TYPED_EVENT_DEBOUNCE_TIME = 2000 // 2s
function useFocusInputOnWiderScreens(ref: React.ForwardedRef<HTMLInputElement>) {
const isBelowSm = useBreakpoint('sm')
const isBelowSmSynchronous = useRef(isBelowSm)
isBelowSmSynchronous.current = isBelowSm
const internalRef = useRef<HTMLInputElement | null>(null)
const combinedRef = (element: HTMLInputElement) => {
if (ref instanceof Function) {
ref(element)
} else if (!!ref) {
ref.current = element
}
internalRef.current = element
}
useEffect(() => {
// This will always be false in the first iteration (since isBelowSm
// switches from false -> true on narrow screens). To avoid a preemptive
// focus, we need to delay this. But then we need to access the current
// value of isBelowSm, not the stale value, which explains the business
// with syncing state into a ref above.
setTimeout(() => {
if (!isBelowSmSynchronous.current) {
internalRef.current?.focus()
}
})
}, [])
return combinedRef
}
const CommandMenuInput = forwardRef<
React.ElementRef<typeof CommandInput>,
React.ComponentPropsWithoutRef<typeof CommandInput>
>(({ className, wrapperClassName, ...props }, ref) => {
const inputRef = useFocusInputOnWiderScreens(ref)
const query = useQuery()
const setQuery = useSetQuery()
const [inputValue, setInputValue] = useState(query)
useEffect(() => {
setInputValue(query)
previousValueRef.current = query
}, [query])
// Get telemetry context
const telemetryContext = useCommandMenuTelemetryContext()
const previousValueRef = useRef<string>(inputValue)
const inputTelemetryEvent = useCallback(
(value: string) => {
if (telemetryContext?.onTelemetry) {
const event = {
action: 'command_menu_search_submitted' as const,
properties: {
value: value,
app: telemetryContext.app,
},
groups: {},
}
telemetryContext.onTelemetry(event)
}
},
[telemetryContext]
)
const debouncedTelemetry = useDebounce(
useCallback(() => {
inputTelemetryEvent(inputValue)
previousValueRef.current = inputValue
}, [inputTelemetryEvent, inputValue]),
INPUT_TYPED_EVENT_DEBOUNCE_TIME
)
const handleValueChange = useCallback(
(value: string) => {
setInputValue(value)
// Only trigger telemetry if the user is adding characters (not removing with backspace)
const isAddingCharacters = value.length > previousValueRef.current.length
if (!isAddingCharacters) {
previousValueRef.current = value
return
}
// Trigger debounced telemetry
debouncedTelemetry()
},
[debouncedTelemetry]
)
// To handle CJK input
const [imeComposing, setImeComposing] = useState(false)
useEffect(() => {
if (!imeComposing) {
setQuery(inputValue)
}
}, [inputValue, imeComposing])
return (
<div className="relative w-full" cmdk-input-wrapper="">
<CommandInput
// Focus needs to be manually handled to check breakpoint first, due to
// delays from useEffect
autoFocus={false}
ref={inputRef}
value={inputValue}
onValueChange={handleValueChange}
placeholder="Run a command or search..."
onCompositionStart={() => setImeComposing(true)}
onCompositionEnd={() => setImeComposing(false)}
wrapperClassName={cn('px-4', wrapperClassName)}
className={cn(
'flex h-11 w-full rounded-md bg-transparent px-2 y-4 md:py-7 outline-hidden',
'focus:shadow-none focus:ring-transparent',
'text-base text-foreground-light placeholder:text-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 border-0',
className
)}
{...props}
/>
</div>
)
})
CommandMenuInput.displayName = 'CommandMenuInput'
export { CommandMenuInput }