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-library/src/accordion-item/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function block_core_accordion_item_render( $attributes, $content ) {
$p->set_attribute( 'data-wp-context', '{ "id": "' . $unique_id . '", "openByDefault": ' . $open_by_default . ' }' );
$p->set_attribute( 'data-wp-class--is-open', 'state.isOpen' );
$p->set_attribute( 'data-wp-init', 'callbacks.initAccordionItems' );
$p->set_attribute( 'data-wp-on-window--hashchange', 'callbacks.hashChange' );

if ( $p->next_tag( array( 'class_name' => 'wp-block-accordion-heading__toggle' ) ) ) {
$p->set_attribute( 'data-wp-on--click', 'actions.toggle' );
Expand Down
63 changes: 60 additions & 3 deletions packages/block-library/src/accordion/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
*/
import { store, getContext, withSyncEvent } from '@wordpress/interactivity';

store(
// Whether the hash has been handled for the current page load.
// This is used to prevent the hash from being handled multiple times.
let hashHandled = false;

const { actions } = store(
'core/accordion',
{
state: {
Expand Down Expand Up @@ -75,15 +79,68 @@ store(
nextButton.focus();
}
} ),
openPanelByHash: () => {
if ( hashHandled || ! window.location?.hash?.length ) {
return;
}

const context = getContext();
const { id, accordionItems, autoclose } = context;
const hash = decodeURIComponent(
window.location.hash.slice( 1 )
);
const targetElement = window.document.getElementById( hash );

if ( ! targetElement ) {
return;
}

const panelElement = window.document.querySelector(
'.wp-block-accordion-panel[aria-labelledby="' + id + '"]'
);

if (
! panelElement ||
! panelElement.contains( targetElement )
) {
return;
}

hashHandled = true;

if ( autoclose ) {
accordionItems.forEach( ( item ) => {
item.isOpen = item.id === id;
} );
} else {
const targetItem = accordionItems.find(
( item ) => item.id === id
);

if ( targetItem ) {
targetItem.isOpen = true;
}
}

// Wait for the panel to be opened before scrolling to it.
window.setTimeout( () => {
targetElement.scrollIntoView();
}, 0 );
},
},
callbacks: {
initAccordionItems: () => {
const context = getContext();
const { id, openByDefault } = context;
context.accordionItems.push( {
const { id, openByDefault, accordionItems } = context;
accordionItems.push( {
id,
isOpen: openByDefault,
} );
actions.openPanelByHash();
},
hashChange: () => {
hashHandled = false;
actions.openPanelByHash();
},
},
},
Expand Down
241 changes: 241 additions & 0 deletions test/e2e/specs/editor/blocks/accordion.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

test.describe( 'Accordion', () => {
test.beforeEach( async ( { admin } ) => {
await admin.createNewPost();
} );

test( 'should open by default when openByDefault is true', async ( {
editor,
page,
} ) => {
await editor.insertBlock( {
name: 'core/accordion',
innerBlocks: [
{
name: 'core/accordion-item',
attributes: { openByDefault: true },
innerBlocks: [
{
name: 'core/accordion-heading',
attributes: { title: 'Accordion Title' },
},
{
name: 'core/accordion-panel',
innerBlocks: [
{
name: 'core/paragraph',
attributes: {
content: 'Accordion Panel Content',
},
},
],
},
],
},
],
} );

const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );

const accordionToggle = page.getByRole( 'button', {
name: 'Accordion Title',
} );
await expect( accordionToggle ).toHaveAttribute(
'aria-expanded',
'true'
);
const accordionPanel = page.getByRole( 'region', {
name: 'Accordion Title',
} );
await expect( accordionPanel ).toBeVisible();
} );

test( 'should close other accordion items when autoclose is true', async ( {
editor,
page,
} ) => {
const accordionItems = Array.from( { length: 2 }, ( _, index ) => ( {
name: 'core/accordion-item',
// Open the first accordion item by default
attributes: { openByDefault: index === 0 },
innerBlocks: [
{
name: 'core/accordion-heading',
attributes: { title: `Accordion Title ${ index + 1 }` },
},
{
name: 'core/accordion-panel',
innerBlocks: [
{
name: 'core/paragraph',
attributes: {
content: `Accordion Panel Content ${
index + 1
}`,
},
},
],
},
],
} ) );

await editor.insertBlock( {
name: 'core/accordion',
attributes: { autoclose: true },
innerBlocks: accordionItems,
} );

const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );

const firstAccordionToggle = page.getByRole( 'button', {
name: 'Accordion Title 1',
} );
const firstAccordionPanel = page.getByRole( 'region', {
name: 'Accordion Title 1',
} );
const secondAccordionToggle = page.getByRole( 'button', {
name: 'Accordion Title 2',
} );
const secondAccordionPanel = page.getByRole( 'region', {
name: 'Accordion Title 2',
} );

// Check that the first accordion item is open and the second is closed
await expect( firstAccordionToggle ).toHaveAttribute(
'aria-expanded',
'true'
);
await expect( secondAccordionToggle ).toHaveAttribute(
'aria-expanded',
'false'
);
await expect( firstAccordionPanel ).toBeVisible();
await expect( secondAccordionPanel ).toBeHidden();

// Click the second accordion item
await secondAccordionToggle.click();

// Check that the first accordion item is closed and the second is open
await expect( firstAccordionToggle ).toHaveAttribute(
'aria-expanded',
'false'
);
await expect( firstAccordionPanel ).toBeHidden();
await expect( secondAccordionToggle ).toHaveAttribute(
'aria-expanded',
'true'
);
await expect( secondAccordionPanel ).toBeVisible();
} );

test( 'should open accordion panel by default when it contains the URL hash target', async ( {
editor,
page,
} ) => {
// Insert a tall spacer block to ensure anchor scrolling behaves as expected.
await editor.insertBlock( {
name: 'core/spacer',
attributes: { height: '1000px' },
} );
await editor.insertBlock( {
name: 'core/accordion',
innerBlocks: [
{
name: 'core/accordion-item',
innerBlocks: [
{
name: 'core/accordion-heading',
attributes: { title: 'Accordion Title' },
},
{
name: 'core/accordion-panel',
innerBlocks: [
{
name: 'core/paragraph',
attributes: {
anchor: 'target',
content: 'Accordion Panel Content',
},
},
],
},
],
},
],
} );
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }#target` );

const accordionPanel = page.getByRole( 'region', {
name: 'Accordion Title',
} );
await expect( accordionPanel ).toBeVisible();
const targetParagraph = page.locator( '#target' );
await expect( targetParagraph ).toBeInViewport();
} );

test( 'should open accordion panel when clicking a link whose target is inside the panel', async ( {
editor,
page,
} ) => {
await editor.insertBlock( {
name: 'core/paragraph',
attributes: {
content:
'<a href="#target">Open panel and scroll to target</a>',
},
} );
// Insert a tall spacer block to ensure anchor scrolling behaves as expected.
await editor.insertBlock( {
name: 'core/spacer',
attributes: { height: '1000px' },
} );
await editor.insertBlock( {
name: 'core/accordion',
innerBlocks: [
{
name: 'core/accordion-item',
innerBlocks: [
{
name: 'core/accordion-heading',
attributes: { title: 'Accordion Title' },
},
{
name: 'core/accordion-panel',
innerBlocks: [
{
name: 'core/paragraph',
attributes: {
anchor: 'target',
content: 'Accordion Panel Content',
},
},
],
},
],
},
],
} );
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );

const link = page.getByRole( 'link', {
name: 'Open panel and scroll to target',
} );
const accordionPanel = page.getByRole( 'region', {
name: 'Accordion Title',
} );
const targetParagraph = page.locator( '#target' );

await expect( accordionPanel ).toBeHidden();
await link.click();
await expect( accordionPanel ).toBeVisible();
await expect( targetParagraph ).toBeInViewport();
} );
} );
Loading