Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

### Bug Fixes

- `DuotoneControl`: Keep the picked preset's identity when applying a duotone to a block. Two presets can hold the same pair of colors, and the applied preset was resolved by matching colors, so the first of any duplicate pair was saved and both appeared selected ([#81605](https://github.com/WordPress/gutenberg/pull/81605)).
- `InnerBlocks`: Resolve the `default` of a block's `layout` support before providing it to inner blocks. A block that declared its layout only as a support default, such as the Gallery, previously handed its children the raw support config, which has no `type`, so the children resolved to the flow layout instead. As a result an Image nested in a Gallery offered left/center/right alignment, which the flex layout does not permit ([#81606](https://github.com/WordPress/gutenberg/pull/81606)).
- Never apply Spotlight mode in a preview canvas, which cannot be edited and so rendered most of its content faded ([#81615](https://github.com/WordPress/gutenberg/pull/81615)).
- Grid: Keep child layout changes made with the resizer scoped to the selected viewport.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function DuotoneControl( {
disableCustomColors,
disableCustomDuotone,
value,
selectedSlug,
onChange,
} ) {
let toolbarIcon;
Expand Down Expand Up @@ -75,6 +76,7 @@ function DuotoneControl( {
disableCustomColors={ disableCustomColors }
disableCustomDuotone={ disableCustomDuotone }
value={ value }
selectedSlug={ selectedSlug }
onChange={ onChange }
/>
</MenuGroup>
Expand Down
100 changes: 100 additions & 0 deletions packages/block-editor/src/components/duotone-control/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import DuotoneControl from '../';

// Adding two custom duotones in Global Styles seeds both from the palette's
// darkest and lightest colors, so two presets holding the same pair is the
// ordinary case. Only the slug tells them apart.
const DUPLICATE_PALETTE = [
{
name: 'Duotone 1',
slug: 'custom-duotone-1',
colors: [ '#000000', '#ffffff' ],
},
{
name: 'Duotone 2',
slug: 'custom-duotone-2',
colors: [ '#000000', '#ffffff' ],
},
];

const COLOR_PALETTE = [
{ color: '#000000', name: 'Black', slug: 'black' },
{ color: '#ffffff', name: 'White', slug: 'white' },
];

async function openControl( user ) {
await user.click(
screen.getByRole( 'button', { name: 'Apply duotone filter' } )
);
}

describe( 'DuotoneControl', () => {
it( 'reports the slug of the duplicate that was picked', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

render(
<DuotoneControl
duotonePalette={ DUPLICATE_PALETTE }
colorPalette={ COLOR_PALETTE }
value={ undefined }
onChange={ onChange }
/>
);
await openControl( user );

await user.click(
screen.getByRole( 'option', { name: 'Duotone: Duotone 2' } )
);

expect( onChange ).toHaveBeenCalledWith(
[ '#000000', '#ffffff' ],
1,
'custom-duotone-2'
);
} );

it( 'marks only the stored preset as selected when two share colors', async () => {
const user = userEvent.setup();

render(
<DuotoneControl
duotonePalette={ DUPLICATE_PALETTE }
colorPalette={ COLOR_PALETTE }
value={ [ '#000000', '#ffffff' ] }
selectedSlug="custom-duotone-2"
onChange={ jest.fn() }
/>
);
await openControl( user );

expect(
screen.getByRole( 'option', { name: 'Duotone: Duotone 1' } )
).toHaveAttribute( 'aria-selected', 'false' );
expect(
screen.getByRole( 'option', { name: 'Duotone: Duotone 2' } )
).toHaveAttribute( 'aria-selected', 'true' );
} );

it( 'falls back to color matching when no slug is stored', async () => {
const user = userEvent.setup();

render(
<DuotoneControl
duotonePalette={ DUPLICATE_PALETTE }
colorPalette={ COLOR_PALETTE }
value={ [ '#000000', '#ffffff' ] }
onChange={ jest.fn() }
/>
);
await openControl( user );

expect(
screen.getByRole( 'option', { name: 'Duotone: Duotone 1' } )
).toHaveAttribute( 'aria-selected', 'true' );
expect(
screen.getByRole( 'option', { name: 'Duotone: Duotone 2' } )
).toHaveAttribute( 'aria-selected', 'true' );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { __, _x } from '@wordpress/i18n';
import { useCallback, useMemo, useRef } from '@wordpress/element';
import { reset as resetIcon } from '@wordpress/icons';
import { getValueFromVariable } from '@wordpress/global-styles-engine';
import { useToolsPanelDropdownMenuProps } from './utils';
import {
useToolsPanelDropdownMenuProps,
getDuotoneSlugFromPreset,
} from './utils';
import { setImmutably } from '../../utils/object';
import {
getInheritanceProps,
Expand Down Expand Up @@ -202,31 +205,41 @@ export default function FiltersPanel( {
const localDuotone = decodeValue( value?.filter?.duotone );
const inheritedDuotone = decodeValue( inheritedValue?.filter?.duotone );
const duotone = localDuotone ?? inheritedDuotone;
// Read from the raw value, since decoding resolves a preset reference to
// its colors and two presets can hold the same pair.
const localDuotoneSlug = getDuotoneSlugFromPreset( value?.filter?.duotone );
const inheritedDuotoneSlug = getDuotoneSlugFromPreset(
inheritedValue?.filter?.duotone
);
const duotoneSlug =
localDuotone !== undefined ? localDuotoneSlug : inheritedDuotoneSlug;
const isDuotonePlaceholder =
localDuotone === undefined && inheritedDuotone !== undefined;
const setDuotone = ( newValue ) => {
const duotonePreset = duotonePalette.find( ( { colors } ) => {
return colors === newValue;
} );
const duotoneValue = duotonePreset
? `var:preset|duotone|${ duotonePreset.slug }`
const setDuotone = ( newValue, slug ) => {
// A slug identifies the picked preset exactly. Falling back to a value
// match cannot tell two presets holding the same colors apart.
const presetSlug =
slug ??
duotonePalette.find( ( { colors } ) => colors === newValue )?.slug;
const duotoneValue = presetSlug
? `var:preset|duotone|${ presetSlug }`
: newValue;
onChange(
setImmutably( value, [ 'filter', 'duotone' ], duotoneValue )
);
};
// Commit the inherited value when the user clicks the active preset
// while the picker is showing inherited duotone at rest.
const setDuotoneWithInheritedCommit = ( newValue ) => {
const setDuotoneWithInheritedCommit = ( newValue, index, slug ) => {
if (
newValue === undefined &&
isDuotonePlaceholder &&
inheritedDuotone !== undefined
) {
setDuotone( inheritedDuotone );
setDuotone( inheritedDuotone, inheritedDuotoneSlug );
return;
}
setDuotone( newValue );
setDuotone( newValue, slug );
};
const hasDuotone = () => !! value?.filter?.duotone;
const resetDuotone = () => setDuotone( undefined );
Expand Down Expand Up @@ -292,6 +305,7 @@ export default function FiltersPanel( {
disableCustomColors
disableCustomDuotone
value={ duotone }
selectedSlug={ duotoneSlug }
onChange={
setDuotoneWithInheritedCommit
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,87 @@ describe( 'FiltersPanel — visual treatment and display-without-commit', () =>
expect( onChange ).not.toHaveBeenCalled();
} );
} );

// Adding two custom duotones in Global Styles seeds both from the palette's
// darkest and lightest colors, so duplicates are the ordinary case. Only the
// slug tells them apart.
const duplicateSettings = {
color: {
customDuotone: true,
defaultDuotone: true,
duotone: {
custom: [
{
name: 'Duotone 1',
slug: 'custom-duotone-1',
colors: [ '#000000', '#ffffff' ],
},
{
name: 'Duotone 2',
slug: 'custom-duotone-2',
colors: [ '#000000', '#ffffff' ],
},
],
},
palette: {
default: [
{ name: 'Black', slug: 'black', color: '#000000' },
{ name: 'White', slug: 'white', color: '#ffffff' },
],
},
},
};

async function openDuotone( user ) {
await user.click( screen.getByRole( 'button', { name: /duotone/i } ) );
}

describe( 'FiltersPanel — duplicate duotone presets', () => {
it( 'marks only the stored preset as selected', async () => {
const user = userEvent.setup();

render(
<FiltersPanel
value={ {
filter: { duotone: 'var:preset|duotone|custom-duotone-2' },
} }
inheritedValue={ {} }
settings={ duplicateSettings }
onChange={ () => {} }
panelId="test-panel"
/>
);
await openDuotone( user );

expect(
screen.getByRole( 'option', { name: 'Duotone: Duotone 1' } )
).toHaveAttribute( 'aria-selected', 'false' );
expect(
screen.getByRole( 'option', { name: 'Duotone: Duotone 2' } )
).toHaveAttribute( 'aria-selected', 'true' );
} );

it( 'saves the slug of the preset that was picked', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

render(
<FiltersPanel
value={ {} }
inheritedValue={ {} }
settings={ duplicateSettings }
onChange={ onChange }
panelId="test-panel"
/>
);
await openDuotone( user );

await user.click(
screen.getByRole( 'option', { name: 'Duotone: Duotone 2' } )
);

expect( onChange ).toHaveBeenCalledWith( {
filter: { duotone: 'var:preset|duotone|custom-duotone-2' },
} );
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getDuotoneSlugFromPreset } from '../utils';

describe( 'global styles utils', () => {
describe( 'getDuotoneSlugFromPreset', () => {
it( 'should return the slug of a preset reference', () => {
expect(
getDuotoneSlugFromPreset( 'var:preset|duotone|grayscale' )
).toBe( 'grayscale' );
} );

it( 'should return the slug of a custom preset', () => {
expect(
getDuotoneSlugFromPreset(
'var:preset|duotone|custom-duotone-2'
)
).toBe( 'custom-duotone-2' );
} );

it( 'should return undefined for custom colors', () => {
expect(
getDuotoneSlugFromPreset( [ '#000000', '#ffffff' ] )
).toBeUndefined();
} );

it( 'should return undefined for unset and for no value', () => {
expect( getDuotoneSlugFromPreset( 'unset' ) ).toBeUndefined();
expect( getDuotoneSlugFromPreset( undefined ) ).toBeUndefined();
} );
} );
} );
23 changes: 23 additions & 0 deletions packages/block-editor/src/components/global-styles/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,26 @@ export function scopeSelector( scope, selector ) {

return selectorsScoped.join( ', ' );
}

/**
* Reads the preset slug out of a duotone style value.
*
* Two presets can hold the same pair of colors, so the slug is the only thing
* that identifies which one is applied. The colors alone do not.
*
* Lives here rather than beside its siblings in `hooks/duotone` because that
* module imports the filters panel, so the panel cannot import back from it.
*
* @param {string|string[]|undefined} duotone A duotone style value.
*
* @return {string|undefined} The preset slug, if the value references one.
*/
export function getDuotoneSlugFromPreset( duotone ) {
if ( typeof duotone !== 'string' ) {
return undefined;
}

const [ , slug ] = duotone.match( /^var:preset\|duotone\|(.+)$/ ) ?? [];

return slug;
}
30 changes: 23 additions & 7 deletions packages/block-editor/src/hooks/duotone.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
__experimentalDuotoneControl as DuotoneControl,
useSettings,
} from '../components';
import { scopeSelector } from '../components/global-styles/utils';
import {
scopeSelector,
getDuotoneSlugFromPreset,
} from '../components/global-styles/utils';
import {
cleanEmptyObject,
useBlockSettings,
Expand Down Expand Up @@ -141,8 +144,13 @@ function DuotonePanelPure( { style, setAttributes, name, clientId } ) {
<>
<InspectorControls group="filter">
<StylesFiltersPanel
// The raw value, not the resolved colors. The panel
// decodes it for display, but needs the preset reference
// to know which preset is applied: two presets can hold
// the same pair of colors, and resolving first throws the
// slug away.
value={ {
filter: { duotone: duotonePresetOrColors },
filter: { duotone: duotoneStyle },
} }
onChange={ ( newDuotone ) => {
const newStyle = {
Expand All @@ -166,11 +174,19 @@ function DuotonePanelPure( { style, setAttributes, name, clientId } ) {
disableCustomDuotone={ disableCustomDuotone }
disableCustomColors={ disableCustomColors }
value={ duotonePresetOrColors }
onChange={ ( newDuotone ) => {
const maybePreset = getDuotonePresetFromColors(
newDuotone,
duotonePalette
);
selectedSlug={ getDuotoneSlugFromPreset( duotoneStyle ) }
onChange={ ( newDuotone, index, slug ) => {
// A slug means a preset was picked out of the palette,
// so it identifies the choice exactly. Matching back by
// color would resolve to whichever preset holding those
// colors comes first, which is the wrong one whenever
// two presets share a pair.
const maybePreset = slug
? `var:preset|duotone|${ slug }`
: getDuotonePresetFromColors(
newDuotone,
duotonePalette
);

const newStyle = {
...style,
Expand Down
Loading
Loading