diff --git a/changelogs/DP-45228.yml b/changelogs/DP-45228.yml new file mode 100644 index 0000000000..7297b3d02c --- /dev/null +++ b/changelogs/DP-45228.yml @@ -0,0 +1,41 @@ +# +# Write your changelog entry here. Every pull request must have a changelog yml file. +# +# Change types: +# ############################################################################# +# You can use one of the following types: +# - Added: For new features. +# - Changed: For changes to existing functionality. +# - Deprecated: For soon-to-be removed features. +# - Removed: For removed features. +# - Fixed: For any bug fixes. +# - Security: In case of vulnerabilities. +# +# Format +# ############################################################################# +# The format is crucial. Please follow the examples below. For reference, the requirements are: +# - All 3 parts are required and you must include "Type", "description" and "issue". +# - "Type" must be left aligned and followed by a colon. +# - "description" must be indented with 2 spaces followed by a colon +# - "issue" must be indented with 4 spaces followed by a colon. +# - "issue" is for the Jira ticket number only e.g. DP-1234 +# - No extra spaces, indents, or blank lines are allowed. +# +# Example: +# ############################################################################# +# Fixed: +# - description: Fixes scrolling on edit pages in Safari. +# issue: DP-13314 +# +# You may add more than 1 description & issue for each type using the following format: +# Changed: +# - description: Automating the release branch. +# issue: DP-10166 +# - description: Second change item that needs a description. +# issue: DP-19875 +# - description: Third change item that needs a description along with an issue. +# issue: DP-19843 +# +Changed: + - description: CSV Table / Datatables changes + issue: DP-45228 diff --git a/composer.json b/composer.json index 308e3ea3b8..cc3d07a6b3 100644 --- a/composer.json +++ b/composer.json @@ -185,7 +185,7 @@ "drupal/core-composer-scaffold": "~11.3.3", "drupal/core-project-message": "~11.3.3", "drupal/core-recommended": "~11.3.3", - "drupal/csv_field": "^3.0", + "drupal/csv_field": "3.0.x-dev@dev", "drupal/csv_serialization": "^4.0", "drupal/datalayer": "^2", "drupal/devel": "^5", diff --git a/composer.lock b/composer.lock index 12ddb30da9..203a50b2ef 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f4ed688d02b82a8256a0bc070a63651", + "content-hash": "74e55d67a60c12dd1190f335673ba578", "packages": [ { "name": "akamai-open/edgegrid-auth", @@ -4323,17 +4323,11 @@ }, { "name": "drupal/csv_field", - "version": "3.0.2", + "version": "dev-3.0.x", "source": { "type": "git", "url": "https://git.drupalcode.org/project/csv_field.git", - "reference": "3.0.2" - }, - "dist": { - "type": "zip", - "url": "https://ftp.drupal.org/files/projects/csv_field-3.0.2.zip", - "reference": "3.0.2", - "shasum": "61dcee998b5c61fb72821504e229edace125b4a5" + "reference": "88e4de3dbcec0bb3a92b81c603e131b34515d2eb" }, "require": { "drupal/core": "^9.3 || ^10 || ^11", @@ -4341,12 +4335,15 @@ }, "type": "drupal-module", "extra": { + "branch-alias": { + "dev-3.0.x": "3.0.x-dev" + }, "drupal": { - "version": "3.0.2", - "datestamp": "1774425928", + "version": "3.0.2+5-dev", + "datestamp": "1776367221", "security-coverage": { - "status": "covered", - "message": "Covered by Drupal's security advisory policy" + "status": "not-covered", + "message": "Dev releases are not covered by Drupal security advisories." } } }, @@ -25733,6 +25730,7 @@ "drupal/clamav": 20, "drupal/color": 15, "drupal/conditional_fields": 15, + "drupal/csv_field": 20, "drupal/entity_usage": 10, "drupal/entityreference_filter": 10, "drupal/imageapi_optimize_binaries": 10, diff --git a/docroot/modules/custom/mass_content/tests/src/ExistingSiteJavascript/CsvFieldUserFlowTest.php b/docroot/modules/custom/mass_content/tests/src/ExistingSiteJavascript/CsvFieldUserFlowTest.php index afd422da38..ac58073430 100644 --- a/docroot/modules/custom/mass_content/tests/src/ExistingSiteJavascript/CsvFieldUserFlowTest.php +++ b/docroot/modules/custom/mass_content/tests/src/ExistingSiteJavascript/CsvFieldUserFlowTest.php @@ -401,6 +401,59 @@ public function testCsvFlowResponsiveChildRowInteraction(): void { $this->assertNotNull($control, 'Responsive control column should be visible at narrow width.'); } + /** + * Ensures hidden responsive column headers are not keyboard focusable. + */ + public function testCsvFlowHiddenResponsiveHeadersNotFocusable(): void { + $this->drupalLogin($this->createAdminUser()); + + $file = $this->createWideCsvFile('csv-responsive-hidden-headers.csv'); + $csv_table = $this->createCsvTableParagraph($file, [ + 'searching' => 1, + 'pageLength' => 5, + 'lengthChange' => 1, + 'responsive' => 'childRow', + 'download' => 1, + 'urls' => [ + 'autolink' => 0, + ], + ], 'CSV Responsive Header Focus'); + $section = $this->createSectionParagraph($csv_table); + $node = $this->createOrgPageWithCsvTable($section, 'CSV Flow Hidden Responsive Headers'); + + $this->drupalGet('node/' . $node->id()); + + $assert = $this->assertSession(); + $assert->waitForElement('css', '.dataTables_wrapper'); + $this->getSession()->resizeWindow(480, 900, 'current'); + $this->getSession()->wait(3000); + + $non_focusable_hidden_header_violations = $this->getSession()->evaluateScript( + "(function() { + var headers = document.querySelectorAll('.dataTables_wrapper thead th'); + var violations = 0; + headers.forEach(function(th) { + var rect = th.getBoundingClientRect(); + var style = window.getComputedStyle(th); + var isResponsiveHidden = th.classList.contains('dtr-hidden') || + (style.position === 'absolute' && rect.width <= 1 && rect.height <= 1); + + if (!isResponsiveHidden) { + return; + } + + var headerTabIndex = th.getAttribute('tabindex'); + var hasFocusableDescendant = th.querySelector('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])') !== null; + if (headerTabIndex !== '-1' || hasFocusableDescendant) { + violations++; + } + }); + return violations; + })();" + ); + $this->assertSame(0, (int) $non_focusable_hidden_header_violations, 'Hidden responsive headers must not be keyboard focusable.'); + } + /** * Ensures first column is rendered as table row headers. */ @@ -426,10 +479,45 @@ public function testCsvFlowFirstColumnRowHeaderInteraction(): void { $assert = $this->assertSession(); $assert->waitForElement('css', '.dataTables_wrapper'); + $table = $assert->elementExists('css', '.dataTable.display'); + $settings = $table->getAttribute('data-settings'); + $this->assertStringContainsString('"firstColumnRowHeader":1', $settings); $assert->elementExists('css', 'table.dataTable tbody tr th'); $assert->pageTextContains('Alpha Office'); } + /** + * Ensures first column defaults to regular data cells when unchecked. + */ + public function testCsvFlowFirstColumnRowHeaderDefaultsUnchecked(): void { + $this->drupalLogin($this->createAdminUser()); + + $file = $this->createLargeCsvFile('csv-row-header-default-unchecked.csv'); + $csv_table = $this->createCsvTableParagraph($file, [ + 'searching' => 1, + 'pageLength' => 5, + 'lengthChange' => 1, + 'responsive' => 'childRow', + 'download' => 1, + 'urls' => [ + 'autolink' => 0, + ], + ], 'CSV Row Header Default Unchecked'); + $section = $this->createSectionParagraph($csv_table); + $node = $this->createOrgPageWithCsvTable($section, 'CSV Flow First Column Row Header Default Unchecked'); + + $this->drupalGet('node/' . $node->id()); + + $assert = $this->assertSession(); + $assert->waitForElement('css', '.dataTables_wrapper'); + $table = $assert->elementExists('css', '.dataTable.display'); + $settings = $table->getAttribute('data-settings'); + $this->assertStringNotContainsString('"firstColumnRowHeader":1', $settings); + $assert->elementNotExists('css', 'table.dataTable tbody tr th'); + $assert->elementExists('css', 'table.dataTable tbody tr td'); + $assert->pageTextContains('Alpha Office'); + } + /** * Ensures download link points to the CSV and returns expected content. */ diff --git a/docroot/themes/custom/mass_theme/overrides/css/datatables.css b/docroot/themes/custom/mass_theme/overrides/css/datatables.css index bb720329da..a048d0a61a 100644 --- a/docroot/themes/custom/mass_theme/overrides/css/datatables.css +++ b/docroot/themes/custom/mass_theme/overrides/css/datatables.css @@ -132,15 +132,10 @@ button.dt-paging-button.next { -/* sort icons */ -table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after, table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before, table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:after, table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:before, table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after, table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before, table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:after, table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:before, table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after { - line-height: 12px; - opacity: 0.25; -} - -table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before, -table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after { - opacity: 1; +/* Sort icon SVGs — handled in csv_field/css/csv-table.css */ +/* Hide default DataTables pseudo-element arrows */ +table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before, table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:before, table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before, table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:before, table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after, table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:after, table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after, table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:after, table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after { + display: none; } table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order,