(null);
+ const mergedRef = useMergedRefs(inputRef, ref);
+ // A controlled Search with `onClear` resets `value` itself. Otherwise the
+ // clear goes through `onChange` and `onValueChange`.
+ const clear = () => {
+ if (value !== undefined && onClear) return;
+ if (inputRef.current) clearInputValue(inputRef.current);
+ };
+
+ const handleKeyDown: InputProps['onKeyDown'] = event => {
+ onKeyDown?.(event);
+ if (event.defaultPrevented) return;
+
+ const input = event.currentTarget;
+ if (
+ event.key !== 'Escape' ||
+ event.nativeEvent.isComposing ||
+ input.disabled ||
+ input.readOnly ||
+ input.value === ''
+ ) {
+ return;
+ }
+
+ // Stop only when there is a value to clear, so an empty field lets
+ // Escape reach an enclosing Dialog, Popover or Menu.
+ event.preventDefault();
+ event.stopPropagation();
+ clear();
+ onClear?.(event);
+ };
+
const trailingIconWithClear = showClearButton ? (
{
e.stopPropagation();
- if (!disabled && onClear) {
- onClear();
- }
+ if (disabled || inputRef.current?.readOnly) return;
+ clear();
+ onClear?.(e);
+ // The button hides once the input is empty, so focus would
+ // otherwise fall back to .
+ inputRef.current?.focus();
}}
disabled={disabled}
aria-label='Clear search'
@@ -61,6 +114,9 @@ export function Search({
size={size}
aria-label={placeholder}
variant={variant}
+ type={type}
+ ref={mergedRef}
+ onKeyDown={handleKeyDown}
{...props}
/>