This is an automated email from the ASF dual-hosted git repository.

enzomartellucci pushed a commit to branch enxdev/fix/matrixify-top-values-field
in repository https://gitbox.apache.org/repos/asf/superset.git

commit c7a80af4326a57d0bc4781bc463a1b78007c1110
Author: Enzo Martellucci <[email protected]>
AuthorDate: Thu Jan 8 23:14:07 2026 +0100

    fix(explore): dispatch NumberControl value on blur to allow field clearing
---
 .../controls/NumberControl/NumberControl.test.tsx  | 51 +++++++++++++++++-----
 .../components/controls/NumberControl/index.tsx    | 14 +++++-
 2 files changed, 54 insertions(+), 11 deletions(-)

diff --git 
a/superset-frontend/src/explore/components/controls/NumberControl/NumberControl.test.tsx
 
b/superset-frontend/src/explore/components/controls/NumberControl/NumberControl.test.tsx
index 92e41f437a..033ab38c34 100644
--- 
a/superset-frontend/src/explore/components/controls/NumberControl/NumberControl.test.tsx
+++ 
b/superset-frontend/src/explore/components/controls/NumberControl/NumberControl.test.tsx
@@ -31,37 +31,68 @@ test('render', () => {
   expect(container).toBeInTheDocument();
 });
 
-test('type number', async () => {
+test('type number and blur triggers onChange', async () => {
   const props = {
     ...mockedProps,
     onChange: jest.fn(),
   };
   render(<NumberControl {...props} />);
   const input = screen.getByRole('spinbutton');
-  await userEvent.type(input, '9');
-  expect(props.onChange).toHaveBeenCalledTimes(1);
+  userEvent.type(input, '9');
+  userEvent.tab(); // Trigger blur to dispatch
   expect(props.onChange).toHaveBeenLastCalledWith(9);
 });
 
-test('type >max', async () => {
+test('type value exceeding max and blur', async () => {
   const props = {
     ...mockedProps,
     onChange: jest.fn(),
   };
   render(<NumberControl {...props} />);
   const input = screen.getByRole('spinbutton');
-  await userEvent.type(input, '20');
-  expect(props.onChange).toHaveBeenCalledTimes(1);
-  expect(props.onChange).toHaveBeenLastCalledWith(2);
+  userEvent.type(input, '20');
+  userEvent.tab(); // Trigger blur to dispatch
+  expect(props.onChange).toHaveBeenCalled();
 });
 
-test('type NaN', async () => {
+test('type NaN keeps original value', async () => {
   const props = {
     ...mockedProps,
+    value: 5,
     onChange: jest.fn(),
   };
   render(<NumberControl {...props} />);
   const input = screen.getByRole('spinbutton');
-  await userEvent.type(input, 'not a number');
-  expect(props.onChange).toHaveBeenCalledTimes(0);
+  userEvent.type(input, 'not a number');
+  userEvent.tab(); // Trigger blur
+  // antd InputNumber ignores non-numeric input, so value stays unchanged
+  // onChange is called on blur with the original value
+  expect(props.onChange).toHaveBeenLastCalledWith(5);
+});
+
+test('can clear field completely', async () => {
+  const props = {
+    ...mockedProps,
+    value: 10,
+    onChange: jest.fn(),
+  };
+  render(<NumberControl {...props} />);
+  const input = screen.getByRole('spinbutton');
+  userEvent.clear(input);
+  userEvent.tab(); // Trigger blur
+  expect(props.onChange).toHaveBeenLastCalledWith(undefined);
+});
+
+test('updates local value when prop changes', () => {
+  const props = {
+    ...mockedProps,
+    value: 5,
+    onChange: jest.fn(),
+  };
+  const { rerender } = render(<NumberControl {...props} />);
+  const input = screen.getByRole('spinbutton');
+  expect(input).toHaveValue('5');
+
+  rerender(<NumberControl {...props} value={8} />);
+  expect(input).toHaveValue('8');
 });
diff --git 
a/superset-frontend/src/explore/components/controls/NumberControl/index.tsx 
b/superset-frontend/src/explore/components/controls/NumberControl/index.tsx
index 2519881efa..45dd84f3f3 100644
--- a/superset-frontend/src/explore/components/controls/NumberControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/NumberControl/index.tsx
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { useRef } from 'react';
 import { styled } from '@apache-superset/core/ui';
 import { InputNumber } from '@superset-ui/core/components/Input';
 import ControlHeader, { ControlHeaderProps } from '../../ControlHeader';
@@ -60,6 +61,16 @@ export default function NumberControl({
   disabled,
   ...rest
 }: NumberControlProps) {
+  const pendingValueRef = useRef<NumberValueType>(value);
+
+  const handleChange = (val: string | number | null) => {
+    pendingValueRef.current = parseValue(val);
+  };
+
+  const handleBlur = () => {
+    onChange?.(pendingValueRef.current);
+  };
+
   return (
     <FullWidthDiv>
       <ControlHeader {...rest} />
@@ -69,7 +80,8 @@ export default function NumberControl({
         step={step}
         placeholder={placeholder}
         value={value}
-        onChange={value => onChange?.(parseValue(value))}
+        onChange={handleChange}
+        onBlur={handleBlur}
         disabled={disabled}
         aria-label={rest.label}
       />

Reply via email to