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} />
