import React, { MutableRefObject } from "react"; import styled from "styled-components"; import { Alignment, Intent, NumericInput, IconName, InputGroup, Classes, ControlGroup, TextArea, Tag, IRef, } from "@blueprintjs/core"; import _, { isNil } from "lodash"; import { ComponentProps } from "widgets/BaseComponent"; import { Colors } from "constants/Colors"; import { createMessage, INPUT_WIDGET_DEFAULT_VALIDATION_ERROR, } from "@appsmith/constants/messages"; import { InputTypes } from "../constants"; // TODO(abhinav): All of the following imports should not be in widgets. import ErrorTooltip from "components/editorComponents/ErrorTooltip"; import Icon from "components/ads/Icon"; import { InputType } from "widgets/InputWidget/constants"; import { getBaseWidgetClassName } from "constants/componentClassNameConstants"; import { LabelPosition } from "components/constants"; import LabelWithTooltip, { labelLayoutStyles, LABEL_CONTAINER_CLASS, } from "components/ads/LabelWithTooltip"; import { lightenColor } from "widgets/WidgetUtils"; /** * All design system component specific logic goes here. * Ex. Blueprint has a separate numeric input and text input so switching between them goes here * Ex. To set the icon as currency, blue print takes in a set of defined types * All generic logic like max characters for phone numbers should be 10, should go in the widget */ const InputComponentWrapper = styled((props) => ( ))<{ numeric: boolean; multiline: string; hasError: boolean; allowCurrencyChange?: boolean; disabled?: boolean; inputType: InputType; compactMode: boolean; labelPosition: LabelPosition; borderRadius?: string; boxShadow?: string; accentColor?: string; }>` ${labelLayoutStyles} cursor: ${({ disabled }) => (disabled ? "not-allowed" : "auto")}; .${Classes.INPUT_GROUP} { display: flex; pointer-events: ${({ disabled }) => (disabled ? "none" : "auto")}; background: ${(props) => props.disabled ? "var(--wds-color-bg-disabled)" : "white"}; span, input, textarea { background: ${(props) => props.disabled ? "var(--wds-color-bg-disabled)" : Colors.WHITE}; color: ${(props) => props.disabled ? "var(--wds-color-text-disabled)" : "var(--wds-color-text)"}; } > { input:not(:first-child) { padding-left: 0rem; z-index: 16; line-height: 16px; } } } &&&& { ${({ inputType, labelPosition }) => { if (!labelPosition && inputType !== InputTypes.TEXT) { return "flex-direction: row"; } }}; & .${LABEL_CONTAINER_CLASS} { flex-grow: 0; ${({ inputType, labelPosition }) => { if (!labelPosition && inputType !== InputTypes.TEXT) { return "flex: 1; margin-right: 5px; label { margin-right: 5px; margin-bottom: 0;}"; } }} align-items: centert; ${({ compactMode, labelPosition }) => { if (!labelPosition && !compactMode) { return "max-height: 20px; .bp3-popover-wrapper {max-height: 20px}"; } }}; } .currency-type-filter, .country-type-filter { width: fit-content; height: 100%; position: static; display: inline-block; left: 0; z-index: 16; svg { path { fill: ${(props) => props.theme.colors.icon?.hover}; } } .${Classes.INPUT} { padding-left: 0.5rem; min-height: 36px; box-shadow: none; border: 1px solid; border-radius: 0; height: ${(props) => (props.multiline === "true" ? "100%" : "inherit")}; width: 100%; border-color: ${({ hasError }) => { return hasError ? `${Colors.DANGER_SOLID} !important;` : `${Colors.GREY_3};`; }} ${(props) => props.numeric && ` border-top-right-radius: 0px; border-bottom-right-radius: 0px; ${props.hasError ? "" : "border-right-width: 0px;"} `} &:active { border-color: ${({ hasError }) => hasError ? Colors.DANGER_SOLID : Colors.HIT_GRAY}; } } } .currency-type-filter .bp3-popover-open > div, .country-type-filter .bp3-popover-open > div { border: 0px solid !important; box-shadow: none !important; } .currency-type-filter .bp3-popover-open button .country-type-filter .bp3-popover-open button { border: 0px solid !important; box-shadow: none !important; background: ${Colors.GREY_3}; } textarea { background: ${(props) => props.disabled ? "var(--wds-color-bg-disabled)" : Colors.WHITE}; color: ${(props) => props.disabled ? "var(--wds-color-text-disabled)" : "var(--wds-color-text)"}; } .${Classes.INPUT} { box-shadow: none; border-radius: 0; height: ${(props) => (props.multiline === "true" ? "100%" : "inherit")}; width: 100%; ::placeholder { color: ${({ disabled }) => { if (disabled) { return "var(--wds-color-text-disabled-light) !important"; } return "var(--wds-color-text-light)"; }}; } ${(props) => props.inputType === "PASSWORD" && ` & + .bp3-input-action { height: 100%; width: 36px; cursor: pointer; .password-input { color: ${ props.disabled ? "var(--wds-color-icon-disabled)" : "var(--wds-color-icon)" }; justify-content: center; height: 100%; svg { width: 20px; height: 20px; } &:hover { background-color: var(--wds-color-bg-hover); } } } `} } & .${Classes.INPUT_GROUP} { display: flex; margin: 0; .bp3-tag { background-color: transparent; color: var(--wds-color-text-danger); } .${Classes.INPUT_ACTION} { height: 100%; .${Classes.TAG} { height: 100%; padding: 0; margin: 0; display: flex; align-items: center; } } .${Classes.ICON} { height: 100%; margin: 0; display: flex; align-items: center; padding: 0 10px; position: relative; color: ${({ disabled }) => disabled ? "var(--wds-color-icon-disabled)" : "var(--wds-color-icon)"}; svg { width: 14px; height: 14px; } } &.${Classes.DISABLED} + .bp3-button-group.bp3-vertical { button { background: var(--wds-color-bg-disabled); color: var(--wds-color-icon-disabled) !important; } } } .${Classes.CONTROL_GROUP} { justify-content: flex-start; } height: 100%; align-items: ${({ compactMode, inputType, labelPosition }) => { if (!labelPosition && inputType !== InputTypes.TEXT) { return "center"; } if (labelPosition === LabelPosition.Top) { return "flex-start"; } if (compactMode) { return "center"; } if (labelPosition === LabelPosition.Left) { if (inputType === InputTypes.TEXT) { return "stretch"; } return "center"; } return "flex-start"; }}; } `; const StyledNumericInput = styled(NumericInput)` &&&& .bp3-button-group.bp3-vertical { button { background: ${Colors.WHITE}; box-shadow: none; min-width: 24px; width: 24px; border-radius: 0; &:hover { background: ${Colors.GREY_2}; span { color: ${Colors.GREY_10}; } } span { color: var(--wds-color-icon); svg { width: 12px; } } } } `; const TextInputWrapper = styled.div<{ inputHtmlType?: InputHTMLType; compact: boolean; labelPosition?: LabelPosition; borderRadius?: string; boxShadow?: string; accentColor?: string; hasError?: boolean; disabled?: boolean; }>` width: 100%; display: flex; flex: 1; height: 100%; border: 1px solid; overflow: hidden; border-color: ${({ disabled, hasError }) => { if (disabled) { return "var(--wds-color-border-disabled)"; } if (hasError) { return "var(--wds-color-border-danger)"; } return "var(--wds-color-border)"; }}; border-radius: ${({ borderRadius }) => borderRadius} !important; box-shadow: ${({ boxShadow }) => `${boxShadow}`} !important; min-height: 32px; &:hover { border-color: ${({ disabled, hasError }) => { if (disabled) { return "var(--wds-color-border-disabled)"; } if (hasError) { return "var(--wds-color-border-danger-hover)"; } return "var(--wds-color-border-hover)"; }}; } &:focus-within { outline: 0; border-color: ${({ accentColor, hasError }) => hasError ? Colors.DANGER_SOLID : accentColor}; box-shadow: ${({ accentColor, hasError }) => `0px 0px 0px 2px ${lightenColor( hasError ? Colors.DANGER_SOLID : accentColor, )} !important;`}; } ${({ inputHtmlType }) => inputHtmlType && inputHtmlType !== InputTypes.TEXT && `&&& {flex-grow: 0;}`} `; export type InputHTMLType = "TEXT" | "NUMBER" | "PASSWORD" | "EMAIL" | "TEL"; export const isNumberInputType = (inputHTMLType: InputHTMLType = "TEXT") => { return inputHTMLType === "NUMBER"; }; class BaseInputComponent extends React.Component< BaseInputComponentProps, InputComponentState > { constructor(props: BaseInputComponentProps) { super(props); this.state = { showPassword: false }; } componentDidMount() { if (isNumberInputType(this.props.inputHTMLType) && this.props.onStep) { const element = document.querySelector( `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button-group`, ); if (element !== null && element.childNodes) { element.childNodes[0].addEventListener( "mousedown", this.onStepIncrement, ); element.childNodes[1].addEventListener( "mousedown", this.onStepDecrement, ); } } } componentWillUnmount() { if (isNumberInputType(this.props.inputHTMLType) && this.props.onStep) { const element = document.querySelector( `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button-group`, ); if (element !== null && element.childNodes) { element.childNodes[0].removeEventListener( "click", this.onStepIncrement, ); element.childNodes[1].removeEventListener( "click", this.onStepDecrement, ); } } } setFocusState = (isFocused: boolean) => { this.props.onFocusChange(isFocused); }; onTextChange = ( event: | React.ChangeEvent | React.ChangeEvent, ) => { this.props.onValueChange(event.target.value); }; onNumberChange = (valueAsNum: number, valueAsString: string) => { this.props.onValueChange(valueAsString); }; getLeftIcon = () => { if (this.props.iconName && this.props.iconAlign === "left") { return this.props.iconName; } return this.props.leftIcon; }; getType(inputType: InputHTMLType = "TEXT") { switch (inputType) { case "PASSWORD": return this.state.showPassword ? "text" : "password"; case "TEL": return "tel"; case "EMAIL": return "email"; default: return "text"; } } onKeyDownTextArea = (e: React.KeyboardEvent) => { const isEnterKey = e.key === "Enter" || e.keyCode === 13; const { disableNewLineOnPressEnterKey } = this.props; if (isEnterKey && disableNewLineOnPressEnterKey && !e.shiftKey) { e.preventDefault(); } if (typeof this.props.onKeyDown === "function") { this.props.onKeyDown(e); } }; onKeyDown = (e: React.KeyboardEvent) => { if (typeof this.props.onKeyDown === "function") { this.props.onKeyDown(e); } }; onKeyUp = ( e: | React.KeyboardEvent | React.KeyboardEvent, ) => { this.props.onKeyUp?.(e); }; private numericInputComponent = () => { const leftIcon = this.getLeftIcon(); const conditionalProps: Record = {}; if (!isNil(this.props.maxNum)) { conditionalProps.max = this.props.maxNum; } if (!isNil(this.props.minNum)) { conditionalProps.min = this.props.minNum; } return ( { if (this.props.inputRef && el) { this.props.inputRef.current = el; } }} intent={this.props.intent} leftIcon={leftIcon} majorStepSize={null} minorStepSize={null} onBlur={() => this.setFocusState(false)} onFocus={() => this.setFocusState(true)} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onValueChange={this.onNumberChange} placeholder={this.props.placeholder} stepSize={this.props.stepSize} value={this.props.value} {...conditionalProps} /> ); }; private textAreaInputComponent = () => (