@carbon/react
Version:
React components for the Carbon Design System
191 lines (189 loc) • 7.06 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* 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 { usePrefix } from "../../internal/usePrefix.js";
import { Enter, Escape, Space } from "../../internal/keyboard/keys.js";
import { match } from "../../internal/keyboard/match.js";
import { useId } from "../../internal/useId.js";
import { noopFn } from "../../internal/noopFn.js";
import { deprecate } from "../../prop-types/deprecate.js";
import { useMergedRefs } from "../../internal/useMergedRefs.js";
import { Tooltip } from "../Tooltip/Tooltip.js";
import { composeEventHandlers } from "../../tools/events.js";
import { FormContext } from "../FluidForm/FormContext.js";
import { isSearchValuePresent } from "./utils.js";
import classNames from "classnames";
import React, { useContext, useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { jsx, jsxs } from "react/jsx-runtime";
import { Close, Search } from "@carbon/icons-react";
//#region src/components/Search/Search.tsx
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const Search$1 = React.forwardRef(({ autoComplete = "off", className, closeButtonLabelText = "Clear search input", defaultValue, disabled, isExpanded = true, id, labelText, light, onChange = () => {}, onClear = () => {}, onKeyDown, onExpand, placeholder = "Search", renderIcon, role, size = "md", type = "search", value, ...rest }, forwardRef) => {
const hasPropValue = isSearchValuePresent(value) || isSearchValuePresent(defaultValue);
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 searchClasses = classNames({
[`${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 = classNames({
[`${prefix}--search-close`]: true,
[`${prefix}--search-close--hidden`]: !hasContent || !isExpanded
});
useEffect(() => {
if (typeof value !== "undefined") setHasContent(isSearchValuePresent(value));
}, [value]);
function clearInput() {
if (!value && inputRef.current) inputRef.current.value = "";
if (inputRef.current) {
const inputTarget = Object.assign({}, inputRef.current, { value: "" });
onChange({
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"
});
}
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();
else if (onExpand && isExpanded) expandButtonRef.current?.focus();
}
}
function handleExpandButtonKeyDown(event) {
if (match(event, Enter) || match(event, Space)) {
event.stopPropagation();
if (onExpand) onExpand(event);
}
}
const magnifierButton = /* @__PURE__ */ jsx("div", {
"aria-labelledby": onExpand ? searchId : void 0,
role: onExpand ? "button" : void 0,
className: `${prefix}--search-magnifier`,
onClick: onExpand,
onKeyDown: handleExpandButtonKeyDown,
tabIndex: onExpand && !isExpanded ? 0 : -1,
ref: expandButtonRef,
"aria-expanded": onExpand && isExpanded ? true : onExpand && !isExpanded ? false : void 0,
"aria-controls": onExpand ? uniqueId : void 0,
children: /* @__PURE__ */ jsx(CustomSearchIcon, { icon: renderIcon })
});
return /* @__PURE__ */ jsxs("div", {
role: "search",
"aria-label": placeholder,
className: searchClasses,
children: [
onExpand && !isExpanded ? /* @__PURE__ */ jsx(Tooltip, {
className: `${prefix}--search-tooltip ${prefix}--search-magnifier-tooltip ${prefix}--icon-tooltip`,
align: "top",
label: "Search",
children: magnifierButton
}) : magnifierButton,
/* @__PURE__ */ jsx("label", {
id: searchId,
htmlFor: uniqueId,
className: `${prefix}--label`,
children: labelText
}),
/* @__PURE__ */ jsx("input", {
autoComplete,
className: `${prefix}--search-input`,
defaultValue,
disabled,
role,
ref,
id: uniqueId,
onChange: composeEventHandlers([onChange, handleChange]),
onKeyDown: composeEventHandlers([onKeyDown, handleKeyDown]),
placeholder,
type,
value,
tabIndex: onExpand && !isExpanded ? -1 : void 0,
...rest
}),
/* @__PURE__ */ jsx("button", {
"aria-label": closeButtonLabelText,
className: clearClasses,
disabled,
onClick: clearInput,
title: closeButtonLabelText,
type: "button",
children: /* @__PURE__ */ jsx(Close, {})
})
]
});
});
Search$1.displayName = "Search";
Search$1.propTypes = {
autoComplete: PropTypes.string,
className: PropTypes.string,
closeButtonLabelText: PropTypes.string,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
disabled: PropTypes.bool,
id: PropTypes.string,
isExpanded: PropTypes.bool,
labelText: PropTypes.node.isRequired,
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."),
onChange: PropTypes.func,
onClear: PropTypes.func,
onExpand: PropTypes.func,
onKeyDown: PropTypes.func,
placeholder: PropTypes.string,
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
role: 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."),
size: PropTypes.oneOf([
"sm",
"md",
"lg"
]),
type: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
function CustomSearchIcon({ icon: Icon }) {
const prefix = usePrefix();
if (Icon) return /* @__PURE__ */ jsx(Icon, { className: `${prefix}--search-magnifier-icon` });
return /* @__PURE__ */ jsx(Search, { className: `${prefix}--search-magnifier-icon` });
}
CustomSearchIcon.propTypes = { icon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]) };
//#endregion
export { Search$1 as default };