office-ui-fabric-react
Version:
Reusable React components for building experiences for Microsoft 365.
449 lines • 22.3 kB
JavaScript
import { __assign, __extends } from "tslib";
import * as React from 'react';
import { Label } from '../../Label';
import { Icon } from '../../Icon';
import { Async, DelayedRender, classNamesFunction, getId, getNativeProps, getWindow, initializeComponentRef, inputProperties, isControlled, isIE11, textAreaProperties, warn, warnControlledUsage, warnMutuallyExclusive, } from '../../Utilities';
var getClassNames = classNamesFunction();
var DEFAULT_STATE_VALUE = '';
var COMPONENT_NAME = 'TextField';
var REVEAL_ICON_NAME = 'RedEye';
var HIDE_ICON_NAME = 'Hide';
var TextFieldBase = /** @class */ (function (_super) {
__extends(TextFieldBase, _super);
function TextFieldBase(props) {
var _this = _super.call(this, props) || this;
_this._textElement = React.createRef();
_this._onFocus = function (ev) {
if (_this.props.onFocus) {
_this.props.onFocus(ev);
}
_this.setState({ isFocused: true }, function () {
if (_this.props.validateOnFocusIn) {
_this._validate(_this.value);
}
});
};
_this._onBlur = function (ev) {
if (_this.props.onBlur) {
_this.props.onBlur(ev);
}
_this.setState({ isFocused: false }, function () {
if (_this.props.validateOnFocusOut) {
_this._validate(_this.value);
}
});
};
_this._onRenderLabel = function (props) {
var label = props.label, required = props.required;
// IProcessedStyleSet definition requires casting for what Label expects as its styles prop
var labelStyles = _this._classNames.subComponentStyles
? _this._classNames.subComponentStyles.label
: undefined;
if (label) {
return (React.createElement(Label, { required: required, htmlFor: _this._id, styles: labelStyles, disabled: props.disabled, id: _this._labelId }, props.label));
}
return null;
};
_this._onRenderDescription = function (props) {
if (props.description) {
return React.createElement("span", { className: _this._classNames.description }, props.description);
}
return null;
};
_this._onRevealButtonClick = function (event) {
_this.setState(function (prevState) { return ({ isRevealingPassword: !prevState.isRevealingPassword }); });
};
_this._onInputChange = function (event) {
// Previously, we needed to call both onInput and onChange due to some weird IE/React issues,
// which have *probably* been fixed now:
// - https://github.com/microsoft/fluentui/issues/744 (likely fixed)
// - https://github.com/microsoft/fluentui/issues/824 (confirmed fixed)
// TODO (Fabric 8?) - Switch to calling only onChange. This switch is pretty disruptive for
// tests (ours and maybe consumers' too), so it seemed best to do the switch in a major bump.
var element = event.target;
var value = element.value;
// Ignore this event if the value is undefined (in case one of the IE bugs comes back)
if (value === undefined || value === _this._lastChangeValue) {
return;
}
_this._lastChangeValue = value;
// This is so developers can access the event properties in asynchronous callbacks
// https://reactjs.org/docs/events.html#event-pooling
event.persist();
var isSameValue;
_this.setState(function (prevState, props) {
var prevValue = _getValue(props, prevState) || '';
isSameValue = value === prevValue;
// Avoid doing unnecessary work when the value has not changed.
if (isSameValue) {
return null;
}
// ONLY if this is an uncontrolled component, update the displayed value.
// (Controlled components must update the `value` prop from `onChange`.)
return _this._isControlled ? null : { uncontrolledValue: value };
}, function () {
// If the value actually changed, call onChange (for either controlled or uncontrolled)
var onChange = _this.props.onChange;
if (!isSameValue && onChange) {
onChange(event, value);
}
});
};
initializeComponentRef(_this);
_this._async = new Async(_this);
if (process.env.NODE_ENV !== 'production') {
warnMutuallyExclusive(COMPONENT_NAME, props, {
errorMessage: 'onGetErrorMessage',
});
}
_this._fallbackId = getId(COMPONENT_NAME);
_this._descriptionId = getId(COMPONENT_NAME + 'Description');
_this._labelId = getId(COMPONENT_NAME + 'Label');
_this._warnControlledUsage();
var _a = props.defaultValue, defaultValue = _a === void 0 ? DEFAULT_STATE_VALUE : _a;
if (typeof defaultValue === 'number') {
// This isn't allowed per the props, but happens anyway.
defaultValue = String(defaultValue);
}
_this.state = {
uncontrolledValue: _this._isControlled ? undefined : defaultValue,
isFocused: false,
errorMessage: '',
};
_this._delayedValidate = _this._async.debounce(_this._validate, _this.props.deferredValidationTime);
_this._lastValidation = 0;
return _this;
}
Object.defineProperty(TextFieldBase.prototype, "value", {
/**
* Gets the current value of the text field.
*/
get: function () {
return _getValue(this.props, this.state);
},
enumerable: true,
configurable: true
});
TextFieldBase.prototype.componentDidMount = function () {
this._adjustInputHeight();
if (this.props.validateOnLoad) {
this._validate(this.value);
}
};
TextFieldBase.prototype.componentWillUnmount = function () {
this._async.dispose();
};
TextFieldBase.prototype.getSnapshotBeforeUpdate = function (prevProps, prevState) {
return {
selection: [this.selectionStart, this.selectionEnd],
};
};
TextFieldBase.prototype.componentDidUpdate = function (prevProps, prevState, snapshot) {
var props = this.props;
var _a = (snapshot || {}).selection, selection = _a === void 0 ? [null, null] : _a;
var start = selection[0], end = selection[1];
if (!!prevProps.multiline !== !!props.multiline && prevState.isFocused) {
// The text field has just changed between single- and multi-line, so we need to reset focus
// and selection/cursor.
this.focus();
if (start !== null && end !== null && start >= 0 && end >= 0) {
this.setSelectionRange(start, end);
}
}
var prevValue = _getValue(prevProps, prevState);
var value = this.value;
if (prevValue !== value) {
// Handle controlled/uncontrolled warnings and status
this._warnControlledUsage(prevProps);
// Clear error message if needed
// TODO: is there any way to do this without an extra render?
if (this.state.errorMessage && !props.errorMessage) {
this.setState({ errorMessage: '' });
}
// Adjust height if needed based on new value
this._adjustInputHeight();
// Reset the record of the last value seen by a change/input event
this._lastChangeValue = undefined;
// TODO: #5875 added logic to trigger validation in componentWillReceiveProps and other places.
// This seems a bit odd and hard to integrate with the new approach.
// (Starting to think we should just put the validation logic in a separate wrapper component...?)
if (_shouldValidateAllChanges(props)) {
this._delayedValidate(value);
}
}
};
TextFieldBase.prototype.render = function () {
var _a = this.props, borderless = _a.borderless, className = _a.className, disabled = _a.disabled, iconProps = _a.iconProps, inputClassName = _a.inputClassName, label = _a.label, multiline = _a.multiline, required = _a.required, underlined = _a.underlined, prefix = _a.prefix, resizable = _a.resizable, suffix = _a.suffix, theme = _a.theme, styles = _a.styles, autoAdjustHeight = _a.autoAdjustHeight, canRevealPassword = _a.canRevealPassword, type = _a.type, _b = _a.onRenderPrefix, onRenderPrefix = _b === void 0 ? this._onRenderPrefix : _b, _c = _a.onRenderSuffix, onRenderSuffix = _c === void 0 ? this._onRenderSuffix : _c, _d = _a.onRenderLabel, onRenderLabel = _d === void 0 ? this._onRenderLabel : _d, _e = _a.onRenderDescription, onRenderDescription = _e === void 0 ? this._onRenderDescription : _e;
var _f = this.state, isFocused = _f.isFocused, isRevealingPassword = _f.isRevealingPassword;
var errorMessage = this._errorMessage;
var hasRevealButton = !!canRevealPassword && type === 'password' && _browserNeedsRevealButton();
var classNames = (this._classNames = getClassNames(styles, {
theme: theme,
className: className,
disabled: disabled,
focused: isFocused,
required: required,
multiline: multiline,
hasLabel: !!label,
hasErrorMessage: !!errorMessage,
borderless: borderless,
resizable: resizable,
hasIcon: !!iconProps,
underlined: underlined,
inputClassName: inputClassName,
autoAdjustHeight: autoAdjustHeight,
hasRevealButton: hasRevealButton,
}));
return (React.createElement("div", { className: classNames.root },
React.createElement("div", { className: classNames.wrapper },
onRenderLabel(this.props, this._onRenderLabel),
React.createElement("div", { className: classNames.fieldGroup },
(prefix !== undefined || this.props.onRenderPrefix) && (React.createElement("div", { className: classNames.prefix }, onRenderPrefix(this.props, this._onRenderPrefix))),
multiline ? this._renderTextArea() : this._renderInput(),
iconProps && React.createElement(Icon, __assign({ className: classNames.icon }, iconProps)),
hasRevealButton && (
// Explicitly set type="button" since the default button type within a form is "submit"
React.createElement("button", { className: classNames.revealButton, onClick: this._onRevealButtonClick, type: "button" },
React.createElement("span", { className: classNames.revealSpan },
React.createElement(Icon, { className: classNames.revealIcon, iconName: isRevealingPassword ? HIDE_ICON_NAME : REVEAL_ICON_NAME })))),
(suffix !== undefined || this.props.onRenderSuffix) && (React.createElement("div", { className: classNames.suffix }, onRenderSuffix(this.props, this._onRenderSuffix))))),
this._isDescriptionAvailable && (React.createElement("span", { id: this._descriptionId },
onRenderDescription(this.props, this._onRenderDescription),
errorMessage && (React.createElement("div", { role: "alert" },
React.createElement(DelayedRender, null,
React.createElement("p", { className: classNames.errorMessage },
React.createElement("span", { "data-automation-id": "error-message" }, errorMessage)))))))));
};
/**
* Sets focus on the text field
*/
TextFieldBase.prototype.focus = function () {
if (this._textElement.current) {
this._textElement.current.focus();
}
};
/**
* Blurs the text field.
*/
TextFieldBase.prototype.blur = function () {
if (this._textElement.current) {
this._textElement.current.blur();
}
};
/**
* Selects the text field
*/
TextFieldBase.prototype.select = function () {
if (this._textElement.current) {
this._textElement.current.select();
}
};
/**
* Sets the selection start of the text field to a specified value
*/
TextFieldBase.prototype.setSelectionStart = function (value) {
if (this._textElement.current) {
this._textElement.current.selectionStart = value;
}
};
/**
* Sets the selection end of the text field to a specified value
*/
TextFieldBase.prototype.setSelectionEnd = function (value) {
if (this._textElement.current) {
this._textElement.current.selectionEnd = value;
}
};
Object.defineProperty(TextFieldBase.prototype, "selectionStart", {
/**
* Gets the selection start of the text field
*/
get: function () {
return this._textElement.current ? this._textElement.current.selectionStart : -1;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TextFieldBase.prototype, "selectionEnd", {
/**
* Gets the selection end of the text field
*/
get: function () {
return this._textElement.current ? this._textElement.current.selectionEnd : -1;
},
enumerable: true,
configurable: true
});
/**
* Sets the start and end positions of a selection in a text field.
* @param start - Index of the start of the selection.
* @param end - Index of the end of the selection.
*/
TextFieldBase.prototype.setSelectionRange = function (start, end) {
if (this._textElement.current) {
this._textElement.current.setSelectionRange(start, end);
}
};
TextFieldBase.prototype._warnControlledUsage = function (prevProps) {
// Show warnings if props are being used in an invalid way
warnControlledUsage({
componentId: this._id,
componentName: COMPONENT_NAME,
props: this.props,
oldProps: prevProps,
valueProp: 'value',
defaultValueProp: 'defaultValue',
onChangeProp: 'onChange',
readOnlyProp: 'readOnly',
});
if (this.props.value === null && !this._hasWarnedNullValue) {
this._hasWarnedNullValue = true;
warn("Warning: 'value' prop on '" + COMPONENT_NAME + "' should not be null. Consider using an " +
'empty string to clear the component or undefined to indicate an uncontrolled component.');
}
};
Object.defineProperty(TextFieldBase.prototype, "_id", {
/** Returns `props.id` if available, or a fallback if not. */
get: function () {
return this.props.id || this._fallbackId;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TextFieldBase.prototype, "_isControlled", {
get: function () {
return isControlled(this.props, 'value');
},
enumerable: true,
configurable: true
});
TextFieldBase.prototype._onRenderPrefix = function (props) {
var prefix = props.prefix;
return React.createElement("span", { style: { paddingBottom: '1px' } }, prefix);
};
TextFieldBase.prototype._onRenderSuffix = function (props) {
var suffix = props.suffix;
return React.createElement("span", { style: { paddingBottom: '1px' } }, suffix);
};
Object.defineProperty(TextFieldBase.prototype, "_errorMessage", {
/**
* Current error message from either `props.errorMessage` or the result of `props.onGetErrorMessage`.
*
* - If there is no validation error or we have not validated the input value, errorMessage is an empty string.
* - If we have done the validation and there is validation error, errorMessage is the validation error message.
*/
get: function () {
var _a = this.props.errorMessage, errorMessage = _a === void 0 ? this.state.errorMessage : _a;
return errorMessage || '';
},
enumerable: true,
configurable: true
});
Object.defineProperty(TextFieldBase.prototype, "_isDescriptionAvailable", {
/**
* If a custom description render function is supplied then treat description as always available.
* Otherwise defer to the presence of description or error message text.
*/
get: function () {
var props = this.props;
return !!(props.onRenderDescription || props.description || this._errorMessage);
},
enumerable: true,
configurable: true
});
TextFieldBase.prototype._renderTextArea = function () {
var textAreaProps = getNativeProps(this.props, textAreaProperties, ['defaultValue']);
var ariaLabelledBy = this.props['aria-labelledby'] || (this.props.label ? this._labelId : undefined);
return (React.createElement("textarea", __assign({ id: this._id }, textAreaProps, { ref: this._textElement, value: this.value || '', onInput: this._onInputChange, onChange: this._onInputChange, className: this._classNames.field, "aria-labelledby": ariaLabelledBy, "aria-describedby": this._isDescriptionAvailable ? this._descriptionId : this.props['aria-describedby'], "aria-invalid": !!this._errorMessage, "aria-label": this.props.ariaLabel, readOnly: this.props.readOnly, onFocus: this._onFocus, onBlur: this._onBlur })));
};
TextFieldBase.prototype._renderInput = function () {
var inputProps = __assign(__assign({ type: this.state.isRevealingPassword ? 'text' : this.props.type || 'text', id: this._id }, getNativeProps(this.props, inputProperties, ['defaultValue', 'type'])), { 'aria-labelledby': this.props['aria-labelledby'] || (this.props.label ? this._labelId : undefined), ref: this._textElement, value: this.value || '', onInput: this._onInputChange, onChange: this._onInputChange, className: this._classNames.field, 'aria-label': this.props.ariaLabel, 'aria-describedby': this._isDescriptionAvailable ? this._descriptionId : this.props['aria-describedby'], 'aria-invalid': !!this._errorMessage, onFocus: this._onFocus, onBlur: this._onBlur });
var defaultRender = function (updatedInputProps) {
return React.createElement("input", __assign({}, updatedInputProps));
};
var onRenderInput = this.props.onRenderInput || defaultRender;
return onRenderInput(inputProps, defaultRender);
};
TextFieldBase.prototype._validate = function (value) {
var _this = this;
// In case _validate is called again while validation promise is executing
if (this._latestValidateValue === value && _shouldValidateAllChanges(this.props)) {
return;
}
this._latestValidateValue = value;
var onGetErrorMessage = this.props.onGetErrorMessage;
var result = onGetErrorMessage && onGetErrorMessage(value || '');
if (result !== undefined) {
if (typeof result === 'string' || !('then' in result)) {
this.setState({ errorMessage: result });
this._notifyAfterValidate(value, result);
}
else {
var currentValidation_1 = ++this._lastValidation;
result.then(function (errorMessage) {
if (currentValidation_1 === _this._lastValidation) {
_this.setState({ errorMessage: errorMessage });
}
_this._notifyAfterValidate(value, errorMessage);
});
}
}
else {
this._notifyAfterValidate(value, '');
}
};
TextFieldBase.prototype._notifyAfterValidate = function (value, errorMessage) {
if (value === this.value && this.props.onNotifyValidationResult) {
this.props.onNotifyValidationResult(errorMessage, value);
}
};
TextFieldBase.prototype._adjustInputHeight = function () {
if (this._textElement.current && this.props.autoAdjustHeight && this.props.multiline) {
var textField = this._textElement.current;
textField.style.height = '';
textField.style.height = textField.scrollHeight + 'px';
}
};
TextFieldBase.defaultProps = {
resizable: true,
deferredValidationTime: 200,
validateOnLoad: true,
canRevealPassword: false,
};
return TextFieldBase;
}(React.Component));
export { TextFieldBase };
/** Get the value from the given state and props (converting from number to string if needed) */
function _getValue(props, state) {
var _a = props.value, value = _a === void 0 ? state.uncontrolledValue : _a;
if (typeof value === 'number') {
// not allowed per typings, but happens anyway
return String(value);
}
return value;
}
/**
* If `validateOnFocusIn` or `validateOnFocusOut` is true, validation should run **only** on that event.
* Otherwise, validation should run on every change.
*/
function _shouldValidateAllChanges(props) {
return !(props.validateOnFocusIn || props.validateOnFocusOut);
}
// Only calculate this once across all TextFields, since will stay the same
var __browserNeedsRevealButton;
function _browserNeedsRevealButton() {
var _a;
if (typeof __browserNeedsRevealButton !== 'boolean') {
var win = getWindow();
if ((_a = win) === null || _a === void 0 ? void 0 : _a.navigator) {
// Edge, Chromium Edge
var isEdge = /Edg/.test(win.navigator.userAgent || '');
__browserNeedsRevealButton = !(isIE11() || isEdge);
}
else {
__browserNeedsRevealButton = true;
}
}
return __browserNeedsRevealButton;
}
//# sourceMappingURL=TextField.base.js.map