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
57 changes: 27 additions & 30 deletions config/visual-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,33 +90,29 @@
*/

'enabled_blocks' => [
'core/paragraph',
'core/heading',
'core/list',
'core/quote',
'core/code',
'core/preformatted',
'core/pullquote',
'core/verse',
'core/table',
'core/image',
'core/gallery',
'core/video',
'core/audio',
'core/file',
'core/embed',
'core/cover',
'core/media-text',
'core/columns',
'core/group',
'core/row',
'core/stack',
'core/buttons',
'core/separator',
'core/spacer',
'core/details',
'core/search',
'core/latest-posts',
// Content cluster — forked to artisanpack/* (I0 #408, I1 #409).
// The core/* counterparts stay registered (so existing content
// deserializes and the from:core/* transforms keep working) but are
// dropped from this allow-list, so only the forks surface in the
// inserter.
'artisanpack/paragraph',
'artisanpack/heading',
'artisanpack/list',
'artisanpack/quote',
'artisanpack/code',
'artisanpack/preformatted',
'artisanpack/pullquote',
'artisanpack/verse',
'artisanpack/table',
// Media cluster — forked to artisanpack/* (I2 #410).
'artisanpack/image',
'artisanpack/gallery',
'artisanpack/video',
'artisanpack/audio',
'artisanpack/file',
'artisanpack/embed',
'artisanpack/cover',
'artisanpack/media-text',
// E4 — re-enabled on the back of B1's expanded core-data shim
// and the C1–C5 REST surface. Each block has a renderer in
// every renderer package (Blade / React / Vue) and round-trips
Expand Down Expand Up @@ -151,9 +147,7 @@
'core/query',
'core/post-template',
'artisanpack/callout',
// I3 (#411) — layout cluster fork. The artisanpack/* equivalents
// register alongside their core/* siblings so authors can compare
// during the V1 rollout. Cutover (I7) removes the core/* names.
// Layout cluster — forked to artisanpack/* (I3 #411).
'artisanpack/group',
'artisanpack/row',
'artisanpack/stack',
Expand All @@ -164,6 +158,9 @@
'artisanpack/separator',
'artisanpack/spacer',
'artisanpack/details',
// Widgets cluster — forked to artisanpack/* (I4 #412).
'artisanpack/search',
'artisanpack/latest-posts',
],

'disabled_blocks' => [
Expand Down
3 changes: 2 additions & 1 deletion packages/renderer-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"artisanpack/button",
"artisanpack/separator",
"artisanpack/spacer",
"artisanpack/details"
"artisanpack/details",
"artisanpack/search"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{-- artisanpack/search — delegates to core/search (carries the #338 button-icon a11y fix). --}}
@include('visual-editor-renderer-blade::blocks.core.search', ['attributes' => $attributes ?? [], 'innerContent' => $innerContent ?? '', 'blockName' => $blockName ?? 'artisanpack/search'])
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,17 @@ public function render( array $attrs )
expect( $html )->toContain( 'aria-label="Find stuff"' );
} );

it( 'carries the #338 a11y fix forward to artisanpack/search (I4 fork)', function () {
$tree = [ makeBlock( 'artisanpack/search', [ 'buttonText' => 'Go', 'buttonUseIcon' => true ] ) ];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'class="wp-block-search__button has-icon"' )
->toContain( 'aria-label="Go"' )
->toContain( '<svg class="wp-block-search__button-icon"' )
->not->toContain( '<button type="submit" class="wp-block-search__button"></button>' );
} );

