@carbon/react
Version:
React components for the Carbon Design System
278 lines (274 loc) • 8.65 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import { Close, Search as Search$1 } from '@carbon/icons-react';
import cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useRef, useState } from 'react';
import { Enter, Space, Escape } from '../../internal/keyboard/keys.js';
import { match } from '../../internal/keyboard/match.js';
import { useId } from '../../internal/useId.js';
import { usePrefix } from '../../internal/usePrefix.js';
import { composeEventHandlers } from '../../tools/events.js';
import { useMergedRefs } from '../../internal/useMergedRefs.js';
import { deprecate } from '../../prop-types/deprecate.js';
import '../FluidForm/FluidForm.js';
import { FormContext } from '../FluidForm/FormContext.js';
import { noopFn } from '../../internal/noopFn.js';
var _Close;
const Search = /*#__PURE__*/React.forwardRef(function Search({
autoComplete = 'off',
className,
closeButtonLabelText = 'Clear search input',
defaultValue,
disabled,
isExpanded = true,
id,
labelText,
// @ts-expect-error: deprecated prop
light,
onChange = () => {},
onClear = () => {},
onKeyDown,
onExpand,
placeholder = 'Search',
renderIcon,
role = 'searchbox',
size = 'md',
type = 'text',
value,
...rest
}, forwardRef) {
const hasPropValue = value || defaultValue ? true : false;
const prefix = usePrefix();
const {
isFluid
} = useContext(FormContext);
const inputRef = useRef(null);
const ref = useMergedRefs([forwardRef, inputRef]);
const expandButtonRef = useRef(null);
const inputId = useId('search-input');
const uniqueId = id || inputId;
const searchId = `${uniqueId}-search`;
const [hasContent, setHasContent] = useState(hasPropValue || false);
const [prevValue, setPrevValue] = useState(value);
const searchClasses = cx({
[`${prefix}--search`]: true,
[`${prefix}--search--sm`]: size === 'sm',
[`${prefix}--search--md`]: size === 'md',
[`${prefix}--search--lg`]: size === 'lg',
[`${prefix}--search--light`]: light,
[`${prefix}--search--disabled`]: disabled,
[`${prefix}--search--fluid`]: isFluid
}, className);
const clearClasses = cx({
[`${prefix}--search-close`]: true,
[`${prefix}--search-close--hidden`]: !hasContent || !isExpanded
});
if (value !== prevValue) {
setHasContent(!!value);
setPrevValue(value);
}
function clearInput() {
if (!value && inputRef.current) {
inputRef.current.value = '';
}
if (inputRef.current) {
const inputTarget = Object.assign({}, inputRef.current, {
value: ''
});
const syntheticEvent = {
bubbles: false,
cancelable: false,
currentTarget: inputRef.current,
defaultPrevented: false,
eventPhase: 0,
isDefaultPrevented: () => false,
isPropagationStopped: () => false,
isTrusted: false,
nativeEvent: new Event('change'),
persist: noopFn,
preventDefault: noopFn,
stopPropagation: noopFn,
target: inputTarget,
timeStamp: 0,
type: 'change'
};
onChange(syntheticEvent);
}
onClear();
setHasContent(false);
inputRef.current?.focus();
}
function handleChange(event) {
setHasContent(event.target.value !== '');
}
function handleKeyDown(event) {
if (match(event, Escape)) {
event.stopPropagation();
if (inputRef.current?.value) {
clearInput();
}
// ExpandableSearch closes on escape when isExpanded, focus search activation button
else if (onExpand && isExpanded) {
expandButtonRef.current?.focus();
}
}
}
function handleExpandButtonKeyDown(event) {
if (match(event, Enter) || match(event, Space)) {
event.stopPropagation();
if (onExpand) {
onExpand(event);
}
}
}
return /*#__PURE__*/React.createElement("div", {
role: "search",
"aria-label": placeholder,
className: searchClasses
}, /*#__PURE__*/React.createElement("div", {
"aria-labelledby": onExpand ? searchId : undefined,
role: onExpand ? 'button' : undefined,
className: `${prefix}--search-magnifier`,
onClick: onExpand,
onKeyDown: handleExpandButtonKeyDown,
tabIndex: onExpand && !isExpanded ? 0 : -1,
ref: expandButtonRef,
"aria-expanded": onExpand && isExpanded ? true : onExpand && !isExpanded ? false : undefined,
"aria-controls": onExpand ? uniqueId : undefined
}, /*#__PURE__*/React.createElement(CustomSearchIcon, {
icon: renderIcon
})), /*#__PURE__*/React.createElement("label", {
id: searchId,
htmlFor: uniqueId,
className: `${prefix}--label`
}, labelText), /*#__PURE__*/React.createElement("input", _extends({
autoComplete: autoComplete,
className: `${prefix}--search-input`,
defaultValue: defaultValue,
disabled: disabled,
role: role,
ref: ref,
id: uniqueId,
onChange: composeEventHandlers([onChange, handleChange]),
onKeyDown: composeEventHandlers([onKeyDown, handleKeyDown]),
placeholder: placeholder,
type: type,
value: value,
tabIndex: onExpand && !isExpanded ? -1 : undefined
}, rest)), /*#__PURE__*/React.createElement("button", {
"aria-label": closeButtonLabelText,
className: clearClasses,
disabled: disabled,
onClick: clearInput,
title: closeButtonLabelText,
type: "button"
}, _Close || (_Close = /*#__PURE__*/React.createElement(Close, null))));
});
Search.displayName = 'Search';
Search.propTypes = {
/**
* Specify an optional value for the `autocomplete` property on the underlying
* `<input>`, defaults to "off"
*/
autoComplete: PropTypes.string,
/**
* Specify an optional className to be applied to the container node
*/
className: PropTypes.string,
/**
* Specify a label to be read by screen readers on the "close" button
*/
closeButtonLabelText: PropTypes.string,
/**
* Optionally provide the default value of the `<input>`
*/
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Specify whether the `<input>` should be disabled
*/
disabled: PropTypes.bool,
/**
* Specify a custom `id` for the input
*/
id: PropTypes.string,
/**
* Specify whether or not ExpandableSearch should render expanded or not
*/
isExpanded: PropTypes.bool,
/**
* Provide the label text for the Search icon
*/
labelText: PropTypes.node.isRequired,
/**
* Specify light version or default version of this control
*/
light: deprecate(PropTypes.bool, 'The `light` prop for `Search` is no longer needed and has ' + 'been deprecated in v11 in favor of the new `Layer` component. It will be moved in the next major release.'),
/**
* Optional callback called when the search value changes.
*/
onChange: PropTypes.func,
/**
* Optional callback called when the search value is cleared.
*/
onClear: PropTypes.func,
/**
* Optional callback called when the magnifier icon is clicked in ExpandableSearch.
*/
onExpand: PropTypes.func,
/**
* Provide a handler that is invoked on the key down event for the input
*/
onKeyDown: PropTypes.func,
/**
* Provide an optional placeholder text for the Search.
* Note: if the label and placeholder differ,
* VoiceOver on Mac will read both
*/
placeholder: PropTypes.string,
/**
* A component used to render an icon.
*/
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Specify the role for the underlying `<input>`, defaults to `searchbox`
*/
role: PropTypes.string,
/**
* Specify the size of the Search
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
/**
* Optional prop to specify the type of the `<input>`
*/
type: PropTypes.string,
/**
* Specify the value of the `<input>`
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
function CustomSearchIcon({
icon: Icon
}) {
const prefix = usePrefix();
if (Icon) {
return /*#__PURE__*/React.createElement(Icon, {
className: `${prefix}--search-magnifier-icon`
});
}
return /*#__PURE__*/React.createElement(Search$1, {
className: `${prefix}--search-magnifier-icon`
});
}
CustomSearchIcon.propTypes = {
/**
* Rendered icon for the Search. Can be a React component class
*/
icon: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
};
export { Search as default };