PromucFlow_constructor/app/client/src/widgets/BaseInputWidget/component/index.tsx
Pawan Kumar ea04404de9
fix: Reskinning Input Widgets (#15546)
* update checkbox

* fix margin in checkbox

* fix color bug

* fix border radius

* fixes

* remove unused imports

* bug fixes

* ui fixes

* code review feedback fixes

* css updates for toggle

* fix bg bug in checkbox

* fix disabled checkbox style

* update styles for radio and switch

* update label align

* add hover interaction for switch

* add wds css variables

* add error state

* add error state

* fix css variable

* fix css

* fix checkbox group column height

* move checkbox icons to assets

* update input

* fix input

* fix input

* fix input fix

* fix disalbed icon color

* empty commit to deploy again

* address review feedbacks

* fix font in dropdown

* fix colors

* fix search icon alignment

* fix space in right cross icon

* fix colors and label alignment

* fix margin top in tooltip

* replace red color with css variable

* Json form field spec fix

* fix css variable typo in checkbox

* fix css variable typo in checkbox

* fix label spec

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
2022-08-18 11:56:07 +05:30

761 lines
20 KiB
TypeScript

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) => (
<ControlGroup
{..._.omit(props, [
"hasError",
"numeric",
"labelTextColor",
"allowCurrencyChange",
"compactMode",
"labelPosition",
"labelStyle",
"labelTextSize",
"multiline",
"numeric",
"inputType",
"borderRadius",
"boxShadow",
"accentColor",
])}
/>
))<{
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<HTMLDivElement>(
`.${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<HTMLDivElement>(
`.${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<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>,
) => {
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<HTMLTextAreaElement>) => {
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<HTMLInputElement>) => {
if (typeof this.props.onKeyDown === "function") {
this.props.onKeyDown(e);
}
};
onKeyUp = (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => {
this.props.onKeyUp?.(e);
};
private numericInputComponent = () => {
const leftIcon = this.getLeftIcon();
const conditionalProps: Record<string, number> = {};
if (!isNil(this.props.maxNum)) {
conditionalProps.max = this.props.maxNum;
}
if (!isNil(this.props.minNum)) {
conditionalProps.min = this.props.minNum;
}
return (
<StyledNumericInput
allowNumericCharactersOnly
autoFocus={this.props.autoFocus}
className={this.props.isLoading ? "bp3-skeleton" : Classes.FILL}
disabled={this.props.disabled}
inputRef={(el) => {
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 = () => (
<TextArea
autoFocus={this.props.autoFocus}
className={this.props.isLoading ? "bp3-skeleton" : ""}
disabled={this.props.disabled}
growVertically={false}
inputRef={this.props.inputRef as IRef<HTMLTextAreaElement>}
intent={this.props.intent}
maxLength={this.props.maxChars}
onBlur={() => this.setFocusState(false)}
onChange={this.onTextChange}
onFocus={() => this.setFocusState(true)}
onKeyDown={this.onKeyDownTextArea}
onKeyUp={this.onKeyUp}
placeholder={this.props.placeholder}
style={{ resize: "none" }}
value={this.props.value}
/>
);
private textInputComponent = (isTextArea: boolean) =>
isTextArea ? (
this.textAreaInputComponent()
) : (
<InputGroup
autoFocus={this.props.autoFocus}
className={this.props.isLoading ? "bp3-skeleton" : ""}
disabled={this.props.disabled}
inputRef={this.props.inputRef as IRef<HTMLInputElement>}
intent={this.props.intent}
leftIcon={
this.props.iconName && this.props.iconAlign === "left"
? this.props.iconName
: this.props.leftIcon
}
maxLength={this.props.maxChars}
onBlur={() => this.setFocusState(false)}
onChange={this.onTextChange}
onFocus={() => this.setFocusState(true)}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
placeholder={this.props.placeholder}
rightElement={
this.props.inputType === "PASSWORD" ? (
<Icon
className="password-input"
name={this.state.showPassword ? "eye-off" : "eye-on"}
onClick={() => {
this.setState({ showPassword: !this.state.showPassword });
}}
/>
) : this.props.iconName && this.props.iconAlign === "right" ? (
<Tag icon={this.props.iconName} />
) : (
undefined
)
}
spellCheck={this.props.spellCheck}
type={this.getType(this.props.inputHTMLType)}
value={this.props.value}
/>
);
private renderInputComponent = (
inputHTMLType: InputHTMLType = "TEXT",
isTextArea: boolean,
) =>
isNumberInputType(inputHTMLType)
? this.numericInputComponent()
: this.textInputComponent(isTextArea);
onStepIncrement = (e: Event) => {
e.preventDefault();
e.stopPropagation();
this.props.onStep && this.props.onStep(1);
};
onStepDecrement = (e: Event) => {
e.preventDefault();
e.stopPropagation();
this.props.onStep && this.props.onStep(-1);
};
render() {
const {
compactMode,
disabled,
errorMessage,
inputHTMLType,
inputType,
isInvalid,
isLoading,
label,
labelAlignment,
labelPosition,
labelStyle,
labelTextColor,
labelTextSize,
labelWidth,
multiline,
showError,
tooltip,
} = this.props;
const showLabelHeader = label || tooltip;
return (
<InputComponentWrapper
compactMode={compactMode}
data-testid="input-container"
disabled={disabled}
fill
hasError={isInvalid}
inputType={inputType}
labelPosition={labelPosition}
labelStyle={labelStyle}
labelTextColor={labelTextColor}
labelTextSize={labelTextSize ?? "inherit"}
multiline={(!!multiline).toString()}
numeric={isNumberInputType(inputHTMLType)}
>
{showLabelHeader && (
<LabelWithTooltip
alignment={labelAlignment}
className="t--input-widget-label"
color={labelTextColor}
compact={compactMode}
cyHelpTextClassName="t--input-widget-tooltip"
disabled={disabled}
fontSize={labelTextSize}
fontStyle={labelStyle}
helpText={tooltip}
loading={isLoading}
position={labelPosition}
text={label}
width={labelWidth}
/>
)}
<TextInputWrapper
accentColor={this.props.accentColor}
borderRadius={this.props.borderRadius}
boxShadow={this.props.boxShadow}
className="text-input-wrapper"
compact={compactMode}
disabled={this.props.disabled}
hasError={this.props.isInvalid}
inputHtmlType={inputHTMLType}
labelPosition={labelPosition}
>
<ErrorTooltip
isOpen={isInvalid && showError}
message={
errorMessage ||
createMessage(INPUT_WIDGET_DEFAULT_VALIDATION_ERROR)
}
>
{this.renderInputComponent(inputHTMLType, !!multiline)}
</ErrorTooltip>
</TextInputWrapper>
</InputComponentWrapper>
);
}
}
export interface InputComponentState {
showPassword?: boolean;
}
export interface BaseInputComponentProps extends ComponentProps {
value: string;
inputType: InputType;
inputHTMLType?: InputHTMLType;
disabled?: boolean;
intent?: Intent;
defaultValue?: string | number;
label: string;
labelAlignment?: Alignment;
labelPosition?: LabelPosition;
labelWidth?: number;
labelTextColor?: string;
labelTextSize?: string;
labelStyle?: string;
tooltip?: string;
leftIcon?: IconName | JSX.Element;
allowNumericCharactersOnly?: boolean;
fill?: boolean;
errorMessage?: string;
onValueChange: (valueAsString: string) => void;
stepSize?: number;
placeholder?: string;
isLoading: boolean;
multiline?: boolean;
compactMode: boolean;
isInvalid: boolean;
autoFocus?: boolean;
iconName?: IconName;
iconAlign?: Omit<Alignment, "center">;
showError: boolean;
onFocusChange: (state: boolean) => void;
disableNewLineOnPressEnterKey?: boolean;
onKeyDown?: (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => void;
onKeyUp?: (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => void;
maxChars?: number;
widgetId: string;
onStep?: (direction: number) => void;
spellCheck?: boolean;
maxNum?: number;
minNum?: number;
inputRef?: MutableRefObject<
HTMLTextAreaElement | HTMLInputElement | undefined | null
>;
borderRadius?: string;
boxShadow?: string;
accentColor?: string;
}
export default BaseInputComponent;