diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.jsx b/superset-frontend/src/explore/components/controls/BoundsControl.jsx
deleted file mode 100644
index 39a0d560b..000000000
--- a/superset-frontend/src/explore/components/controls/BoundsControl.jsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { InputNumber } from 'src/common/components';
-import { t, styled } from '@superset-ui/core';
-import { isEqual, debounce } from 'lodash';
-import ControlHeader from 'src/explore/components/ControlHeader';
-
-const propTypes = {
- onChange: PropTypes.func,
- value: PropTypes.array,
-};
-
-const defaultProps = {
- onChange: () => {},
- value: [null, null],
-};
-
-const StyledDiv = styled.div`
- display: flex;
-`;
-
-const MinInput = styled(InputNumber)`
- flex: 1;
- margin-right: ${({ theme }) => theme.gridUnit}px;
-`;
-
-const MaxInput = styled(InputNumber)`
- flex: 1;
- margin-left: ${({ theme }) => theme.gridUnit}px;
-`;
-
-export default class BoundsControl extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- minMax: [
- Number.isNaN(this.props.value[0]) ? '' : props.value[0],
- Number.isNaN(this.props.value[1]) ? '' : props.value[1],
- ],
- };
- this.onChange = debounce(this.onChange.bind(this), 300);
- this.onMinChange = this.onMinChange.bind(this);
- this.onMaxChange = this.onMaxChange.bind(this);
- this.update = this.update.bind(this);
- }
-
- componentDidUpdate(prevProps) {
- if (!isEqual(prevProps.value, this.props.value)) {
- this.update();
- }
- }
-
- update() {
- this.setState({
- minMax: [
- Number.isNaN(this.props.value[0]) ? '' : this.props.value[0],
- Number.isNaN(this.props.value[1]) ? '' : this.props.value[1],
- ],
- });
- }
-
- onMinChange(value) {
- this.setState(
- prevState => ({
- minMax: [value, prevState.minMax[1]],
- }),
- this.onChange,
- );
- }
-
- onMaxChange(value) {
- this.setState(
- prevState => ({
- minMax: [prevState.minMax[0], value],
- }),
- this.onChange,
- );
- }
-
- onChange() {
- const mm = this.state.minMax;
- const min = Number.isNaN(parseFloat(mm[0])) ? null : parseFloat(mm[0]);
- const max = Number.isNaN(parseFloat(mm[1])) ? null : parseFloat(mm[1]);
- this.props.onChange([min, max]);
- }
-
- render() {
- return (
-
-
-
-
-
-
-
- );
- }
-}
-
-BoundsControl.propTypes = propTypes;
-BoundsControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.stories.tsx b/superset-frontend/src/explore/components/controls/BoundsControl.stories.tsx
new file mode 100644
index 000000000..04c197682
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/BoundsControl.stories.tsx
@@ -0,0 +1,54 @@
+/**
+ * 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 React from 'react';
+import BoundsControl, { BoundsControlProps } from './BoundsControl';
+
+export default {
+ title: 'BoundsControl',
+ component: BoundsControl,
+};
+
+export const InteractiveBoundsControl = (
+ args: BoundsControlProps & { initialMin: number; initialMax: number },
+) => {
+ const { initialMin, initialMax, ...props } = args;
+
+ return (
+ <>
+
+ >
+ );
+};
+
+InteractiveBoundsControl.args = {
+ initialMin: 0,
+ initialMax: 50,
+};
+
+InteractiveBoundsControl.argTypes = {
+ onChange: { action: 'onChange' },
+};
+
+InteractiveBoundsControl.story = {
+ parameters: {
+ knobs: {
+ disable: true,
+ },
+ },
+};
diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.tsx b/superset-frontend/src/explore/components/controls/BoundsControl.tsx
new file mode 100644
index 000000000..1565ce7d3
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/BoundsControl.tsx
@@ -0,0 +1,105 @@
+/**
+ * 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 React, { useEffect, useRef, useState } from 'react';
+import { InputNumber } from 'src/common/components';
+import { t, styled } from '@superset-ui/core';
+import { debounce } from 'lodash';
+import ControlHeader from 'src/explore/components/ControlHeader';
+
+type ValueType = (number | null)[];
+
+export type BoundsControlProps = {
+ onChange?: (value: ValueType) => void;
+ value?: ValueType;
+};
+
+const StyledDiv = styled.div`
+ display: flex;
+`;
+
+const MinInput = styled(InputNumber)`
+ flex: 1;
+ margin-right: ${({ theme }) => theme.gridUnit}px;
+`;
+
+const MaxInput = styled(InputNumber)`
+ flex: 1;
+ margin-left: ${({ theme }) => theme.gridUnit}px;
+`;
+
+const parseNumber = (value: undefined | number | string | null) =>
+ value === null || Number.isNaN(Number(value)) ? null : Number(value);
+
+export default function BoundsControl({
+ onChange = () => {},
+ value = [null, null],
+ ...props
+}: BoundsControlProps) {
+ const [minMax, setMinMax] = useState([
+ parseNumber(value[0]),
+ parseNumber(value[1]),
+ ]);
+ const min = value[0];
+ const max = value[1];
+ const debouncedOnChange = useRef(debounce(onChange, 300)).current;
+
+ const update = (mm: ValueType) => {
+ setMinMax(mm);
+ debouncedOnChange([
+ mm[0] === undefined ? null : mm[0],
+ mm[1] === undefined ? null : mm[1],
+ ]);
+ };
+
+ useEffect(() => {
+ setMinMax([parseNumber(min), parseNumber(max)]);
+ }, [min, max]);
+
+ const onMinChange = (value: number | string | undefined) => {
+ update([parseNumber(value), minMax[1]]);
+ };
+
+ const onMaxChange = (value: number | string | undefined) => {
+ update([minMax[0], parseNumber(value)]);
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}