Skip to content

Conversation

@carolinan
Copy link
Contributor

@carolinan carolinan commented May 16, 2023

What?

Adds a playlist and it's inner playlist track block.

Closes #805

Why?

The playlist block has been requested since 2017. Without the block, a playlist can be added by using the classic block, but that is overly complicated.

How?

For this block, I started with one media type, audio, rather than trying to build it for both audio and video because it would increase the complexity.

I pictured that the playlist should work similarly to the "classic" WordPress playlist.
Meaning:

  • There would be only one audio element followed by an optional Tracklist with information like song title, artist, and track length.
  • Clicking on a track in the Tracklist should play that track.
  • When one track ends, the next track in the Tracklist should start.
  • The block options should match the playlist options in the Media Library.

Testing Instructions

Audio file for testing: https://wpthemetestdata.files.wordpress.com/2008/06/originaldixielandjazzbandwithalbernard-stlouisblues.mp3

Enable the experimental blocks option from the Gutenberg > Experiments plugin menu.
Upload a couple of audio files through the Media Library.
Edit the files in the Media Library and add title, artist and album to some files but not all.
Add a featured image to at least one file.

Insert the playlist block in the editor.

  1. There should be a media placeholder from which you can open the media library and select audio files. Hold down Shift to select more than one file.

  2. Confirm that all selected files are inserted in the block and that the first file is presented at the top of the block, above the tracklist.

  3. Select the Edit button in the toolbar of the Playlist block . Upload or select a one or more files from the media library. Confirm that the files you choose replaces the current items in the block.

  4. Please test the alignment options in the block toolbar.

  5. Clicking on the play button of the audio element should be disabled.

  6. Clicking on a playlist track should select the inner block and the block settings sidebar should show the options for the track, not the playlist itself.

  7. Confirm that you can select one or more playlist track blocks and move them up and down using the move down, move up, or drag options.

  8. Confirm that there are no issues/errors when deleting or duplicating a playlist track block.

  9. Select a playlist track block and edit the title, album and artist by editing the text directly on the inner block in the canvas.

  10. Select a playlist track block and edit the title, album and artist by editing the text using the options in the sidebar.

  11. Select a playlist track block and delete all text contents. Confirm that placeholder texts are showing.

  12. Select a playlist track block and upload an album cover image.

  13. Replace the album cover image.

  14. Remove the album cover image.

  15. Try the sidebar options: Select the parent playlist block.

  • Show Tracklist -hides and shows the tracklist.
  • Show artists in Tracklist -hides and shows artists in the tracklist if the artist info is available.
  • Show number in Tracklist -hides and shows the number in the tracklist.
  • Show images -hides and shows the image for the track.
  • Order -change the order of the tracks.
  • In the Styles tab, change colors, spacing and borders.

View the front of the website.

  • Confirm that clicking the play button plays the track.
  • When a track ends, the next track plays until you reach the last track.
  • Clicking on a track in the tracklist switches the current track.
  • The current track is marked in bold in the tracklist.
  • Repeat this step with more than one playlist block placed on the same page.

Screen Readers

Description of the block:

The block has three different sections.
At the top, there is an image, for example, an album cover, which is set to decorative with an empty alt attribute.
Next to the image is information about the current track: Title, album, and artist.
Below is a native audio HTML element with the standard controls for playing, pausing, volume, etc.
Below the audio element is the tracklist.
It is a list with buttons inside. Each list item and button represents a track and the button text is the song title, artist, and the length of the track.
The currently selected track: The one that is at the top of the block, has aria-current=true.
Activating a button selects that track.

Instructions

(I have only tested these steps with VoiceOver on mac)

Enable the experimental blocks option from the Gutenberg > Experiments plugin menu.
Upload a couple of audio files through the Media Library.
Edit the files in the Media Library and add title, artist and album to some but not all.
Add a featured image to at least oner file.

