import React, { ButtonHTMLAttributes, MouseEvent, ReactNode, forwardRef, useCallback, useEffect, useMemo, useRef, useState, } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' import { useForkedRef } from '../../hooks' import { formatShortcutTokens, getPreferredShortcut, getPressedKeys, matchesShortcut, parseShortcut, shouldIgnoreShortcut, } from './utils' export interface CSearchButtonProps extends ButtonHTMLAttributes { /** * Content to customize the full button body. */ children?: ReactNode /** * A string of all className you want applied to the base component. */ className?: string /** * Custom icon displayed before the placeholder text. */ icon?: ReactNode /** * Placeholder content rendered inside `.search-button-placeholder`. */ placeholder?: ReactNode /** * Callback fired when the component is activated by click or keyboard shortcut. */ onTrigger?: () => void /** * Prevent the browser's default behavior when the configured shortcut matches. */ preventDefault?: boolean /** * Comma-separated shortcut list. The component matches all configured shortcuts and renders the platform-preferred one. */ shortcut?: string } export const CSearchButton = forwardRef( ( { children, className, disabled, icon, onClick, onTrigger, placeholder = 'Search', preventDefault = true, shortcut = 'meta+/,ctrl+/', type = 'button', ...rest }, ref ) => { const buttonRef = useRef(null) const forkedRef = useForkedRef(ref, buttonRef) const [activeKeys, setActiveKeys] = useState([]) const shortcuts = useMemo(() => parseShortcut(shortcut), [shortcut]) const preferredShortcut = useMemo(() => getPreferredShortcut(shortcuts), [shortcuts]) const shortcutTokens = useMemo( () => formatShortcutTokens(preferredShortcut?.shortcut || ''), [preferredShortcut] ) const handleShortcut = useCallback( (event: KeyboardEvent) => { if (disabled || event.defaultPrevented || event.repeat || shouldIgnoreShortcut(event)) { return } const matchedShortcut = shortcuts.find((shortcut) => matchesShortcut(shortcut, event)) if (!matchedShortcut) { return } if (preventDefault) { event.preventDefault() } onTrigger?.() }, [disabled, onTrigger, preventDefault, shortcuts] ) const handleDocumentKeydown = useCallback( (event: KeyboardEvent) => { setActiveKeys(Array.from(getPressedKeys(event))) handleShortcut(event) }, [handleShortcut] ) const handleDocumentKeyup = useCallback((event: KeyboardEvent) => { setActiveKeys(Array.from(getPressedKeys(event))) }, []) const handleWindowBlur = useCallback(() => { setActiveKeys([]) }, []) useEffect(() => { if (typeof document === 'undefined' || typeof window === 'undefined') { return } document.addEventListener('keydown', handleDocumentKeydown) document.addEventListener('keyup', handleDocumentKeyup) window.addEventListener('blur', handleWindowBlur) return () => { document.removeEventListener('keydown', handleDocumentKeydown) document.removeEventListener('keyup', handleDocumentKeyup) window.removeEventListener('blur', handleWindowBlur) } }, [handleDocumentKeydown, handleDocumentKeyup, handleWindowBlur]) const handleClick = (event: MouseEvent) => { onClick?.(event) onTrigger?.() } return ( ) } ) CSearchButton.propTypes = { children: PropTypes.node, className: PropTypes.string, disabled: PropTypes.bool, icon: PropTypes.node, onTrigger: PropTypes.func, placeholder: PropTypes.node, preventDefault: PropTypes.bool, shortcut: PropTypes.string, } CSearchButton.displayName = 'CSearchButton'