Skip to content

Commit

Permalink
[TASK] Unify table sort rendering
Browse files Browse the repository at this point in the history
Use actions-sort-* icons for table
sorting and use accessible elements (button/a)
in sorting triggers.

Resolves: #101881
Releases: main, 12.4
Change-Id: Ie6de4500bf3819ea2c9b260456d05d9baee35f23
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80949
Reviewed-by: Benjamin Franzke <ben@bnf.dev>
Tested-by: Benjamin Franzke <ben@bnf.dev>
Tested-by: core-ci <typo3@b13.com>
  • Loading branch information
benjaminkott authored and bnf committed Sep 11, 2023
1 parent 8cc9ba2 commit 9e5bde8
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 73 deletions.
5 changes: 0 additions & 5 deletions Build/Sources/Sass/backend.scss
Expand Up @@ -24,11 +24,6 @@
//
@import "libs/sortable";

//
// tablesort.js
//
@import "libs/tablesort";

//
// NProgress
//
Expand Down
5 changes: 5 additions & 0 deletions Build/Sources/Sass/component/_root.scss
Expand Up @@ -14,6 +14,7 @@

// Light
--typo3-light-color: #{$body-color};
--typo3-light-primary-color: #{$primary};
--typo3-light-secondary-color: #{tint-color($body-color, 40%)};
--typo3-light-bg: #{$white};
--typo3-light-border-color: #{$gray-300};
Expand All @@ -34,6 +35,7 @@

// Dark
--typo3-dark-color: #{$white};
--typo3-dark-primary-color: #{tint-color($primary, 40%)};
--typo3-dark-secondary-color: #{shade-color($white, 40%)};
--typo3-dark-bg: #{$gray-900};
--typo3-dark-border-color: #{$gray-800};
Expand All @@ -54,6 +56,7 @@

// Component
--typo3-component-color: var(--typo3-light-color);
--typo3-component-primary-color: var(--typo3-light-primary-color);
--typo3-component-secondary-color: var(--typo3-light-secondary-color);
--typo3-component-bg: var(--typo3-light-bg);
--typo3-component-link-color: var(--typo3-light-link-color);
Expand Down Expand Up @@ -118,6 +121,7 @@

@mixin lightmode {
--typo3-component-color: var(--typo3-light-color);
--typo3-component-primary-color: var(--typo3-light-primary-color);
--typo3-component-secondary-color: var(--typo3-light-secondary-color);
--typo3-component-bg: var(--typo3-light-bg);
--typo3-component-border-color: var(--typo3-light-border-color);
Expand All @@ -139,6 +143,7 @@

@mixin darkmode {
--typo3-component-color: var(--typo3-dark-color);
--typo3-component-primary-color: var(--typo3-dark-primary-color);
--typo3-component-secondary-color: var(--typo3-dark-secondary-color);
--typo3-component-bg: var(--typo3-dark-bg);
--typo3-component-border-color: var(--typo3-dark-border-color);
Expand Down
25 changes: 25 additions & 0 deletions Build/Sources/Sass/component/_table.scss
Expand Up @@ -277,3 +277,28 @@ td.selected {
width: auto;
}
}

/**
* Sorting
*/
.table-sorting-button {
padding: 0;
border: 0;
font-weight: inherit;
font-size: inherit;
background: transparent;
display: inline-flex;
align-items: center;
gap: .25em;
}

.table-sorting-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--typo3-light-secondary-color);

.table-sorting-button-active & {
color: var(--typo3-light-primary-color);
}
}
32 changes: 0 additions & 32 deletions Build/Sources/Sass/libs/_tablesort.scss

This file was deleted.

106 changes: 104 additions & 2 deletions Build/Sources/TypeScript/backend/sortable-table.ts
Expand Up @@ -11,12 +11,114 @@
* The TYPO3 project - inspiring people to share!
*/

import Tablesort from 'tablesort';
import Tablesort, { TablesortOptions } from 'tablesort';
import 'tablesort.dotsep';
import 'tablesort.number';
import { IconElement } from './element/icon-element';
import { Sizes } from './enum/icon-types';

class TablesortWithButtons extends Tablesort {
public init(el: HTMLTableElement, options: TablesortOptions) {
let firstRow: HTMLTableRowElement, defaultSort;

this.table = el;
this.thead = false;
this.options = options;

if (el.rows && el.rows.length > 0) {
if (el.tHead && el.tHead.rows.length > 0) {
for (let i = 0; i < el.tHead.rows.length; i++) {
if (el.tHead.rows[i].getAttribute('data-sort-method') === 'thead') {
firstRow = el.tHead.rows[i];
break;
}
}
if (!firstRow) {
firstRow = el.tHead.rows[el.tHead.rows.length - 1];
}
this.thead = true;
} else {
firstRow = el.rows[0];
}
}

if (!firstRow) {
return;
}

const onClick = (event: PointerEvent): void => {
event.preventDefault();
event.stopImmediatePropagation();
const target = (event.currentTarget as HTMLButtonElement).parentNode as HTMLTableCellElement;
if (this.current && this.current !== target) {
this.current.removeAttribute('aria-sort');
}

this.current = target;
this.sortTable(target);
};

// Assume first row is the header and attach a click handler to each.
for (let i = 0; i < firstRow.cells.length; i++) {
const cell = firstRow.cells[i] as HTMLTableCellElement;
cell.setAttribute('role', 'columnheader');
if (cell.getAttribute('data-sort-method') !== 'none') {

const button = document.createElement('button');
button.classList.add('table-sorting-button');

const labelWrap = document.createElement('span');
labelWrap.classList.add('table-sorting-label');
labelWrap.textContent = cell.textContent;
button.appendChild(labelWrap);

const iconWrap = document.createElement('span');
iconWrap.classList.add('table-sorting-icon');
const icon = new IconElement();
icon.identifier = 'actions-sort-amount';
icon.size = Sizes.small;
iconWrap.appendChild(icon);
button.appendChild(iconWrap);

button.addEventListener('click', onClick, false);
cell.replaceChildren(button);

if (cell.getAttribute('data-sort-default') !== null) {
defaultSort = cell;
}
}
}

if (defaultSort) {
this.current = defaultSort;
this.sortTable(defaultSort);
}
}
}

