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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 3.1.1 (2026-10-08)

### Fixed

- Column mode no longer renders a literal `classnames="font-icon-plus"` HTML attribute on the copy
button. `classNames` is action-menu data, so it is now added in `getExtraData()` only; the menu
item keeps its plus icon (#6).
- The stylesheet's `.dropdown-menu .gridfield-button-copy` rules are retired (commented out): the
admin's action menu never puts that class on a dropdown item on Silverstripe 5 or 6, so they never
applied. The menu item looks as before, with the admin's own plus icon like the core items (#6).

## 3.1.0 (2026-09-29)

### Added
Expand Down
11 changes: 9 additions & 2 deletions code/GridField/CopyButton.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ private function getCopyAction($gridField, $record, $columnName)
['RecordID' => $record->ID]
)
->addExtraClass('gridfield-button-copy')
->setAttribute('classNames', 'font-icon-plus')
# `classNames` is action-menu data, not an HTML attribute: set here it also rendered as a
# literal classnames="..." attribute on the column button (#6). getExtraData() adds it
# to the menu data instead.
//->setAttribute('classNames', 'font-icon-plus')
->setAttribute('title', $title)
->setDescription(_t('GridAction.COPY_DESCRIPTION', 'Copy'))
->setAttribute('aria-label', $title);
Expand Down Expand Up @@ -253,7 +256,11 @@ public function getExtraData($gridField, $record, $columnName)
$field = $this->getCopyAction($gridField, $record, $columnName);

if ($field) {
return $field->getAttributes();
# The admin's React action menu builds the dropdown item's class from `action` plus this
# data's `classNames` only; the button's own `class` (gridfield-button-copy) never
# reaches it, on framework 5 or 6. font-icon-plus gives the item the admin's plus icon,
# as font-icon-edit/-trash do for the core items on framework 5.
return array_merge($field->getAttributes(), ['classNames' => 'font-icon-plus']);
}

return null;
Expand Down
19 changes: 12 additions & 7 deletions css/GridFieldCopyButton.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.dropdown-menu .gridfield-button-copy:before {
content: "f";
margin-right: 2px;
}
.dropdown-menu .gridfield-button-copy:after {
content: attr(title);
}
/* Retired (#6): these never match on Silverstripe 4.13, 5 or 6. The admin's React action menu */
/* gives a dropdown item only `action` plus the menu data's classNames (font-icon-plus, see */
/* CopyButton::getExtraData()), never .gridfield-button-copy, so the menu item draws the admin's */
/* own plus icon and title text like the core items. Re-targeting these rules at the menu item */
/* would also pull in the column-button rules above (negative margins, min sizes). */
/* .dropdown-menu .gridfield-button-copy:before { */
/* content: "f"; */
/* margin-right: 2px; */
/* } */
/* .dropdown-menu .gridfield-button-copy:after { */
/* content: attr(title); */
/* } */
35 changes: 35 additions & 0 deletions tests/CopyButtonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,23 @@ public function testColumnModeRendersTheCopyButtonForUsersWhoCanCreate()
$this->assertNull($button->getExtraData($gridField, $record, 'Actions'));
}

/**
* Regression for #6: up to 3.1.0 the column button carried a literal classnames="font-icon-plus"
* HTML attribute. `classNames` is menu data (the admin's React action menu reads it from
* getExtraData()), not an HTML attribute, and does nothing on a plain button.
*/
public function testIssue6ColumnButtonHasNoLiteralClassNamesAttribute()
{
$this->logInWithPermission('ADMIN');
$button = new CopyButton(true);
$record = $this->objFromFixture(CopyRecord::class, 'first');

$html = (string) $button->getColumnContent($this->gridField($button, true), $record, 'Actions');

$this->assertStringContainsString('gridfield-button-copy', $html, 'control: the button rendered');
$this->assertDoesNotMatchRegularExpression('/\sclassnames=/i', $html);
}

public function testColumnModeRendersNothingForUsersWhoCannotCreate()
{
$this->logOut();
Expand Down Expand Up @@ -275,6 +292,24 @@ public function testMenuModeOffersTheCopyItemForUsersWhoCanCreate()
$this->assertArrayHasKey('data-url', $data);
}

/**
* #6: the menu item's icon comes from the `classNames` menu data, which the admin's action menu
* puts on the dropdown item (framework 5 and 6 alike). Moving it off the column button must not
* lose it here.
*/
public function testIssue6MenuItemKeepsItsIconClassInTheMenuData()
{
$this->logInWithPermission('ADMIN');
$button = new CopyButton();
$record = $this->objFromFixture(CopyRecord::class, 'first');

$data = $button->getExtraData($this->gridField($button, true), $record, 'Actions');

$this->assertIsArray($data);
$this->assertArrayHasKey('classNames', $data);
$this->assertContains('font-icon-plus', explode(' ', $data['classNames']));
}

/**
* Regression for the menu item being offered to users who cannot create (up to 2.0.1 it was shown
* to everyone and refused only once clicked). A null group is what makes GridField_ActionMenu
Expand Down
31 changes: 31 additions & 0 deletions tests/browser/specs/copy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,34 @@ test.describe('Column mode (CopyButton::create(true))', () => {
await expect(locked.locator('.action-menu__toggle')).toBeVisible();
});
});

test.describe('Icons and markup (#6)', () => {
test('the menu "Copy" item draws the admin plus icon like the core items, and no module rule restyles it', async ({ page }) => {
// The admin's React action menu puts only `action` + the menu data's classNames on a
// dropdown item (framework 5 and 6 alike), never the column button's `class`. The icon
// therefore comes from font-icon-plus in classNames, the admin's own icon class.
const grid = await openTab(page, 'menu');
const items = await openActionMenu(rowsTitled(grid, 'Menu copy').first());
const copy = items.filter({ hasText: /^Copy$/ });
await expect(copy).toHaveCount(1);
await expect(copy).toHaveClass(/(^|\s)font-icon-plus(\s|$)/);
const glyph = await copy.evaluate((el) => {
const before = getComputedStyle(el, '::before');
const after = getComputedStyle(el, '::after');
return { content: before.content, font: before.fontFamily, after: after.content };
});
expect(glyph.font.toLowerCase()).toContain('silverstripe');
expect(glyph.content).not.toBe('none');
// The pre-#6 stylesheet appended the title as ::after text inside the menu; nothing does now.
expect(glyph.after).toBe('none');
});

test('the column button carries no literal classnames attribute', async ({ page }) => {
// `classNames` is menu data for the React action menu, not an HTML attribute. Up to 3.1.0
// it was set on the column button too and rendered as classnames="font-icon-plus".
const grid = await openTab(page, 'column');
const button = copyColumnButton(rowsTitled(grid, 'Column copy').first());
await expect(button).toBeVisible();
expect(await button.getAttribute('classnames')).toBeNull();
});
});
Loading