Skip to content

Commit

Permalink
STCOM-1240 Added support for clear icon in TextArea (#2181)
Browse files Browse the repository at this point in the history
* STCOM-1240 Added support for clear icon in TextArea

* STCOM-1240 Handle resize of TextArea

* STCOM-1240 Fix TextArea end controls position with rtl and when losing focus
  • Loading branch information
BogdanDenis authored Jan 9, 2024
1 parent c984830 commit 6ea7862
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Enable spinner on Datepicker year input. Refs STCOM-1225.
* TextLink - underline showing up on nested spans with 'display: inline-flex'. Refs STCOM-1226.
* Use the default search option instead of an unsupported one in Advanced search. Refs STCOM-1242.
* Added support for clear icon in `<TextArea>`. Refs STCOM-1240.

## [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
6 changes: 3 additions & 3 deletions lib/SearchField/SearchField.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ const SearchField = (props) => {
readOnly: loading || rest.readOnly,
placeholder: inputPlaceholder,
inputRef,
hasClearIcon: typeof onClear === 'function',
clearFieldId: clearSearchId,
onClearField: onClear,
};

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

.startControls,
.endControls {
position: absolute;
inset-inline-end: 8px; /* leave some space for textarea's resize control */
bottom: 1px; /* makes the controls look more vertically centered in single line textareas */

pointer-events: none;
height: auto;
display: flex;
justify-content: flex-start;
align-items: stretch;
flex: 0;
}

.startControls {
justify-content: flex-start;
padding: 0 0 0 var(--input-horizontal-padding);
}

[dir="rtl"] .startControls {
padding: 0 var(--input-horizontal-padding) 0 0;
}

.endControls {
justify-content: flex-end;
padding: 0 var(--input-horizontal-padding) 0 0;
}

[dir="rtl"] .endControls {
padding: 0 0 0 var(--input-horizontal-padding);
}

.controlGroup {
pointer-events: all;
display: flex;
align-items: center;
}
166 changes: 159 additions & 7 deletions lib/TextArea/TextArea.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import className from 'classnames';
import { FormattedMessage } from 'react-intl';
import uniqueId from 'lodash/uniqueId';
import noop from 'lodash/noop';

import Label from '../Label';
import parseMeta from '../FormField/parseMeta';
import formField from '../FormField';
import TextFieldIcon from '../TextField/TextFieldIcon';
import omitProps from '../../util/omitProps';
import sharedInputStylesHelper from '../sharedStyles/sharedInputStylesHelper';

import formStyles from '../sharedStyles/form.css';
import css from './TextArea.css';

const RESIZE_HANDLE_WIDTH = 8;

class TextArea extends Component {
static propTypes = {
ariaLabel: PropTypes.string,
ariaLabelledBy: PropTypes.string,
autoFocus: PropTypes.bool,
/**
* Id to apply to clear field button.
*/
clearFieldId: PropTypes.string,
dirty: PropTypes.bool,
disabled: PropTypes.bool,
endControl: PropTypes.element,
Expand All @@ -30,6 +38,10 @@ class TextArea extends Component {
* Will resize the textarea to be 100% of parent element's width
*/
fullWidth: PropTypes.bool,
/**
* When set to false, will not show clear button.
*/
hasClearIcon: PropTypes.bool,
id: PropTypes.string,
inputRef: PropTypes.oneOfType([
PropTypes.func,
Expand All @@ -52,10 +64,22 @@ class TextArea extends Component {
* Removes border.
*/
noBorder: PropTypes.bool,
/**
* Callback fired when the input is blurred.
*/
onBlur: PropTypes.func,
/**
* Event handler for text input. Required if a value is supplied.
*/
onChange: PropTypes.func,
/**
* Callback fired when the input is cleared.
*/
onClearField: PropTypes.func,
/**
* Callback fired when the input is focused.
*/
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
/**
* Event handler for submit. Will fire when `newLineOnShiftEnter` is true and user presses Enter key.
Expand All @@ -81,6 +105,9 @@ class TextArea extends Component {
validStylesEnabled: false,
onKeyDown: noop,
onSubmitSearch: noop,
onBlur: noop,
onFocus: noop,
onClearField: noop,
value: '',
};

Expand All @@ -91,9 +118,24 @@ class TextArea extends Component {
this.inputId = props.id ?? uniqueId('textarea-input-');

this.state = {
focused: false,
prevPropsValue: props.value,
endControlInset: 0,
value: props.value,
};

this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { borderBoxSize } = entry;

const dimensions = {
width: borderBoxSize[0].inlineSize,
height: borderBoxSize[0].blockSize,
};

this.moveEndContent(dimensions)
}
});
}

static getDerivedStateFromProps(props, state) {
Expand All @@ -111,15 +153,41 @@ class TextArea extends Component {
return null;
}

containerRef = React.createRef();

moveEndContent = (dimensions) => {
const containerWidth = this.containerRef?.current?.offsetWidth;

const resizeDiff = dimensions.width - containerWidth;

this.setState({ endControlInset: -(resizeDiff - RESIZE_HANDLE_WIDTH) });
};

setInputRef = (ref) => {
if (this.props.inputRef) {
this.props.inputRef.current = ref;
}

if (ref) {
this.resizeObserver.observe(ref);
}
}

getRootStyle() {
return className(
css.textArea,
formStyles.inputGroup,
this.props.rootClass,
{ [`${css.fullWidth}`]: this.props.fullWidth },
);
}

getInputGroupStyle() {
return className(
formStyles.inputGroup,
{ [`${css.hasClearIcon}`]: this.props.hasClearIcon },
);
}

getInputStyle() {
const endControl = this.props.endControl ? css.hasEndControl : '';
const startControl = this.props.startControl ? css.hasStartControl : '';
Expand All @@ -132,6 +200,42 @@ class TextArea extends Component {
);
}

onFocus = event => {
const { onFocus } = this.props;

if (!this.state.focused) {
this.setState({
focused: true
});

onFocus(event);

setTimeout(() => {
const dimensions = {
width: this.props.inputRef?.current?.offsetWidth,
};
this.moveEndContent(dimensions);
});
}
}

onBlur = event => {
const { onBlur } = this.props;
const { currentTarget, relatedTarget } = event;

if (!(relatedTarget && currentTarget.contains(relatedTarget))) {
// delay focus stay setting for a whole tick. This is intended to keep the clear button around long enough for its
// click event to fire on iOS devices.
this.resetFocusTO = setTimeout(() => {
this.setState({
focused: false
});
});

onBlur(event);
}
}

handleChange = (event) => {
const { onChange } = this.props;

Expand Down Expand Up @@ -183,6 +287,9 @@ class TextArea extends Component {
valid,
validStylesEnabled,
warning,
hasClearIcon,
clearFieldId,
onClearField,
...rest
} = this.props;

Expand All @@ -208,7 +315,7 @@ class TextArea extends Component {
className={this.getInputStyle()}
id={this.inputId}
name={name}
ref={inputRef}
ref={this.setInputRef}
cols={fitContent ? this.props.value.length : undefined}
value={this.state.value}
onChange={this.handleChange}
Expand All @@ -218,6 +325,44 @@ class TextArea extends Component {
/>
);

let clearField = null;
let endControlElement;

if (hasClearIcon
&& !loading
&& this.state.focused
&& this.state.value) {
clearField = (
<FormattedMessage id="stripes-components.clearThisField">
{([_ariaLabel]) => (
<TextFieldIcon
aria-label={ariaLabel}
icon="times-circle-solid"
id={clearFieldId || `clickable-${this.testId}-clear-field`}
onClick={onClearField}
tabIndex="-1"
/>
)}
</FormattedMessage>
);
}

if ((!readOnly && clearField) || endControl) {
endControlElement = (
<div
className={css.endControls}
style={{
'inset-inline-end': `${this.state.endControlInset}px`,
}}
>
<div className={css.controlGroup}>
{!readOnly && clearField}
{endControl}
</div>
</div>
);
}

const warningElement = warning ?
<div className={formStyles.feedbackWarning}>{warning}</div> : null;

Expand All @@ -235,12 +380,19 @@ class TextArea extends Component {
: null;

return (
<div className={this.getRootStyle()}>
<div className={this.getRootStyle()} ref={this.containerRef}>
{labelElement}
{component}
<div role="alert">
{warningElement}
{errorElement}
<div
className={this.getInputGroupStyle()}
onFocus={this.onFocus}
onBlur={this.onBlur}
>
{component}
{endControlElement}
<div role="alert">
{warningElement}
{errorElement}
</div>
</div>
</div>
);
Expand Down
Loading

0 comments on commit 6ea7862

Please sign in to comment.