diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0d9fa..dc38eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/code/GridField/CopyButton.php b/code/GridField/CopyButton.php index 90a0fca..d8e1f59 100644 --- a/code/GridField/CopyButton.php +++ b/code/GridField/CopyButton.php @@ -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); @@ -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; diff --git a/css/GridFieldCopyButton.css b/css/GridFieldCopyButton.css index efc8a03..f8b8b9a 100644 --- a/css/GridFieldCopyButton.css +++ b/css/GridFieldCopyButton.css @@ -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); */ +/* } */ diff --git a/tests/CopyButtonTest.php b/tests/CopyButtonTest.php index 0d89cbb..b0f9773 100644 --- a/tests/CopyButtonTest.php +++ b/tests/CopyButtonTest.php @@ -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(); @@ -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 diff --git a/tests/browser/specs/copy.spec.ts b/tests/browser/specs/copy.spec.ts index 9995e82..131a492 100644 --- a/tests/browser/specs/copy.spec.ts +++ b/tests/browser/specs/copy.spec.ts @@ -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(); + }); +});