Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STCOM-1240 Added support for clear icon in TextArea #2181

Merged
merged 8 commits into from
Jan 9, 2024
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
Loading