diff --git a/CHANGELOG.md b/CHANGELOG.md index ce27ac9b2..1b25d3fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add `hasMatchSelection` to `` to hide/show search match selection dropdown. Refs STCOM-1211. * Add z-index of 1 to callout out to have it always render on top of sibling elements. Fixes STCOM-1217. +* Make `` support input and textarea as an input field. Refs STCOM-1220. ## [12.0.0](https://github.com/folio-org/stripes-components/tree/v12.0.0) (2023-10-11) [Full Changelog](https://github.com/folio-org/stripes-components/compare/v11.0.0...v12.0.0) diff --git a/lib/SearchField/SearchField.js b/lib/SearchField/SearchField.js index af3d5f016..9405a73e9 100644 --- a/lib/SearchField/SearchField.js +++ b/lib/SearchField/SearchField.js @@ -8,12 +8,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useIntl } from 'react-intl'; +import noop from 'lodash/noop'; import TextField from '../TextField'; +import TextArea from '../TextArea'; import Select from '../Select'; import TextFieldIcon from '../TextField/TextFieldIcon'; + import css from './SearchField.css'; +const INPUT_TYPES = { + INPUT: 'input', + TEXTAREA: 'textarea', +}; + +const INPUT_COMPONENTS = { + [INPUT_TYPES.INPUT]: TextField, + [INPUT_TYPES.TEXTAREA]: TextArea, +}; + +const TEXTAREA_DEFAULT_HEIGHT = 1; + // Accepts the same props as TextField const propTypes = { ariaLabel: PropTypes.string, @@ -24,10 +39,14 @@ const propTypes = { indexName: PropTypes.string, inputClass: PropTypes.string, inputRef: PropTypes.object, + inputType: PropTypes.oneOf(Object.values(INPUT_TYPES)), loading: PropTypes.bool, + lockWidth: PropTypes.bool, + newLineOnShiftEnter: PropTypes.bool, onChange: PropTypes.func, onChangeIndex: PropTypes.func, onClear: PropTypes.func, + onSubmitSearch: PropTypes.func, placeholder: PropTypes.string, searchableIndexes: PropTypes.arrayOf(PropTypes.shape({ disabled: PropTypes.bool, @@ -64,6 +83,10 @@ const SearchField = (props) => { searchableIndexesPlaceholder, inputClass, disabled, + inputType, + onSubmitSearch, + lockWidth, + newLineOnShiftEnter, ...rest } = props; @@ -112,27 +135,50 @@ const SearchField = (props) => { inputPlaceholder = selectedIndexConfig.placeholder || ''; } + const getInputComponentProps = () => { + // TextField and TextArea have slightly different APIs so we need to pass props correctly + const commonProps = { + ...rest, + 'aria-label': rest['aria-label'] || ariaLabel, + disabled, + id, + loading, + onChange, + startControl: !hasSearchableIndexes && !searchableOptions ? searchIcon : null, + type: 'search', + value: value || '', + readOnly: loading || rest.readOnly, + placeholder: inputPlaceholder, + }; + + const textFieldProps = { + focusedClass: css.isFocused, + inputClass: classNames(css.input, inputClass), + hasClearIcon: typeof onClear === 'function' && loading !== true, + onClearField: onClear, + clearFieldId: clearSearchId, + }; + const textAreaProps = { + rootClass: rest.className, + lockWidth, + onSubmitSearch, + newLineOnShiftEnter, + rows: TEXTAREA_DEFAULT_HEIGHT, + }; + + return { + ...commonProps, + ...(inputType === INPUT_TYPES.INPUT ? textFieldProps : {}), + ...(inputType === INPUT_TYPES.TEXTAREA ? textAreaProps : {}), + } + }; + + const Component = INPUT_COMPONENTS[inputType]; + return (
{searchableIndexesDropdown} - +
); }; @@ -140,7 +186,11 @@ const SearchField = (props) => { SearchField.propTypes = propTypes; SearchField.defaultProps = { loading: false, + lockWidth: false, + newLineOnShiftEnter: false, searchableOptions: null, + inputType: INPUT_TYPES.INPUT, + onSubmitSearch: noop, }; export default SearchField; diff --git a/lib/SearchField/readme.md b/lib/SearchField/readme.md index b550734db..867ea7af9 100644 --- a/lib/SearchField/readme.md +++ b/lib/SearchField/readme.md @@ -78,9 +78,12 @@ placeholder | string | Adds a placeholder to the search input field id | string | Adds an ID to the input field className | string | Adds a className to the root element inputClass | string | Adds a className to the input +inputType | string | Controls if input box should be `input` or `textarea`. Accepted values are `input` and `textarea`. aria-label | string | Adds an aria label to the input field. Camel-case `ariaLabel` is also accepted. value | string | The value of the input field loading | boolean | Adds a loading state to icon (on fetch etc.) +lockWidth | boolean | Prevent user from changing textarea width. Applies only when `inputType` is `textarea` +newLineOnShiftEnter | boolean | Make pressing Shift+Enter enter a new line, and pressing Enter - submit a form. Applies only when `inputType` is `textarea` onChange | function | On change handler for the input field onClear | function | On clear search field callback clearSearchId | string | Adds id to the clear search icon diff --git a/lib/SearchField/tests/SearchField-test.js b/lib/SearchField/tests/SearchField-test.js index 91921c215..de73f889b 100644 --- a/lib/SearchField/tests/SearchField-test.js +++ b/lib/SearchField/tests/SearchField-test.js @@ -67,6 +67,25 @@ describe('SearchField', () => { }); }); + describe('supplying an inputType="textarea"', () => { + beforeEach(async () => { + await mountWithContext( + + ); + }); + + describe('changing the field', () => { + beforeEach(async () => { + await searchField.fillIn('testing text'); + }); + + it('should update value', () => searchField.has({ value: 'testing text' })); + }); + }); + describe('using with indexes', () => { const searchableIndexes = [ { label: 'ID', value: 'id' }, diff --git a/lib/TextArea/TextArea.css b/lib/TextArea/TextArea.css index 9e2fc3e57..e3fbc7d3c 100644 --- a/lib/TextArea/TextArea.css +++ b/lib/TextArea/TextArea.css @@ -15,3 +15,8 @@ max-width: 100%; } } + +.lockWidth { + min-width: 100%; + max-width: 100%; +} diff --git a/lib/TextArea/TextArea.js b/lib/TextArea/TextArea.js index 491fd8bae..c8ad85a58 100644 --- a/lib/TextArea/TextArea.js +++ b/lib/TextArea/TextArea.js @@ -2,14 +2,16 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import className from 'classnames'; import uniqueId from 'lodash/uniqueId'; +import noop from 'lodash/noop'; +import Label from '../Label'; +import parseMeta from '../FormField/parseMeta'; import formField from '../FormField'; -import css from './TextArea.css'; import omitProps from '../../util/omitProps'; import sharedInputStylesHelper from '../sharedStyles/sharedInputStylesHelper'; -import parseMeta from '../FormField/parseMeta'; + import formStyles from '../sharedStyles/form.css'; -import Label from '../Label'; +import css from './TextArea.css'; class TextArea extends Component { static propTypes = { @@ -35,16 +37,30 @@ class TextArea extends Component { ]), label: PropTypes.node, loading: PropTypes.bool, + /** + * Prevent user from changing textarea width + */ + lockWidth: PropTypes.bool, marginBottom0: PropTypes.bool, name: PropTypes.string, /** - * Removes border. + * When true - make textarea enter a new line on Shift+Enter press, and submit on Enter press + * When false - do whatever the default behaviour is */ + newLineOnShiftEnter: PropTypes.bool, + /** + * Removes border. + */ noBorder: PropTypes.bool, /** * Event handler for text input. Required if a value is supplied. */ onChange: PropTypes.func, + onKeyDown: PropTypes.func, + /** + * Event handler for submit. Will fire when `newLineOnShiftEnter` is true and user presses Enter key. + */ + onSubmitSearch: PropTypes.func, readOnly: PropTypes.bool, required: PropTypes.bool, rootClass: PropTypes.string, @@ -64,17 +80,18 @@ class TextArea extends Component { }; static defaultProps = { + newLineOnShiftEnter: false, type: 'text', validationEnabled: true, validStylesEnabled: false, + onKeyDown: noop, + onSubmitSearch: noop, value: '', }; constructor(props) { super(props); - this.handleChange = this.handleChange.bind(this); - // if no id has been supplied, generate a unique one this.inputId = props.id ?? uniqueId('textarea-input-'); @@ -116,10 +133,11 @@ class TextArea extends Component { startControl, endControl, sharedInputStylesHelper(this.props), + { [`${css.lockWidth}`]: this.props.lockWidth }, ); } - handleChange(event) { + handleChange = (event) => { const { onChange } = this.props; this.setState({ @@ -132,6 +150,22 @@ class TextArea extends Component { } } + handleKeyDown = (event) => { + const { + onKeyDown, + onSubmitSearch, + newLineOnShiftEnter, + } = this.props; + + if (newLineOnShiftEnter && event.key === 'Enter') { + if (!event.shiftKey) { + onSubmitSearch(event); + } + } + + onKeyDown(event); + } + render() { /* eslint-disable no-unused-vars */ const { @@ -157,7 +191,18 @@ class TextArea extends Component { ...rest } = this.props; - const inputCustom = omitProps(rest, ['id', 'validationEnabled', 'value', 'onChange']); + const inputCustom = omitProps(rest, [ + 'id', + 'validationEnabled', + 'value', + 'onChange', + 'newLineOnShiftEnter', + 'onSubmitSearch', + 'lockWidth', + 'rootClass', + 'marginBottom0', + ]); + const component = (