From 1b4f61689a2b3cc12608abbdf3be58e97fbe831e Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Tue, 9 Nov 2021 02:41:02 -0600 Subject: [PATCH 01/15] feat(search): update search to functional component --- .../ComposedModal/next/ModalHeader-test.js | 3 - packages/react/src/components/Search/index.js | 12 +- .../src/components/Search/next/Search-test.js | 88 ++++++ .../src/components/Search/next/Search.js | 260 ++++++++++++++++++ 4 files changed, 358 insertions(+), 5 deletions(-) create mode 100644 packages/react/src/components/Search/next/Search-test.js create mode 100644 packages/react/src/components/Search/next/Search.js diff --git a/packages/react/src/components/ComposedModal/next/ModalHeader-test.js b/packages/react/src/components/ComposedModal/next/ModalHeader-test.js index 42a480f1b293..4e18cae1c262 100644 --- a/packages/react/src/components/ComposedModal/next/ModalHeader-test.js +++ b/packages/react/src/components/ComposedModal/next/ModalHeader-test.js @@ -25,6 +25,3 @@ describe('ModalHeader', () => { }); }); -// 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..a25e09afe529 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; \ No newline at end of file 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..89697c643203 --- /dev/null +++ b/packages/react/src/components/Search/next/Search-test.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { default as Search } from './Search'; +import { render, screen } from '@testing-library/react'; + +describe('Search', () => { + afterEach(cleanup); + + it('adds extra classes that are passed via className prop', () => { + render( + + ); + + const search = screen.getByDisplayValue('test'); + expect(search.classList.contains('extra-class')).toBe(true); + }); + + it('should render type as expected', () => { + render( + + ); + + expect(screen.getByRole('searchbox')).toHaveAttribute('type', 'text'); + }); + + it('should have placeholder', () => { + render( + + ); + + expect(screen.getByRole('searchbox')).toHaveAttribute('placeholder', 'Placeholder'); + }); + + it('should have a label', () => { + render( + + ); + + expect(screen.getByRole('searchbox')).toHaveAttribute('placeholder', 'Placeholder'); + }); + + it('should have small size class', () => { + const { container } = render( + + ); + + expect(container.classList.contains('bx--search--sm')).toBe(true); + }); + + it('should render skeleton', () => { + const { container } = render( + + ); + + expect(container.classList.contains(`${prefix}--skeleton`)).toEqual(true); + }); +}); + + 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..f689cadfb2cf --- /dev/null +++ b/packages/react/src/components/Search/next/Search.js @@ -0,0 +1,260 @@ +/** + * 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 PropTypes from 'prop-types'; + import React, { useState } from 'react'; + import cx from 'classnames'; + import { Search16, Close16 } from '@carbon/icons-react'; + import { composeEventHandlers } from '../../../tools/events'; + import { keys, match } from '../../../internal/keyboard'; + import deprecate from '../../../prop-types/deprecate'; + import { usePrefix } from '../../../internal/usePrefix'; + +const Search = React.forwardRef(function Search( + { + className, + closeButtonLabelText, + defaultValue, + disabled, + id = (_inputId = + _inputId || + `search__input__id_${Math.random().toString(36).substr(2)}`), + labelText, + light, + onChange, + onClear, + onKeyDown, + placeHolderText, + placeholder, + renderIcon, + size = !small ? 'xl' : 'sm', + small, + type, + value, + ...rest + }, + ref +) { + const prefix = usePrefix(); + + const [ hasContent, setHasContent ] = useState( + value || defaultValue || false, + ); + + const [prevValue, setPrevValue] = useState(value); + + if (value !== prevValue) { + setHasContent(!!value); + setPrevValue(value); + } + + function clearInput(evt) { + if (!value) { + input.value = ''; + onChange(evt); + } else { + const clearedEvt = Object.assign({}, evt.target, { + target: { + value: '', + }, + }); + onChange(clearedEvt); + } + + onClear(); + + setHasContent(false, () => input.focus()); + }; + + function handleChange(evt) { + setHasContent(evt.target.value !== '') + }; + + function handleKeyDown(evt) { + if (match(evt, keys.Escape)) { + clearInput(evt); + } + }; + + 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, + }); + + let customIcon; + if (renderIcon) { + customIcon = React.cloneElement(renderIcon, { + className: `${prefix}--search-magnifier-icon`, + }); + } + + const searchId = `${id}-search`; + const searchIcon = renderIcon ? ( + customIcon + ) : ( + + ); + + return ( +
+
{ + // this.magnifier = magnifier; + // }} + > + {searchIcon} +
+ + { + // this.input = input; + // }} + /> + +
+ ); + }); + +Search.propTypes = { + /** + * 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 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]), +}; + +Search.defaultProps = { + type: 'text', + placeholder: '', + closeButtonLabelText: 'Clear search input', + onChange: () => {}, + onClear: () => {}, +}; + +export default Search; \ No newline at end of file From 5c299e46fdbb50f080ee039c908fb4cf9991c93f Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Tue, 9 Nov 2021 02:41:26 -0600 Subject: [PATCH 02/15] feat(search): remove test --- packages/react/src/components/Search/next/Search-test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 89697c643203..dac988ee50d4 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -3,8 +3,6 @@ import { default as Search } from './Search'; import { render, screen } from '@testing-library/react'; describe('Search', () => { - afterEach(cleanup); - it('adds extra classes that are passed via className prop', () => { render( { size="sm" /> ); - expect(container.classList.contains(`${prefix}--skeleton`)).toEqual(true); + expect(container.classList.contains(`bx--skeleton`)).toEqual(true); }); }); From 7151823e345cdb501943f44ca8a9867a983e3038 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Tue, 9 Nov 2021 16:42:32 -0600 Subject: [PATCH 03/15] chore(search): remove ref --- .../src/components/Search/next/Search.js | 269 +++++++++--------- 1 file changed, 130 insertions(+), 139 deletions(-) diff --git a/packages/react/src/components/Search/next/Search.js b/packages/react/src/components/Search/next/Search.js index f689cadfb2cf..70b29d4771ad 100644 --- a/packages/react/src/components/Search/next/Search.js +++ b/packages/react/src/components/Search/next/Search.js @@ -5,149 +5,142 @@ * LICENSE file in the root directory of this source tree. */ - import PropTypes from 'prop-types'; - import React, { useState } from 'react'; - import cx from 'classnames'; - import { Search16, Close16 } from '@carbon/icons-react'; - import { composeEventHandlers } from '../../../tools/events'; - import { keys, match } from '../../../internal/keyboard'; - import deprecate from '../../../prop-types/deprecate'; - import { usePrefix } from '../../../internal/usePrefix'; - -const Search = React.forwardRef(function Search( - { - className, - closeButtonLabelText, - defaultValue, - disabled, - id = (_inputId = - _inputId || - `search__input__id_${Math.random().toString(36).substr(2)}`), - labelText, - light, - onChange, - onClear, - onKeyDown, - placeHolderText, - placeholder, - renderIcon, - size = !small ? 'xl' : 'sm', - small, - type, - value, - ...rest - }, - ref -) { - const prefix = usePrefix(); - - const [ hasContent, setHasContent ] = useState( - value || defaultValue || false, - ); - - const [prevValue, setPrevValue] = useState(value); - - if (value !== prevValue) { - setHasContent(!!value); - setPrevValue(value); +import PropTypes from 'prop-types'; +import React, { useState, useRef } from 'react'; +import cx from 'classnames'; +import { Search16, Close16 } from '@carbon/icons-react'; +import { composeEventHandlers } from '../../../tools/events'; +import { keys, match } from '../../../internal/keyboard'; +import { useId } from '../../../internal/useId'; +import deprecate from '../../../prop-types/deprecate'; +import { usePrefix } from '../../../internal/usePrefix'; + +export default function Search({ + className, + closeButtonLabelText, + defaultValue, + disabled, + id, + labelText, + light, + onChange, + onClear, + onKeyDown, + placeHolderText, + placeholder, + renderIcon, + size = !small ? 'lg' : 'sm', + small, + type, + 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); + + if (value !== prevValue) { + setHasContent(!!value); + setPrevValue(value); + } + + function clearInput(evt) { + if (!value) { + input.current.value = ''; + onChange(evt); + } else { + const clearedEvt = Object.assign({}, evt.target, { + target: { + value: '', + }, + }); + onChange(clearedEvt); } - function clearInput(evt) { - if (!value) { - input.value = ''; - onChange(evt); - } else { - const clearedEvt = Object.assign({}, evt.target, { - target: { - value: '', - }, - }); - onChange(clearedEvt); - } - - onClear(); - - setHasContent(false, () => input.focus()); - }; - - function handleChange(evt) { - setHasContent(evt.target.value !== '') - }; - - function handleKeyDown(evt) { - if (match(evt, keys.Escape)) { - clearInput(evt); - } - }; - - 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, - }); + onClear(); - const clearClasses = cx({ - [`${prefix}--search-close`]: true, - [`${prefix}--search-close--hidden`]: !hasContent, - }); + setHasContent(false, () => input.current.focus()); + } - let customIcon; - if (renderIcon) { - customIcon = React.cloneElement(renderIcon, { - className: `${prefix}--search-magnifier-icon`, - }); - } + function handleChange(evt) { + setHasContent(evt.target.value !== ''); + } - const searchId = `${id}-search`; - const searchIcon = renderIcon ? ( - customIcon - ) : ( - - ); - - return ( -
-
{ - // this.magnifier = magnifier; - // }} - > - {searchIcon} -
- - { - // this.input = input; - // }} - /> - + function handleKeyDown(evt) { + if (match(evt, keys.Escape)) { + clearInput(evt); + } + } + + 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, + }); + + let customIcon; + if (renderIcon) { + customIcon = React.cloneElement(renderIcon, { + className: `${prefix}--search-magnifier-icon`, + }); + } + + const searchIcon = renderIcon ? ( + customIcon + ) : ( + + ); + + return ( +
+
+ {searchIcon}
- ); - }); + + + +
+ ); +} Search.propTypes = { /** @@ -256,5 +249,3 @@ Search.defaultProps = { onChange: () => {}, onClear: () => {}, }; - -export default Search; \ No newline at end of file From e748898019ea66b19873d70f933102343167b820 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Tue, 9 Nov 2021 16:58:34 -0600 Subject: [PATCH 04/15] chore(search): format --- .../ComposedModal/next/ModalHeader-test.js | 1 - packages/react/src/components/Search/index.js | 8 ++--- .../src/components/Search/next/Search-test.js | 29 ++++++++++++------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/ComposedModal/next/ModalHeader-test.js b/packages/react/src/components/ComposedModal/next/ModalHeader-test.js index 4e18cae1c262..2c63bd96b230 100644 --- a/packages/react/src/components/ComposedModal/next/ModalHeader-test.js +++ b/packages/react/src/components/ComposedModal/next/ModalHeader-test.js @@ -24,4 +24,3 @@ describe('ModalHeader', () => { expect(container.firstChild).toHaveTextContent('Carbon label'); }); }); - diff --git a/packages/react/src/components/Search/index.js b/packages/react/src/components/Search/index.js index a25e09afe529..5b710f70130e 100644 --- a/packages/react/src/components/Search/index.js +++ b/packages/react/src/components/Search/index.js @@ -4,9 +4,9 @@ * 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'; +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 @@ -14,4 +14,4 @@ const Search = FeatureFlags.enabled('enable-v11-release') export * from './Search.Skeleton'; -export default Search; \ No newline at end of file +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 index dac988ee50d4..244c8619e2b4 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -37,10 +37,14 @@ describe('Search', () => { className="extra-class" label="Search Field" labelText="testlabel" - placeholder="Placeholder" /> + placeholder="Placeholder" + /> ); - expect(screen.getByRole('searchbox')).toHaveAttribute('placeholder', 'Placeholder'); + expect(screen.getByRole('searchbox')).toHaveAttribute( + 'placeholder', + 'Placeholder' + ); }); it('should have a label', () => { @@ -53,7 +57,10 @@ describe('Search', () => { /> ); - expect(screen.getByRole('searchbox')).toHaveAttribute('placeholder', 'Placeholder'); + expect(screen.getByRole('searchbox')).toHaveAttribute( + 'placeholder', + 'Placeholder' + ); }); it('should have small size class', () => { @@ -63,10 +70,11 @@ describe('Search', () => { className="extra-class" label="Search Field" labelText="testlabel" - size="sm" /> - ); + size="sm" + /> + ); - expect(container.classList.contains('bx--search--sm')).toBe(true); + expect(container.classList.contains('bx--search--sm')).toBe(true); }); it('should render skeleton', () => { @@ -76,11 +84,10 @@ describe('Search', () => { className="extra-class" label="Search Field" labelText="testlabel" - size="sm" /> - ); + size="sm" + /> + ); - expect(container.classList.contains(`bx--skeleton`)).toEqual(true); + expect(container.classList.contains(`bx--skeleton`)).toEqual(true); }); }); - - From 66effdf2c601dc2cec03e0974b0adc203286fd41 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Wed, 10 Nov 2021 00:30:14 -0600 Subject: [PATCH 05/15] chore(search): import skeleton --- .../src/components/Search/next/Search-test.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 244c8619e2b4..f6ded19ef542 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -1,8 +1,11 @@ import React from 'react'; import { default as Search } from './Search'; -import { render, screen } from '@testing-library/react'; +import SearchSkeleton from '../Search.Skeleton'; +import { render, screen, cleanup } from '@testing-library/react'; describe('Search', () => { + afterEach(cleanup); + it('adds extra classes that are passed via className prop', () => { render( { /> ); - const search = screen.getByDisplayValue('test'); + const search = screen.getByRole('search'); expect(search.classList.contains('extra-class')).toBe(true); }); @@ -33,17 +36,17 @@ describe('Search', () => { it('should have placeholder', () => { render( ); expect(screen.getByRole('searchbox')).toHaveAttribute( 'placeholder', - 'Placeholder' + 'test' ); }); @@ -78,15 +81,7 @@ describe('Search', () => { }); it('should render skeleton', () => { - const { container } = render( - - ); + const { container } = render(); expect(container.classList.contains(`bx--skeleton`)).toEqual(true); }); From c83694f06d8c9312112306e387cbc9b1a7135646 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Wed, 10 Nov 2021 01:10:43 -0600 Subject: [PATCH 06/15] test(search): add next search tests --- .../src/components/Search/next/Search-test.js | 284 ++++++++++++++---- 1 file changed, 232 insertions(+), 52 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index f6ded19ef542..95b2f8a7a744 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -1,13 +1,21 @@ +/** + * 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 React from 'react'; -import { default as Search } from './Search'; -import SearchSkeleton from '../Search.Skeleton'; -import { render, screen, cleanup } from '@testing-library/react'; +import { Search16, Close16 } from '@carbon/icons-react'; +import Search from '../Search'; +import { mount, shallow } from 'enzyme'; +import { settings } from 'carbon-components'; -describe('Search', () => { - afterEach(cleanup); +const { prefix } = settings; - it('adds extra classes that are passed via className prop', () => { - render( +describe('Search', () => { + describe('renders as expected', () => { + const wrapper = mount( { /> ); - const search = screen.getByRole('search'); - expect(search.classList.contains('extra-class')).toBe(true); - }); + const label = wrapper.find('label'); + const textInput = wrapper.find('input'); + const container = wrapper.find(`.${prefix}--search`); - it('should render type as expected', () => { - render( - - ); + describe('container', () => { + it('should add extra classes that are passed via className', () => { + expect(container.hasClass('extra-class')).toBe(true); + }); + }); + + describe('input', () => { + it('renders as expected', () => { + expect(textInput.length).toBe(1); + }); + + it('has the expected classes', () => { + expect(textInput.hasClass(`${prefix}--search-input`)).toBe(true); + }); + + it('should set type as expected', () => { + expect(textInput.props().type).toBe('text'); + wrapper.setProps({ type: 'email' }); + expect(textInput.props().type).toBe('email'); + }); + + it('should set value as expected', () => { + expect(textInput.props().defaultValue).toBe(undefined); + wrapper.setProps({ defaultValue: 'test' }); + expect(textInput.props().defaultValue).toBe('test'); + expect(textInput.props().value).toBe(undefined); + }); + + it('should set placeholder as expected', () => { + expect(textInput.props().placeholder).toBe(''); + wrapper.setProps({ placeholder: 'Enter text' }); + expect(textInput.props().placeholder).toBe('Enter text'); + }); + }); + + describe('label', () => { + it('renders a label', () => { + expect(label.length).toBe(1); + }); + + it('has the expected classes', () => { + expect(label.hasClass(`${prefix}--label`)).toBe(true); + }); + + it('should set label as expected', () => { + expect(wrapper.props().label).toBe('Search Field'); + wrapper.setProps({ label: 'Email Input' }); + expect(wrapper.props().label).toBe('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`)).toBe(true); + }); + + it('should only have 1 button (clear)', () => { + const btn = large.find('button'); + expect(btn.length).toBe(1); + }); + + it('renders two Icons', () => { + const iconTypes = [Search16, Close16]; + const icons = large.findWhere((n) => iconTypes.includes(n.type())); + expect(icons.length).toBe(2); + }); + + describe('buttons', () => { + const btns = wrapper.find('button'); + + it('should be one button', () => { + expect(btns.length).toBe(1); + }); - expect(screen.getByRole('searchbox')).toHaveAttribute('type', 'text'); + it('should have type="button"', () => { + const type1 = btns.first().instance().getAttribute('type'); + const type2 = btns.last().instance().getAttribute('type'); + expect(type1).toBe('button'); + expect(type2).toBe('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).toBe(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`)).toBe(true); + }); + + it('should only have 1 button (clear)', () => { + const btn = small.find('button'); + expect(btn.length).toBe(1); + }); + + it('renders two Icons', () => { + const iconTypes = [Search16, Close16]; + const icons = wrapper.findWhere((n) => iconTypes.includes(n.type())); + expect(icons.length).toBe(2); + }); + }); }); - it('should have placeholder', () => { - render( - - ); + describe('events', () => { + describe('enabled textinput', () => { + const onClick = jest.fn(); + const onChange = jest.fn(); + const onClear = jest.fn(); - expect(screen.getByRole('searchbox')).toHaveAttribute( - 'placeholder', - 'test' - ); + 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 invoke onClear when input value is cleared', () => { + wrapper.setProps({ value: 'test' }); + const focus = jest.fn(); + input.getElement().ref({ + focus, + }); + wrapper.find('button').simulate('click', { target: { value: 'test' } }); + expect(onClear).toHaveBeenCalled(); + expect(focus).toHaveBeenCalled(); + }); + }); }); +}); + +// TODO Add skeleton tests - it('should have a label', () => { - render( +describe('Detecting change in value from props', () => { + it('changes the hasContent state upon change in props', () => { + const wrapper = shallow( ); - - expect(screen.getByRole('searchbox')).toHaveAttribute( - 'placeholder', - 'Placeholder' - ); + expect( + wrapper.classList.contains(`${prefix}--search-close--hidden`) + ).toBeFalsy(); + wrapper.setProps({ value: '' }); + expect( + wrapper.classList.contains(`${prefix}--search-close--hidden`) + ).toBeTruthy(); }); - it('should have small size class', () => { - const { container } = render( + it('avoids change the hasContent state upon setting props, unless the value actually changes', () => { + const wrapper = shallow( ); - - expect(container.classList.contains('bx--search--sm')).toBe(true); - }); - - it('should render skeleton', () => { - const { container } = render(); - - expect(container.classList.contains(`bx--skeleton`)).toEqual(true); + expect( + wrapper.classList.contains(`${prefix}--search-close--hidden`) + ).toBeTruthy(); + wrapper.setState({ hasContent: false }); + wrapper.setProps({ value: 'foo' }); + expect( + wrapper.classList.contains(`${prefix}--search-close--hidden`) + ).toBeFalsy(); }); }); From fc7a6f2fdbd83b99437d71138c2c5d33ed84486b Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Wed, 10 Nov 2021 03:03:35 -0600 Subject: [PATCH 07/15] test(search): update tests --- .../src/components/Search/next/Search-test.js | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 95b2f8a7a744..f548bb5e8ff5 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -44,22 +44,19 @@ describe('Search', () => { }); it('should set type as expected', () => { - expect(textInput.props().type).toBe('text'); + expect(textInput.toHaveAttribute('type', 'text')).toBe(true); wrapper.setProps({ type: 'email' }); - expect(textInput.props().type).toBe('email'); - }); - - it('should set value as expected', () => { - expect(textInput.props().defaultValue).toBe(undefined); - wrapper.setProps({ defaultValue: 'test' }); - expect(textInput.props().defaultValue).toBe('test'); - expect(textInput.props().value).toBe(undefined); + expect(wrapper.find('input').toHaveAttribute('type', 'email')).toBe( + true + ); }); it('should set placeholder as expected', () => { - expect(textInput.props().placeholder).toBe(''); + expect(textInput.getByPlaceholderText('')).toBe(true); wrapper.setProps({ placeholder: 'Enter text' }); - expect(textInput.props().placeholder).toBe('Enter text'); + expect(wrapper.find('input').getByPlaceholderText('Enter text')).toBe( + true + ); }); }); @@ -73,9 +70,9 @@ describe('Search', () => { }); it('should set label as expected', () => { - expect(wrapper.props().label).toBe('Search Field'); + expect(wrapper.getByLabelText('Search Field')).toBe(true); wrapper.setProps({ label: 'Email Input' }); - expect(wrapper.props().label).toBe('Email Input'); + expect(wrapper.getByLabelText('Email Input')).toBe(true); }); }); @@ -237,13 +234,9 @@ describe('Detecting change in value from props', () => { value="foo" /> ); - expect( - wrapper.classList.contains(`${prefix}--search-close--hidden`) - ).toBeFalsy(); + expect(wrapper).not.toHaveClass(`${prefix}--search-close--hidden`); wrapper.setProps({ value: '' }); - expect( - wrapper.classList.contains(`${prefix}--search-close--hidden`) - ).toBeTruthy(); + expect(wrapper).toHaveClass(`${prefix}--search-close--hidden`); }); it('avoids change the hasContent state upon setting props, unless the value actually changes', () => { @@ -256,13 +249,9 @@ describe('Detecting change in value from props', () => { value="foo" /> ); - expect( - wrapper.classList.contains(`${prefix}--search-close--hidden`) - ).toBeTruthy(); + expect(wrapper).toHaveClass(`${prefix}--search-close--hidden`); wrapper.setState({ hasContent: false }); wrapper.setProps({ value: 'foo' }); - expect( - wrapper.classList.contains(`${prefix}--search-close--hidden`) - ).toBeFalsy(); + expect(wrapper).not.toHaveClass(`${prefix}--search-close--hidden`); }); }); From f0db59c106d6c4e818b0649e2aecef16cf40e4c3 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Wed, 10 Nov 2021 03:38:35 -0600 Subject: [PATCH 08/15] test(search): update tests --- .../src/components/Search/next/Search-test.js | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index f548bb5e8ff5..6b0cd52894c6 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -9,6 +9,7 @@ import React from 'react'; import { Search16, Close16 } from '@carbon/icons-react'; import Search from '../Search'; import { mount, shallow } from 'enzyme'; +import '@testing-library/jest-dom'; import { settings } from 'carbon-components'; const { prefix } = settings; @@ -44,11 +45,9 @@ describe('Search', () => { }); it('should set type as expected', () => { - expect(textInput.toHaveAttribute('type', 'text')).toBe(true); + expect(textInput).toHaveAttribute('type', 'text'); wrapper.setProps({ type: 'email' }); - expect(wrapper.find('input').toHaveAttribute('type', 'email')).toBe( - true - ); + expect(wrapper.find('input')).toHaveAttribute('type', 'email'); }); it('should set placeholder as expected', () => { @@ -234,9 +233,13 @@ describe('Detecting change in value from props', () => { value="foo" /> ); - expect(wrapper).not.toHaveClass(`${prefix}--search-close--hidden`); + expect(wrapper.find('input')).not.toHaveClass( + `${prefix}--search-close--hidden` + ); wrapper.setProps({ value: '' }); - expect(wrapper).toHaveClass(`${prefix}--search-close--hidden`); + expect(wrapper.find('input')).toHaveClass( + `${prefix}--search-close--hidden` + ); }); it('avoids change the hasContent state upon setting props, unless the value actually changes', () => { @@ -249,9 +252,13 @@ describe('Detecting change in value from props', () => { value="foo" /> ); - expect(wrapper).toHaveClass(`${prefix}--search-close--hidden`); + expect(wrapper.find('input')).toHaveClass( + `${prefix}--search-close--hidden` + ); wrapper.setState({ hasContent: false }); wrapper.setProps({ value: 'foo' }); - expect(wrapper).not.toHaveClass(`${prefix}--search-close--hidden`); + expect(wrapper.find('input')).not.toHaveClass( + `${prefix}--search-close--hidden` + ); }); }); From 87415a29e21846fe58ba727d29528275088c25e7 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Wed, 10 Nov 2021 23:41:02 -0600 Subject: [PATCH 09/15] chore(search): test commit --- .../src/components/Search/next/Search-test.js | 120 ++++++++++++------ 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 6b0cd52894c6..c34f10a12735 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -8,26 +8,68 @@ import React from 'react'; import { Search16, Close16 } from '@carbon/icons-react'; import Search from '../Search'; -import { mount, shallow } from 'enzyme'; +// import { render, render } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { settings } from 'carbon-components'; const { prefix } = settings; +// /** +// * Find the
element thats the search. +// * @param {Enzymecontainer} container +// * @returns {Enzymecontainer} +// */ +// const container = (container) => { +// return container.find(`.${prefix}--search`); +// }; + +// /** +// * Find the element. +// * @param {Enzymecontainer} container +// * @returns {Enzymecontainer} +// */ +// const label = (container) => { +// return container.find(`.${prefix}--search .${prefix}--label`); +// }; + +// /** +// * Find the element. +// * @param {Enzymecontainer} container +// * @returns {Enzymecontainer} +// */ +// const textInput = (container) => { +// return container.find(`.${prefix}--search-input`); +// }; + describe('Search', () => { + // let search; + + // beforeEach(() => { + // search = render( + // + // ); + // }); + describe('renders as expected', () => { - const wrapper = mount( + const container = render( ); - const label = wrapper.find('label'); - const textInput = wrapper.find('input'); - const container = wrapper.find(`.${prefix}--search`); + const textInput = screen.getByRole('searchbox'); + const label = screen.getByLabelText('Search Field', { selector: 'input' }); + + screen.debug(); describe('container', () => { it('should add extra classes that are passed via className', () => { @@ -46,14 +88,14 @@ describe('Search', () => { it('should set type as expected', () => { expect(textInput).toHaveAttribute('type', 'text'); - wrapper.setProps({ type: 'email' }); - expect(wrapper.find('input')).toHaveAttribute('type', 'email'); + container.setProps({ type: 'email' }); + expect(container.find('input')).toHaveAttribute('type', 'email'); }); it('should set placeholder as expected', () => { expect(textInput.getByPlaceholderText('')).toBe(true); - wrapper.setProps({ placeholder: 'Enter text' }); - expect(wrapper.find('input').getByPlaceholderText('Enter text')).toBe( + container.setProps({ placeholder: 'Enter text' }); + expect(container.find('input').getByPlaceholderText('Enter text')).toBe( true ); }); @@ -69,16 +111,16 @@ describe('Search', () => { }); it('should set label as expected', () => { - expect(wrapper.getByLabelText('Search Field')).toBe(true); - wrapper.setProps({ label: 'Email Input' }); - expect(wrapper.getByLabelText('Email Input')).toBe(true); + expect(container.getByLabelText('Search Field')).toBe(true); + container.setProps({ label: 'Email Input' }); + expect(container.getByLabelText('Email Input')).toBe(true); }); }); describe('Large Search', () => { - const large = mount( + const large = render( { }); describe('buttons', () => { - const btns = wrapper.find('button'); + const btns = container.find('button'); it('should be one button', () => { expect(btns.length).toBe(1); @@ -125,21 +167,23 @@ describe('Search', () => { describe('icons', () => { it('renders "search" icon', () => { - const icons = wrapper.find(Search16); + const icons = container.find(Search16); expect(icons.length).toBe(1); }); it('renders two Icons', () => { - wrapper.setProps({ size: undefined }); + container.setProps({ size: undefined }); const iconTypes = [Search16, Close16]; - const icons = wrapper.findWhere((n) => iconTypes.includes(n.type())); + const icons = container.findWhere((n) => + iconTypes.includes(n.type()) + ); expect(icons.length).toBe(2); }); }); }); describe('Small Search', () => { - const small = mount( + const small = render( { it('renders two Icons', () => { const iconTypes = [Search16, Close16]; - const icons = wrapper.findWhere((n) => iconTypes.includes(n.type())); + const icons = container.findWhere((n) => iconTypes.includes(n.type())); expect(icons.length).toBe(2); }); }); @@ -179,9 +223,9 @@ describe('Search', () => { const onChange = jest.fn(); const onClear = jest.fn(); - const wrapper = shallow( + const container = render( { /> ); - const input = wrapper.find('input'); + const input = container.find('input'); const eventObject = { target: { defaultValue: 'test', @@ -207,12 +251,14 @@ describe('Search', () => { }); it('should invoke onClear when input value is cleared', () => { - wrapper.setProps({ value: 'test' }); + container.setProps({ value: 'test' }); const focus = jest.fn(); input.getElement().ref({ focus, }); - wrapper.find('button').simulate('click', { target: { value: 'test' } }); + container + .find('button') + .simulate('click', { target: { value: 'test' } }); expect(onClear).toHaveBeenCalled(); expect(focus).toHaveBeenCalled(); }); @@ -224,40 +270,40 @@ describe('Search', () => { describe('Detecting change in value from props', () => { it('changes the hasContent state upon change in props', () => { - const wrapper = shallow( + const container = render( ); - expect(wrapper.find('input')).not.toHaveClass( + expect(container.find('input')).not.toHaveClass( `${prefix}--search-close--hidden` ); - wrapper.setProps({ value: '' }); - expect(wrapper.find('input')).toHaveClass( + container.setProps({ value: '' }); + expect(container.find('input')).toHaveClass( `${prefix}--search-close--hidden` ); }); it('avoids change the hasContent state upon setting props, unless the value actually changes', () => { - const wrapper = shallow( + const container = render( ); - expect(wrapper.find('input')).toHaveClass( + expect(container.find('input')).toHaveClass( `${prefix}--search-close--hidden` ); - wrapper.setState({ hasContent: false }); - wrapper.setProps({ value: 'foo' }); - expect(wrapper.find('input')).not.toHaveClass( + container.setState({ hasContent: false }); + container.setProps({ value: 'foo' }); + expect(container.find('input')).not.toHaveClass( `${prefix}--search-close--hidden` ); }); From 83ba1a57aab175a074bdde5f502bfec3116579e5 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Thu, 11 Nov 2021 00:17:17 -0600 Subject: [PATCH 10/15] test(search): update tests --- packages/react/src/components/Search/next/Search-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index c34f10a12735..04092ee43139 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -67,7 +67,7 @@ describe('Search', () => { ); const textInput = screen.getByRole('searchbox'); - const label = screen.getByLabelText('Search Field', { selector: 'input' }); + const label = screen.getByLabelText('testlabel'); screen.debug(); @@ -111,9 +111,9 @@ describe('Search', () => { }); it('should set label as expected', () => { - expect(container.getByLabelText('Search Field')).toBe(true); - container.setProps({ label: 'Email Input' }); - expect(container.getByLabelText('Email Input')).toBe(true); + expect(label).toBe(true); + container.setProps({ labelText: 'email label' }); + expect(label.getByLabelText('email label')).toBe(true); }); }); From 1aef26efcdefc9638f4f4dad21772ca69bdd96f7 Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Thu, 11 Nov 2021 01:21:54 -0600 Subject: [PATCH 11/15] test(search): update tests --- .../src/components/Search/next/Search-test.js | 195 +++++++----------- 1 file changed, 74 insertions(+), 121 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 04092ee43139..be11ed503c1f 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2021 + * 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. @@ -7,73 +7,30 @@ import React from 'react'; import { Search16, Close16 } from '@carbon/icons-react'; -import Search from '../Search'; -// import { render, render } from 'enzyme'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; +import Search from './Search'; +import { mount, shallow } from 'enzyme'; import { settings } from 'carbon-components'; const { prefix } = settings; -// /** -// * Find the
element thats the search. -// * @param {Enzymecontainer} container -// * @returns {Enzymecontainer} -// */ -// const container = (container) => { -// return container.find(`.${prefix}--search`); -// }; - -// /** -// * Find the element. -// * @param {Enzymecontainer} container -// * @returns {Enzymecontainer} -// */ -// const label = (container) => { -// return container.find(`.${prefix}--search .${prefix}--label`); -// }; - -// /** -// * Find the element. -// * @param {Enzymecontainer} container -// * @returns {Enzymecontainer} -// */ -// const textInput = (container) => { -// return container.find(`.${prefix}--search-input`); -// }; - describe('Search', () => { - // let search; - - // beforeEach(() => { - // search = render( - // - // ); - // }); - describe('renders as expected', () => { - const container = render( + const wrapper = mount( ); - const textInput = screen.getByRole('searchbox'); - const label = screen.getByLabelText('testlabel'); - - screen.debug(); + 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')).toBe(true); + expect(container.hasClass('extra-class')).toEqual(true); }); }); @@ -83,21 +40,26 @@ describe('Search', () => { }); it('has the expected classes', () => { - expect(textInput.hasClass(`${prefix}--search-input`)).toBe(true); + expect(textInput.hasClass(`${prefix}--search-input`)).toEqual(true); }); it('should set type as expected', () => { - expect(textInput).toHaveAttribute('type', 'text'); - container.setProps({ type: 'email' }); - expect(container.find('input')).toHaveAttribute('type', 'email'); + 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.getByPlaceholderText('')).toBe(true); - container.setProps({ placeholder: 'Enter text' }); - expect(container.find('input').getByPlaceholderText('Enter text')).toBe( - true - ); + expect(textInput.props().placeholder).toEqual(''); + wrapper.setProps({ placeholder: 'Enter text' }); + expect(wrapper.find('input').props().placeholder).toEqual('Enter text'); }); }); @@ -107,20 +69,20 @@ describe('Search', () => { }); it('has the expected classes', () => { - expect(label.hasClass(`${prefix}--label`)).toBe(true); + expect(label.hasClass(`${prefix}--label`)).toEqual(true); }); it('should set label as expected', () => { - expect(label).toBe(true); - container.setProps({ labelText: 'email label' }); - expect(label.getByLabelText('email label')).toBe(true); + expect(wrapper.props().label).toEqual('Search Field'); + wrapper.setProps({ label: 'Email Input' }); + expect(wrapper.props().label).toEqual('Email Input'); }); }); describe('Large Search', () => { - const large = render( + const large = mount( { }); it('should have the expected large class', () => { - expect(largeContainer.hasClass(`${prefix}--search--lg`)).toBe(true); + expect(largeContainer.hasClass(`${prefix}--search--lg`)).toEqual(true); }); it('should only have 1 button (clear)', () => { const btn = large.find('button'); - expect(btn.length).toBe(1); + 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).toBe(2); + expect(icons.length).toEqual(2); }); describe('buttons', () => { - const btns = container.find('button'); + const btns = wrapper.find('button'); it('should be one button', () => { expect(btns.length).toBe(1); @@ -160,30 +122,28 @@ describe('Search', () => { it('should have type="button"', () => { const type1 = btns.first().instance().getAttribute('type'); const type2 = btns.last().instance().getAttribute('type'); - expect(type1).toBe('button'); - expect(type2).toBe('button'); + expect(type1).toEqual('button'); + expect(type2).toEqual('button'); }); }); describe('icons', () => { it('renders "search" icon', () => { - const icons = container.find(Search16); + const icons = wrapper.find(Search16); expect(icons.length).toBe(1); }); it('renders two Icons', () => { - container.setProps({ size: undefined }); + wrapper.setProps({ size: undefined }); const iconTypes = [Search16, Close16]; - const icons = container.findWhere((n) => - iconTypes.includes(n.type()) - ); - expect(icons.length).toBe(2); + const icons = wrapper.findWhere((n) => iconTypes.includes(n.type())); + expect(icons.length).toEqual(2); }); }); }); describe('Small Search', () => { - const small = render( + const small = mount( { }); it('should have the expected small class', () => { - expect(smallContainer.hasClass(`${prefix}--search--sm`)).toBe(true); + expect(smallContainer.hasClass(`${prefix}--search--sm`)).toEqual(true); }); it('should only have 1 button (clear)', () => { const btn = small.find('button'); - expect(btn.length).toBe(1); + expect(btn.length).toEqual(1); }); it('renders two Icons', () => { const iconTypes = [Search16, Close16]; - const icons = container.findWhere((n) => iconTypes.includes(n.type())); - expect(icons.length).toBe(2); + const icons = wrapper.findWhere((n) => iconTypes.includes(n.type())); + expect(icons.length).toEqual(2); }); }); }); @@ -223,9 +183,9 @@ describe('Search', () => { const onChange = jest.fn(); const onClear = jest.fn(); - const container = render( + const wrapper = shallow( { /> ); - const input = container.find('input'); + const input = wrapper.find('input'); const eventObject = { target: { defaultValue: 'test', @@ -251,14 +211,12 @@ describe('Search', () => { }); it('should invoke onClear when input value is cleared', () => { - container.setProps({ value: 'test' }); + wrapper.setProps({ value: 'test' }); const focus = jest.fn(); input.getElement().ref({ focus, }); - container - .find('button') - .simulate('click', { target: { value: 'test' } }); + wrapper.find('button').simulate('click', { target: { value: 'test' } }); expect(onClear).toHaveBeenCalled(); expect(focus).toHaveBeenCalled(); }); @@ -266,45 +224,40 @@ describe('Search', () => { }); }); -// TODO Add skeleton tests +/** + * 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('changes the hasContent state upon change in props', () => { - const container = render( - - ); - expect(container.find('input')).not.toHaveClass( - `${prefix}--search-close--hidden` - ); - container.setProps({ value: '' }); - expect(container.find('input')).toHaveClass( - `${prefix}--search-close--hidden` - ); + it('should have empty value', () => { + shallow(); + expect(getInputValue).toBe(''); }); - it('avoids change the hasContent state upon setting props, unless the value actually changes', () => { - const container = render( + it('should set value if value prop is added', () => { + shallow( ); - expect(container.find('input')).toHaveClass( - `${prefix}--search-close--hidden` - ); - container.setState({ hasContent: false }); - container.setProps({ value: 'foo' }); - expect(container.find('input')).not.toHaveClass( - `${prefix}--search-close--hidden` - ); + + expect(getInputValue).toBe('foo'); }); }); From 67ceab94f7d6b94d006a913d3d34c031c049feab Mon Sep 17 00:00:00 2001 From: Ale Davila Date: Thu, 11 Nov 2021 12:04:17 -0600 Subject: [PATCH 12/15] test(search): update tests --- .../react/src/components/Search/next/Search-test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index be11ed503c1f..1403290a6ae8 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -244,12 +244,14 @@ const getInputValue = (wrapper) => { describe('Detecting change in value from props', () => { it('should have empty value', () => { - shallow(); - expect(getInputValue).toBe(''); + const search = shallow( + + ); + expect(getInputValue(search)).toBe(''); }); it('should set value if value prop is added', () => { - shallow( + const search = shallow( { /> ); - expect(getInputValue).toBe('foo'); + expect(getInputValue(search)).toBe('foo'); }); }); From f9133e91bb956de635fdd7c0bcf3aafd4dd88436 Mon Sep 17 00:00:00 2001 From: Taylor Jones Date: Thu, 11 Nov 2021 15:53:38 -0600 Subject: [PATCH 13/15] test(search): reapply props from destructure, update tests --- .../src/components/Search/next/Search-test.js | 2 +- .../src/components/Search/next/Search.js | 65 ++++++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 1403290a6ae8..4ed03c25884d 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -247,7 +247,7 @@ describe('Detecting change in value from props', () => { const search = shallow( ); - expect(getInputValue(search)).toBe(''); + expect(getInputValue(search)).toBe(undefined); }); it('should set value if value prop is added', () => { diff --git a/packages/react/src/components/Search/next/Search.js b/packages/react/src/components/Search/next/Search.js index 70b29d4771ad..8ddad6463af9 100644 --- a/packages/react/src/components/Search/next/Search.js +++ b/packages/react/src/components/Search/next/Search.js @@ -6,7 +6,7 @@ */ import PropTypes from 'prop-types'; -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import cx from 'classnames'; import { Search16, Close16 } from '@carbon/icons-react'; import { composeEventHandlers } from '../../../tools/events'; @@ -15,26 +15,29 @@ import { useId } from '../../../internal/useId'; import deprecate from '../../../prop-types/deprecate'; import { usePrefix } from '../../../internal/usePrefix'; -export default function Search({ - className, - closeButtonLabelText, - defaultValue, - disabled, - id, - labelText, - light, - onChange, - onClear, - onKeyDown, - placeHolderText, - placeholder, - renderIcon, - size = !small ? 'lg' : 'sm', - small, - type, - value, - ...rest -}) { +const Search = React.forwardRef(function Search( + { + className, + closeButtonLabelText, + defaultValue, + disabled, + id, + labelText, + light, + onChange, + onClear, + onKeyDown, + placeHolderText, + placeholder, + renderIcon, + size = !small ? 'lg' : 'sm', + small, + type, + value, + ...rest + }, + ref +) { const prefix = usePrefix(); const input = useRef(null); @@ -68,9 +71,15 @@ export default function Search({ onClear(); - setHasContent(false, () => input.current.focus()); + setHasContent(false); } + useEffect(() => { + if (!hasContent) { + input.current.focus(); + } + }, [hasContent]); + function handleChange(evt) { setHasContent(evt.target.value !== ''); } @@ -110,7 +119,11 @@ export default function Search({ ); return ( -
+
{searchIcon}
@@ -120,6 +133,8 @@ export default function Search({
); -} +}); Search.propTypes = { /** @@ -249,3 +264,5 @@ Search.defaultProps = { onChange: () => {}, onClear: () => {}, }; + +export default Search; From a16c05598a98e43fb36e2b217060fb37eb2f3d4a Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 7 Dec 2021 10:40:49 -0600 Subject: [PATCH 14/15] refactor(search): update tests and implementation --- .../src/components/Search/next/Search-test.js | 30 ++- .../src/components/Search/next/Search.js | 188 +++++++++--------- 2 files changed, 113 insertions(+), 105 deletions(-) diff --git a/packages/react/src/components/Search/next/Search-test.js b/packages/react/src/components/Search/next/Search-test.js index 4ed03c25884d..68fd7b93136b 100644 --- a/packages/react/src/components/Search/next/Search-test.js +++ b/packages/react/src/components/Search/next/Search-test.js @@ -210,15 +210,31 @@ describe('Search', () => { expect(onChange).toHaveBeenCalledWith(eventObject); }); - it('should invoke onClear when input value is cleared', () => { - wrapper.setProps({ value: 'test' }); - const focus = jest.fn(); - input.getElement().ref({ - focus, + 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', + }, }); - wrapper.find('button').simulate('click', { target: { value: 'test' } }); expect(onClear).toHaveBeenCalled(); - expect(focus).toHaveBeenCalled(); + expect(wrapper.find('input').getDOMNode()).toHaveFocus(); + + document.body.removeChild(node); }); }); }); diff --git a/packages/react/src/components/Search/next/Search.js b/packages/react/src/components/Search/next/Search.js index 8ddad6463af9..d354897622bc 100644 --- a/packages/react/src/components/Search/next/Search.js +++ b/packages/react/src/components/Search/next/Search.js @@ -5,63 +5,72 @@ * LICENSE file in the root directory of this source tree. */ -import PropTypes from 'prop-types'; -import React, { useState, useRef, useEffect } from 'react'; -import cx from 'classnames'; import { Search16, Close16 } from '@carbon/icons-react'; -import { composeEventHandlers } from '../../../tools/events'; +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 deprecate from '../../../prop-types/deprecate'; import { usePrefix } from '../../../internal/usePrefix'; +import deprecate from '../../../prop-types/deprecate'; +import { composeEventHandlers } from '../../../tools/events'; -const Search = React.forwardRef(function Search( - { - className, - closeButtonLabelText, - defaultValue, - disabled, - id, - labelText, - light, - onChange, - onClear, - onKeyDown, - placeHolderText, - placeholder, - renderIcon, - size = !small ? 'lg' : 'sm', - small, - type, - value, - ...rest - }, - ref -) { +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(evt) { + function clearInput(event) { if (!value) { input.current.value = ''; - onChange(evt); + onChange(event); } else { - const clearedEvt = Object.assign({}, evt.target, { + const clearedEvt = Object.assign({}, event.target, { target: { value: '', }, @@ -70,94 +79,62 @@ const Search = React.forwardRef(function Search( } onClear(); - setHasContent(false); + focus(input); } - useEffect(() => { - if (!hasContent) { - input.current.focus(); - } - }, [hasContent]); - - function handleChange(evt) { - setHasContent(evt.target.value !== ''); + function handleChange(event) { + setHasContent(event.target.value !== ''); } - function handleKeyDown(evt) { - if (match(evt, keys.Escape)) { - clearInput(evt); + function handleKeyDown(event) { + if (match(event, keys.Escape)) { + clearInput(event); } } - 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, - }); - - let customIcon; - if (renderIcon) { - customIcon = React.cloneElement(renderIcon, { - className: `${prefix}--search-magnifier-icon`, - }); - } - - const searchIcon = renderIcon ? ( - customIcon - ) : ( - - ); - return ( -
+
- {searchIcon} +
); -}); +} 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 */ @@ -229,6 +206,11 @@ Search.propTypes = { */ renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + /** + * Specify the role for the underlying ``, defaults to `searchbox` + */ + role: PropTypes.string, + /** * Specify the search size */ @@ -257,12 +239,22 @@ Search.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; -Search.defaultProps = { - type: 'text', - placeholder: '', - closeButtonLabelText: 'Clear search input', - onChange: () => {}, - onClear: () => {}, +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; From a34c97c9266f26015575c188aac39424db8dc0be Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 7 Dec 2021 10:43:07 -0600 Subject: [PATCH 15/15] fix(search): stop propagation from escape keydown event --- packages/react/src/components/Search/next/Search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/components/Search/next/Search.js b/packages/react/src/components/Search/next/Search.js index d354897622bc..d1d7b2b77c34 100644 --- a/packages/react/src/components/Search/next/Search.js +++ b/packages/react/src/components/Search/next/Search.js @@ -89,6 +89,7 @@ function Search({ function handleKeyDown(event) { if (match(event, keys.Escape)) { + event.stopPropagation(); clearInput(event); } }