@carbon/react
Version:
React components for the Carbon Design System
293 lines (286 loc) • 9.55 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.
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var iconsReact = require('@carbon/icons-react');
var cx = require('classnames');
var PropTypes = require('prop-types');
var React = require('react');
var keys = require('../../internal/keyboard/keys.js');
var match = require('../../internal/keyboard/match.js');
var useId = require('../../internal/useId.js');
var usePrefix = require('../../internal/usePrefix.js');
var events = require('../../tools/events.js');
var useMergedRefs = require('../../internal/useMergedRefs.js');
var deprecate = require('../../prop-types/deprecate.js');
require('../FluidForm/FluidForm.js');
var FormContext = require('../FluidForm/FormContext.js');
var noopFn = require('../../internal/noopFn.js');
require('../Tooltip/DefinitionTooltip.js');
var Tooltip = require('../Tooltip/Tooltip.js');
var _Close;
const Search = /*#__PURE__*/React.forwardRef(({
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,
size = 'md',
type = 'search',
value,
...rest
}, forwardRef) => {
const hasPropValue = Boolean(value || defaultValue);
const prefix = usePrefix.usePrefix();
const {
isFluid
} = React.useContext(FormContext.FormContext);
const inputRef = React.useRef(null);
const ref = useMergedRefs.useMergedRefs([forwardRef, inputRef]);
const expandButtonRef = React.useRef(null);
const inputId = useId.useId('search-input');
const uniqueId = id || inputId;
const searchId = `${uniqueId}-search`;
const [hasContent, setHasContent] = React.useState(hasPropValue || false);
const [prevValue, setPrevValue] = React.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.noopFn,
preventDefault: noopFn.noopFn,
stopPropagation: noopFn.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.match(event, keys.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.match(event, keys.Enter) || match.match(event, keys.Space)) {
event.stopPropagation();
if (onExpand) {
onExpand(event);
}
}
}
const magnifierButton = /*#__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
}));
// Wrap magnifierButton in a tooltip if it's expandable
const magnifierWithTooltip = onExpand && !isExpanded ? /*#__PURE__*/React.createElement(Tooltip.Tooltip, {
className: `${prefix}--search-tooltip ${prefix}--search-magnifier-tooltip`,
align: "top",
label: "Search"
}, magnifierButton) : magnifierButton;
return /*#__PURE__*/React.createElement("div", {
role: "search",
"aria-label": placeholder,
className: searchClasses
}, magnifierWithTooltip, /*#__PURE__*/React.createElement("label", {
id: searchId,
htmlFor: uniqueId,
className: `${prefix}--label`
}, labelText), /*#__PURE__*/React.createElement("input", _rollupPluginBabelHelpers.extends({
autoComplete: autoComplete,
className: `${prefix}--search-input`,
defaultValue: defaultValue,
disabled: disabled,
role: role,
ref: ref,
id: uniqueId,
onChange: events.composeEventHandlers([onChange, handleChange]),
onKeyDown: events.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(iconsReact.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.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]),
/**
* Deprecated, since <input type="search"> already provides correct semantics.
* Specify the role for the underlying `<input>`, defaults to `searchbox`
*/
role: deprecate.deprecate(PropTypes.string, 'The `role` prop has been deprecated since <input type="search"> already provides correct semantics. ' + 'It will be removed in the next major release of Carbon.'),
/**
* Specify the size of the Search
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
/**
* 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.usePrefix();
if (Icon) {
return /*#__PURE__*/React.createElement(Icon, {
className: `${prefix}--search-magnifier-icon`
});
}
return /*#__PURE__*/React.createElement(iconsReact.Search, {
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])
};
exports.default = Search;