Add a playlist block in the block editor.
Focus is moved to an upload button in the block's placeholder.
Instructions about uploading an audio file or selecting one from the media library are announced.
Pressing the right arrow key or the tab key moves the focus from the Upload button to the Media Library button. Each button opens its respective file selection flow.
Confirm that it is possible to select multiple files.
When clicking on the button with the text "Select", the modal for the Media Library is closed.

After adding the audio files, the focus is moved away from the block to the editor canvas and one has to navigate to the block again to select it. (I found this confusing, but learned that other media placeholders do the same)

Navigate to the block in the editor.
The block name is announced.
Pressing the down arrow key or the right arrow key moves focus to the audio element inside the playlist block.
The element type, the track title, the artist name, and the album name are announced if this information is available. Some tracks will only have a title. The same information that is announced is printed above the audio element, available to sighted users.

Pressing the spacebar or Enter key will play the track, and pressing the spacebar or Enter again will pause the track.
Let me know if playing the track in the editor should be disabled.

Pressing the down arrow key will move the focus to the button of the first track in the tracklist. The screen reader should announce the title, the band, the length of the track (if available), and the instructions "Select to play this track".

  • Pressing the down arrow key now selects the richText element for editing the song title.
  • Pressing the down arrow key now selects the second richText element, which is for editing the artist name.

Pressing the down arrow key again will select the next track in the tracklist, and so on, until you reach the last track.

Replacing or adding tracks

Because the tracks are inner blocks, there will be two block toolbars where you can edit tracks.
From the block toolbar of the inner block, you can replace the individual, single track.
From the block toolbar of the parent block, the playlist, you can add multiple new tracks at once.

