From 0b95aa435e4c3af8819d2c4fc018aa3d7a0d7dd3 Mon Sep 17 00:00:00 2001
From: Benjamin Kott <benjamin.kott@outlook.com>
Date: Mon, 14 Nov 2022 12:01:07 +0100
Subject: [PATCH] [TASK] Avoid additional nesting complexity in
 typo3-backend-column-selector-button

The custom element typo3-backend-column-selector-button is a
valid HTML and should be used as such. There is no need to nest
buttons/links inside the element to add CSS styling.

We are adding keyboard events to react on the "Enter" and "Space"
keys to mimic the behavior of a button and setting defaults for the
role and tabindex to make it keyboard accessible. CSS classes are
now directly set to the element itself.

Resolves: #99080
Releases: main
Change-Id: I71dfa68174255a5d445af5fadbf1924b54a6b687
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76593
Tested-by: core-ci <typo3@b13.com>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../backend/column-selector-button.ts         | 22 +++++++++++++++++--
 .../Classes/RecordList/DatabaseRecordList.php | 11 +++++-----
 .../JavaScript/column-selector-button.js      |  2 +-
 .../Private/Templates/File/List.html          |  3 +--
 4 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/Build/Sources/TypeScript/backend/column-selector-button.ts b/Build/Sources/TypeScript/backend/column-selector-button.ts
index c090dce5c60f..658721b3d677 100644
--- a/Build/Sources/TypeScript/backend/column-selector-button.ts
+++ b/Build/Sources/TypeScript/backend/column-selector-button.ts
@@ -11,7 +11,7 @@
  * The TYPO3 project - inspiring people to share!
  */
 
