Skip to content

Commit

Permalink
STCOM-1220 Make <SearchField> support input and textarea as an inpu…
Browse files Browse the repository at this point in the history
…t field (#2158)

* STCOM-1220 Make `<SearchField>` support input and textarea as an input field

* STCOM-1220 remove debugging comments

* STCOM-1220 Remove unused styles

* STCOM-1220 Added TextArea tests and prop descriptions

* STCOM-1220 Added SearchField tests and prop descriptions

* STCOM-1220 Fixed a typo

* STCOM-1220 Omit some props in TextArea so they are not passed to dom element

* STCOM-1220 Make SearchField textarea have default height of 1 row
  • Loading branch information
BogdanDenis authored Oct 30, 2023
1 parent 051940f commit 2d693f6
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Add `hasMatchSelection` to `<AdvancedSearch>` 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 `<SearchField>` 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)
Expand Down
86 changes: 68 additions & 18 deletions lib/SearchField/SearchField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -64,6 +83,10 @@ const SearchField = (props) => {
searchableIndexesPlaceholder,
inputClass,
disabled,
inputType,
onSubmitSearch,
lockWidth,
newLineOnShiftEnter,
...rest
} = props;

Expand Down Expand Up @@ -112,35 +135,62 @@ 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 (
<div className={rootStyles}>
{searchableIndexesDropdown}
<TextField
{...rest}
aria-label={rest['aria-label'] || ariaLabel}
clearFieldId={clearSearchId}
disabled={disabled}
focusedClass={css.isFocused}
id={id}
hasClearIcon={typeof onClear === 'function' && loading !== true}
inputClass={classNames(css.input, inputClass)}
loading={loading}
onChange={onChange}
onClearField={onClear}
placeholder={inputPlaceholder}
startControl={!hasSearchableIndexes && !searchableOptions ? searchIcon : null}
type="search"
value={value || ''}
readOnly={loading || rest.readOnly}
/>
<Component {...getInputComponentProps()} />
</div>
);
};

SearchField.propTypes = propTypes;
SearchField.defaultProps = {
loading: false,
lockWidth: false,
newLineOnShiftEnter: false,
searchableOptions: null,
inputType: INPUT_TYPES.INPUT,
onSubmitSearch: noop,
};

export default SearchField;
3 changes: 3 additions & 0 deletions lib/SearchField/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions lib/SearchField/tests/SearchField-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ describe('SearchField', () => {
});
});

describe('supplying an inputType="textarea"', () => {
beforeEach(async () => {
await mountWithContext(
<SearchField
id="searchFieldTest"
inputType="textarea"
/>
);
});

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' },
Expand Down
5 changes: 5 additions & 0 deletions lib/TextArea/TextArea.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@
max-width: 100%;
}
}

.lockWidth {
min-width: 100%;
max-width: 100%;
}
62 changes: 54 additions & 8 deletions lib/TextArea/TextArea.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -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-');

Expand Down Expand Up @@ -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({
Expand All @@ -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 {
Expand All @@ -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 = (
<textarea
aria-required={required}
Expand All @@ -173,6 +218,7 @@ class TextArea extends Component {
value={this.state.value}
onChange={this.handleChange}
required={required}
onKeyDownCapture={this.handleKeyDown}
{...inputCustom}
/>
);
Expand Down
44 changes: 43 additions & 1 deletion lib/TextArea/tests/TextArea-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import { describe, beforeEach, it } from 'mocha';
import { expect } from 'chai';
import { TextArea as Interactor, Label as LabelInteractor, runAxeTest } from '@folio/stripes-testing';
import sinon from 'sinon';

import { TextArea as Interactor, Label as LabelInteractor, runAxeTest, Keyboard } from '@folio/stripes-testing';

import { mount } from '../../../tests/helpers';
import TextArea from '../TextArea';
Expand Down Expand Up @@ -119,4 +121,44 @@ describe('TextArea', () => {

it('applies the value to the textarea', () => textArea.has({ value: 'test value' }));
});

describe('supplying newLineOnShiftEnter', () => {
const textArea = Interactor();
const onSubmitSearchSpy = sinon.spy();

beforeEach(async () => {
onSubmitSearchSpy.resetHistory();

await mount(
<TextArea
ariaLabel="test label"
value="test value"
newLineOnShiftEnter
onSubmitSearch={onSubmitSearchSpy}
/>
);
});

describe('when pressing Shift+Enter', () => {
beforeEach(async () => {
await textArea.focus();
await Keyboard.pressKey('Enter', { shiftKey: true });
});

it('should not call submit callback', () => {
expect(onSubmitSearchSpy.called).to.be.false;
});
});

describe('when pressing Enter', () => {
beforeEach(async () => {
await textArea.focus();
await Keyboard.pressKey('Enter');
});

it('should call submit callback', () => {
expect(onSubmitSearchSpy.called).to.be.true;
});
});
});
});

0 comments on commit 2d693f6

Please sign in to comment.