Adding `TextFieldSearch` component (#16296)
* Adding TextFieldSearch component * Updating docs and stories * Moving controlled test into testing utils * Fixing spelling in prop types af => offeature/default_network_editable
parent
0a5c46b156
commit
6907c4a565
@ -0,0 +1,92 @@ |
|||||||
|
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { TextFieldSearch } from './text-field-search'; |
||||||
|
|
||||||
|
# TextFieldSearch |
||||||
|
|
||||||
|
The `TextFieldSearch` allows users to enter text to search. It wraps the `TextField` component that adds a search icon to the left of the input. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-text-field-search-text-field-search-stories-js--default-story" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
## Props |
||||||
|
|
||||||
|
The `TextFieldSearch` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props), [TextField](/docs/ui-components-component-library-text-field-text-field-stories-js--default-story#props) component props |
||||||
|
|
||||||
|
<ArgsTable of={TextFieldSearch} /> |
||||||
|
|
||||||
|
### Show Clear Button |
||||||
|
|
||||||
|
Use the `showClearButton` prop to display a clear button when `TextFieldSearch` has a value. Use the `clearButtonOnClick` prop to pass an `onClick` event handler to clear the value of the input. |
||||||
|
|
||||||
|
Defaults to `true` |
||||||
|
|
||||||
|
The clear button uses [ButtonIcon](/docs/ui-components-component-library-button-icon-button-icon-stories-js--default-story) and accepts all props from that component. |
||||||
|
|
||||||
|
**NOTE: The `showClearButton` only works with a controlled input.** |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-text-field-search-text-field-search-stories-js--show-clear-button" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { TextFieldSearch } from '../../ui/component-library/text-field'; |
||||||
|
|
||||||
|
const [value, setValue] = useState('show clear'); |
||||||
|
|
||||||
|
const handleOnChange = (e) => { |
||||||
|
setValue(e.target.value); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOnClear = () => { |
||||||
|
setValue(''); |
||||||
|
}; |
||||||
|
|
||||||
|
<TextFieldSearch |
||||||
|
placeholder="Enter text to show clear" |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
clearButtonOnClick={handleOnClear} |
||||||
|
/>; |
||||||
|
``` |
||||||
|
|
||||||
|
### Clear Button Props |
||||||
|
|
||||||
|
Use the `clearButtonProps` to access other props of the clear button. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-text-field-search-text-field-search-stories-js--clear-button-props" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import { |
||||||
|
SIZES, |
||||||
|
COLORS, |
||||||
|
BORDER_RADIUS, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import { TextFieldSearch } from '../../ui/component-library/text-field'; |
||||||
|
|
||||||
|
const [value, setValue] = useState('show clear'); |
||||||
|
|
||||||
|
const handleOnChange = (e) => { |
||||||
|
setValue(e.target.value); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOnClear = () => { |
||||||
|
setValue(''); |
||||||
|
}; |
||||||
|
|
||||||
|
<TextFieldSearch |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
clearButtonOnClick={handleOnClear} |
||||||
|
clearButtonProps={{ |
||||||
|
backgroundColor: COLORS.BACKGROUND_ALTERNATIVE, |
||||||
|
borderRadius: BORDER_RADIUS.XS, |
||||||
|
'data-testid': 'clear-button', |
||||||
|
}} |
||||||
|
/>; |
||||||
|
``` |
@ -0,0 +1 @@ |
|||||||
|
export { TextFieldSearch } from './text-field-search'; |
@ -0,0 +1,62 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import classnames from 'classnames'; |
||||||
|
|
||||||
|
import { SIZES } from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import { ButtonIcon } from '../button-icon'; |
||||||
|
import { Icon, ICON_NAMES } from '../icon'; |
||||||
|
import { TextFieldBase, TEXT_FIELD_BASE_TYPES } from '../text-field-base'; |
||||||
|
import { TextField } from '../text-field'; |
||||||
|
|
||||||
|
export const TextFieldSearch = ({ |
||||||
|
value, |
||||||
|
onChange, |
||||||
|
showClearButton = true, |
||||||
|
clearButtonOnClick, |
||||||
|
clearButtonProps, |
||||||
|
className, |
||||||
|
...props |
||||||
|
}) => ( |
||||||
|
<TextField |
||||||
|
className={classnames('mm-text-field-search', className)} |
||||||
|
value={value} |
||||||
|
onChange={onChange} |
||||||
|
type={TEXT_FIELD_BASE_TYPES.SEARCH} |
||||||
|
leftAccessory={<Icon name={ICON_NAMES.SEARCH_FILLED} size={SIZES.SM} />} |
||||||
|
showClearButton={showClearButton} |
||||||
|
clearButtonOnClick={clearButtonOnClick} |
||||||
|
clearButtonProps={clearButtonProps} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
); |
||||||
|
|
||||||
|
TextFieldSearch.propTypes = { |
||||||
|
/** |
||||||
|
* The value of the TextFieldSearch |
||||||
|
*/ |
||||||
|
value: TextFieldBase.propTypes.value, |
||||||
|
/** |
||||||
|
* The onChange handler of the TextFieldSearch |
||||||
|
*/ |
||||||
|
onChange: TextFieldBase.propTypes.onChange, |
||||||
|
/** |
||||||
|
* Show a clear button to clear the input |
||||||
|
* Defaults to true |
||||||
|
*/ |
||||||
|
showClearButton: PropTypes.bool, |
||||||
|
/** |
||||||
|
* The onClick handler for the clear button |
||||||
|
*/ |
||||||
|
clearButtonOnClick: PropTypes.func, |
||||||
|
/** |
||||||
|
* The props to pass to the clear button |
||||||
|
*/ |
||||||
|
clearButtonProps: PropTypes.shape(ButtonIcon.PropTypes), |
||||||
|
/** |
||||||
|
* An additional className to apply to the TextFieldSearch |
||||||
|
*/ |
||||||
|
className: PropTypes.string, |
||||||
|
}; |
||||||
|
|
||||||
|
TextFieldSearch.displayName = 'TextFieldSearch'; |
@ -0,0 +1,5 @@ |
|||||||
|
.mm-text-field-search { |
||||||
|
::-webkit-search-cancel-button { |
||||||
|
display: none; // hides the default search cancel button |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,212 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { useArgs } from '@storybook/client-api'; |
||||||
|
|
||||||
|
import { |
||||||
|
SIZES, |
||||||
|
COLORS, |
||||||
|
BORDER_RADIUS, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import { TEXT_FIELD_SIZES, TEXT_FIELD_TYPES } from '../text-field'; |
||||||
|
|
||||||
|
import { TextFieldSearch } from './text-field-search'; |
||||||
|
import README from './README.mdx'; |
||||||
|
|
||||||
|
const marginSizeControlOptions = [ |
||||||
|
undefined, |
||||||
|
0, |
||||||
|
1, |
||||||
|
2, |
||||||
|
3, |
||||||
|
4, |
||||||
|
5, |
||||||
|
6, |
||||||
|
7, |
||||||
|
8, |
||||||
|
9, |
||||||
|
10, |
||||||
|
11, |
||||||
|
12, |
||||||
|
'auto', |
||||||
|
]; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/ComponentLibrary/TextFieldSearch', |
||||||
|
id: __filename, |
||||||
|
component: TextFieldSearch, |
||||||
|
parameters: { |
||||||
|
docs: { |
||||||
|
page: README, |
||||||
|
}, |
||||||
|
}, |
||||||
|
argTypes: { |
||||||
|
value: { |
||||||
|
control: 'text', |
||||||
|
}, |
||||||
|
onChange: { |
||||||
|
action: 'onChange', |
||||||
|
}, |
||||||
|
showClearButton: { |
||||||
|
control: 'boolean', |
||||||
|
}, |
||||||
|
clearButtonOnClick: { |
||||||
|
action: 'clearButtonOnClick', |
||||||
|
}, |
||||||
|
clearButtonProps: { |
||||||
|
control: 'object', |
||||||
|
}, |
||||||
|
autoComplete: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
autoFocus: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
className: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
disabled: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
error: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
id: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
inputProps: { |
||||||
|
control: 'object', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
leftAccessory: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
maxLength: { |
||||||
|
control: 'number', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
name: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onBlur: { |
||||||
|
action: 'onBlur', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onClick: { |
||||||
|
action: 'onClick', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onFocus: { |
||||||
|
action: 'onFocus', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onKeyDown: { |
||||||
|
action: 'onKeyDown', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onKeyUp: { |
||||||
|
action: 'onKeyUp', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
placeholder: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
readOnly: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
required: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
rightAccessory: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
size: { |
||||||
|
control: 'select', |
||||||
|
options: Object.values(TEXT_FIELD_SIZES), |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
type: { |
||||||
|
control: 'select', |
||||||
|
options: Object.values(TEXT_FIELD_TYPES), |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
truncate: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
marginTop: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
marginRight: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
marginBottom: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
marginLeft: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
showClearButton: true, |
||||||
|
placeholder: 'Search', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const Template = (args) => { |
||||||
|
const [{ value }, updateArgs] = useArgs(); |
||||||
|
const handleOnChange = (e) => { |
||||||
|
updateArgs({ value: e.target.value }); |
||||||
|
}; |
||||||
|
const handleOnClear = () => { |
||||||
|
updateArgs({ value: '' }); |
||||||
|
}; |
||||||
|
return ( |
||||||
|
<TextFieldSearch |
||||||
|
{...args} |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
clearButtonOnClick={handleOnClear} |
||||||
|
/> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = Template.bind({}); |
||||||
|
DefaultStory.storyName = 'Default'; |
||||||
|
|
||||||
|
export const ShowClearButton = Template.bind({}); |
||||||
|
|
||||||
|
ShowClearButton.args = { |
||||||
|
placeholder: 'Enter text to show clear', |
||||||
|
showClearButton: true, |
||||||
|
}; |
||||||
|
|
||||||
|
export const ClearButtonProps = Template.bind({}); |
||||||
|
ClearButtonProps.args = { |
||||||
|
value: 'clear button props', |
||||||
|
size: SIZES.LG, |
||||||
|
showClearButton: true, |
||||||
|
clearButtonProps: { |
||||||
|
backgroundColor: COLORS.BACKGROUND_ALTERNATIVE, |
||||||
|
borderRadius: BORDER_RADIUS.XS, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,57 @@ |
|||||||
|
/* eslint-disable jest/require-top-level-describe */ |
||||||
|
import React from 'react'; |
||||||
|
import { render } from '@testing-library/react'; |
||||||
|
import { renderControlledInput } from '../../../../test/lib/render-helpers'; |
||||||
|
import { TextFieldSearch } from './text-field-search'; |
||||||
|
|
||||||
|
describe('TextFieldSearch', () => { |
||||||
|
it('should render correctly', () => { |
||||||
|
const { getByRole } = render(<TextFieldSearch />); |
||||||
|
expect(getByRole('searchbox')).toBeDefined(); |
||||||
|
}); |
||||||
|
it('should render showClearButton button when showClearButton is true and value exists', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const { user, getByRole } = renderControlledInput(TextFieldSearch, { |
||||||
|
showClearButton: true, |
||||||
|
}); |
||||||
|
await user.type(getByRole('searchbox'), 'test value'); |
||||||
|
expect(getByRole('searchbox')).toHaveValue('test value'); |
||||||
|
expect(getByRole('button', { name: /Clear/u })).toBeDefined(); |
||||||
|
}); |
||||||
|
it('should still render with the rightAccessory when showClearButton is true', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const { user, getByRole, getByText } = renderControlledInput( |
||||||
|
TextFieldSearch, |
||||||
|
{ |
||||||
|
showClearButton: true, |
||||||
|
rightAccessory: <div>right-accessory</div>, |
||||||
|
}, |
||||||
|
); |
||||||
|
await user.type(getByRole('searchbox'), 'test value'); |
||||||
|
expect(getByRole('searchbox')).toHaveValue('test value'); |
||||||
|
expect(getByRole('button', { name: /Clear/u })).toBeDefined(); |
||||||
|
expect(getByText('right-accessory')).toBeDefined(); |
||||||
|
}); |
||||||
|
it('should fire onClick event when passed to clearButtonOnClick when clear button is clicked', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const fn = jest.fn(); |
||||||
|
const { user, getByRole } = renderControlledInput(TextFieldSearch, { |
||||||
|
showClearButton: true, |
||||||
|
clearButtonOnClick: fn, |
||||||
|
}); |
||||||
|
await user.type(getByRole('searchbox'), 'test value'); |
||||||
|
await user.click(getByRole('button', { name: /Clear/u })); |
||||||
|
expect(fn).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
it('should fire onClick event when passed to clearButtonProps.onClick prop', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const fn = jest.fn(); |
||||||
|
const { user, getByRole } = renderControlledInput(TextFieldSearch, { |
||||||
|
showClearButton: true, |
||||||
|
clearButtonProps: { onClick: fn }, |
||||||
|
}); |
||||||
|
await user.type(getByRole('searchbox'), 'test value'); |
||||||
|
await user.click(getByRole('button', { name: /Clear/u })); |
||||||
|
expect(fn).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue