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