airbnb/caravel

View on GitHub
superset-frontend/src/explore/components/controls/TextControl/index.tsx

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { Component, ChangeEvent } from 'react';
import { legacyValidateNumber, legacyValidateInteger } from '@superset-ui/core';
import { debounce } from 'lodash';
import { FAST_DEBOUNCE } from 'src/constants';
import ControlHeader from 'src/explore/components/ControlHeader';
import { Input } from 'src/components/Input';

type InputValueType = string | number;

export interface TextControlProps<T extends InputValueType = InputValueType> {
  label?: string;
  disabled?: boolean;
  isFloat?: boolean;
  isInt?: boolean;
  onChange?: (value: T, errors: any) => void;
  onFocus?: () => {};
  placeholder?: string;
  value?: T | null;
  controlId?: string;
  renderTrigger?: boolean;
}

export interface TextControlState {
  value: string;
}

const safeStringify = (value?: InputValueType | null) =>
  value == null ? '' : String(value);

export default class TextControl<
  T extends InputValueType = InputValueType,
> extends Component<TextControlProps<T>, TextControlState> {
  initialValue?: TextControlProps['value'];

  constructor(props: TextControlProps<T>) {
    super(props);
    this.initialValue = props.value;
    this.state = {
      value: safeStringify(this.initialValue),
    };
  }

  onChange = (inputValue: string) => {
    let parsedValue: InputValueType = inputValue;
    // Validation & casting
    const errors = [];
    if (inputValue !== '' && this.props.isFloat) {
      const error = legacyValidateNumber(inputValue);
      if (error) {
        errors.push(error);
      } else {
        parsedValue = inputValue.match(/.*([.0])$/g)
          ? inputValue
          : parseFloat(inputValue);
      }
    }
    if (inputValue !== '' && this.props.isInt) {
      const error = legacyValidateInteger(inputValue);
      if (error) {
        errors.push(error);
      } else {
        parsedValue = parseInt(inputValue, 10);
      }
    }
    this.props.onChange?.(parsedValue as T, errors);
  };

  debouncedOnChange = debounce((inputValue: string) => {
    this.onChange(inputValue);
  }, FAST_DEBOUNCE);

  onChangeWrapper = (event: ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target;
    this.setState({ value }, () => {
      this.debouncedOnChange(value);
    });
  };

  render() {
    let { value } = this.state;
    if (this.initialValue !== this.props.value) {
      this.initialValue = this.props.value;
      value = safeStringify(this.props.value);
    }
    return (
      <div>
        <ControlHeader {...this.props} />
        <Input
          type="text"
          data-test="inline-name"
          placeholder={this.props.placeholder}
          onChange={this.onChangeWrapper}
          onFocus={this.props.onFocus}
          value={value}
          disabled={this.props.disabled}
          aria-label={this.props.label}
        />
      </div>
    );
  }
}