From d5d10a0c528eb8c47b8ec3511616a39e061c6869 Mon Sep 17 00:00:00 2001 From: Zach Gianos Date: Tue, 4 Oct 2022 07:05:04 -1000 Subject: [PATCH] fix(ExpandableSearch): compose prop handlers with internal (#12135) (#12143) * fix(ExpandableSearch): compose prop handlers with internal (#12135) * fix(ExpandableSearch): compose prop handlers with internal * fix(Search): forward `onExpand` to icon `onClick` for event composition Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../ExpandableSearch/ExpandableSearch-test.js | 206 ++++++++++++++++++ .../ExpandableSearch/ExpandableSearch.js | 23 +- .../react/src/components/Search/Search.js | 1 + .../src/components/Search/next/Search.js | 8 +- 4 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js diff --git a/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js b/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js new file mode 100644 index 000000000000..93f8338639b8 --- /dev/null +++ b/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js @@ -0,0 +1,206 @@ +/** + * 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 { mount } from 'enzyme'; +import React from 'react'; +import Search from './ExpandableSearch'; + +const prefix = 'cds'; + +describe('ExpandableSearch', () => { + let wrapper; + + const container = () => wrapper.find(`.${prefix}--search`); + const button = () => wrapper.find('button'); + const input = () => wrapper.find('input'); + const label = () => wrapper.find('label'); + + const render = (props) => { + if (wrapper) { + return wrapper.setProps(props); + } + + wrapper = mount(); + + return wrapper; + }; + + describe('container', () => { + beforeEach(() => { + render(); + }); + + it('has the class `${prefix}--search--expandable`', () => { + const value = container().hasClass(`${prefix}--search--expandable`); + expect(value).toEqual(true); + }); + + describe('expanded', () => { + const value = () => container().hasClass(`${prefix}--search--expanded`); + + describe('when input has no content', () => { + beforeEach(() => { + input().simulate('change', { target: { value: '' } }); + }); + + it('is false', () => { + expect(value()).toEqual(false); + }); + }); + + describe.skip('when input has content', () => { + beforeEach(() => { + input().simulate('change', { target: { value: 'text' } }); + }); + + it('is true', () => { + expect(value()).toEqual(true); + }); + + describe('when content is cleared', () => { + beforeEach(() => { + button().simulate('click'); + }); + + it('is false', () => { + expect(value()).toEqual(false); + }); + }); + }); + }); + }); + + describe('label', () => { + beforeEach(() => { + render(); + }); + + it('is rendered', () => { + expect(label().text()).toEqual('testlabel'); + }); + }); + + describe('onBlur', () => { + const onBlur = jest.fn(); + + beforeEach(() => { + render({ onBlur }); + }); + + afterEach(() => { + onBlur.mockReset(); + }); + + it('is called on blur', () => { + input().simulate('blur'); + expect(onBlur).toHaveBeenCalled(); + }); + }); + + describe('onChange', () => { + const onChange = jest.fn(); + + beforeEach(() => { + render({ onChange }); + }); + + afterEach(() => { + onChange.mockReset(); + }); + + it('is called on change', () => { + input().simulate('change', { target: { value: 'text' } }); + expect(onChange).toHaveBeenCalled(); + }); + }); + + describe('onClick', () => { + const onClick = jest.fn(); + + beforeEach(() => { + render({ onClick }); + }); + + afterEach(() => { + onClick.mockReset(); + }); + + it('is called on click', () => { + input().simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); + }); + + describe('onClear', () => { + const onClear = jest.fn(); + + beforeEach(() => { + render({ onClear }); + }); + + afterEach(() => { + onClear.mockReset(); + }); + + describe('when input has no content', () => { + beforeEach(() => { + input().simulate('change', { target: { value: '' } }); + }); + + it('is called on clear', () => { + button().simulate('click'); + expect(onClear).toHaveBeenCalled(); + }); + }); + + describe('when input has content', () => { + beforeEach(() => { + input().simulate('change', { target: { value: 'text' } }); + }); + + it('is called on clear', () => { + button().simulate('click'); + expect(onClear).toHaveBeenCalled(); + }); + }); + }); + + describe('onExpand', () => { + const onExpand = jest.fn(); + + beforeEach(() => { + render({ onExpand }); + }); + + afterEach(() => { + onExpand.mockReset(); + }); + + // This won't work until v11 + it.skip('is called on focus', () => { + input().simulate('focus'); + expect(onExpand).toHaveBeenCalled(); + }); + }); + + describe('onFocus', () => { + const onFocus = jest.fn(); + + beforeEach(() => { + render({ onFocus }); + }); + + afterEach(() => { + onFocus.mockReset(); + }); + + it('is called on focus', () => { + input().simulate('focus'); + expect(onFocus).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/react/src/components/ExpandableSearch/ExpandableSearch.js b/packages/react/src/components/ExpandableSearch/ExpandableSearch.js index 912b6d675e34..143544adb094 100644 --- a/packages/react/src/components/ExpandableSearch/ExpandableSearch.js +++ b/packages/react/src/components/ExpandableSearch/ExpandableSearch.js @@ -9,8 +9,9 @@ import React, { useState, useRef } from 'react'; import classnames from 'classnames'; import Search from '../Search'; import { usePrefix } from '../../internal/usePrefix'; +import { composeEventHandlers } from '../../tools/events'; -function ExpandableSearch(props) { +function ExpandableSearch({ onBlur, onChange, onExpand, onFocus, ...props }) { const [expanded, setExpanded] = useState(false); const [hasContent, setHasContent] = useState(false); const searchRef = useRef(null); @@ -32,6 +33,14 @@ function ExpandableSearch(props) { } } + function handleChange(evt) { + setHasContent(evt.target.value !== ''); + } + + function handleExpand() { + searchRef.current.focus?.(); + } + const classes = classnames( `${prefix}--search--expandable`, { @@ -45,14 +54,10 @@ function ExpandableSearch(props) { {...props} ref={searchRef} className={classes} - onFocus={handleFocus} - onBlur={handleBlur} - onChange={(event) => { - setHasContent(event.target.value !== ''); - }} - onExpand={() => { - searchRef.current.focus?.(); - }} + onFocus={composeEventHandlers([onFocus, handleFocus])} + onBlur={composeEventHandlers([onBlur, handleBlur])} + onChange={composeEventHandlers([onChange, handleChange])} + onExpand={composeEventHandlers([onExpand, handleExpand])} /> ); } diff --git a/packages/react/src/components/Search/Search.js b/packages/react/src/components/Search/Search.js index fa0b20f0cfe2..a23773ff2e07 100644 --- a/packages/react/src/components/Search/Search.js +++ b/packages/react/src/components/Search/Search.js @@ -190,6 +190,7 @@ export default class Search extends Component { onKeyDown, renderIcon, onClear, // eslint-disable-line no-unused-vars + onExpand, // eslint-disable-line no-unused-vars, react/prop-types ...other } = this.props; diff --git a/packages/react/src/components/Search/next/Search.js b/packages/react/src/components/Search/next/Search.js index 7ada713e3354..69e78223a080 100644 --- a/packages/react/src/components/Search/next/Search.js +++ b/packages/react/src/components/Search/next/Search.js @@ -95,17 +95,13 @@ const Search = React.forwardRef(function Search( return (
- {/* the magnifier is used in ExpandableSearch as a click target to expand, + {/* the magnifier is used in ExpandableSearch as a click target to expand, however, it does not need a keyboard event bc the input element gets focus on keyboard nav and expands that way*/} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
{ - if (onExpand) { - onExpand(); - } - }}> + onClick={onExpand}>