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'