it( 'preserves fractional column widths', function () {
$tree = [
makeBlock( 'core/columns', [], [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ const CORE_BLOCKS: Record<string, BlockRenderer> = {
'core/details': DetailsBlock,
'artisanpack/details': DetailsBlock,
'core/search': SearchBlock,
// Fork: artisanpack/search reuses the core/search renderer, which
// already carries the #338 button-icon a11y fix, so the forked block's
// front-end output is byte-equivalent. Phase I4 widgets cluster (#412).
'artisanpack/search': SearchBlock,
'core/separator': SeparatorBlock,
'artisanpack/separator': SeparatorBlock,
'core/spacer': SpacerBlock,
Expand Down
11 changes: 11 additions & 0 deletions packages/visual-editor-renderer-react/tests/BlockTree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,17 @@ describe('Core design blocks', () => {
expect(renderTree(tree)).toContain('aria-label="Find stuff"');
});

it('carries the #338 a11y fix forward to artisanpack/search (I4 fork)', () => {
const tree = [makeBlock('artisanpack/search', { buttonText: 'Go', buttonUseIcon: true })];

const html = renderTree(tree);

expect(html).toContain('class="wp-block-search__button has-icon"');
expect(html).toContain('aria-label="Go"');
expect(html).toContain('<svg class="wp-block-search__button-icon"');
expect(html).not.toContain('<button type="submit" class="wp-block-search__button"></button>');
});

it('renders the artisanpack/callout reference block', () => {
const tree = [
makeBlock('artisanpack/callout', {
Expand Down
54 changes: 48 additions & 6 deletions packages/visual-editor-renderer-vue/src/blocks/core/design.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,23 @@ export const SearchBlock = defineComponent({
const buttonText = attrString(props.attributes.buttonText, 'Search');
const useIcon = attrBoolean(props.attributes.buttonUseIcon);
const queryName = attrString(attrRecord(props.attributes.query).name, 's');
const buttonLabel = useIcon ? '' : buttonText;
const className = attrString(props.attributes.className);

// #338: an icon-only button must still expose an accessible
// name. Precedence matches the Blade + React renderers so all
// three stay byte-identical: buttonText → label → "Search".
const ariaLabel =
buttonText.trim() !== ''
? buttonText
: label.trim() !== ''
? label
: 'Search';

const buttonClasses = classList([
'wp-block-search__button',
useIcon ? 'has-icon' : null,
]);

const classes = classList([
'wp-block-search',
!showLabel ? 'wp-block-search__button-inside' : null,
Expand Down Expand Up @@ -330,11 +344,39 @@ export const SearchBlock = defineComponent({
name: queryName,
placeholder: placeholder === '' ? undefined : placeholder,
}),
h(
'button',
{ type: 'submit', class: 'wp-block-search__button' },
buttonLabel
),
useIcon
? h(
'button',
{
type: 'submit',
class: buttonClasses,
'aria-label': ariaLabel,
},
[
h(
'svg',
{
class: 'wp-block-search__button-icon',
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 24 24',
width: '24',
height: '24',
'aria-hidden': 'true',
focusable: 'false',
},
[
h('path', {
d: 'M13.5 6C10.5 6 8 8.5 8 11.5c0 1.1.3 2.1.9 3l-3.4 3 1 1.1 3.4-3c1 .9 2.2 1.4 3.6 1.4 3 0 5.5-2.5 5.5-5.5C19 8.5 16.5 6 13.5 6zm0 9.5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z',
}),
]
),
]
)
: h(
'button',
{ type: 'submit', class: buttonClasses },
buttonText
),
])
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ const CORE_BLOCKS: Record<string, BlockRenderer> = {
'core/details': DetailsBlock,
'artisanpack/details': DetailsBlock,
'core/search': SearchBlock,
// Fork: artisanpack/search reuses the core/search renderer, which
// already carries the #338 button-icon a11y fix, so the forked block's
// front-end output is byte-equivalent. Phase I4 widgets cluster (#412).
'artisanpack/search': SearchBlock,
'core/separator': SeparatorBlock,
'artisanpack/separator': SeparatorBlock,
'core/spacer': SpacerBlock,
Expand Down
28 changes: 26 additions & 2 deletions packages/visual-editor-renderer-vue/tests/BlockTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,34 @@ describe('Core design blocks', () => {
expect(ids[0]).not.toBe(ids[1]);
});

it('uses an empty button label when buttonUseIcon is true', () => {
it('renders an icon-only submit button with accessible name when buttonUseIcon is true', () => {
const tree = [makeBlock('core/search', { buttonText: 'Go', buttonUseIcon: true })];

expect(renderTree(tree)).toContain('<button type="submit" class="wp-block-search__button"></button>');
const html = renderTree(tree);

expect(html).toContain('class="wp-block-search__button has-icon"');
expect(html).toContain('aria-label="Go"');
expect(html).toContain('<svg class="wp-block-search__button-icon"');
expect(html).not.toContain('<button type="submit" class="wp-block-search__button"></button>');
});

it('falls back to label when buttonText is empty and buttonUseIcon is true', () => {
const tree = [
makeBlock('core/search', { label: 'Find stuff', buttonText: '', buttonUseIcon: true }),
];

expect(renderTree(tree)).toContain('aria-label="Find stuff"');
});

it('carries the #338 a11y fix forward to artisanpack/search (I4 fork)', () => {
const tree = [makeBlock('artisanpack/search', { buttonText: 'Go', buttonUseIcon: true })];

const html = renderTree(tree);

expect(html).toContain('class="wp-block-search__button has-icon"');
expect(html).toContain('aria-label="Go"');
expect(html).toContain('<svg class="wp-block-search__button-icon"');
expect(html).not.toContain('<button type="submit" class="wp-block-search__button"></button>');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('renders the artisanpack/callout reference block', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Locks the deprecation chain for `artisanpack/latest-posts`.
*
* Mirrors upstream's single v1 entry, which migrates the legacy string
* `categories` attribute (a bare term id) to the array-of-objects shape.
*/

import { describe, it, expect } from 'vitest';

import deprecated from '../deprecated';

describe('latest-posts deprecation chain', () => {
it('ships one deprecation entry (v1) matching upstream', () => {
expect(Array.isArray(deprecated)).toBe(true);
expect(deprecated).toHaveLength(1);
});

it('v1 declares the legacy string categories attribute', () => {
const v1 = deprecated[0];
expect((v1.attributes as Record<string, { type: string }>).categories.type).toBe('string');
});

it('v1 is eligible only when categories is a string', () => {
const v1 = deprecated[0];
expect(v1.isEligible({ categories: '7' })).toBe(true);
expect(v1.isEligible({ categories: [{ id: 7 }] as unknown as string })).toBe(false);
expect(v1.isEligible({})).toBe(false);
});

it('v1 migrate() upgrades the string id to an array of objects', () => {
const v1 = deprecated[0];
const migrated = v1.migrate({ categories: '7', postsToShow: 3 });
expect(migrated.categories).toEqual([{ id: 7 }]);
expect(migrated.postsToShow).toBe(3);
});

it('v1 migrate() drops a malformed category instead of emitting { id: NaN }', () => {
const v1 = deprecated[0];
const migrated = v1.migrate({ categories: 'not-a-number', postsToShow: 3 });
expect(migrated.categories).toBeUndefined();
expect(migrated.postsToShow).toBe(3);
});

it('v1 save() returns null (dynamic block)', () => {
const v1 = deprecated[0];
expect(v1.save()).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Tests for the `artisanpack/latest-posts` edit component.
*
* The preview is server-rendered, so these assertions focus on the
* inspector controls and that the edit wires the package's
* <ServerSideRender> seam to the `artisanpack/latest-posts` block name.
*/

import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';

vi.mock('@wordpress/i18n', () => ({
__: (text: string) => text,
}));

vi.mock('../../../vendor/i18n', () => ({
TEXT_DOMAIN: 'artisanpack-visual-editor',
}));

vi.mock('../../../editor/server-side-render', () => ({
ServerSideRender: ({ block, attributes }: { block: string; attributes: Record<string, unknown> }) => (
<div data-testid="ssr" data-block={block} data-attrs={JSON.stringify(attributes)} />
),
}));

vi.mock('@wordpress/components', () => {
const Panel = ({ title, children }: { title?: string; children?: React.ReactNode }) => (
<section data-panel={title}>{children}</section>
);
const Toggle = ({ label, checked }: { label?: string; checked?: boolean }) => (
<label data-toggle={label} data-checked={checked ? 'true' : 'false'} />
);
return {
PanelBody: Panel,
ToggleControl: Toggle,
RadioControl: ({ label }: { label?: string }) => <div data-radio={label} />,
RangeControl: ({ label }: { label?: string }) => <div data-range={label} />,
SelectControl: ({ label }: { label?: string }) => <div data-select={label} />,
};
});

vi.mock('@wordpress/block-editor', () => ({
InspectorControls: ({ children }: { children?: React.ReactNode }) => (
<div data-testid="inspector">{children}</div>
),
useBlockProps: (props?: Record<string, unknown>) => ({ ...props }),
}));

(globalThis as { React?: unknown }).React = require('react');

import LatestPostsEdit from '../edit';

describe('LatestPostsEdit', () => {
it('previews via ServerSideRender for the artisanpack/latest-posts block', () => {
const { getByTestId } = render(
<LatestPostsEdit attributes={{ postsToShow: 5 }} setAttributes={vi.fn()} />
);
const ssr = getByTestId('ssr');
expect(ssr.getAttribute('data-block')).toBe('artisanpack/latest-posts');
});

it('mounts the inspector with the sorting + filtering panel', () => {
const { getByTestId, container } = render(
<LatestPostsEdit attributes={{}} setAttributes={vi.fn()} />
);
expect(getByTestId('inspector')).toBeTruthy();
expect(container.querySelector('[data-range="Number of items"]')).toBeTruthy();
expect(container.querySelector('[data-select="Order by"]')).toBeTruthy();
});

it('reveals the excerpt-length control only when showing excerpt content', () => {
const { container } = render(
<LatestPostsEdit
attributes={{ displayPostContent: true, displayPostContentRadio: 'excerpt' }}
setAttributes={vi.fn()}
/>
);
expect(container.querySelector('[data-range="Max number of words in excerpt"]')).toBeTruthy();
});

it('reveals the columns control only in grid layout', () => {
const { container } = render(
<LatestPostsEdit attributes={{ postLayout: 'grid' }} setAttributes={vi.fn()} />
);
expect(container.querySelector('[data-range="Columns"]')).toBeTruthy();
});
});
Loading