UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

180 lines (179 loc) 12.2 kB
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"; } } } }