azure-devops-ui
Version:
React components for building web UI in Azure DevOps
180 lines (179 loc) • 12.2 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import "./TextField.css";
import * as React from "react";
import { FocusWithin } from '../../FocusWithin';
import { FocusZoneContext } from '../../FocusZone';
import { FormItemContext, Severity } from '../../FormItem';
import { Icon, IconSize } from '../../Icon';
import { Observer } from '../../Observer';
import { Tooltip } from '../../TooltipEx';
import { css, getSafeId, KeyCode } from '../../Util';
import { getTabIndex } from '../../Utilities/Focus';
import { TextFieldFocusTreatmentBehavior, TextFieldStyle, TextFieldWidth } from "./TextField.Props";
let inputId = 1;
export class TextField extends React.Component {
constructor(props) {
super(props);
this.select = () => {
if (this.inputElement.current) {
this.inputElement.current.select();
}
};
this.getTextLength = (value) => {
var _a;
if (!value) {
return 0;
}
else if (typeof value === "string") {
return value.length;
}
else {
return ((_a = value.value) === null || _a === void 0 ? void 0 : _a.length) || 0;
}
};
this.inputId = `textfield-input-${inputId++}`;
this.inputElement = props.inputElement || React.createRef();
this.state = { textLength: props.showCharacterCounter ? this.getTextLength(props.value) : 0 };
}
focus() {
if (this.inputElement.current) {
this.inputElement.current.focus();
}
}
get selectionEnd() {
return this.inputElement.current ? this.inputElement.current.selectionEnd : null;
}
get selectionStart() {
return this.inputElement.current ? this.inputElement.current.selectionStart : null;
}
setSelectionRange(start, end, direction) {
if (this.inputElement.current) {
this.inputElement.current.setSelectionRange(start, end, direction);
}
}
render() {
const { autoAdjustHeight, className, containerClassName, disabled, focusTreatment = TextFieldFocusTreatmentBehavior.all, inputId, label, onBlur, onFocus, onChange, required, style, value, width } = this.props;
const localOnChange = (event, newValue) => {
onChange && onChange(event, newValue);
if (this.props.maxLength && this.props.showCharacterCounter) {
this.setState({ textLength: newValue.length });
}
};
const input = (React.createElement(FocusWithin, { onFocus: onFocus, onBlur: onBlur }, (focusStatus) => (React.createElement(FormItemContext.Consumer, null, formItemContext => {
return (React.createElement("div", { className: css("flex-column", containerClassName, width !== TextFieldWidth.auto && width) },
React.createElement("div", { className: css(!label && className, "bolt-textfield flex-row flex-center", disabled && "disabled", focusTreatment === TextFieldFocusTreatmentBehavior.all && "focus-treatment", focusTreatment === TextFieldFocusTreatmentBehavior.keyboardOnly && "focus-keyboard-only", focusStatus.hasFocus && "focused", style === TextFieldStyle.inline && "bolt-textfield-inline", formItemContext.error && (!formItemContext.severity || formItemContext.severity === Severity.Error) && "bolt-textfield-error", formItemContext.error && formItemContext.severity === Severity.Warning && "bolt-textfield-warning") },
React.createElement(FocusZoneContext.Consumer, null, zoneContext => (React.createElement(Observer, { value: value }, (observedProps) => {
return (React.createElement(TextFieldInnerValue, Object.assign({}, this.props, { onKeyDown: event => {
var _a, _b;
if (this.props.multiline && event.keyCode === KeyCode.enter && !event.ctrlKey) {
event.stopPropagation();
return;
}
(_b = (_a = this.props).onKeyDown) === null || _b === void 0 ? void 0 : _b.call(_a, event);
}, onChange: localOnChange, focus: () => this.focus(), required: required, focusStatus: focusStatus, formItemContext: formItemContext, inputElement: this.inputElement, inputId: this.props.inputId || this.inputId, value: observedProps.value, zoneContext: zoneContext })));
})))),
this.props.maxLength && this.props.showCharacterCounter && (React.createElement("div", { className: "bolt-textfield-counter" },
React.createElement("span", null,
this.state.textLength,
"/",
this.props.maxLength)))));
}))));
if (label) {
return (React.createElement("div", { className: css(className, "flex-column") },
React.createElement("label", { htmlFor: getSafeId(inputId || this.inputId), className: required ? "bolt-textfield-label bolt-textfield-label--required" : "bolt-textfield-label" }, label),
input));
}
else {
return input;
}
}
}
class TextFieldInnerValue extends React.Component {
constructor() {
super(...arguments);
this.adjustedHeight = -1;
this.adjustedHeightValue = "";
this.hiddenElement = React.createRef();
}
render() {
const { activatable, ariaActiveDescendant, ariaAutoComplete, ariaControls, ariaExpanded, ariaHasPopup, ariaInvalid, ariaLabel, ariaLabelledBy, ariaRoleDescription, autoAdjustHeight, autoComplete, autoFocus, autoSelect, disabled, excludeFocusZone, focus, focusStatus, formItemContext, inputClassName, inputElement, inputId, inputType, maxLength, maxWidth, multiline, onClick, onKeyDown, onKeyPress, onKeyUp, onPaste, placeholder, prefixIconProps, readOnly, required, resizable, role, rows, spellCheck, tooltipProps, value, zoneContext, inputWrapperClassName } = this.props;
let { ariaDescribedBy, suffixIconProps } = this.props;
const TagName = multiline ? "textarea" : "input";
const tagSpecificProps = multiline ? { rows: rows } : { type: inputType, autoComplete: autoComplete ? "on" : "off" };
if (suffixIconProps === undefined && formItemContext.error) {
if (formItemContext.severity === Severity.Warning) {
suffixIconProps = {
className: "bolt-textfield-message-warning",
iconName: "Warning"
};
}
else {
suffixIconProps = {
className: "bolt-textfield-message-error",
iconName: "Error"
};
}
}
if (ariaDescribedBy === undefined) {
ariaDescribedBy = formItemContext.ariaDescribedById;
}
const className = css(inputClassName, "bolt-textfield-input flex-grow", autoAdjustHeight && "bolt-textfield-auto-adjust", resizable && "bolt-textfield-auto-unresizable", prefixIconProps && "bolt-textfield-input-with-prefix", suffixIconProps && "bolt-textfield-input-with-suffix", activatable && "activatable");
const style = maxWidth !== undefined ? { maxWidth: maxWidth } : {};
let element = (React.createElement(TagName, Object.assign({}, tagSpecificProps, { "aria-activedescendant": getSafeId(ariaActiveDescendant), "aria-autocomplete": ariaAutoComplete, "aria-controls": getSafeId(ariaControls), "aria-describedby": getSafeId(ariaDescribedBy), "aria-disabled": disabled, "aria-expanded": ariaExpanded, "aria-haspopup": ariaHasPopup, "aria-invalid": ariaInvalid, "aria-label": ariaLabel === undefined && placeholder ? placeholder : ariaLabel, "aria-labelledby": getSafeId(ariaLabelledBy) || getSafeId(formItemContext.ariaLabelledById), "aria-readonly": inputType && inputType !== "text" ? readOnly : undefined, "aria-roledescription": ariaRoleDescription, autoFocus: autoFocus, "data-focuszone": !disabled && !excludeFocusZone ? zoneContext.focuszoneId : undefined, disabled: disabled, className: className, id: getSafeId(inputId), maxLength: maxLength, onBlur: focusStatus.onBlur, onClick: onClick, onChange: (e) => {
this.props.onChange && this.props.onChange(e, e.target.value);
// Adjust height synchronously. If we wait until the React update effect, then
// the text area has already been painted in a partially-scrolled state which
// causes some ugly flickering.
this.adjustHeight();
}, onFocus: (event) => {
if (autoSelect && inputElement.current) {
inputElement.current.select();
}
focusStatus.onFocus && focusStatus.onFocus(event);
}, onKeyDown: onKeyDown, onKeyPress: onKeyPress, onKeyUp: onKeyUp, onPaste: onPaste, placeholder: placeholder, readOnly: readOnly, required: required, ref: inputElement, role: role, style: style, spellCheck: spellCheck, tabIndex: getTabIndex(this.props), value: value || "" })));
// We will use a hidden element behind the input to measure the height.
// This prevents the page from performing re-layout when measuring.
if (multiline && autoAdjustHeight) {
element = (React.createElement("div", { className: css("flex-row flex-grow relative", inputWrapperClassName) },
React.createElement(TagName, { "aria-hidden": true, className: css("bolt-textfield-auto-adjust-hidden", className), ref: this.hiddenElement, role: "presentation" }),
element));
}
return (React.createElement(React.Fragment, null,
prefixIconProps &&
Icon(Object.assign(Object.assign({ size: IconSize.medium }, prefixIconProps), { className: css(prefixIconProps.className, "prefix", !prefixIconProps.render && "bolt-textfield-icon", ((placeholder && !value) || prefixIconProps.render) && "bolt-textfield-no-text"), onClick: e => {
prefixIconProps && prefixIconProps.onClick && prefixIconProps.onClick(e);
focus();
} })),
tooltipProps ? React.createElement(Tooltip, Object.assign({}, tooltipProps), element) : element,
suffixIconProps &&
Icon(Object.assign(Object.assign({ size: IconSize.medium }, suffixIconProps), { className: css(suffixIconProps.className, "suffix", !suffixIconProps.render && "bolt-textfield-icon", ((placeholder && !value) || suffixIconProps.render) && "bolt-textfield-no-text", suffixIconProps.onClick && "cursor-pointer"), onClick: e => {
suffixIconProps && suffixIconProps.onClick && suffixIconProps.onClick(e);
focus();
} }))));
}
componentDidMount() {
this.adjustHeight();
}
componentDidUpdate() {
this.adjustHeight();
}
adjustHeight() {
const { hiddenElement } = this;
const { autoAdjustHeight, inputElement, multiline, value } = this.props;
if (!autoAdjustHeight) {
this.adjustedHeightValue = "";
this.adjustedHeight = -1;
return;
}
// If this is a multi-line, auto-adjust text area, adjust the height based on the current content
if (multiline && inputElement.current && hiddenElement.current && value !== this.adjustedHeightValue) {
hiddenElement.current.value = inputElement.current.value;
if (this.adjustedHeight !== hiddenElement.current.scrollHeight && hiddenElement.current.scrollHeight > 0) {
this.adjustedHeight = hiddenElement.current.scrollHeight;
this.adjustedHeightValue = inputElement.current.value;
inputElement.current.style.height = this.adjustedHeight + "px";
}
}
}
}