export default class SortableTable {
constructor(table: HTMLTableElement) {
new Tablesort(table);
new TablesortWithButtons(table);

table.addEventListener('afterSort', (event: CustomEvent) => {
const table = (event.target as HTMLTableElement);
const buttons = table.tHead.querySelectorAll('.table-sorting-button');
buttons.forEach((button) => {
button.classList.remove('table-sorting-button-active');
const icon = button.querySelector('typo3-backend-icon');
icon.identifier = 'actions-sort-amount';
});

const sortingCell = table.tHead.querySelector('th[aria-sort]') as HTMLTableCellElement;
const sortingButton = sortingCell.querySelector('.table-sorting-button');
sortingButton.classList.add('table-sorting-button-active');

const sortingIcon = sortingButton.querySelector('typo3-backend-icon');
if (sortingCell.ariaSort === 'ascending') {
sortingIcon.identifier = 'actions-sort-amount-down';
} else {
sortingIcon.identifier = 'actions-sort-amount-up';
}
});
}
}
20 changes: 8 additions & 12 deletions Build/types/tablesort/index.d.ts
@@ -1,18 +1,14 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Tablesort {
}

type TablesortOptions = {
export type TablesortOptions = {
descending: boolean;
sortAttribute: string;
};

declare const Tablesort: {
new(table: Element, options?: {[key: string]: TablesortOptions}): Tablesort;
// eslint-disable-next-line @typescript-eslint/ban-types
extend(name: string, pattern: Function, sort: Function): void;
}
export default class Tablesort {
protected table: HTMLTableElement;
protected thead: boolean;
protected options: TablesortOptions;
protected current: HTMLTableCellElement;

declare module 'tablesort' {
export default Tablesort;
constructor(table: HTMLElement, options?: {[key: string]: TablesortOptions});
protected sortTable(table: HTMLTableCellElement): void;
}
29 changes: 20 additions & 9 deletions typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
Expand Up @@ -1987,31 +1987,42 @@ class="btn btn-sm btn-default"
* It will automatically detect if sorting should be ascending or descending depending on $this->sortRev.
* Also some fields will not be possible to sort (including if single-table-view is disabled).
*
* @param string $code The string to link (text)
* @param string $label The string to link (text)
* @param string $field The fieldname represented by the title ($code)
* @param string $table Table name
* @return string Linked $code variable
*/
public function addSortLink($code, $field, $table)
public function addSortLink($label, $field, $table): string
{
// Certain circumstances just return string right away (no links):
if ($this->disableSingleTableView
|| in_array($field, ['_SELECTOR', '_CONTROL_', '_LOCALIZATION_', '_REF_'], true)
) {
return $code;
return $label;
}

// If "_PATH_" (showing record path) is selected, force sorting by pid field (will at least group the records!)
if ($field === '_PATH_') {
$field = 'pid';
}
// Create the sort link:
$sortUrl = $this->listURL('', $table, 'sortField,sortRev,table,pointer')

// Create the sort link:
$url = $this->listURL('', $table, 'sortField,sortRev,table,pointer')
. '&sortField=' . $field . '&sortRev=' . ($this->sortRev || $this->sortField != $field ? 0 : 1);
$sortArrow = $this->sortField === $field
? $this->iconFactory->getIcon('status-status-sorting-' . ($this->sortRev ? 'desc' : 'asc'), Icon::SIZE_SMALL)->render()
: '';
$icon = $this->sortField === $field
? $this->iconFactory->getIcon('actions-sort-amount-' . ($this->sortRev ? 'down' : 'up'), Icon::SIZE_SMALL)->render()
: $this->iconFactory->getIcon('actions-sort-amount', Icon::SIZE_SMALL)->render();

// Return linked field:
return '<a href="' . htmlspecialchars($sortUrl) . '">' . $code . $sortArrow . '</a>';
$attributes = [
'class' => 'table-sorting-button ' . ($this->sortField === $field ? 'table-sorting-button-active' : ''),
'href' => $url,
];

return '<a ' . GeneralUtility::implodeAttributes($attributes, true) . '>
<span class="table-sorting-label">' . $label . '</span>
<span class="table-sorting-icon">' . $icon . '</span>
</a>';
}

/**
Expand Down

0 comments on commit 9e5bde8

Please sign in to comment.