Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

### Enhancements

- `BlockAlignmentControl`: List Wide and Full width as unavailable when a parent layout withholds them but the theme itself offers them, instead of removing them from the menu without explanation. A theme that offers neither keeps them hidden, since it is curating its own options. Blocks whose only alignments are wide and full, such as Group and Columns, now keep an alignment control in layouts that offer neither ([#82600](https://github.com/WordPress/gutenberg/pull/82600)).
- Block alignment: Explain in the alignment menu when the template rather than a block on the page limits width, with a way to edit that template. Hosts supply the explanation through a private setting, since the block editor cannot know what a template is ([#82675](https://github.com/WordPress/gutenberg/pull/82675)).
- Block alignment: Name the containing block whose layout withholds wide and full alignments in the alignment menu itself, and offer to select it so the setting can be changed where it lives ([#82607](https://github.com/WordPress/gutenberg/pull/82607)).
- Borders: rename the "Border & Shadow" panel to "Borders", whichever of its controls are available, and always show the Border and Shadow controls' visible labels. A stable panel title is what lets the Border label render unconditionally, so its "Unlink sides" toggle lines up with the border radius one ([#82163](https://github.com/WordPress/gutenberg/pull/82163)).
- `ListView`: Updated to use `outset-ring__focus()` mixin for focus outline wherever applicable instead of the previous box-shadow implementation. ([#82129](https://github.com/WordPress/gutenberg/pull/82129))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BlockAlignmentUI from '../ui';
import { LayoutProvider } from '../../block-list/layout';
import { BlockEditorProvider } from '../../provider';

describe( 'BlockAlignmentUI', () => {
const alignment = 'left';
Expand Down Expand Up @@ -115,3 +117,279 @@ describe( 'BlockAlignmentUI', () => {
expect( onChange ).toHaveBeenCalledWith( 'center' );
} );
} );

/**
* A theme offering both wide and full. Without one, those alignments were never
* on the table and are not reported as withheld.
*/
const THEME_LAYOUT = { contentSize: '600px', wideSize: '1200px' };

function renderWithTheme( ui, layout = THEME_LAYOUT ) {
return render(
<BlockEditorProvider
value={ [] }
settings={ { __experimentalFeatures: { layout } } }
>
{ ui }
</BlockEditorProvider>
);
}

describe( 'BlockAlignmentUI unavailable alignments', () => {
// The block supports every alignment; the default flow layout offers only
// none/left/center/right, so wide and full are taken away by the parent.
const controls = [ 'left', 'center', 'right', 'wide', 'full' ];
const onChange = vi.fn();

afterEach( () => {
onChange.mockClear();
} );

async function openMenu() {
const user = userEvent.setup();
renderWithTheme(
<BlockAlignmentUI onChange={ onChange } controls={ controls } />
);
await user.click(
screen.getByRole( 'button', { name: 'Align block' } )
);
return user;
}

test( 'lists them rather than omitting them, in their usual position', async () => {
await openMenu();

expect(
screen
.getAllByRole( 'menuitemradio' )
.map( ( item ) => item.textContent )
).toEqual( [
'None',
'Wide widthNot available',
'Full widthNot available',
'Align left',
'Align center',
'Align right',
] );
} );

test( 'marks them as disabled while keeping them reachable', async () => {
await openMenu();

const wide = screen.getByRole( 'menuitemradio', {
name: /Wide width/,
} );

expect( wide ).toHaveAttribute( 'aria-disabled', 'true' );
// `disabled` would remove it from the accessibility tree entirely,
// taking the explanation with it.
expect( wide ).toBeEnabled();
} );

test( 'does not change the alignment when one is chosen', async () => {
const user = await openMenu();

await user.click(
screen.getByRole( 'menuitemradio', { name: /Full width/ } )
);

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

test( 'leaves the menu alone when the layout offers every alignment', async () => {
const user = userEvent.setup();
renderWithTheme(
<LayoutProvider
value={ {
type: 'constrained',
contentSize: '600px',
wideSize: '1200px',
} }
>
<BlockAlignmentUI onChange={ onChange } controls={ controls } />
</LayoutProvider>
);
await user.click(
screen.getByRole( 'button', { name: 'Align block' } )
);

expect( screen.queryAllByText( 'Not available' ) ).toHaveLength( 0 );
expect(
screen
.getAllByRole( 'menuitemradio' )
.map( ( item ) => item.textContent )
).toEqual( [
'NoneMax 600px wide',
'Wide widthMax 1200px wide',
'Full width',
'Align left',
'Align center',
'Align right',
] );
} );
} );

describe( 'BlockAlignmentUI with no available alignments', () => {
// Group supports only wide and full, so a flow layout leaves it nothing.
const controls = [ 'wide', 'full' ];
const onChange = vi.fn();

afterEach( () => {
onChange.mockClear();
} );

test( 'still offers None alongside what the layout withholds', async () => {
const user = userEvent.setup();
renderWithTheme(
<BlockAlignmentUI onChange={ onChange } controls={ controls } />
);

await user.click(
screen.getByRole( 'button', { name: 'Align block' } )
);

expect(
screen
.getAllByRole( 'menuitemradio' )
.map( ( item ) => item.textContent )
).toEqual( [
'None',
'Wide widthNot available',
'Full widthNot available',
] );
} );

test( 'renders nothing when the layout places children itself', () => {
const { container } = renderWithTheme(
<LayoutProvider value={ { type: 'flex' } }>
<BlockAlignmentUI onChange={ onChange } controls={ controls } />
</LayoutProvider>
);

expect( container ).toBeEmptyDOMElement();
} );
} );

describe( 'BlockAlignmentUI when the theme withholds alignments', () => {
// A theme offering neither is curating its own options globally, so those
// alignments stay hidden rather than being reported as unavailable.
const controls = [ 'left', 'center', 'right', 'wide', 'full' ];
const onChange = vi.fn();

afterEach( () => {
onChange.mockClear();
} );

async function openMenuForTheme( layout ) {
const user = userEvent.setup();
renderWithTheme(
<BlockAlignmentUI onChange={ onChange } controls={ controls } />,
layout
);
await user.click(
screen.getByRole( 'button', { name: 'Align block' } )
);
}

function menuItems() {
return screen
.getAllByRole( 'menuitemradio' )
.map( ( item ) => item.textContent );
}

test( 'hides both when the theme sets no layout at all', async () => {
// No provider at all, so the store holds no global layout.
const user = userEvent.setup();
render(
<BlockAlignmentUI onChange={ onChange } controls={ controls } />
);
await user.click(
screen.getByRole( 'button', { name: 'Align block' } )
);

expect( menuItems() ).toEqual( [
'None',
'Align left',
'Align center',
'Align right',
] );
} );

test( 'hides Wide when the theme sets no wide size', async () => {
await openMenuForTheme( { contentSize: '600px' } );

expect( menuItems() ).toEqual( [
'None',
'Full widthNot available',
'Align left',
'Align center',
'Align right',
] );
} );
} );

describe( 'BlockAlignmentUI constraint', () => {
const controls = [ 'left', 'center', 'right', 'wide', 'full' ];
const onChange = vi.fn();
const onSelect = vi.fn();
const constraint = {
description: 'It limits this block\u2019s width.',
action: { label: 'Select Group', onClick: onSelect },
};

afterEach( () => {
onChange.mockClear();
onSelect.mockClear();
} );

async function openMenu() {
const user = userEvent.setup();
renderWithTheme(
<BlockAlignmentUI
onChange={ onChange }
controls={ controls }
constraint={ constraint }
/>
);
await user.click(
screen.getByRole( 'button', { name: 'Align block' } )
);
return user;
}

test( 'explains the constraint as the action description', async () => {
await openMenu();

// Hung off the item rather than a group heading, so it reads as one
// thing with the action that resolves it.
expect(
screen.getByRole( 'menuitem', {
name: `Select Group ${ constraint.description }`,
} )
).toBeInTheDocument();
} );

test( 'offers the action as a command, not an alignment choice', async () => {
await openMenu();

// `menuitem`, not `menuitemradio`, so it is not announced as an
// alignment option alongside the real ones.
expect(
screen.getByRole( 'menuitem', { name: /Select Group/ } )
).toBeInTheDocument();
expect(
screen.queryByRole( 'menuitemradio', { name: /Select Group/ } )
).not.toBeInTheDocument();
} );

test( 'runs the action without changing the alignment', async () => {
const user = await openMenu();

await user.click(
screen.getByRole( 'menuitem', { name: /Select Group/ } )
);

expect( onSelect ).toHaveBeenCalledTimes( 1 );
expect( onChange ).not.toHaveBeenCalled();
} );
} );
Loading
Loading