Press Shift + Tab to open the block toolbar.
Use the arrow keys to navigate to the button named "Edit" or "Replace" respectively.
Activating the button opens a dropdown with two options: Open Media Library, and Upload.
Each option opens its respective file selection.
When the selection is complete, the screen reader announces that the media has been replaced.
(This is not accurate if you are adding a file, not replacing it, but I don't know if this can be solved?)
The focus is returned to the Edit button.

Block Sidebar

Both the playlist block and the inner track block has options in the block sidebar.

The Playlist has options both in the settings tab and styles tab.
Some of these options are visual, such as padding and margin and hiding the decorative image.
Optionally, test the first option in the settings tab, which is a toggle used to hide and show the tracklist:
With the block selected, press the tab key to move the focus to the sidebar.
Navigate to the block settings sidebar, past the Close button and the button that opens the Settings tab,
and the button that opens the Settings panel.
Find the checkbox option for "Show tracklist". Uncheck the checkbox.
Press the tab key repeatedly until you reach the "Skip to the selected block" button.
With the block selected, confirm that using the down arrow key does not move focus to the tracklist, since it has been turned off in the option.

The track has options in the settings tab.
With the block selected, press the tab key to move the focus to the sidebar.
Navigate to the block settings sidebar, past the Close button and the button that opens the Settings tab,
and the button that opens the Settings panel.
There will be three text input fields where you can update artist, album and title.
Below is an option for selecting an album cover image from the Media Library.

@carolinan carolinan added New Block Suggestion for a new block [Type] Experimental Experimental feature or API. labels May 16, 2023
@carolinan carolinan requested a review from antpb May 16, 2023 12:42
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@carolinan

This comment was marked as resolved.

@github-actions
Copy link

github-actions bot commented Jan 8, 2024

This pull request has changed or added PHP files. Please confirm whether these changes need to be synced to WordPress Core, and therefore featured in the next release of WordPress.

If so, it is recommended to create a new Trac ticket and submit a pull request to the WordPress Core Github repository soon after this pull request is merged.

If you're unsure, you can always ask for help in the #core-editor channel in WordPress Slack.

Thank you! ❤️

View changed files
❔ lib/blocks.php
❔ lib/experimental/editor-settings.php
❔ lib/experiments-page.php

@carolinan
Copy link
Contributor Author

I had to change how the block uses the interactivity API global state.
When a playlist block was duplicated, each track's unique id was no longer truly unique, and this caused a problem with the album cover image.
The image that was used on the playlist block that was placed last in the content, was also used in all other playlist blocks
(as long as the "show image" option was enabled).

It still uses the state, but, how do I say this in plain English,.. each playlist is isolated because the track data is nested one level deeper under a playlist id.

Copy link
Contributor

@MaggieCabrera MaggieCabrera left a comment

Choose a reason for hiding this comment

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

This is testing really nicely for me, since it's under an experiment I think it's fair to bring it in and we can iterate if we feel like we need to

<MediaReplaceFlow
name={ __( 'Replace' ) }
onSelect={ onSelectTrack }
accept="audio/*"
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between accept and allowedTypes?

@MaggieCabrera
Copy link
Contributor

MaggieCabrera commented Sep 10, 2025

I might have jumped the gun :D I get an error when trying to change the sort order and it doesn't work: Uncaught TypeError: can't access property "localeCompare", b.attributes.uniqueId is undefined. This doesn't happen consistently, I added a new song and then it worked!

@scruffian
Copy link
Contributor

I'm not sure I fully see the value of the sort control:
Screenshot 2025-09-10 at 15 00 02
Since I can reorder the inner blocks, I'm not sure when this would be useful....

@MaggieCabrera
Copy link
Contributor

Since I can reorder the inner blocks, I'm not sure when this would be useful....

If the numbers update when manually sorting I agree with this

@scruffian
Copy link
Contributor

When the block saves it outputs an <ol> in the markup - I wonder if it would be better to omit the <ol>, as it doesn't add much useful semantic information, and it will make it more complex to update the markup in future.


// Ensure that each inner block has a unique ID,
// even if a track is duplicated.
useEffect( () => {
Copy link
Member

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 fixed in a follow-up, but it looks like this useEffect is running on every render. We could try memoising the unique IDs to avoid unnecessary re-renders.

Something like:

const uniqueIds = useMemo( () => {
	return innerBlockTracks.map( ( block ) => block.attributes.uniqueId );
}, [ innerBlockTracks ] );

And then add uniqueIds as a dependency to the useEffect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have tried many different alternatives but was not able to get it working :(

Copy link
Member

Choose a reason for hiding this comment

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

I think it's OK to come back to this later, don't let it block this PR!

Copy link
Contributor

@scruffian scruffian left a comment

Choose a reason for hiding this comment

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

Thanks for working on this, I love it ❤️ 🎧 . I think we should bring this in and keep iterating.

Copy link
Contributor

@getdave getdave left a comment

Choose a reason for hiding this comment

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

Couple of things to consider prior to merge.

sprintf(
/* translators: %s: track length in minutes:seconds */
'<span class="screen-reader-text">' . esc_html__( 'Length:' ) . ' </span>%s',
$length
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$length
esc_html( $length )

Comment on lines +61 to +68
$tracks_data[ $unique_id ] = array(
'url' => $url,
'title' => $title,
'artist' => $artist,
'album' => $album,
'image' => $image,
'ariaLabel' => $aria_label,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

These values may need escaping.

For example ariaLabel is used in data-wp-bind--aria-label so will need esc_attr.

@carolinan
Copy link
Contributor Author

carolinan commented Sep 11, 2025

Thank you all for the reviews ❤️

The ascending/descending sorting option, if I remember correctly, was included to match the ordering option in the classic playlist in the media library. I don't have strong preference for or against it.
Anyone who has the availability please feel free to continue. I might be able to catch up with the notes on Saturday.

@carolinan
Copy link
Contributor Author

I'd be OK with it not being reviewed for accessibility while it stays experimental. But it needs to be tested before removing that flag.

Copy link
Contributor

@t-hamano t-hamano left a comment

Choose a reason for hiding this comment

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

I've only seen the final code, but I'll comment on some points that I noticed. I apologize if I'm mistaken.

Also, I think it would be better to merge trunk into this branch again before shipping this PR. For example, this if statement already exists in trunk, so this diff is weird.

Comment on lines +93 to +94
'playlist.php' => 'core/playlist',
'playlist-track.php' => 'core/playlist-track',
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's put them in strict alphabetical order (directly under patterns.php).

Comment on lines +25 to +33
className={
'wp-block-playlist__tracklist' +
( ! showTracklist
? ' wp-block-playlist__tracklist-is-hidden'
: '' ) +
( ! showArtists
? ' wp-block-playlist__tracklist-artist-is-hidden'
: '' )
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's avoid concatenating class names as strings, as this can introduce unintended spaces.

Suggested change
className={
'wp-block-playlist__tracklist' +
( ! showTracklist
? ' wp-block-playlist__tracklist-is-hidden'
: '' ) +
( ! showArtists
? ' wp-block-playlist__tracklist-artist-is-hidden'
: '' )
}
className={ clsx( 'wp-block-playlist__tracklist', {
'wp-block-playlist__tracklist-is-hidden': ! showTracklist,
'wp-block-playlist__tracklist-artist-is-hidden':
! showArtists,
} ) }

/**
* External dependencies
*/
import { v4 as uuid } from 'uuid';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why you didn't use useInstanceId hook?

/>
</BlockControls>
<InspectorControls>
<PanelBody title={ __( 'Settings' ) }>
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a blocker, but it would be great to see this refactored to use ToolsPanel in the future. See #67813

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, all blocks should use ToolsPanel AFAIK.

onChange={ ( value ) => {
setAttributes( { title: value } );
} }
keepPlaceholderOnFocus
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no such prop in the RichText component.

} }
/>
<MediaUploadCheck>
<div className="editor-video-poster-control">
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a blocker: There is a reusable PosterImage component for poster images that we might be able to use.

Image

Comment on lines +239 to +244
data-playlist-track-url={ src }
data-playlist-track-title={ stripHTML( title ) }
data-playlist-track-artist={ stripHTML( artist ) }
data-playlist-track-album={ stripHTML( album ) }
data-playlist-track-image-src={ image ?? null }
data-wp-context={ JSON.stringify( { uniqueId } ) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these attributes needed?


$wrapper_attributes = get_block_wrapper_attributes();

$unique_id = isset( $attributes['uniqueId'] ) ? $attributes['uniqueId'] : wp_generate_uuid4();
Copy link
Contributor

Choose a reason for hiding this comment

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

wp_unique_id might be more appropriate

$html .= '<span class="wp-block-playlist-track__length">' .
sprintf(
/* translators: %s: track length in minutes:seconds */
'<span class="screen-reader-text">' . esc_html__( 'Length:' ) . ' </span>%s',
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
'<span class="screen-reader-text">' . esc_html__( 'Length:' ) . ' </span>%s',
'<span class="screen-reader-text">Length:</span>%s',

The text and placeholder order may vary depending on the locale.

Comment on lines +435 to +436
{ label: __( 'Descending' ), value: 'DESC' },
{ label: __( 'Ascending' ), value: 'ASC' },
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
{ label: __( 'Descending' ), value: 'DESC' },
{ label: __( 'Ascending' ), value: 'ASC' },
{ label: __( 'Descending' ), value: 'desc' },
{ label: __( 'Ascending' ), value: 'asc' },

In general, I think it's best to use lowercase values.

@MaggieCabrera
Copy link
Contributor

Thank you all for the reviews ❤️

Thank you for all your work here

The ascending/descending sorting option, if I remember correctly, was included to match the ordering option in the classic playlist in the media library. I don't have strong preference for or against it. Anyone who has the availability please feel free to continue. I might be able to catch up with the notes on Saturday.

Yeah, makes sense if we didn't have visual editing for it back then, but less so when just dragging things around will achieve the same result

@carolinan
Copy link
Contributor Author

Unless you have 100+ items... but then you probably want to use a more custom block anyway.

@t-hamano t-hamano mentioned this pull request Oct 1, 2025
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Technical Feedback Needs testing from a developer perspective. New Block Suggestion for a new block [Type] Experimental Experimental feature or API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Block: Playlist