Skip to content

Duotone Picker: Stop the duotone bar offering to move its control points - #81850

Draft
ramonjd wants to merge 4 commits into
trunkfrom
fix/81799-duotone-bar-announcement
Draft

Duotone Picker: Stop the duotone bar offering to move its control points#81850
ramonjd wants to merge 4 commits into
trunkfrom
fix/81799-duotone-bar-announcement

Conversation

@ramonjd

@ramonjd ramonjd commented Aug 20, 2026

Copy link
Copy Markdown
Member

What?

Stops the duotone bar telling users they can move its control points.

Closes #81799.

Why?

Each control point is described as:

Use your left or right arrow keys or drag and drop with the mouse to change the gradient position. Press the button to change the color or remove the control point.

Both halves are wrong for a duotone:

  • Position. CustomDuotoneBar passes the stops through getColorsFromColorStops(), which keeps only the colors. A duotone is two colors with no positions, so a move is discarded.
  • Removal. It needs controlPoints.length > 2, and a duotone has exactly two.

Screen reader users get the whole sentence, so they are the ones told to perform operations that do nothing.

How?

CustomGradientBar gains disablePositioning, set by CustomDuotoneBar. It guards the arrow key and drag handlers, and swaps the description for:

Press the button to change the color.

Opt-in, so the gradient bar is unchanged.

Two notes on scope:

  • One flag, not two. CustomDuotoneBar is the only consumer that disables anything, and it is also the only place removal is unavailable, so a second axis would be untested.
  • The button's aria-label still reads "Gradient control point at position 0%…". It states a fact rather than promising an action, so it is left alone.

Testing Instructions

The duotone bar

Add an Image block, open the duotone control in the toolbar, pick a duotone. The bar appears below the swatches.

  1. Focus a control point and press ← or →. Nothing moves. On trunk the stop shifts.
  2. Drag a control point. Nothing moves. On trunk it drags, then snaps back.
  3. Click a control point. The color picker still opens.

Screen reader

With VoiceOver or NVDA, focus a control point.

  • Expected: "…Press the button to change the color."
  • On trunk: "…Use your left or right arrow keys or drag and drop with the mouse to change the gradient position. Press the button to change the color or remove the control point."

Without a screen reader, inspect the control point and follow aria-describedby.

Gradient bar, unchanged

Site Editor → Styles → Colors → Edit palette → Gradient. Add a gradient, click its swatch.

  1. Arrow keys move a stop.
  2. Dragging works.
  3. With three or more stops, "Remove control point" appears.
  4. The description is still the original sentence.

The bar announced that arrow keys and dragging change the gradient
position, and that the button removes a control point. A duotone is two
colors with no positions, so a move was discarded, and removal needs
more than two control points, so it was never available either.

CustomGradientBar gains a disablePositioning prop, set by
CustomDuotoneBar, which guards the arrow key and drag handlers and
swaps the description for one that matches what the control can do.
@github-actions github-actions Bot added the [Package] Components /packages/components label Aug 20, 2026
@ramonjd
ramonjd requested a balanced review from Copilot August 20, 2026 04:25
@github-actions

Copy link
Copy Markdown

Warning: Type of PR label mismatch

To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.

  • Required label: Any label starting with [Type].
  • Labels found: [Package] Components.

Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents duotone controls from advertising or performing unsupported stop positioning.

Changes:

  • Adds disablePositioning to CustomGradientBar.
  • Disables duotone dragging and position updates.
  • Adds accessibility and keyboard tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/components/CHANGELOG.md Documents the fix.
packages/components/src/custom-gradient-picker/types.ts Defines the new prop.
packages/components/src/custom-gradient-picker/gradient-bar/index.tsx Forwards positioning state.
packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx Adjusts handlers and description.
packages/components/src/duotone-picker/custom-duotone-bar.tsx Disables positioning for duotones.
packages/components/src/duotone-picker/test/custom-duotone-bar.tsx Tests description and arrow keys.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Size Change: +98 B (0%)

Total Size: 7.74 MB

📦 View Changed
Filename Size Change
build/scripts/components/index.min.js 257 kB +98 B (+0.04%)

compressed-size-action

return (
<CustomGradientBar
disableInserter
disablePositioning

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomDuotoneBar sets the flag internally, so no public change (and not opt-out). This is okay presumably since you can't perform the position action for duotones anyway.

The early return skipped the stopPropagation calls that live inside the
arrow key branches, so left and right bubbled out of the control and
could move focus to another editor area. Measured against trunk: two
arrow presses reached an ancestor listener where trunk let none through.

The keys are now consumed before returning. Other keys are untouched and
still bubble.
isOpen,
position,
color,
disablePositioning,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right prop name?

aria-label={ sprintf(
// translators: 1: gradient position e.g: 70. 2: gradient color code e.g: rgb(52,121,151).
__(
'Gradient control point at position %1$d%% with color code %2$s.'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned in the PR desc.

I left thearia-label unchanged as it states a fact rather than promising an action.

The smallest version could be to flip the string to something like:

'Duotone control point with color code %s.'

?

onToggle();
} }
onMouseDown={ () => {
if ( disablePositioning ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No touch/point change handling to miss AFAIK so we can return early here.

}
} }
onKeyDown={ ( event ) => {
if ( disablePositioning ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be optional, but a nice to have.

Trunk swallowed the arrow keys (below), but "handles" them for position changes, which we don't need for duotone.

…click path

Nothing asserted that arrow keys move a control point on the gradient
bar, so the claim that disablePositioning is opt-in had no test behind
it. Adds that, plus its negative counterpart.

Also covers clicking a duotone control point, since disabling
positioning must not disable the one action the point still has.
@github-actions

Copy link
Copy Markdown

Flaky tests detected in a7c3418.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/32333633400
📝 Reported tests:

Should save the changes in /test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js, passed after 2 failed attempts.
TimeoutError: page.waitForFunction: Timeout 10000ms exceeded.
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js:34:14
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for locator('role=button[name="Visual"i]')

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js:33:55

@ciampo ciampo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach looking sound 🔉

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a regression test for disabled mouse dragging? In its simplest form, something like

it( 'does not move a control point when dragged', () => {
	const onChange = jest.fn();

	render( <CustomDuotoneBar value={ VALUE } onChange={ onChange } /> );

	const [ firstPoint ] = screen.getAllByRole( 'button', {
		name: /Gradient control point/,
	} );

	fireEvent.mouseDown( firstPoint );
	fireEvent.mouseMove( window, { clientX: 50 } );
	fireEvent.mouseUp( window );

	expect( onChange ).not.toHaveBeenCalled();
} );

For completeness, even a positive CustomGradientBar case that mocks the markers container’s getBoundingClientRect(), performs the same window-level events, and confirms onChange receives a moved position

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Components /packages/components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duotone bar tells screen reader users to reposition stops, but positions are discarded

3 participants