-import {html, TemplateResult, LitElement} from 'lit';
+import {html, css, TemplateResult, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators';
 import {SeverityEnum} from '@typo3/backend/enum/severity';
 import Severity from '@typo3/backend/severity';
@@ -39,6 +39,7 @@ enum SelectorActions {
  *
  * @example
  * <typo3-backend-column-selector-button
+ *    class="btn btn-default"
  *    url="/url/to/column/selector/form"
  *    target="/url/to/go/after/column/selection"
  *    title="Show columns"
@@ -46,11 +47,13 @@ enum SelectorActions {
  *    close="Cancel"
  *    close="Error"
  * >
- *   <button>Show columns/button>
+ *   Show columns
  * </typo3-backend-column-selector-button>
  */
 @customElement('typo3-backend-column-selector-button')
 class ColumnSelectorButton extends LitElement {
+  static styles = [css`:host { cursor: pointer; appearance: button; }`];
+
   @property({type: String}) url: string;
   @property({type: String}) target: string;
   @property({type: String}) title: string = 'Show columns';
@@ -138,6 +141,21 @@ class ColumnSelectorButton extends LitElement {
       e.preventDefault();
       this.showColumnSelectorModal();
     });
+    this.addEventListener('keydown', (e: KeyboardEvent): void => {
+      if (e.key === 'Enter' || e.key === ' ') {
+        e.preventDefault();
+        this.showColumnSelectorModal();
+      }
+    })
+  }
+
+  public connectedCallback(): void {
+    if (!this.hasAttribute('role')) {
+      this.setAttribute('role', 'button');
+    }
+    if (!this.hasAttribute('tabindex')) {
+      this.setAttribute('tabindex', '0');
+    }
   }
 
   protected render(): TemplateResult {
diff --git a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
index ce3e2d023a4e..bfe76e93d74f 100644
--- a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
+++ b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
@@ -1961,18 +1961,17 @@ class DatabaseRecordList
         return '
             <div class="float-end me-2 p-0">
                 <typo3-backend-column-selector-button
+                    class="btn btn-default btn-sm"
                     url="' . htmlspecialchars($columnSelectorUrl) . '"
                     target="' . htmlspecialchars($this->listURL() . '#t3-table-' . $tableIdentifier) . '"
                     title="' . htmlspecialchars($columnSelectorTitle) . '"
                     ok="' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView')) . '"
                     close="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel')) . '"
                     error="' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.error')) . '"
-                >
-                    <button type="button" class="btn btn-default btn-sm" title="' . htmlspecialchars($columnSelectorTitle) . '">' .
-                        $this->iconFactory->getIcon('actions-options', Icon::SIZE_SMALL) . ' ' .
-                        htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumns')) .
-                    '</button>
-                </typo3-backend-column-selector-button>
+                >'
+                    . $this->iconFactory->getIcon('actions-options', Icon::SIZE_SMALL) . ' '
+                    . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumns')) .
+                '</typo3-backend-column-selector-button>
             </div>';
     }
 
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/column-selector-button.js b/typo3/sysext/backend/Resources/Public/JavaScript/column-selector-button.js
index dcfc21e62556..eae949c97831 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/column-selector-button.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/column-selector-button.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var ColumnSelectorButton_1,Selectors,SelectorActions,__decorate=function(e,t,o,l){var r,n=arguments.length,c=n<3?t:null===l?l=Object.getOwnPropertyDescriptor(t,o):l;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)c=Reflect.decorate(e,t,o,l);else for(var s=e.length-1;s>=0;s--)(r=e[s])&&(c=(n<3?r(c):n>3?r(t,o,c):r(t,o))||c);return n>3&&c&&Object.defineProperty(t,o,c),c};import{html,LitElement}from"lit";import{customElement,property}from"lit/decorators.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import Severity from"@typo3/backend/severity.js";import{default as Modal}from"@typo3/backend/modal.js";import{lll}from"@typo3/core/lit-helper.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Notification from"@typo3/backend/notification.js";!function(e){e.columnsSelector=".t3js-column-selector",e.columnsContainerSelector=".t3js-column-selector-container",e.columnsFilterSelector='input[name="columns-filter"]',e.columnsSelectorActionsSelector=".t3js-column-selector-actions"}(Selectors||(Selectors={})),function(e){e.toggle="select-toggle",e.all="select-all",e.none="select-none"}(SelectorActions||(SelectorActions={}));let ColumnSelectorButton=ColumnSelectorButton_1=class extends LitElement{constructor(){super(),this.title="Show columns",this.ok=lll("button.ok")||"Update",this.close=lll("button.close")||"Close",this.error="Could not update columns",this.addEventListener("click",(e=>{e.preventDefault(),this.showColumnSelectorModal()}))}static toggleSelectorActions(e,t,o,l=!1){t.classList.add("disabled");for(let o=0;o<e.length;o++)if(!e[o].disabled&&!e[o].checked&&(l||!ColumnSelectorButton_1.isColumnHidden(e[o]))){t.classList.remove("disabled");break}o.classList.add("disabled");for(let t=0;t<e.length;t++)if(!e[t].disabled&&e[t].checked&&(l||!ColumnSelectorButton_1.isColumnHidden(e[t]))){o.classList.remove("disabled");break}}static isColumnHidden(e){return e.closest(Selectors.columnsContainerSelector)?.classList.contains("hidden")}static filterColumns(e,t){t.forEach((t=>{const o=t.closest(Selectors.columnsContainerSelector);if(!t.disabled&&null!==o){const t=o.querySelector(".form-check-label-text")?.textContent;t&&t.length&&o.classList.toggle("hidden",""!==e.value&&!RegExp(e.value,"i").test(t.trim().replace(/\[\]/g,"").replace(/\s+/g," ")))}}))}render(){return html`<slot></slot>`}showColumnSelectorModal(){if(!this.url||!this.target)return;const e=Modal.advanced({content:this.url,title:this.title,severity:SeverityEnum.notice,size:Modal.sizes.medium,type:Modal.types.ajax,buttons:[{text:this.close,active:!0,btnClass:"btn-default",name:"cancel",trigger:(e,t)=>t.hideModal()},{text:this.ok,btnClass:"btn-"+Severity.getCssClass(SeverityEnum.info),name:"update",trigger:(e,t)=>this.proccessSelection(t)}],ajaxCallback:()=>this.handleModalContentLoaded(e)})}proccessSelection(e){const t=e.querySelector("form");null!==t?new AjaxRequest(TYPO3.settings.ajaxUrls.show_columns).post("",{body:new FormData(t)}).then((async e=>{const t=await e.resolve();!0===t.success?(this.ownerDocument.location.href=this.target,this.ownerDocument.location.reload()):Notification.error(t.message||"No update was performed"),Modal.dismiss()})).catch((()=>{this.abortSelection()})):this.abortSelection()}handleModalContentLoaded(e){const t=e.querySelector("form");if(null===t)return;t.addEventListener("submit",(e=>{e.preventDefault()}));const o=e.querySelectorAll(Selectors.columnsSelector),l=e.querySelector(Selectors.columnsFilterSelector),r=e.querySelector(Selectors.columnsSelectorActionsSelector),n=r.querySelector('button[data-action="'+SelectorActions.all+'"]'),c=r.querySelector('button[data-action="'+SelectorActions.none+'"]');o.length&&null!==l&&null!==n&&null!==c&&(ColumnSelectorButton_1.toggleSelectorActions(o,n,c,!0),o.forEach((e=>{e.addEventListener("change",(()=>{ColumnSelectorButton_1.toggleSelectorActions(o,n,c)}))})),l.addEventListener("keydown",(e=>{const t=e.target;"Escape"===e.code&&(e.stopImmediatePropagation(),t.value="")})),l.addEventListener("keyup",(e=>{ColumnSelectorButton_1.filterColumns(e.target,o),ColumnSelectorButton_1.toggleSelectorActions(o,n,c)})),l.addEventListener("search",(e=>{ColumnSelectorButton_1.filterColumns(e.target,o),ColumnSelectorButton_1.toggleSelectorActions(o,n,c)})),r.querySelectorAll("button[data-action]").forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.currentTarget;if(t.dataset.action){switch(t.dataset.action){case SelectorActions.toggle:o.forEach((e=>{e.disabled||ColumnSelectorButton_1.isColumnHidden(e)||(e.checked=!e.checked)}));break;case SelectorActions.all:o.forEach((e=>{e.disabled||ColumnSelectorButton_1.isColumnHidden(e)||(e.checked=!0)}));break;case SelectorActions.none:o.forEach((e=>{e.disabled||ColumnSelectorButton_1.isColumnHidden(e)||(e.checked=!1)}));break;default:Notification.warning("Unknown selector action")}ColumnSelectorButton_1.toggleSelectorActions(o,n,c)}}))})))}abortSelection(){Notification.error(this.error),Modal.dismiss()}};__decorate([property({type:String})],ColumnSelectorButton.prototype,"url",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"target",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"title",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"ok",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"close",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"error",void 0),ColumnSelectorButton=ColumnSelectorButton_1=__decorate([customElement("typo3-backend-column-selector-button")],ColumnSelectorButton);
\ No newline at end of file
+var ColumnSelectorButton_1,Selectors,SelectorActions,__decorate=function(e,t,o,l){var n,r=arguments.length,c=r<3?t:null===l?l=Object.getOwnPropertyDescriptor(t,o):l;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)c=Reflect.decorate(e,t,o,l);else for(var s=e.length-1;s>=0;s--)(n=e[s])&&(c=(r<3?n(c):r>3?n(t,o,c):n(t,o))||c);return r>3&&c&&Object.defineProperty(t,o,c),c};import{html,css,LitElement}from"lit";import{customElement,property}from"lit/decorators.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import Severity from"@typo3/backend/severity.js";import{default as Modal}from"@typo3/backend/modal.js";import{lll}from"@typo3/core/lit-helper.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Notification from"@typo3/backend/notification.js";!function(e){e.columnsSelector=".t3js-column-selector",e.columnsContainerSelector=".t3js-column-selector-container",e.columnsFilterSelector='input[name="columns-filter"]',e.columnsSelectorActionsSelector=".t3js-column-selector-actions"}(Selectors||(Selectors={})),function(e){e.toggle="select-toggle",e.all="select-all",e.none="select-none"}(SelectorActions||(SelectorActions={}));let ColumnSelectorButton=ColumnSelectorButton_1=class extends LitElement{constructor(){super(),this.title="Show columns",this.ok=lll("button.ok")||"Update",this.close=lll("button.close")||"Close",this.error="Could not update columns",this.addEventListener("click",(e=>{e.preventDefault(),this.showColumnSelectorModal()})),this.addEventListener("keydown",(e=>{"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),this.showColumnSelectorModal())}))}static toggleSelectorActions(e,t,o,l=!1){t.classList.add("disabled");for(let o=0;o<e.length;o++)if(!e[o].disabled&&!e[o].checked&&(l||!ColumnSelectorButton_1.isColumnHidden(e[o]))){t.classList.remove("disabled");break}o.classList.add("disabled");for(let t=0;t<e.length;t++)if(!e[t].disabled&&e[t].checked&&(l||!ColumnSelectorButton_1.isColumnHidden(e[t]))){o.classList.remove("disabled");break}}static isColumnHidden(e){return e.closest(Selectors.columnsContainerSelector)?.classList.contains("hidden")}static filterColumns(e,t){t.forEach((t=>{const o=t.closest(Selectors.columnsContainerSelector);if(!t.disabled&&null!==o){const t=o.querySelector(".form-check-label-text")?.textContent;t&&t.length&&o.classList.toggle("hidden",""!==e.value&&!RegExp(e.value,"i").test(t.trim().replace(/\[\]/g,"").replace(/\s+/g," ")))}}))}connectedCallback(){this.hasAttribute("role")||this.setAttribute("role","button"),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0")}render(){return html`<slot></slot>`}showColumnSelectorModal(){if(!this.url||!this.target)return;const e=Modal.advanced({content:this.url,title:this.title,severity:SeverityEnum.notice,size:Modal.sizes.medium,type:Modal.types.ajax,buttons:[{text:this.close,active:!0,btnClass:"btn-default",name:"cancel",trigger:(e,t)=>t.hideModal()},{text:this.ok,btnClass:"btn-"+Severity.getCssClass(SeverityEnum.info),name:"update",trigger:(e,t)=>this.proccessSelection(t)}],ajaxCallback:()=>this.handleModalContentLoaded(e)})}proccessSelection(e){const t=e.querySelector("form");null!==t?new AjaxRequest(TYPO3.settings.ajaxUrls.show_columns).post("",{body:new FormData(t)}).then((async e=>{const t=await e.resolve();!0===t.success?(this.ownerDocument.location.href=this.target,this.ownerDocument.location.reload()):Notification.error(t.message||"No update was performed"),Modal.dismiss()})).catch((()=>{this.abortSelection()})):this.abortSelection()}handleModalContentLoaded(e){const t=e.querySelector("form");if(null===t)return;t.addEventListener("submit",(e=>{e.preventDefault()}));const o=e.querySelectorAll(Selectors.columnsSelector),l=e.querySelector(Selectors.columnsFilterSelector),n=e.querySelector(Selectors.columnsSelectorActionsSelector),r=n.querySelector('button[data-action="'+SelectorActions.all+'"]'),c=n.querySelector('button[data-action="'+SelectorActions.none+'"]');o.length&&null!==l&&null!==r&&null!==c&&(ColumnSelectorButton_1.toggleSelectorActions(o,r,c,!0),o.forEach((e=>{e.addEventListener("change",(()=>{ColumnSelectorButton_1.toggleSelectorActions(o,r,c)}))})),l.addEventListener("keydown",(e=>{const t=e.target;"Escape"===e.code&&(e.stopImmediatePropagation(),t.value="")})),l.addEventListener("keyup",(e=>{ColumnSelectorButton_1.filterColumns(e.target,o),ColumnSelectorButton_1.toggleSelectorActions(o,r,c)})),l.addEventListener("search",(e=>{ColumnSelectorButton_1.filterColumns(e.target,o),ColumnSelectorButton_1.toggleSelectorActions(o,r,c)})),n.querySelectorAll("button[data-action]").forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.currentTarget;if(t.dataset.action){switch(t.dataset.action){case SelectorActions.toggle:o.forEach((e=>{e.disabled||ColumnSelectorButton_1.isColumnHidden(e)||(e.checked=!e.checked)}));break;case SelectorActions.all:o.forEach((e=>{e.disabled||ColumnSelectorButton_1.isColumnHidden(e)||(e.checked=!0)}));break;case SelectorActions.none:o.forEach((e=>{e.disabled||ColumnSelectorButton_1.isColumnHidden(e)||(e.checked=!1)}));break;default:Notification.warning("Unknown selector action")}ColumnSelectorButton_1.toggleSelectorActions(o,r,c)}}))})))}abortSelection(){Notification.error(this.error),Modal.dismiss()}};ColumnSelectorButton.styles=[css`:host { cursor: pointer; appearance: button; }`],__decorate([property({type:String})],ColumnSelectorButton.prototype,"url",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"target",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"title",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"ok",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"close",void 0),__decorate([property({type:String})],ColumnSelectorButton.prototype,"error",void 0),ColumnSelectorButton=ColumnSelectorButton_1=__decorate([customElement("typo3-backend-column-selector-button")],ColumnSelectorButton);
\ No newline at end of file
diff --git a/typo3/sysext/filelist/Resources/Private/Templates/File/List.html b/typo3/sysext/filelist/Resources/Private/Templates/File/List.html
index 2088330f0e74..6f4d31f5d039 100644
--- a/typo3/sysext/filelist/Resources/Private/Templates/File/List.html
+++ b/typo3/sysext/filelist/Resources/Private/Templates/File/List.html
@@ -145,16 +145,15 @@
                                 <f:if condition="{columnSelector}">
                                     <div class="col">
                                         <typo3-backend-column-selector-button
+                                            class="btn btn-default btn-sm"
                                             url="{columnSelector.url}"
                                             target="{listUrl}"
                                             title="{columnSelector.title}"
                                             ok="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView')}"
                                             close="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel')}"
                                             error="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.error')}">
-                                            <button type="button" class="btn btn-default btn-sm" title="{columnSelector.title}">
                                                 <core:icon identifier="actions-options" size="small" />
                                                 <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumns" />
-                                            </button>
                                         </typo3-backend-column-selector-button>
                                     </div>
                                 </f:if>
-- 
GitLab