Skip to content

Commit 9ed0675

Browse files
arraypressclaude
andcommitted
No chevrons on a picture: a tile drags, and the server ships the tile
The script built its own tile whenever the list was empty, and that tile had drifted from the one the server draws: a pair of move buttons under the picture and no drag handle. The server ships a template tile now -- handle, picture, cross -- and the script clones it and names the attachment in its labels. The handle appears with the cross, on hover or focus, since the tile itself is what a mouse drags and the handle is the keyboard's way in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Kwx2STJ4EaVaJofEJJ9Zo
1 parent df298dc commit 9ed0675

5 files changed

Lines changed: 146 additions & 74 deletions

File tree

‎assets/css/field-kit.css‎

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -712,67 +712,17 @@ input[type="checkbox"].field-kit__toggle:focus-visible {
712712
box-shadow: 0 0 0 1px rgba(0, 0, 0, .12);
713713
}
714714

715-
/* The move buttons the script adds for the keyboard. Nobody with a mouse
716-
needs them -- the tile drags -- so they sit along the bottom edge and
717-
show up with the cross: on hover, on focus, and always where there is
718-
no hover to wait for. Left as flow content they came out as a bar of
719-
chevrons under every picture. */
720-
.wp-core-ui .field-kit__gallery-item .field-kit__gallery-move {
721-
position: absolute;
722-
bottom: .2rem;
723-
z-index: 1;
724-
background: rgba(255, 255, 255, .92);
725-
color: #1d2327;
726-
opacity: 0;
727-
transition: opacity .1s ease-in-out;
728-
}
729-
730-
.wp-core-ui .field-kit__gallery-item .field-kit__gallery-move[data-direction="up"] {
731-
inset-inline-start: .2rem;
732-
}
733-
734-
.wp-core-ui .field-kit__gallery-item .field-kit__gallery-move[data-direction="down"] {
735-
inset-inline-end: .2rem;
736-
}
737-
738-
.field-kit__gallery-item:hover .field-kit__gallery-move,
739-
.field-kit__gallery-item:focus-within .field-kit__gallery-move,
740-
.wp-core-ui .field-kit__gallery-move:focus-visible {
741-
opacity: 1;
742-
}
743-
744-
.wp-core-ui .field-kit__gallery-move:hover,
745-
.wp-core-ui .field-kit__gallery-move:focus {
746-
background: #fff;
747-
color: var(--wp-admin-theme-color, #2271b1);
748-
}
749-
750-
.wp-core-ui .field-kit__gallery-move:disabled {
751-
color: #c3c4c7;
752-
cursor: default;
753-
}
754-
755-
/* In a row -- a file with a name and a player rather than a picture -- the
756-
controls sit in the line beside the name, always visible. */
757-
.wp-core-ui .field-kit__gallery-item--file .field-kit__gallery-move,
758-
.wp-core-ui .field-kit__gallery-item--file .field-kit__gallery-remove {
759-
position: static;
760-
opacity: 1;
761-
box-shadow: none;
762-
background: none;
763-
}
764-
765715
/* Nothing may be revealed by hover alone where hovering is not possible. */
766716
@media (hover: none) {
767717
.wp-core-ui .field-kit__gallery-remove,
768-
.wp-core-ui .field-kit__gallery-item .field-kit__gallery-move {
718+
.field-kit__gallery-item .field-kit__gallery-handle {
769719
opacity: 1;
770720
}
771721
}
772722

773723
@media (prefers-reduced-motion: reduce) {
774724
.wp-core-ui .field-kit__gallery-remove,
775-
.wp-core-ui .field-kit__gallery-item .field-kit__gallery-move {
725+
.field-kit__gallery-item .field-kit__gallery-handle {
776726
transition: none;
777727
}
778728
}
@@ -1887,7 +1837,6 @@ li.field-kit__repeater-row {
18871837
it, so a bare class here loses and every chevron carries a line under it.
18881838
Matching the prefix is what makes these win. */
18891839
.wp-core-ui .field-kit__gallery-remove,
1890-
.wp-core-ui .field-kit__gallery-move,
18911840
.wp-core-ui .field-kit__repeater-remove,
18921841
.wp-core-ui .field-kit__sortable-move {
18931842
display: inline-flex;
@@ -1927,7 +1876,6 @@ li.field-kit__repeater-row {
19271876
line-height of surrounding text. */
19281877
.field-kit__sortable-move .dashicons,
19291878
.field-kit__gallery-remove .dashicons,
1930-
.field-kit__gallery-move .dashicons,
19311879
.field-kit__repeater-remove .dashicons {
19321880
width: 1rem;
19331881
height: 1rem;
@@ -3671,6 +3619,17 @@ li.field-kit__repeater-row--collapsible.is-closed .field-kit__repeater-toggle .t
36713619
z-index: 1;
36723620
border-radius: 2px;
36733621
background: rgba( 255, 255, 255, .85 );
3622+
opacity: 0;
3623+
transition: opacity .1s ease-in-out;
3624+
}
3625+
3626+
/* A tile is dragged by any part of it, so the handle is for the keyboard:
3627+
it appears with the cross, on hover or when something in the tile has
3628+
focus, and stays out of the picture the rest of the time. */
3629+
.field-kit__gallery-item:hover .field-kit__gallery-handle,
3630+
.field-kit__gallery-item:focus-within .field-kit__gallery-handle,
3631+
.field-kit__gallery-item .field-kit__gallery-handle:focus-visible {
3632+
opacity: 1;
36743633
}
36753634

36763635
.field-kit__gallery-item--file .field-kit__gallery-handle {

‎assets/js/field-kit.js‎

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,24 +2007,20 @@
20072007
return;
20082008
}
20092009

2010-
var template = list.querySelector( '.field-kit__gallery-item' );
2011-
var item;
2012-
2013-
if ( template ) {
2014-
// Cloning an existing item keeps the markup — and so the
2015-
// labelling — identical to what the server rendered.
2016-
item = template.cloneNode( true );
2017-
} else {
2018-
item = document.createElement( 'li' );
2019-
item.className = 'field-kit__gallery-item';
2020-
item.innerHTML = '<img alt="" /><span class="field-kit__gallery-position screen-reader-text"></span>' +
2021-
'<span class="field-kit__gallery-actions">' +
2022-
'<button type="button" class="button-link field-kit__gallery-move" data-direction="up"><span class="dashicons dashicons-arrow-up-alt2" aria-hidden="true"></span></button>' +
2023-
'<button type="button" class="button-link field-kit__gallery-move" data-direction="down"><span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span></button>' +
2024-
'<button type="button" class="button-link field-kit__gallery-remove"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button>' +
2025-
'</span>';
2010+
// The server ships the tile to clone, so what the script adds is
2011+
// the tile it draws: a drag handle, the picture, a cross. The
2012+
// tile the script once built for itself had chevrons instead
2013+
// and no handle. A gallery from an older render has no
2014+
// template, and its first tile is the next best copy.
2015+
var template = wrap.querySelector( '.field-kit__gallery-template' );
2016+
var source = template && template.content ? template.content.firstElementChild : list.querySelector( '.field-kit__gallery-item' );
2017+
2018+
if ( ! source ) {
2019+
return;
20262020
}
20272021

2022+
var item = source.cloneNode( true );
2023+
20282024
item.dataset.id = attachment.id;
20292025

20302026
var image = item.querySelector( 'img' );
@@ -2042,8 +2038,17 @@
20422038

20432039
var name = attachment.title || attachment.filename || String( attachment.id );
20442040

2045-
item.querySelectorAll( '[aria-label]' ).forEach( function ( button ) {
2046-
button.setAttribute( 'aria-label', button.getAttribute( 'aria-label' ).replace( /—.*$/, '' ) + name );
2041+
// The template names its item {name}; a cloned tile names the
2042+
// one it was cloned from. Either way the label ends up naming
2043+
// this attachment.
2044+
item.querySelectorAll( '[aria-label], [data-label-template]' ).forEach( function ( button ) {
2045+
[ 'aria-label', 'data-label-template' ].forEach( function ( attribute ) {
2046+
var value = button.getAttribute( attribute );
2047+
2048+
if ( null !== value ) {
2049+
button.setAttribute( attribute, value.split( '{name}' ).join( name ).replace( /—.*$/, '' ) );
2050+
}
2051+
} );
20472052
} );
20482053

20492054
list.appendChild( item );

‎src/Types/GalleryType.php‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,32 @@ protected function render_preview( Field $field ): string {
149149
}
150150

151151
return sprintf(
152-
'<ol class="field-kit__gallery-items" data-empty="%s">%s</ol>',
152+
'<ol class="field-kit__gallery-items" data-empty="%s">%s</ol>%s',
153153
$total > 0 ? 'false' : 'true',
154-
$items
154+
$items,
155+
$this->render_template()
156+
);
157+
}
158+
159+
/**
160+
* The tile the script clones for an attachment it adds.
161+
*
162+
* The script used to build its own tile when the list was empty, and
163+
* that tile drifted from the one the server draws: it grew a pair of
164+
* move buttons and lost the drag handle, so a gallery started from
165+
* nothing looked unlike one that came from the database. One tile,
166+
* rendered here, cloned there. The name is a placeholder the script
167+
* fills in.
168+
*
169+
* @return string
170+
*/
171+
private function render_template(): string {
172+
return sprintf(
173+
'<template class="field-kit__gallery-template"><li class="field-kit__gallery-item" data-id="0">%s<img alt="" />' .
174+
'<span class="field-kit__gallery-position screen-reader-text"></span>' .
175+
'<span class="field-kit__gallery-actions">%s</span></li></template>',
176+
$this->handle( '{name}', 0, 0 ),
177+
$this->remove_button( '{name}' )
155178
);
156179
}
157180

‎tests/GalleryTest.php‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,25 @@ private function with_attachment( string $mime, string $name, array $config = []
239239

240240
return $html;
241241
}
242+
243+
/**
244+
* The tile the script clones is the tile the server draws.
245+
*
246+
* The script built its own when the list was empty, with a pair of
247+
* chevrons and no handle. Now the server ships one, and it carries a
248+
* drag handle and a cross and nothing else.
249+
*/
250+
public function test_a_template_tile_ships_with_the_gallery(): void {
251+
$registry = new Registry();
252+
$html = ( new Renderer() )->render(
253+
new Field( 'demo', $registry->get( 'gallery' ), [ 'label' => 'Demo', 'input_name' => 'demo' ], [] )
254+
);
255+
256+
$this->assertStringContainsString( '<template class="field-kit__gallery-template">', $html );
257+
$this->assertStringContainsString( 'field-kit__gallery-handle', $html );
258+
$this->assertStringContainsString( 'field-kit__gallery-remove', $html );
259+
$this->assertStringContainsString( '{name}', $html );
260+
$this->assertStringNotContainsString( 'gallery-move', $html );
261+
$this->assertStringNotContainsString( 'dashicons-arrow-up', $html );
262+
}
242263
}

‎tests/js/smoke.js‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,70 @@ try {
471471
}
472472
} )();
473473

474+
/*
475+
* An added gallery tile is the server's tile.
476+
*
477+
* The script cloned the first tile when there was one and built its own
478+
* when there was not -- a tile with chevrons and no drag handle. It clones
479+
* the template the server ships now, and names the attachment in the
480+
* labels the template left as {name}.
481+
*/
482+
( function () {
483+
const handle = Object.assign( makeElement(), { attributes: {} } );
484+
handle.getAttribute = ( name ) => handle.attributes[ name ] ?? null;
485+
handle.setAttribute = ( name, value ) => { handle.attributes[ name ] = value; };
486+
handle.setAttribute( 'aria-label', 'Reorder {name}, 1 of 1' );
487+
handle.setAttribute( 'data-label-template', 'Reorder {name}, {position} of {total}' );
488+
489+
const tile = makeElement();
490+
tile.querySelectorAll = ( selector ) => ( selector.includes( 'aria-label' ) ? [ handle ] : [] );
491+
tile.cloneNode = () => tile;
492+
493+
const template = makeElement();
494+
template.content = { firstElementChild: tile };
495+
496+
const list = makeElement();
497+
list.children = [];
498+
list.querySelector = () => null;
499+
list.appendChild = ( node ) => list.children.push( node );
500+
501+
const wrap = makeElement();
502+
wrap.dataset = {};
503+
wrap.querySelector = ( selector ) => {
504+
if ( selector.includes( 'template' ) ) {
505+
return template;
506+
}
507+
508+
if ( selector.includes( 'items' ) ) {
509+
return list;
510+
}
511+
512+
return null;
513+
};
514+
515+
try {
516+
modules.Gallery.append( wrap, { id: 7, title: 'Kick drum', sizes: {} } );
517+
} catch ( error ) {
518+
console.error( ` Gallery.append threw: ${ error.message }` );
519+
failures ++;
520+
}
521+
522+
if ( 1 !== list.children.length ) {
523+
console.error( ' Gallery: no tile was added from the template' );
524+
failures ++;
525+
}
526+
527+
if ( 'Reorder Kick drum, 1 of 1' !== handle.getAttribute( 'aria-label' ) ) {
528+
console.error( ` Gallery: the template's {name} was not filled in: ${ handle.getAttribute( 'aria-label' ) }` );
529+
failures ++;
530+
}
531+
532+
if ( source.includes( 'field-kit__gallery-move' ) ) {
533+
console.error( ' Gallery: the script still builds a tile with chevrons' );
534+
failures ++;
535+
}
536+
} )();
537+
474538
/*
475539
* The email panel stands aside where core already toggles postboxes.
476540
*

0 commit comments

Comments
 (0)