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