diff --git a/packages/react/src/components/ComposedModal/next/ModalHeader-test.js b/packages/react/src/components/ComposedModal/next/ModalHeader-test.js
index 42a480f1b293..2c63bd96b230 100644
--- a/packages/react/src/components/ComposedModal/next/ModalHeader-test.js
+++ b/packages/react/src/components/ComposedModal/next/ModalHeader-test.js
@@ -24,7 +24,3 @@ describe('ModalHeader', () => {
expect(container.firstChild).toHaveTextContent('Carbon label');
});
});
-
-// TODO: write tests for composed modal
-// TODO: write tests for modal body
-// TODO: write tests for modal footer
diff --git a/packages/react/src/components/Search/index.js b/packages/react/src/components/Search/index.js
index b617c32e6275..5b710f70130e 100644
--- a/packages/react/src/components/Search/index.js
+++ b/packages/react/src/components/Search/index.js
@@ -1,9 +1,17 @@
/**
- * Copyright IBM Corp. 2016, 2018
+ * Copyright IBM Corp. 2016, 2021
*
* 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 * as FeatureFlags from '@carbon/feature-flags';
+import { default as SearchNext } from './next/Search';
+import { default as SearchClassic } from './Search';
+
+const Search = FeatureFlags.enabled('enable-v11-release')
+ ? SearchNext
+ : SearchClassic;
export * from './Search.Skeleton';
-export default from './Search';
+
+export default Search;
diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js
new file mode 100644
index 000000000000..68fd7b93136b
--- /dev/null
+++ b/packages/react/src/components/Search/next/Search-test.js
@@ -0,0 +1,281 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * 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 React from 'react';
+import { Search16, Close16 } from '@carbon/icons-react';
+import Search from './Search';
+import { mount, shallow } from 'enzyme';
+import { settings } from 'carbon-components';
+
+const { prefix } = settings;
+
+describe('Search', () => {
+ describe('renders as expected', () => {
+ const wrapper = mount(
+
+ );
+
+ const label = wrapper.find('label');
+ const textInput = wrapper.find('input');
+ const container = wrapper.find(`.${prefix}--search`);
+
+ describe('container', () => {
+ it('should add extra classes that are passed via className', () => {
+ expect(container.hasClass('extra-class')).toEqual(true);
+ });
+ });
+
+ describe('input', () => {
+ it('renders as expected', () => {
+ expect(textInput.length).toBe(1);
+ });
+
+ it('has the expected classes', () => {
+ expect(textInput.hasClass(`${prefix}--search-input`)).toEqual(true);
+ });
+
+ it('should set type as expected', () => {
+ expect(textInput.props().type).toEqual('text');
+ wrapper.setProps({ type: 'email' });
+ expect(wrapper.find('input').props().type).toEqual('email');
+ });
+
+ it('should set value as expected', () => {
+ expect(textInput.props().defaultValue).toEqual(undefined);
+ wrapper.setProps({ defaultValue: 'test' });
+ expect(wrapper.find('input').props().defaultValue).toEqual('test');
+ expect(wrapper.find('input').props().value).toEqual(undefined);
+ });
+
+ it('should set placeholder as expected', () => {
+ expect(textInput.props().placeholder).toEqual('');
+ wrapper.setProps({ placeholder: 'Enter text' });
+ expect(wrapper.find('input').props().placeholder).toEqual('Enter text');
+ });
+ });
+
+ describe('label', () => {
+ it('renders a label', () => {
+ expect(label.length).toBe(1);
+ });
+
+ it('has the expected classes', () => {
+ expect(label.hasClass(`${prefix}--label`)).toEqual(true);
+ });
+
+ it('should set label as expected', () => {
+ expect(wrapper.props().label).toEqual('Search Field');
+ wrapper.setProps({ label: 'Email Input' });
+ expect(wrapper.props().label).toEqual('Email Input');
+ });
+ });
+
+ describe('Large Search', () => {
+ const large = mount(
+
+ );
+
+ const largeContainer = large.find(`.${prefix}--search`);
+
+ it('renders correct search icon', () => {
+ const icons = large.find(Search16);
+ expect(icons.length).toBe(1);
+ });
+
+ it('should have the expected large class', () => {
+ expect(largeContainer.hasClass(`${prefix}--search--lg`)).toEqual(true);
+ });
+
+ it('should only have 1 button (clear)', () => {
+ const btn = large.find('button');
+ expect(btn.length).toEqual(1);
+ });
+
+ it('renders two Icons', () => {
+ const iconTypes = [Search16, Close16];
+ const icons = large.findWhere((n) => iconTypes.includes(n.type()));
+ expect(icons.length).toEqual(2);
+ });
+
+ describe('buttons', () => {
+ const btns = wrapper.find('button');
+
+ it('should be one button', () => {
+ expect(btns.length).toBe(1);
+ });
+
+ it('should have type="button"', () => {
+ const type1 = btns.first().instance().getAttribute('type');
+ const type2 = btns.last().instance().getAttribute('type');
+ expect(type1).toEqual('button');
+ expect(type2).toEqual('button');
+ });
+ });
+
+ describe('icons', () => {
+ it('renders "search" icon', () => {
+ const icons = wrapper.find(Search16);
+ expect(icons.length).toBe(1);
+ });
+
+ it('renders two Icons', () => {
+ wrapper.setProps({ size: undefined });
+ const iconTypes = [Search16, Close16];
+ const icons = wrapper.findWhere((n) => iconTypes.includes(n.type()));
+ expect(icons.length).toEqual(2);
+ });
+ });
+ });
+
+ describe('Small Search', () => {
+ const small = mount(
+
+ );
+
+ const smallContainer = small.find(`.${prefix}--search`);
+
+ it('renders correct search icon', () => {
+ const icons = small.find(Search16);
+ expect(icons.length).toBe(1);
+ });
+
+ it('should have the expected small class', () => {
+ expect(smallContainer.hasClass(`${prefix}--search--sm`)).toEqual(true);
+ });
+
+ it('should only have 1 button (clear)', () => {
+ const btn = small.find('button');
+ expect(btn.length).toEqual(1);
+ });
+
+ it('renders two Icons', () => {
+ const iconTypes = [Search16, Close16];
+ const icons = wrapper.findWhere((n) => iconTypes.includes(n.type()));
+ expect(icons.length).toEqual(2);
+ });
+ });
+ });
+
+ describe('events', () => {
+ describe('enabled textinput', () => {
+ const onClick = jest.fn();
+ const onChange = jest.fn();
+ const onClear = jest.fn();
+
+ const wrapper = shallow(
+
+ );
+
+ const input = wrapper.find('input');
+ const eventObject = {
+ target: {
+ defaultValue: 'test',
+ },
+ };
+
+ it('should invoke onClick when input is clicked', () => {
+ input.simulate('click');
+ expect(onClick).toHaveBeenCalled();
+ });
+
+ it('should invoke onChange when input value is changed', () => {
+ input.simulate('change', eventObject);
+ expect(onChange).toHaveBeenCalledWith(eventObject);
+ });
+
+ it('should call onClear when input value is cleared', () => {
+ const node = document.createElement('div');
+ document.body.appendChild(node);
+
+ const wrapper = mount(
+ ,
+ {
+ attachTo: node,
+ }
+ );
+
+ wrapper.find('button').simulate('click', {
+ target: {
+ value: 'test',
+ },
+ });
+ expect(onClear).toHaveBeenCalled();
+ expect(wrapper.find('input').getDOMNode()).toHaveFocus();
+
+ document.body.removeChild(node);
+ });
+ });
+ });
+});
+
+/**
+ * Find the element.
+ * @param {Enzymecontainer} wrapper
+ * @returns {Enzymecontainer}
+ */
+const getInput = (wrapper) => {
+ return wrapper.find(`.${prefix}--search-input`);
+};
+
+/**
+ * Find the value of the element
+ * @param {EnzymeWrapper} wrapper
+ * @returns {number}
+ */
+const getInputValue = (wrapper) => {
+ return getInput(wrapper).prop('value');
+};
+
+describe('Detecting change in value from props', () => {
+ it('should have empty value', () => {
+ const search = shallow(
+
+ );
+ expect(getInputValue(search)).toBe(undefined);
+ });
+
+ it('should set value if value prop is added', () => {
+ const search = shallow(
+
+ );
+
+ expect(getInputValue(search)).toBe('foo');
+ });
+});
diff --git a/packages/react/src/components/Search/next/Search.js b/packages/react/src/components/Search/next/Search.js
new file mode 100644
index 000000000000..d1d7b2b77c34
--- /dev/null
+++ b/packages/react/src/components/Search/next/Search.js
@@ -0,0 +1,261 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * 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 { Search16, Close16 } from '@carbon/icons-react';
+import cx from 'classnames';
+import PropTypes from 'prop-types';
+import React, { useRef, useState } from 'react';
+import { focus } from '../../../internal/focus';
+import { keys, match } from '../../../internal/keyboard';
+import { useId } from '../../../internal/useId';
+import { usePrefix } from '../../../internal/usePrefix';
+import deprecate from '../../../prop-types/deprecate';
+import { composeEventHandlers } from '../../../tools/events';
+
+function Search({
+ autoComplete = 'off',
+ className,
+ closeButtonLabelText = 'Clear search input',
+ defaultValue,
+ disabled,
+ id,
+ labelText,
+ light,
+ onChange = () => {},
+ onClear = () => {},
+ onKeyDown,
+ placeHolderText,
+ placeholder = '',
+ renderIcon,
+ role = 'searchbox',
+ size = !small ? 'lg' : 'sm',
+ small,
+ type = 'text',
+ value,
+ ...rest
+}) {
+ const prefix = usePrefix();
+ const input = useRef(null);
+ const magnifier = useRef(null);
+ const inputId = useId('search-input');
+ const uniqueId = id || inputId;
+ const searchId = `${uniqueId}-search`;
+ const [hasContent, setHasContent] = useState(value || defaultValue || 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,
+ [className]: className,
+ });
+ const clearClasses = cx({
+ [`${prefix}--search-close`]: true,
+ [`${prefix}--search-close--hidden`]: !hasContent,
+ });
+
+ if (value !== prevValue) {
+ setHasContent(!!value);
+ setPrevValue(value);
+ }
+
+ function clearInput(event) {
+ if (!value) {
+ input.current.value = '';
+ onChange(event);
+ } else {
+ const clearedEvt = Object.assign({}, event.target, {
+ target: {
+ value: '',
+ },
+ });
+ onChange(clearedEvt);
+ }
+
+ onClear();
+ setHasContent(false);
+ focus(input);
+ }
+
+ function handleChange(event) {
+ setHasContent(event.target.value !== '');
+ }
+
+ function handleKeyDown(event) {
+ if (match(event, keys.Escape)) {
+ event.stopPropagation();
+ clearInput(event);
+ }
+ }
+
+ return (
+
+
+
+
+
+ {labelText}
+
+
+
+
+
+
+ );
+}
+
+Search.propTypes = {
+ /**
+ * Specify an optional value for the `autocomplete` property on the underlying
+ * ` `, 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 ` `
+ */
+ defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+
+ /**
+ * Specify whether the ` ` should be disabled
+ */
+ disabled: PropTypes.bool,
+
+ /**
+ * Specify a custom `id` for the input
+ */
+ id: PropTypes.string,
+
+ /**
+ * Provide the label text for the Search icon
+ */
+ labelText: PropTypes.node.isRequired,
+
+ /**
+ * Specify light version or default version of this control
+ */
+ light: PropTypes.bool,
+
+ /**
+ * Optional callback called when the search value changes.
+ */
+ onChange: PropTypes.func,
+
+ /**
+ * Optional callback called when the search value is cleared.
+ */
+ onClear: PropTypes.func,
+
+ /**
+ * Provide a handler that is invoked on the key down event for the input
+ */
+ onKeyDown: PropTypes.func,
+
+ /**
+ * Deprecated in favor of `placeholder`
+ */
+ placeHolderText: deprecate(
+ PropTypes.string,
+ `\nThe prop \`placeHolderText\` for Search has been deprecated in favor of \`placeholder\`. Please use \`placeholder\` instead.`
+ ),
+
+ /**
+ * Provide an optional placeholder text for the Search.
+ * Note: if the label and placeholder differ,
+ * VoiceOver on Mac will read both
+ */
+ placeholder: PropTypes.string,
+
+ /**
+ * Rendered icon for the Search.
+ * Can be a React component class
+ */
+ renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+
+ /**
+ * Specify the role for the underlying ` `, defaults to `searchbox`
+ */
+ role: PropTypes.string,
+
+ /**
+ * Specify the search size
+ */
+ size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
+
+ /**
+ * Specify whether the Search should be a small variant
+ */
+
+ /**
+ * Specify whether the load was successful
+ */
+ small: deprecate(
+ PropTypes.bool,
+ `\nThe prop \`small\` for Search has been deprecated in favor of \`size\`. Please use \`size="sm"\` instead.`
+ ),
+
+ /**
+ * Optional prop to specify the type of the ` `
+ */
+ type: PropTypes.string,
+
+ /**
+ * Specify the value of the ` `
+ */
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
+
+function SearchIcon({ icon }) {
+ const prefix = usePrefix();
+
+ if (icon) {
+ return React.cloneElement(icon, {
+ className: `${prefix}--search-magnifier-icon`,
+ });
+ }
+ return ;
+}
+
+SearchIcon.propTypes = {
+ /**
+ * Rendered icon for the Search. Can be a React component class
+ */
+ icon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+};
+
+export default Search;