Skip to content
Snippets Groups Projects
Verified Commit b01b0831 authored by Théodore Biadala's avatar Théodore Biadala
Browse files

Issue #3439580 by dipakmdhrm, balagan, Shriaas, catch, BramDriesen, nicxvan:...

Issue #3439580 by dipakmdhrm, balagan, Shriaas, catch, BramDriesen, nicxvan: Make drupal.tableheader only use CSS for sticky table headers
parent 6980ddf0
No related branches found
No related tags found
No related merge requests found
Showing
with 41 additions and 496 deletions
......@@ -715,15 +715,20 @@ drupal.tabledrag.ajax:
drupal.tableheader:
version: VERSION
js:
misc/tableheader.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/once
- core/drupal.displace
css:
component:
misc/components/sticky-header.module.css: { weight: -10 }
moved_files:
system/base:
deprecation_version: 10.3.0
removed_version: 11.0.0
deprecation_link: https://www.drupal.org/node/3440477
css:
component:
css/components/sticky-header.module.css: misc/components/sticky-header.module.css
js:
misc/tableheader.js: false
drupal.tableresponsive:
version: VERSION
js:
......
......@@ -23,8 +23,8 @@
* - #empty: Text to display when no rows are present.
* - #responsive: Indicates whether to add the drupal.tableresponsive library
* providing responsive tables. Defaults to TRUE.
* - #sticky: Indicates whether to add the drupal.tableheader library that makes
* table headers always visible at the top of the page. Defaults to FALSE.
* - #sticky: Indicates whether to make the table headers sticky at
* the top of the page. Defaults to FALSE.
* - #footer: Table footer rows, in the same format as the #rows property.
* - #caption: A localized string for the <caption> tag.
*
......@@ -420,9 +420,7 @@ public static function preRenderTable($element) {
// Add sticky headers, if applicable.
if (count($element['#header']) && $element['#sticky']) {
$element['#attached']['library'][] = 'core/drupal.tableheader';
// Add 'sticky-enabled' class to the table to identify it for JS.
// This is needed to target tables constructed by this function.
$element['#attributes']['class'][] = 'sticky-enabled';
$element['#attributes']['class'][] = 'sticky-header';
}
// If the table has headers and it should react responsively to columns hidden
// with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
......
table.sticky-header thead {
position: sticky;
z-index: 500;
top: var(--drupal-displace-offset-top, 0);
}
/**
* @file
* Sticky table headers.
*/
(function ($, Drupal, displace) {
/**
* Constructor for the tableHeader object. Provides sticky table headers.
*
* TableHeader will make the current table header stick to the top of the page
* if the table is very long.
*
* @constructor Drupal.TableHeader
*
* @param {HTMLElement} table
* DOM object for the table to add a sticky header to.
*
* @listens event:columnschange
*/
function TableHeader(table) {
const $table = $(table);
/**
* @name Drupal.TableHeader#$originalTable
*
* @type {HTMLElement}
*/
this.$originalTable = $table;
/**
* @type {jQuery}
*/
this.$originalHeader = $table.children('thead');
/**
* @type {jQuery}
*/
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
/**
* @type {null|boolean}
*/
this.displayWeight = null;
this.$originalTable.addClass('sticky-table');
this.tableHeight = $table[0].clientHeight;
this.tableOffset = this.$originalTable.offset();
// React to columns change to avoid making checks in the scroll callback.
this.$originalTable.on(
'columnschange',
{ tableHeader: this },
(e, display) => {
const tableHeader = e.data.tableHeader;
if (
tableHeader.displayWeight === null ||
tableHeader.displayWeight !== display
) {
tableHeader.recalculateSticky();
}
tableHeader.displayWeight = display;
},
);
// Create and display sticky header.
this.createSticky();
}
// Helper method to loop through tables and execute a method.
function forTables(method, arg) {
const tables = TableHeader.tables;
const il = tables.length;
for (let i = 0; i < il; i++) {
tables[i][method](arg);
}
}
// Select and initialize sticky table headers.
function tableHeaderInitHandler(e) {
once('tableheader', $(e.data.context).find('table.sticky-enabled')).forEach(
(table) => {
TableHeader.tables.push(new TableHeader(table));
},
);
forTables('onScroll');
}
/**
* Attaches sticky table headers.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the sticky table header behavior.
*/
Drupal.behaviors.tableHeader = {
attach(context) {
$(window).one(
'scroll.TableHeaderInit',
{ context },
tableHeaderInitHandler,
);
},
};
function scrollValue(position) {
return document.documentElement[position] || document.body[position];
}
function tableHeaderResizeHandler(e) {
forTables('recalculateSticky');
}
function tableHeaderOnScrollHandler(e) {
forTables('onScroll');
}
function tableHeaderOffsetChangeHandler(e, offsets) {
forTables('stickyPosition', offsets.top);
}
// Bind event that need to change all tables.
$(window).on({
/**
* When resizing table width can change, recalculate everything.
*
* @ignore
*/
'resize.TableHeader': tableHeaderResizeHandler,
/**
* Bind only one event to take care of calling all scroll callbacks.
*
* @ignore
*/
'scroll.TableHeader': tableHeaderOnScrollHandler,
});
// Bind to custom Drupal events.
$(document).on({
/**
* Recalculate columns width when window is resized, when show/hide weight
* is triggered, or when toolbar tray is toggled.
*
* @ignore
*/
'columnschange.TableHeader drupalToolbarTrayChange':
tableHeaderResizeHandler,
/**
* Recalculate TableHeader.topOffset when viewport is resized.
*
* @ignore
*/
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
});
/**
* Store the state of TableHeader.
*/
$.extend(
TableHeader,
/** @lends Drupal.TableHeader */ {
/**
* This will store the state of all processed tables.
*
* @type {Array.<Drupal.TableHeader>}
*/
tables: [],
},
);
/**
* Extend TableHeader prototype.
*/
$.extend(
TableHeader.prototype,
/** @lends Drupal.TableHeader# */ {
/**
* Minimum height in pixels for the table to have a sticky header.
*
* @type {number}
*/
minHeight: 100,
/**
* Absolute position of the table on the page.
*
* @type {?Drupal~displaceOffset}
*/
tableOffset: null,
/**
* Absolute position of the table on the page.
*
* @type {?number}
*/
tableHeight: null,
/**
* Boolean storing the sticky header visibility state.
*
* @type {boolean}
*/
stickyVisible: false,
/**
* Create the duplicate header.
*/
createSticky() {
// For caching purposes.
this.$html = $('html');
// Clone the table header so it inherits original jQuery properties.
const $stickyHeader = this.$originalHeader.clone(true);
// Hide the table to avoid a flash of the header clone upon page load.
this.$stickyTable = $(
'<table class="sticky-header" style="visibility: hidden; position: fixed; top: 0;"></table>',
)
.append($stickyHeader)
.insertBefore(this.$originalTable);
this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
// Initialize all computations.
this.recalculateSticky();
},
/**
* Set absolute position of sticky.
*
* @param {number} offsetTop
* The top offset for the sticky header.
* @param {number} offsetLeft
* The left offset for the sticky header.
*
* @return {jQuery}
* The sticky table as a jQuery collection.
*/
stickyPosition(offsetTop, offsetLeft) {
const css = {};
if (typeof offsetTop === 'number') {
css.top = `${offsetTop}px`;
}
if (typeof offsetLeft === 'number') {
css.left = `${this.tableOffset.left - offsetLeft}px`;
}
this.$html[0].style.scrollPaddingTop =
displace.offsets.top +
(this.stickyVisible ? this.$stickyTable.height() : 0);
Object.assign(this.$stickyTable[0].style, css);
return this.$stickyTable;
},
/**
* Returns true if sticky is currently visible.
*
* @return {boolean}
* The visibility status.
*/
checkStickyVisible() {
const scrollTop = scrollValue('scrollTop');
const tableTop = this.tableOffset.top - displace.offsets.top;
const tableBottom = tableTop + this.tableHeight;
let visible = false;
if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) {
visible = true;
}
this.stickyVisible = visible;
return visible;
},
/**
* Check if sticky header should be displayed.
*
* This function is throttled to once every 250ms to avoid unnecessary
* calls.
*
* @param {jQuery.Event} e
* The scroll event.
*/
onScroll(e) {
this.checkStickyVisible();
// Track horizontal positioning relative to the viewport.
this.stickyPosition(null, scrollValue('scrollLeft'));
this.$stickyTable[0].style.visibility = this.stickyVisible
? 'visible'
: 'hidden';
},
/**
* Event handler: recalculates position of the sticky table header.
*
* @param {jQuery.Event} event
* Event being triggered.
*/
recalculateSticky(event) {
// Update table size.
this.tableHeight = this.$originalTable[0].clientHeight;
// Update offset top.
displace.offsets.top = displace.calculateOffset('top');
this.tableOffset = this.$originalTable.offset();
this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
// Update columns width.
let $that = null;
let $stickyCell = null;
let display = null;
// Resize header and its cell widths.
// Only apply width to visible table cells. This prevents the header from
// displaying incorrectly when the sticky header is no longer visible.
const il = this.$originalHeaderCells.length;
for (let i = 0; i < il; i++) {
$that = $(this.$originalHeaderCells[i]);
$stickyCell = this.$stickyHeaderCells.eq($that.index());
display = window.getComputedStyle($that[0]).display;
if (display !== 'none') {
Object.assign($stickyCell[0].style, {
width: window.getComputedStyle($that[0]).width,
display,
});
} else {
$stickyCell[0].style.display = 'none';
}
}
this.$stickyTable[0].style.width = `${this.$originalTable.outerWidth()}px`;
},
},
);
// Expose constructor in the public space.
Drupal.TableHeader = TableHeader;
})(jQuery, Drupal, window.Drupal.displace);
/**
* @file
* Table header behavior.
*
* @see tableheader.js
*/
table.sticky-header {
z-index: 500;
top: 0;
margin-top: 0;
background-color: #fff;
}
......@@ -15,7 +15,6 @@ base:
css/components/position-container.module.css: { weight: -10 }
css/components/reset-appearance.module.css: { weight: -10 }
css/components/resize.module.css: { weight: -10 }
css/components/sticky-header.module.css: { weight: -10 }
css/components/system-status-counter.css: { weight: -10 }
css/components/system-status-report-counters.css: { weight: -10 }
css/components/system-status-report-general-info.css: { weight: -10 }
......
......@@ -37,7 +37,7 @@
set classes = [
'cols-' ~ header|length,
responsive ? 'responsive-enabled',
sticky ? 'sticky-enabled',
sticky ? 'sticky-header',
]
%}
<table{{ attributes.addClass(classes) }}>
......
......@@ -37,7 +37,7 @@
'views-view-table',
'cols-' ~ header|length,
responsive ? 'responsive-enabled',
sticky ? 'sticky-enabled',
sticky ? 'sticky-header',
]
%}
<table{{ attributes.addClass(classes) }}>
......
......@@ -21,7 +21,7 @@ class TableTest extends KernelTestBase {
protected static $modules = ['system', 'form_test'];
/**
* Tableheader.js provides 'sticky' table headers, and is included by default.
* If $sticky is TRUE, `sticky-header` class should be included.
*/
public function testThemeTableStickyHeaders() {
$header = ['one', 'two', 'three'];
......@@ -33,14 +33,11 @@ public function testThemeTableStickyHeaders() {
'#sticky' => TRUE,
];
$this->render($table);
// Make sure tableheader.js was attached.
$tableheader = $this->xpath("//script[contains(@src, 'tableheader.js')]");
$this->assertCount(1, $tableheader);
$this->assertRaw('sticky-enabled');
$this->assertRaw('sticky-header');
}
/**
* If $sticky is FALSE, no tableheader.js should be included.
* If $sticky is FALSE, `sticky-header` class should not be included.
*/
public function testThemeTableNoStickyHeaders() {
$header = ['one', 'two', 'three'];
......@@ -58,10 +55,7 @@ public function testThemeTableNoStickyHeaders() {
'#sticky' => FALSE,
];
$this->render($table);
// Make sure tableheader.js was not attached.
$tableheader = $this->xpath("//script[contains(@src, 'tableheader.js')]");
$this->assertCount(0, $tableheader);
$this->assertNoRaw('sticky-enabled');
$this->assertNoRaw('sticky-header');
}
/**
......
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Theme;
use Drupal\claro\ClaroPreRender;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests Claro specific table functionality.
*
* @group Theme
*/
class ClaroTableTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Confirm that Claro tables override use of the `sticky-enabled` class.
*/
public function testThemeTableStickyHeaders() {
// Enable the Claro theme.
\Drupal::service('theme_installer')->install(['claro']);
$this->config('system.theme')->set('default', 'claro')->save();
$header = ['one', 'two', 'three'];
$rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
$table = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#sticky' => TRUE,
];
$this->render($table);
// Confirm that position-sticky is used instead of sticky-enabled.
$this->assertNoRaw('sticky-enabled');
$this->assertRaw('position-sticky');
}
/**
* Confirm Claro prerender callback is not executed for non-array class.
*/
public function testThemeTablePositionStickyPreRender(): void {
// Enable the Claro theme.
\Drupal::service('theme_installer')->install(['claro']);
$this->config('system.theme')->set('default', 'claro')->save();
$header = ['one', 'two', 'three'];
$rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
$table = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#sticky' => TRUE,
'#attributes' => [
'class' => 'class',
],
'#pre_render' => [
[
ClaroPreRender::class,
'tablePositionSticky',
],
],
];
$renderedTable = (string) \Drupal::service('renderer')->renderRoot($table);
// Confirm that table is rendered.
$this->assertStringContainsString('class="class"', $renderedTable);
}
}
......@@ -295,11 +295,6 @@ function claro_element_info_alter(&$type) {
if (isset($type['status_messages'])) {
$type['status_messages']['#pre_render'][] = [ClaroPreRender::class, 'messagePlaceholder'];
}
// Add a pre-render to tables to use CSS for sticky positioning.
if (isset($type['table'])) {
$type['table']['#pre_render'][] = [ClaroPreRender::class, 'tablePositionSticky'];
}
}
/**
......
......@@ -130,9 +130,3 @@ tr.selected td {
width: auto;
}
}
.position-sticky thead {
position: sticky;
z-index: 500;
top: var(--drupal-displace-offset-top, 0);
}
......@@ -116,9 +116,3 @@ tr.selected td {
width: auto;
}
}
.position-sticky thead {
position: sticky;
z-index: 500;
top: var(--drupal-displace-offset-top, 0);
}
......@@ -189,17 +189,6 @@ public static function messagePlaceholder(array $element) {
return $element;
}
/**
* Prerender callback for table elements.
*/
public static function tablePositionSticky(array $element) {
if (isset($element['#attributes']['class']) && is_array($element['#attributes']['class']) && in_array('sticky-enabled', $element['#attributes']['class'], TRUE)) {
unset($element['#attributes']['class'][array_search('sticky-enabled', $element['#attributes']['class'])]);
$element['#attributes']['class'][] = 'position-sticky';
}
return $element;
}
/**
* {@inheritdoc}
*/
......@@ -211,7 +200,6 @@ public static function trustedCallbacks() {
'container',
'textFormat',
'messagePlaceholder',
'tablePositionSticky',
];
}
......
......@@ -37,7 +37,7 @@
'views-view-table',
'cols-' ~ header|length,
responsive ? 'responsive-enabled',
sticky ? 'position-sticky',
sticky ? 'position-sticky sticky-header',
]
%}
<table{{ attributes.addClass(classes) }}>
......
......@@ -38,7 +38,7 @@
'views-table',
'cols-' ~ header|length,
responsive ? 'responsive-enabled',
sticky ? 'sticky-enabled',
sticky ? 'sticky-header',
]
%}
<table{{ attributes.addClass(classes) }}>
......
table.sticky-header thead {
position: sticky;
z-index: 500;
top: var(--drupal-displace-offset-top, 0);
}
/**
* @file
* Table header behavior.
*
* @see tableheader.js
*/
table.sticky-header {
z-index: 500;
top: 0;
margin-top: 0;
background-color: #fff;
}
......@@ -96,6 +96,11 @@ libraries-override:
misc/components/tabledrag.module.css: css/core/components/tabledrag.module.css
misc/components/tree-child.module.css: css/core/components/tree-child.module.css
core/drupal.tableheader:
css:
component:
misc/components/sticky-header.module.css: css/core/components/sticky-header.module.css
core/drupal.vertical-tabs:
css:
component:
......@@ -243,7 +248,6 @@ libraries-override:
css/components/position-container.module.css: css/system/components/position-container.module.css
css/components/reset-appearance.module.css: css/system/components/reset-appearance.module.css
css/components/resize.module.css: css/system/components/resize.module.css
css/components/sticky-header.module.css: css/system/components/sticky-header.module.css
css/components/system-status-counter.css: css/system/components/system-status-counter.css
css/components/system-status-report-counters.css: css/system/components/system-status-report-counters.css
css/components/system-status-report-general-info.css: css/system/components/system-status-report-general-info.css
......
......@@ -35,7 +35,7 @@
set classes = [
'cols-' ~ header|length,
responsive ? 'responsive-enabled',
sticky ? 'sticky-enabled',
sticky ? 'sticky-enabled sticky-header',
]
%}
<table{{ attributes.addClass(classes) }}>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment