From 9eda8525814007e23ed9305f4830f0b688f4a50f Mon Sep 17 00:00:00 2001 From: Lauri Eskola <lauri.eskola@acquia.com> Date: Thu, 4 Aug 2022 21:21:44 +0300 Subject: [PATCH] =?UTF-8?q?Issue=20#3151553=20by=20mherchel,=20mandclu,=20?= =?UTF-8?q?danflanagan8,=20catch,=20Lendude,=20G=C3=A1bor=20Hojtsy:=20=20C?= =?UTF-8?q?reate=20new=20=E2=80=9CViews=20Responsive=20Grid=E2=80=9D=20for?= =?UTF-8?q?mat=20for=20Views=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 6aed2e94433abe3288a50f8ef770ad541de61d00) --- .../config/schema/views.style.schema.yml | 17 +++ .../views/css/views-responsive-grid.css | 35 ++++++ .../src/Plugin/views/style/GridResponsive.php | 82 +++++++++++++ .../views-view-grid-responsive.html.twig | 55 +++++++++ .../views.view.test_grid_responsive.yml | 65 ++++++++++ .../Kernel/Plugin/StyleGridResponsiveTest.php | 113 ++++++++++++++++++ core/modules/views/views.libraries.yml | 6 + core/modules/views/views.theme.inc | 31 +++++ .../Core/Theme/Stable9LibraryOverrideTest.php | 9 ++ .../Theme/Stable9TemplateOverrideTest.php | 1 + .../Core/Theme/StableLibraryOverrideTest.php | 4 +- .../Core/Theme/StableTemplateOverrideTest.php | 1 + 12 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 core/modules/views/css/views-responsive-grid.css create mode 100644 core/modules/views/src/Plugin/views/style/GridResponsive.php create mode 100644 core/modules/views/templates/views-view-grid-responsive.html.twig create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml create mode 100644 core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php diff --git a/core/modules/views/config/schema/views.style.schema.yml b/core/modules/views/config/schema/views.style.schema.yml index f31f67ac437b..b2d7fdbe00e4 100644 --- a/core/modules/views/config/schema/views.style.schema.yml +++ b/core/modules/views/config/schema/views.style.schema.yml @@ -48,6 +48,23 @@ views.style.grid: type: boolean label: 'Default views column classes' +views.style.grid_responsive: + type: views_style + label: 'Grid - Responsive' + mapping: + columns: + type: integer + label: 'Maximum number of columns' + cell_min_width: + type: integer + label: 'Minimum cell width' + grid_gutter: + type: integer + label: 'Grid gutter' + alignment: + type: string + label: 'Alignment' + views.style.table: type: views_style label: 'Table' diff --git a/core/modules/views/css/views-responsive-grid.css b/core/modules/views/css/views-responsive-grid.css new file mode 100644 index 000000000000..352837182441 --- /dev/null +++ b/core/modules/views/css/views-responsive-grid.css @@ -0,0 +1,35 @@ +/** + * CSS for Views responsive grid style. + */ + +.views-view-responsive-grid { + --views-responsive-grid--layout-gap: 10px; /* Will be overridden by an inline style. */ + --views-responsive-grid--column-count: 4; /* Will be overridden by an inline style. */ + --views-responsive-grid--cell-min-width: 100px; /* Will be overridden by an inline style. */ +} + +.views-view-responsive-grid--horizontal { + /** + * Calculated values. + */ + --views-responsive-grid--gap-count: calc(var(--views-responsive-grid--column-count) - 1); + --views-responsive-grid--total-gap-width: calc(var(--views-responsive-grid--gap-count) * var(--views-responsive-grid--layout-gap)); + --views-responsive-grid-item--max-width: calc((100% - var(--views-responsive-grid--total-gap-width)) / var(--views-responsive-grid--column-count)); + + display: grid; + grid-template-columns: repeat(auto-fill, minmax(max(var(--views-responsive-grid--cell-min-width), var(--views-responsive-grid-item--max-width)), 1fr)); + gap: var(--views-responsive-grid--layout-gap); +} + +.views-view-responsive-grid--vertical { + margin-bottom: calc(var(--views-responsive-grid--layout-gap) * -1); /* Offset the bottom row's padding. */ + column-width: var(--views-responsive-grid--cell-min-width); + column-count: var(--views-responsive-grid--column-count); + column-gap: var(--views-responsive-grid--layout-gap); +} + +.views-view-responsive-grid--vertical .views-view-responsive-grid__item > * { + padding-bottom: var(--views-responsive-grid--layout-gap); + page-break-inside: avoid; + break-inside: avoid; +} diff --git a/core/modules/views/src/Plugin/views/style/GridResponsive.php b/core/modules/views/src/Plugin/views/style/GridResponsive.php new file mode 100644 index 000000000000..f9b9b820c54b --- /dev/null +++ b/core/modules/views/src/Plugin/views/style/GridResponsive.php @@ -0,0 +1,82 @@ +<?php + +namespace Drupal\views\Plugin\views\style; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Style plugin to render each item in a responsive grid cell. + * + * @ingroup views_style_plugins + * + * @ViewsStyle( + * id = "grid_responsive", + * title = @Translation("Responsive Grid"), + * help = @Translation("Displays rows in a responsive grid."), + * theme = "views_view_grid_responsive", + * display_types = {"normal"} + * ) + */ +class GridResponsive extends StylePluginBase { + + /** + * {@inheritdoc} + */ + protected $usesRowPlugin = TRUE; + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['columns'] = ['default' => '4']; + $options['cell_min_width'] = ['default' => '100']; + $options['grid_gutter'] = ['default' => '10']; + $options['alignment'] = ['default' => 'horizontal']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + $form['columns'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum number of columns'), + '#attributes' => ['style' => 'width: 6em;'], + '#description' => $this->t('The maximum number of columns that will be displayed within the grid.'), + '#default_value' => $this->options['columns'], + '#required' => TRUE, + '#min' => 1, + ]; + $form['cell_min_width'] = [ + '#type' => 'number', + '#title' => $this->t('Minimum grid cell width'), + '#field_suffix' => 'px', + '#attributes' => ['style' => 'width: 6em;'], + '#description' => $this->t('The minimum width of the grid cells. If the grid container becomes narrow, the grid cells will reflow onto the next row as needed.'), + '#default_value' => $this->options['cell_min_width'], + '#required' => TRUE, + '#min' => 1, + ]; + $form['grid_gutter'] = [ + '#type' => 'number', + '#title' => $this->t('Grid gutter spacing'), + '#field_suffix' => 'px', + '#attributes' => ['style' => 'width: 6em;'], + '#description' => $this->t('The spacing between the grid cells.'), + '#default_value' => $this->options['grid_gutter'], + '#required' => TRUE, + '#min' => 0, + ]; + $form['alignment'] = [ + '#type' => 'radios', + '#title' => $this->t('Alignment'), + '#options' => ['horizontal' => $this->t('Horizontal'), 'vertical' => $this->t('Vertical')], + '#default_value' => $this->options['alignment'], + '#description' => $this->t('Horizontal alignment will place items starting in the upper left and moving right. Vertical alignment will place items starting in the upper left and moving down.'), + ]; + } + +} diff --git a/core/modules/views/templates/views-view-grid-responsive.html.twig b/core/modules/views/templates/views-view-grid-responsive.html.twig new file mode 100644 index 000000000000..d36a1bf72289 --- /dev/null +++ b/core/modules/views/templates/views-view-grid-responsive.html.twig @@ -0,0 +1,55 @@ +{# +/** + * @file + * Default theme implementation for views to display rows in a responsive grid. + * + * Available variables: + * - attributes: HTML attributes for the wrapping element. + * - title: The title of this group of rows. + * - view: The view object. + * - rows: The rows contained in this view. + * - options: The view plugin style options. + * - alignment: a string set to either 'horizontal' or 'vertical'. + * - columns: A number representing the max number of columns. + * - cell_min_width: A number representing the minimum width of the grid cell. + * - grid_gutter: A number representing the space between the grid cells. + * - items: A list of grid items. + * - attributes: HTML attributes for each row or column. + * - content: A list of columns or rows. Each row or column contains: + * - attributes: HTML attributes for each row or column. + * - content: The row or column contents. + * + * @see template_preprocess_views_view_grid_responsive() + * + * @ingroup themeable + */ +#} + +{{ attach_library('views/views.responsive-grid') }} + +{% + set classes = [ + 'views-view-responsive-grid', + 'views-view-responsive-grid--' ~ options.alignment, + ] +%} + +{% set responsive_grid_styles = [ + '--views-responsive-grid--column-count:' ~ options.columns ~ ';', + '--views-responsive-grid--cell-min-width:' ~ options.cell_min_width ~ 'px;', + '--views-responsive-grid--layout-gap:' ~ options.grid_gutter ~ 'px;', + ] +%} + +{% if title %} + <h3>{{ title }}</h3> +{% endif %} +<div{{ attributes.addClass(classes).setAttribute('style', responsive_grid_styles|join()) }}> + {% for item in items %} + <div class="views-view-responsive-grid__item"> + <div class="views-view-responsive-grid__item-inner"> + {{- item.content -}} + </div> + </div> + {% endfor %} +</div> diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml new file mode 100644 index 000000000000..3a1b395e55a3 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml @@ -0,0 +1,65 @@ +langcode: en +status: true +dependencies: { } +id: test_grid_responsive +label: '' +module: views +description: '' +tag: '' +base_table: views_test_data +base_field: nid +display: + default: + display_options: + defaults: + fields: false + pager: false + sorts: false + fields: + age: + field: age + id: age + relationship: none + table: views_test_data + plugin_id: numeric + id: + field: id + id: id + relationship: none + table: views_test_data + plugin_id: numeric + name: + field: name + id: name + relationship: none + table: views_test_data + plugin_id: string + pager: + options: + offset: 0 + type: none + sorts: + id: + field: id + id: id + order: ASC + relationship: none + table: views_test_data + plugin_id: numeric + style: + type: grid_responsive + options: + grouping: { } + row: + type: fields + display_plugin: default + display_title: Default + id: default + position: 0 + page_1: + display_options: + path: test-grid + display_plugin: page + display_title: 'Page display' + id: page_1 + position: 1 diff --git a/core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php b/core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php new file mode 100644 index 000000000000..61d1d225adb8 --- /dev/null +++ b/core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php @@ -0,0 +1,113 @@ +<?php + +namespace Drupal\Tests\views\Kernel\Plugin; + +use Drupal\views\Views; + +/** + * Tests the grid_responsive style plugin. + * + * @group views + * @see \Drupal\views\Plugin\views\style\GridResponsive + */ +class StyleGridResponsiveTest extends PluginKernelTestBase { + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = ['test_grid_responsive']; + + /** + * Generates a grid_responsive and asserts that it is displaying correctly. + * + * @param array $options + * Options for the style plugin. + * @param array $expected + * Expected values sued for assertions. + * + * @dataProvider providerTestResponsiveGrid + */ + public function testResponsiveGrid(array $options, array $expected): void { + // Create and preview a View with the provided options. + $view = Views::getView('test_grid_responsive'); + $view->setDisplay('default'); + $view->initStyle(); + $view->initHandlers(); + $view->initQuery(); + $view->style_plugin->options = $options + $view->style_plugin->options; + $this->executeView($view); + $output = $view->preview(); + $output = \Drupal::service('renderer')->renderRoot($output); + $this->setRawContent($output); + + // Confirm that the alignment class is added. + $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@class, :alignment)]', [':alignment' => 'views-view-responsive-grid--' . $expected['alignment']]); + $this->assertGreaterThan(0, count($result), "Alignment CSS variable value is detected and correct."); + + // Check for CSS variables in style attribute. + $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@style, :columns)]', [':columns' => '--views-responsive-grid--column-count:' . $expected['columns']]); + $this->assertGreaterThan(0, count($result), "Max-columns CSS variable value is detected and correct."); + $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@style, :min-width)]', [':min-width' => '--views-responsive-grid--cell-min-width:' . $expected['cell_min_width'] . 'px']); + $this->assertGreaterThan(0, count($result), "Min-width CSS variable value is detected and correct."); + $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@style, :gutter)]', [':gutter' => '--views-responsive-grid--layout-gap:' . $expected['grid_gutter'] . 'px']); + $this->assertGreaterThan(0, count($result), "Gutter CSS variable value is detected and correct."); + + // Assert that the correct number of elements have been rendered and that + // markup structure is correct. + $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid")]/div[contains(@class, "views-view-responsive-grid__item")]/div[contains(@class, "views-view-responsive-grid__item-inner")]'); + // There are five results for this test view. See ViewTestData::dataSet(). + $expected_count = 5; + $this->assertSame($expected_count, count($result), "The expected number of items are rendered in the correct structure."); + } + + /** + * Data provider for testing various configurations. + * + * @return array + * Array containing options for the style plugin and expected values. + */ + public function providerTestResponsiveGrid() { + return [ + 'horizontal' => [ + 'settings' => [ + 'columns' => 7, + 'cell_min_width' => 123, + 'grid_gutter' => 13, + 'alignment' => 'horizontal', + ], + 'expected' => [ + 'columns' => 7, + 'cell_min_width' => 123, + 'grid_gutter' => 13, + 'alignment' => 'horizontal', + ], + ], + 'vertical' => [ + 'settings' => [ + 'columns' => 8, + 'cell_min_width' => 50, + 'grid_gutter' => 44, + 'alignment' => 'vertical', + ], + 'expected' => [ + 'columns' => 8, + 'cell_min_width' => 50, + 'grid_gutter' => 44, + 'alignment' => 'vertical', + ], + ], + 'default options' => [ + 'settings' => [], + 'expected' => [ + 'columns' => 4, + 'cell_min_width' => 100, + 'grid_gutter' => 10, + 'alignment' => 'horizontal', + ], + ], + ]; + } + +} diff --git a/core/modules/views/views.libraries.yml b/core/modules/views/views.libraries.yml index 23910062948e..08ff2d164079 100644 --- a/core/modules/views/views.libraries.yml +++ b/core/modules/views/views.libraries.yml @@ -16,3 +16,9 @@ views.ajax: - core/once - core/internal.jquery.form - core/drupal.ajax + +views.responsive-grid: + version: VERSION + css: + layout: + css/views-responsive-grid.css: {} diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index d2cb65e402e4..b332d08f4345 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -794,6 +794,37 @@ function template_preprocess_views_view_grid(&$variables) { $variables['items'] = $items; } +/** + * Prepares variables for views grid - responsive style templates. + * + * Default template: views-view-grid-responsive.html.twig. + * + * @param array $variables + * An associative array containing: + * - view: The view object. + * - rows: An array of row items. Each row is an array of content. + */ +function template_preprocess_views_view_grid_responsive(&$variables) { + $variables['options'] = $variables['view']->style_plugin->options; + $view = $variables['view']; + + $items = []; + + foreach ($variables['rows'] as $id => $item) { + + $attribute = new Attribute(); + if ($row_class = $view->style_plugin->getRowClass($id)) { + $attribute->addClass($row_class); + } + $items[$id] = [ + 'content' => $item, + 'attributes' => $attribute, + ]; + } + + $variables['items'] = $items; +} + /** * Prepares variables for views unformatted rows templates. * diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php index 4df70246c169..ccccf3dac28c 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php @@ -9,6 +9,15 @@ */ class Stable9LibraryOverrideTest extends StableLibraryOverrideTestBase { + /** + * A list of libraries to skip checking, in the format extension/library_name. + * + * @var string[] + */ + protected $librariesToSkip = [ + 'views/views.responsive-grid', + ]; + /** * {@inheritdoc} */ diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php index b4deeefd6e1b..486fe12e646e 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php @@ -27,6 +27,7 @@ class Stable9TemplateOverrideTest extends KernelTestBase { // Registered as a template in the views_theme() function in views.module // but an actual template does not exist. 'views-form-views-form', + 'views-view-grid-responsive', ]; /** diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php index c06ce24755da..167b9eb2877e 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php @@ -42,7 +42,9 @@ class StableLibraryOverrideTest extends StableLibraryOverrideTestBase { * * @var string[] */ - protected $librariesToSkip = []; + protected $librariesToSkip = [ + 'views/views.responsive-grid', + ]; /** * {@inheritdoc} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php index cb01c58a1205..1dd98306f2cb 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php @@ -25,6 +25,7 @@ class StableTemplateOverrideTest extends KernelTestBase { */ protected $templatesToSkip = [ 'views-form-views-form', + 'views-view-grid-responsive', ]; /** -- GitLab