diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ce41753c2b25bb..4ff71c634a01be 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -17,6 +17,7 @@ ### Bug Fixes +- `InputControl`, `NumberControl`, `UnitControl`: Ignore the drag gesture for touch input, where it has no cursor affordance and is easy to trigger by accident ([#38865](https://github.com/WordPress/gutenberg/issues/38865), [#81519](https://github.com/WordPress/gutenberg/pull/81519)). - `navigateRegions`: Only outline the Publish and Save controls when their region has focus ([#84343](https://github.com/WordPress/gutenberg/pull/84343)). ## 42.0.0 (2026-10-07) diff --git a/packages/components/src/input-control/input-field.tsx b/packages/components/src/input-control/input-field.tsx index 9bd9aef6c959fe..c447cfb1a290e6 100644 --- a/packages/components/src/input-control/input-field.tsx +++ b/packages/components/src/input-control/input-field.tsx @@ -133,6 +133,16 @@ function InputField( ( dragProps ) => { const { distance, dragging, event, target } = dragProps; + // The drag gesture is not offered for touch input, where there is + // no cursor affordance for it, and where it's easy to accidentally + // change the value when only meaning to tap or move the caret. + if ( + event.pointerType === 'touch' || + event.type.startsWith( 'touch' ) + ) { + return; + } + // The `target` prop always references the `input` element while, by // default, the `dragProps.event.target` property would reference the real // event target (i.e. any DOM element that the pointer is hovering while diff --git a/packages/components/src/number-control/test/index.browser.test.tsx b/packages/components/src/number-control/test/index.browser.test.tsx new file mode 100644 index 00000000000000..f374be989acb4b --- /dev/null +++ b/packages/components/src/number-control/test/index.browser.test.tsx @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { render } from 'vitest-browser-react'; +import { useState } from '@wordpress/element'; +import NumberControl from '..'; + +function StatefulNumberControl() { + const [ value, setValue ] = useState< string | undefined >( '5' ); + + return ; +} + +function dragInput( input: Element, pointerType: 'touch' | 'mouse' ) { + const pointer = { + pointerId: 1, + pointerType, + isPrimary: true, + button: 0, + buttons: 1, + clientX: 50, + }; + + fireEvent.pointerDown( input, { ...pointer, clientY: 100 } ); + fireEvent.pointerMove( window, { ...pointer, clientY: 80 } ); + fireEvent.pointerMove( window, { ...pointer, clientY: 40 } ); + fireEvent.pointerUp( window, { ...pointer, clientY: 40, buttons: 0 } ); +} + +describe( 'NumberControl drag to change value', () => { + it( 'does not change the value when dragged with touch input', async () => { + const screen = await render( ); + const input = screen.getByRole( 'spinbutton' ); + + dragInput( input.element(), 'touch' ); + + await expect.element( input ).toHaveValue( 5 ); + } ); + + it( 'changes the value when dragged with mouse input', async () => { + const screen = await render( ); + const input = screen.getByRole( 'spinbutton' ); + + dragInput( input.element(), 'mouse' ); + + await expect.element( input ).toHaveValue( 55 ); + } ); +} ); diff --git a/test/unit/vitest-policy-exceptions.json b/test/unit/vitest-policy-exceptions.json index f2da449a99a789..8b4618f1f16ee7 100644 --- a/test/unit/vitest-policy-exceptions.json +++ b/test/unit/vitest-policy-exceptions.json @@ -3,7 +3,8 @@ "packages/components/src/focal-point-picker/test/index.browser.test.tsx": "Starts an unreleased mouse gesture to verify cancellation when native Tab moves focus away; subsequent movement uses native hover.", "packages/media-editor/src/image-editor/react/components/stencils/test/rectangle-stencil.browser.test.tsx": "Uses exact pointer and touch payloads for capture lifecycle coverage, plus direct key events only where fake timers verify resize debounce timing.", "packages/media-editor/src/image-editor/react/components/test/cropper.browser.test.tsx": "Uses exact pointer, wheel, and transition payloads for crop geometry and lifecycle coverage, plus a direct key event only where fake timers isolate resize timing.", - "packages/components/src/color-picker/test/index.browser.test.tsx": "Supplies exact bottom-edge mouse coordinates for click-to-black saturation coverage, where native hit testing excludes the lower boundary. Drag and keyboard paths use native input." + "packages/components/src/color-picker/test/index.browser.test.tsx": "Supplies exact bottom-edge mouse coordinates for click-to-black saturation coverage, where native hit testing excludes the lower boundary. Drag and keyboard paths use native input.", + "packages/components/src/number-control/test/index.browser.test.tsx": "Supplies identical touch and mouse PointerEvent sequences to verify the per-pointer-type drag policy; Browser Mode userEvent does not support touch drags." }, "jsdomBrowserApis": { "test/unit/config/setup-globals.vitest.jsdom.test.js": "This infrastructure regression must exercise the opt-in matchMedia mock in the jsdom project that installs it.",