From 863edd8c814d777ed10739449cd00e190db1e1e4 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Tue, 25 Jun 2024 17:08:59 +0200 Subject: [PATCH 01/23] Publish SNAPSHOT from the epic branch New SNAPSHOT version --- .github/workflows/gradle.yml | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6f20a473e..875d9353d 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest outputs: - publish: ${{ steps.publish_vars.outputs.release != 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/4.')) }} + publish: ${{ steps.publish_vars.outputs.release != 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/4.') || startsWith(github.ref, 'refs/heads/epic-')) }} repo: ${{ steps.publish_vars.outputs.repo }} steps: diff --git a/gradle.properties b/gradle.properties index 61fdabf73..eec34f2b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.10.0-SNAPSHOT +version=4.10.0-WCAG-SNAPSHOT systemProp.org.gradle.internal.http.connectionTimeout=120000 systemProp.org.gradle.internal.http.socketTimeout=120000 From 3c8962362e7982f30993052d9021c3ff0401aeaf Mon Sep 17 00:00:00 2001 From: alansemenov Date: Tue, 25 Jun 2024 15:25:49 +0200 Subject: [PATCH 02/23] Enable setting `lang` and `role` attributes #3615 #3619 --- .../assets/admin/common/js/dom/Body.ts | 16 ++--- .../assets/admin/common/js/dom/ButtonEl.ts | 1 + .../assets/admin/common/js/dom/Element.ts | 66 +++++++++++++++++++ .../admin/common/js/dom/ElementHelper.ts | 2 +- .../assets/admin/common/js/util/Config.ts | 10 ++- 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/dom/Body.ts b/src/main/resources/assets/admin/common/js/dom/Body.ts index cc01e36d0..77796bb25 100644 --- a/src/main/resources/assets/admin/common/js/dom/Body.ts +++ b/src/main/resources/assets/admin/common/js/dom/Body.ts @@ -1,8 +1,8 @@ import {Element, ElementFromHelperBuilder} from './Element'; import {ResponsiveManager} from '../ui/responsive/ResponsiveManager'; import {ElementHelper} from './ElementHelper'; -import {BrowserHelper} from '../BrowserHelper'; import {Store} from '../store/Store'; +import {CONFIG} from '../util/Config'; export const BODY_KEY: string = 'Body'; @@ -15,21 +15,15 @@ export class Body if (!body) { body = document.body; } - let html = Element.fromHtmlElement(body.parentElement); - if (BrowserHelper.isIE() && html.getEl().getChild(0) instanceof HTMLHeadElement) { - html.insertChild(Element.fromHtmlElement(html.getEl().getChild(1) as HTMLElement), 1); - } + const html = Element.fromHtmlElement(body.parentElement); + html.setLang(CONFIG.getLocale()); super(new ElementFromHelperBuilder().setHelper(new ElementHelper(body)).setLoadExistingChildren(loadExistingChildren)); html.appendChild(this); - if (BrowserHelper.isIE()) { - this.addClass('IE'); - } - - let visibilityHandler = () => { + const visibilityHandler = () => { this.init().then(() => { this.childrenLoaded = loadExistingChildren; }); @@ -37,7 +31,7 @@ export class Body if (!document.hidden) { visibilityHandler(); } else { - let visibilityListener = () => { + const visibilityListener = () => { if (!document.hidden && !this.isRendered() && !this.isRendering()) { visibilityHandler(); document.removeEventListener('visibilitychange', visibilityListener); diff --git a/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts b/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts index aec725e3d..b690bc348 100644 --- a/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts +++ b/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts @@ -11,6 +11,7 @@ export class ButtonEl setEnabled(value: boolean) { super.setEnabled(value); this.getEl().setDisabled(!value); + this.setAriaDisabled(!value); return this; } diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index e90119b54..abc0c1979 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -33,6 +33,8 @@ export class ElementBuilder { className: string; + role: string; + parentElement: Element; setGenerateId(value: boolean): ElementBuilder { @@ -57,6 +59,11 @@ export class ElementBuilder { return this; } + setRole(role: string): ElementBuilder { + this.role = role; + return this; + } + private getParsedClass(cls: string): string { return cls.trim().split(/\s+/) .filter((elem, index, arr) => { @@ -233,6 +240,10 @@ export class Element { if (builder.className) { this.setClass(builder.className); } + + if (builder.role) { + this.setRole(builder.role); + } } static fromHtmlElement(element: HTMLElement, loadExistingChildren: boolean = false, parent?: Element): Element { @@ -526,6 +537,21 @@ export class Element { return this.getEl().hasAttribute('spellcheck'); } + makeTabbable(): Element { + this.getEl().setTabIndex(0); + return this; + } + + removeTabbable(): Element { + this.getEl().setTabIndex(-1); + return this; + } + + setTabIndex(tabIndex: number): Element { + this.getEl().setTabIndex(tabIndex); + return this; + } + setLang(value: string): Element { this.getEl().setAttribute('lang', value); return this; @@ -535,6 +561,40 @@ export class Element { return this.getEl().getAttribute('lang'); } + private setAriaAttribute(name: string, value: string): Element { + this.getEl().setAttribute(`aria-${name}`, value); + return this; + } + + private removeAriaAttribute(name: string): Element { + this.getEl().removeAttribute(`aria-${name}`); + return this; + } + + setAriaLabel(label: string): Element { + return this.setAriaAttribute('label', label); + } + + setAriaDisabled(disabled: boolean): Element { + if (disabled) { + return this.setAriaAttribute('disabled', 'true'); + } + return this.removeAriaAttribute('disabled'); + } + + private isAriaRole(value: any): value is AriaRole { + return Object.keys(AriaRole).some(key => (AriaRole as any)[key] === value); + } + + setRole(value: string | AriaRole): Element { + let role = AriaRole.NONE; + if (this.isAriaRole(value)) { + role = value; + } + this.getEl().setAttribute('role', role); + return this; + } + setDir(value: LangDirection): Element { this.getEl().setAttribute('dir', value); return this; @@ -1557,3 +1617,9 @@ export class Element { }); } } + +export enum AriaRole { + NONE = 'presentation', + BUTTON = 'button', + TOOLBAR = 'toolbar' +} diff --git a/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts b/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts index bdb261e71..853807413 100644 --- a/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts +++ b/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts @@ -139,7 +139,7 @@ export class ElementHelper { setData(name: string, value: string): ElementHelper { assert(!StringHelper.isEmpty(name), 'Name cannot be empty'); assert(!StringHelper.isEmpty(value), 'Value cannot be empty'); - this.el.setAttribute('data-' + name, value); + this.setAttribute('data-' + name, value); $(this.el).data(name, value); return this; } diff --git a/src/main/resources/assets/admin/common/js/util/Config.ts b/src/main/resources/assets/admin/common/js/util/Config.ts index bb66894b2..1d7e69b3b 100644 --- a/src/main/resources/assets/admin/common/js/util/Config.ts +++ b/src/main/resources/assets/admin/common/js/util/Config.ts @@ -25,7 +25,7 @@ export class CONFIG { } static has(property: string): boolean { - return CONFIG.CACHE[property] !== undefined; + return CONFIG.CACHE?.[property] !== undefined; } static getString(property: string): string { @@ -40,6 +40,14 @@ export class CONFIG { return parseInt(String(propertyValue)); } + static getLocale(): string { + if (!CONFIG.has('locale')) { + return 'en'; + } + + return CONFIG.getString('locale'); + } + static get(property: string): JSONValue { return CONFIG.getPropertyValue(property); } From 3ad4ee2ed236158c1d830d849188ced5a4641662 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Mon, 1 Jul 2024 11:12:18 +0200 Subject: [PATCH 03/23] Refactor AppIcon & TabbedAppBar #3625 --- .../admin/common/js/app/NavigatedAppPanel.ts | 9 ++++++- .../assets/admin/common/js/app/bar/AppIcon.ts | 4 +-- .../admin/common/js/app/bar/TabbedAppBar.ts | 25 ++++++------------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/app/NavigatedAppPanel.ts b/src/main/resources/assets/admin/common/js/app/NavigatedAppPanel.ts index 9b6910328..6a2c94674 100644 --- a/src/main/resources/assets/admin/common/js/app/NavigatedAppPanel.ts +++ b/src/main/resources/assets/admin/common/js/app/NavigatedAppPanel.ts @@ -47,7 +47,10 @@ export class NavigatedAppPanel addNavigablePanel(item: AppBarTabMenuItem, panel: Panel, select?: boolean) { this.appBarTabMenu.addNavigationItem(item); - let index = this.addPanel(panel); + const index = this.addPanel(panel); + if (index === 1) { + this.appBar.setHomeIconAction(); + } if (select) { this.selectPanelByIndex(index); } @@ -61,6 +64,10 @@ export class NavigatedAppPanel this.appBarTabMenu.removeNavigationItem(navigationItem); } + if (this.appBarTabMenu.countVisible() === 0) { + this.appBar.unsetHomeIconAction(); + } + this.checkBrowsePanelNeedsToBeShown(index, panel); return index; diff --git a/src/main/resources/assets/admin/common/js/app/bar/AppIcon.ts b/src/main/resources/assets/admin/common/js/app/bar/AppIcon.ts index 0e0db3b3a..6e3f09b62 100644 --- a/src/main/resources/assets/admin/common/js/app/bar/AppIcon.ts +++ b/src/main/resources/assets/admin/common/js/app/bar/AppIcon.ts @@ -46,14 +46,14 @@ export class AppIcon this.initListeners(action); } this.addClass('clickable'); - this.getEl().setTabIndex(0); + this.makeTabbable(); this.onClicked(this.clickListener); this.onKeyDown(this.enterListener); } removeAction(): void { this.removeClass('clickable'); - this.getEl().removeAttribute('tabindex'); + this.removeTabbable(); this.unClicked(this.clickListener); this.onKeyDown(this.enterListener); } diff --git a/src/main/resources/assets/admin/common/js/app/bar/TabbedAppBar.ts b/src/main/resources/assets/admin/common/js/app/bar/TabbedAppBar.ts index 3b7cafe0f..2e6a0c593 100644 --- a/src/main/resources/assets/admin/common/js/app/bar/TabbedAppBar.ts +++ b/src/main/resources/assets/admin/common/js/app/bar/TabbedAppBar.ts @@ -1,5 +1,4 @@ import {ActionContainer} from '../../ui/ActionContainer'; -import {ResponsiveManager} from '../../ui/responsive/ResponsiveManager'; import {AppBar} from './AppBar'; import {AppBarTabMenu} from './AppBarTabMenu'; import {Application} from '../Application'; @@ -17,23 +16,13 @@ export class TabbedAppBar this.appendChild(this.tabMenu); - this.tabMenu.onNavigationItemAdded(() => this.updateAppOpenTabs()); - this.tabMenu.onNavigationItemRemoved(() => this.updateAppOpenTabs()); - - // Responsive events to update homeButton styles - ResponsiveManager.onAvailableSizeChanged(this, () => { - if (this.tabMenu.countVisible() > 0) { - if (managedHomeIconAction) { - super.setHomeIconAction(); - } - this.addClass('tabs-present'); - } else { - if (managedHomeIconAction) { - super.unsetHomeIconAction(); - } - this.removeClass('tabs-present'); - } - }); + const onNavigationItemAddedOrRemoved = () => { + this.updateAppOpenTabs(); + this.toggleClass('tabs-present', this.tabMenu.countVisible() > 0); + }; + + this.tabMenu.onNavigationItemAdded(onNavigationItemAddedOrRemoved); + this.tabMenu.onNavigationItemRemoved(onNavigationItemAddedOrRemoved); } getTabMenu(): AppBarTabMenu { From 702d9345361004b0f7b5d303abe770de02b4eadd Mon Sep 17 00:00:00 2001 From: alansemenov Date: Mon, 1 Jul 2024 15:23:59 +0200 Subject: [PATCH 04/23] Accessibility: Toolbar #3626 --- .../resources/assets/admin/common/js/ui/WCAG.ts | 3 +++ .../admin/common/js/ui/toolbar/Toolbar.ts | 17 +++++++++++++++-- src/main/resources/i18n/common.properties | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/assets/admin/common/js/ui/WCAG.ts diff --git a/src/main/resources/assets/admin/common/js/ui/WCAG.ts b/src/main/resources/assets/admin/common/js/ui/WCAG.ts new file mode 100644 index 000000000..de30e2f14 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ui/WCAG.ts @@ -0,0 +1,3 @@ +export interface WCAG { + applyWCAGAttributes(): void; +} diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 2517db982..547978ab5 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -4,13 +4,14 @@ import {DivEl} from '../../dom/DivEl'; import {ActionContainer} from '../ActionContainer'; import {Action} from '../Action'; import {ResponsiveManager} from '../responsive/ResponsiveManager'; -import {Element} from '../../dom/Element'; +import {Element, AriaRole} from '../../dom/Element'; import {ObjectHelper} from '../../ObjectHelper'; import {FoldButton} from './FoldButton'; +import {WCAG} from '../WCAG'; export class Toolbar extends DivEl - implements ActionContainer { + implements ActionContainer, WCAG { protected foldButton: FoldButton; protected actions: Action[] = []; @@ -20,6 +21,8 @@ export class Toolbar constructor(className?: string) { super(!className ? 'toolbar' : className + ' toolbar'); + this.applyWCAGAttributes(); + this.foldButton = new FoldButton(); this.foldButton.hide(); this.appendChild(this.foldButton); @@ -30,6 +33,16 @@ export class Toolbar this.onShown(() => this.foldOrExpand()); } + applyWCAGAttributes(): void { + this.setRole(AriaRole.TOOLBAR) + .makeTabbable() + .setAriaLabel(this.getAriaLabel()); + } + + protected getAriaLabel(): string { + return i18n('wcag.toolbar.label'); + } + addAction(action: Action): ActionButton { this.actions.push(action); diff --git a/src/main/resources/i18n/common.properties b/src/main/resources/i18n/common.properties index cf376e70c..ff8d2f780 100644 --- a/src/main/resources/i18n/common.properties +++ b/src/main/resources/i18n/common.properties @@ -214,3 +214,8 @@ tooltip.header.collapse=Click to collapse # Warnings # warning.optionsview.truncated = The list is truncated + +# +# Accessibility +# +wcag.toolbar.label=Main menu bar From 30aaa9af4714a585c49a694a9f1c61c396b13fd4 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Fri, 5 Jul 2024 16:27:11 +0200 Subject: [PATCH 05/23] Implement an interface with WCAG properties #3641 --- .../assets/admin/common/js/dom/Element.ts | 33 ++++++++++--------- .../assets/admin/common/js/ui/WCAG.ts | 16 +++++++-- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index abc0c1979..198ef864a 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -15,6 +15,7 @@ import {ElementRegistry} from './ElementRegistry'; import {assert, assertNotNull, assertState} from '../util/Assert'; import {ElementEvent} from './ElementEvent'; import * as DOMPurify from 'dompurify'; +import {IWCAG as WCAG, AriaRole} from '../ui/WCAG'; export interface PurifyConfig { addTags?: string[]; @@ -33,8 +34,6 @@ export class ElementBuilder { className: string; - role: string; - parentElement: Element; setGenerateId(value: boolean): ElementBuilder { @@ -59,11 +58,6 @@ export class ElementBuilder { return this; } - setRole(role: string): ElementBuilder { - this.role = role; - return this; - } - private getParsedClass(cls: string): string { return cls.trim().split(/\s+/) .filter((elem, index, arr) => { @@ -240,10 +234,20 @@ export class Element { if (builder.className) { this.setClass(builder.className); } + } - if (builder.role) { - this.setRole(builder.role); + applyWCAGAttributes(): void { + if (!this.implementsWCAG()) { + return; } + + this['tabbable'] && this.makeTabbable(); + this['role'] && this.setRole(this['role']); + this['ariaLabel'] && this.setAriaLabel(this['ariaLabel']); + } + + private implementsWCAG(): boolean { + return this[WCAG] === true; } static fromHtmlElement(element: HTMLElement, loadExistingChildren: boolean = false, parent?: Element): Element { @@ -343,6 +347,7 @@ export class Element { console.log('Element.render started', ClassHelper.getClassName(this)); } this.rendering = true; + return this.doRender().then((rendered) => { let childPromises = []; @@ -572,6 +577,9 @@ export class Element { } setAriaLabel(label: string): Element { + if (StringHelper.isBlank(label)) { + return this; + } return this.setAriaAttribute('label', label); } @@ -1295,6 +1303,7 @@ export class Element { if (this.isRendered() || this.isRendering()) { renderPromise = Q(true); } else { + this.applyWCAGAttributes(); renderPromise = this.doRender(); } this.rendering = true; @@ -1617,9 +1626,3 @@ export class Element { }); } } - -export enum AriaRole { - NONE = 'presentation', - BUTTON = 'button', - TOOLBAR = 'toolbar' -} diff --git a/src/main/resources/assets/admin/common/js/ui/WCAG.ts b/src/main/resources/assets/admin/common/js/ui/WCAG.ts index de30e2f14..287e6f2b7 100644 --- a/src/main/resources/assets/admin/common/js/ui/WCAG.ts +++ b/src/main/resources/assets/admin/common/js/ui/WCAG.ts @@ -1,3 +1,15 @@ -export interface WCAG { - applyWCAGAttributes(): void; +export const IWCAG = Symbol('IWCAG'); + +export interface IWCAG { + [IWCAG]: boolean; + ariaLabel?: string; + role?: AriaRole; + tabbable?: boolean; +} + +export enum AriaRole { + NONE = 'presentation', + BANNER = 'banner', + BUTTON = 'button', + TOOLBAR = 'toolbar' } From e3eeaa084fece670ef085f1f3513551bb57d8dc8 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Fri, 5 Jul 2024 16:28:13 +0200 Subject: [PATCH 06/23] Accessibility: Toolbar #3626 --- .../admin/common/js/ui/toolbar/Toolbar.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 547978ab5..083ff168b 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -4,15 +4,20 @@ import {DivEl} from '../../dom/DivEl'; import {ActionContainer} from '../ActionContainer'; import {Action} from '../Action'; import {ResponsiveManager} from '../responsive/ResponsiveManager'; -import {Element, AriaRole} from '../../dom/Element'; +import {Element} from '../../dom/Element'; import {ObjectHelper} from '../../ObjectHelper'; import {FoldButton} from './FoldButton'; -import {WCAG} from '../WCAG'; +import {IWCAG as WCAG, AriaRole} from '../WCAG'; export class Toolbar extends DivEl implements ActionContainer, WCAG { + [WCAG]: boolean = true; + ariaLabel: string = i18n('wcag.toolbar.label'); + role: AriaRole = AriaRole.TOOLBAR; + tabbable: boolean = true; + protected foldButton: FoldButton; protected actions: Action[] = []; private locked: boolean; @@ -21,8 +26,6 @@ export class Toolbar constructor(className?: string) { super(!className ? 'toolbar' : className + ' toolbar'); - this.applyWCAGAttributes(); - this.foldButton = new FoldButton(); this.foldButton.hide(); this.appendChild(this.foldButton); @@ -33,16 +36,6 @@ export class Toolbar this.onShown(() => this.foldOrExpand()); } - applyWCAGAttributes(): void { - this.setRole(AriaRole.TOOLBAR) - .makeTabbable() - .setAriaLabel(this.getAriaLabel()); - } - - protected getAriaLabel(): string { - return i18n('wcag.toolbar.label'); - } - addAction(action: Action): ActionButton { this.actions.push(action); From 36f1deaeb54b06393a42563d07bccb27dd6d67cb Mon Sep 17 00:00:00 2001 From: alansemenov Date: Fri, 5 Jul 2024 16:28:33 +0200 Subject: [PATCH 07/23] Accessibility: AppBar #3638 --- .../resources/assets/admin/common/js/app/bar/AppBar.ts | 8 +++++++- src/main/resources/i18n/common.properties | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/resources/assets/admin/common/js/app/bar/AppBar.ts b/src/main/resources/assets/admin/common/js/app/bar/AppBar.ts index 1a531c2f8..97c741ac1 100644 --- a/src/main/resources/assets/admin/common/js/app/bar/AppBar.ts +++ b/src/main/resources/assets/admin/common/js/app/bar/AppBar.ts @@ -6,10 +6,16 @@ import {ShowAppLauncherAction} from './ShowAppLauncherAction'; import {AppIcon} from './AppIcon'; import {AppBarActions} from './AppBarActions'; import {Application} from '../Application'; +import {IWCAG as WCAG, AriaRole} from '../../ui/WCAG'; +import {i18n} from '../../util/Messages'; export class AppBar extends DivEl - implements ActionContainer { + implements ActionContainer, WCAG { + + [WCAG]: boolean = true; + ariaLabel: string = i18n('wcag.appbar.label'); + role: AriaRole = AriaRole.BANNER; protected application: Application; diff --git a/src/main/resources/i18n/common.properties b/src/main/resources/i18n/common.properties index ff8d2f780..85d5764cc 100644 --- a/src/main/resources/i18n/common.properties +++ b/src/main/resources/i18n/common.properties @@ -218,4 +218,5 @@ warning.optionsview.truncated = The list is truncated # # Accessibility # +wcag.appbar.label=Header wcag.toolbar.label=Main menu bar From 2c09db83352fda5fc6456f71de5d0d5fee602dd4 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Wed, 10 Jul 2024 12:24:12 +0200 Subject: [PATCH 08/23] Support aria-haspopup attribute on Element #3650 --- .../assets/admin/common/js/dom/Element.ts | 20 ++++++++++++++++--- .../assets/admin/common/js/ui/WCAG.ts | 14 +++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index 198ef864a..35c0aacd4 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -15,7 +15,7 @@ import {ElementRegistry} from './ElementRegistry'; import {assert, assertNotNull, assertState} from '../util/Assert'; import {ElementEvent} from './ElementEvent'; import * as DOMPurify from 'dompurify'; -import {IWCAG as WCAG, AriaRole} from '../ui/WCAG'; +import {IWCAG as WCAG, AriaRole, AriaHasPopup} from '../ui/WCAG'; export interface PurifyConfig { addTags?: string[]; @@ -244,6 +244,7 @@ export class Element { this['tabbable'] && this.makeTabbable(); this['role'] && this.setRole(this['role']); this['ariaLabel'] && this.setAriaLabel(this['ariaLabel']); + this['ariaHasPopup'] && this.setAriaHasPopup(this['ariaHasPopup']); } private implementsWCAG(): boolean { @@ -590,16 +591,29 @@ export class Element { return this.removeAriaAttribute('disabled'); } - private isAriaRole(value: any): value is AriaRole { + private isAriaRole(value: string | AriaRole): value is AriaRole { return Object.keys(AriaRole).some(key => (AriaRole as any)[key] === value); } + private isAriaHasPopup(value: string | AriaHasPopup): value is AriaHasPopup { + return Object.keys(AriaHasPopup).some(key => (AriaHasPopup as any)[key] === value); + } + setRole(value: string | AriaRole): Element { let role = AriaRole.NONE; if (this.isAriaRole(value)) { role = value; } - this.getEl().setAttribute('role', role); + this.getEl().setAttribute('role', role.toLowerCase()); + return this; + } + + setAriaHasPopup(value?: string | AriaHasPopup): Element { + let hasPopup = AriaHasPopup.TRUE; + if (this.isAriaHasPopup(value)) { + hasPopup = value; + } + this.setAriaAttribute('haspopup', hasPopup.toLowerCase()); return this; } diff --git a/src/main/resources/assets/admin/common/js/ui/WCAG.ts b/src/main/resources/assets/admin/common/js/ui/WCAG.ts index 287e6f2b7..7b3090638 100644 --- a/src/main/resources/assets/admin/common/js/ui/WCAG.ts +++ b/src/main/resources/assets/admin/common/js/ui/WCAG.ts @@ -2,9 +2,10 @@ export const IWCAG = Symbol('IWCAG'); export interface IWCAG { [IWCAG]: boolean; - ariaLabel?: string; - role?: AriaRole; tabbable?: boolean; + role?: AriaRole; + ariaLabel?: string; + ariaHasPopup?: AriaHasPopup; } export enum AriaRole { @@ -13,3 +14,12 @@ export enum AriaRole { BUTTON = 'button', TOOLBAR = 'toolbar' } + +export enum AriaHasPopup { + TRUE = 'true', + MENU = 'menu', + LISTBOX = 'listbox', + TREE = 'tree', + GRID = 'grid', + DIALOG = 'dialog' +} From eb9e0c6ef0c06dc727c47b75389070210b73ab2e Mon Sep 17 00:00:00 2001 From: alansemenov Date: Fri, 12 Jul 2024 16:15:32 +0200 Subject: [PATCH 09/23] Accessibility: Toolbar navigation #3642 --- .../admin/common/js/app/browse/BrowsePanel.ts | 11 +- .../common/js/app/view/ItemPreviewPanel.ts | 9 +- .../common/js/app/view/ItemPreviewToolbar.ts | 12 +- .../admin/common/js/app/view/ItemViewPanel.ts | 6 +- .../admin/common/js/app/wizard/WizardPanel.ts | 8 +- .../wizard/WizardStepNavigatorAndToolbar.ts | 6 +- .../assets/admin/common/js/dom/Body.ts | 11 + .../assets/admin/common/js/dom/Element.ts | 6 +- .../assets/admin/common/js/ui/Action.ts | 11 + .../assets/admin/common/js/ui/WCAG.ts | 3 +- .../admin/common/js/ui/button/ActionButton.ts | 24 +- .../common/js/ui/button/DropdownHandle.ts | 4 + .../admin/common/js/ui/button/MenuButton.ts | 22 + .../admin/common/js/ui/dialog/DialogButton.ts | 2 +- .../admin/common/js/ui/dialog/ModalDialog.ts | 1 + .../admin/common/js/ui/toolbar/FoldButton.ts | 10 +- .../admin/common/js/ui/toolbar/Toolbar.ts | 387 ++++++++++++++---- .../common/styles/api/ui/button/button.less | 8 +- .../styles/api/ui/button/dropdown-handle.less | 1 - .../styles/api/ui/button/menu-button.less | 4 +- .../common/styles/api/ui/fold-button.less | 13 +- .../styles/api/ui/time/date-time-picker.less | 15 +- .../api/ui/time/date-time-range-picker.less | 5 +- .../common/styles/api/ui/toggle-slide.less | 64 --- .../admin/common/styles/api/ui/toolbar.less | 42 +- .../assets/admin/common/styles/global.less | 12 + .../assets/admin/common/styles/main.less | 1 - .../assets/admin/common/styles/mixins.less | 14 +- src/main/resources/i18n/common.properties | 1 + 29 files changed, 487 insertions(+), 226 deletions(-) delete mode 100644 src/main/resources/assets/admin/common/styles/api/ui/toggle-slide.less diff --git a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts index c80c3b613..14ebdba56 100644 --- a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts +++ b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts @@ -6,7 +6,7 @@ import {ActionButton} from '../../ui/button/ActionButton'; import {TreeGridActions} from '../../ui/treegrid/actions/TreeGridActions'; import {SplitPanel, SplitPanelAlignment, SplitPanelBuilder} from '../../ui/panel/SplitPanel'; import {Panel} from '../../ui/panel/Panel'; -import {Toolbar} from '../../ui/toolbar/Toolbar'; +import {Toolbar, ToolbarConfig} from '../../ui/toolbar/Toolbar'; import {TreeGrid} from '../../ui/treegrid/TreeGrid'; import {BrowseFilterPanel} from './filter/BrowseFilterPanel'; import {Action} from '../../ui/Action'; @@ -23,7 +23,7 @@ import {SplitPanelSize} from '../../ui/panel/SplitPanelSize'; export class BrowsePanel extends Panel { - protected browseToolbar: Toolbar; + protected browseToolbar: Toolbar; protected treeGrid: TreeGrid; @@ -223,7 +223,7 @@ export class BrowsePanel this.treeGrid.resetFilter(); } - protected createToolbar(): Toolbar { + protected createToolbar(): Toolbar { throw Error('Must be implemented by inheritors'); } @@ -306,10 +306,9 @@ export class BrowsePanel } private addToggleFilterPanelButtonInToolbar() { - this.toggleFilterPanelAction = new ToggleFilterPanelAction(this); - this.toggleFilterPanelButton = new ActionButton(this.toggleFilterPanelAction); + this.toggleFilterPanelAction = new ToggleFilterPanelAction(this).setFoldable(false); + this.toggleFilterPanelButton = this.browseToolbar.prependAction(this.toggleFilterPanelAction); this.toggleFilterPanelButton.setTitle(i18n('tooltip.filterPanel.show')); - this.browseToolbar.prependChild(this.toggleFilterPanelButton); this.toggleFilterPanelAction.setVisible(false); } diff --git a/src/main/resources/assets/admin/common/js/app/view/ItemPreviewPanel.ts b/src/main/resources/assets/admin/common/js/app/view/ItemPreviewPanel.ts index c1cd3c753..733fa7a70 100644 --- a/src/main/resources/assets/admin/common/js/app/view/ItemPreviewPanel.ts +++ b/src/main/resources/assets/admin/common/js/app/view/ItemPreviewPanel.ts @@ -4,15 +4,16 @@ import {IFrameEl} from '../../dom/IFrameEl'; import {Panel} from '../../ui/panel/Panel'; import {Equitable} from '../../Equitable'; import {ItemPreviewToolbar} from './ItemPreviewToolbar'; +import {ToolbarConfig} from '../../ui/toolbar/Toolbar'; -export class ItemPreviewPanel +export class ItemPreviewPanel extends Panel { protected frame: IFrameEl; protected wrapper: DivEl; - protected toolbar: ItemPreviewToolbar; + protected toolbar: ItemPreviewToolbar; protected mask: LoadMask; @@ -26,8 +27,8 @@ export class ItemPreviewPanel this.appendChildren(this.toolbar, this.wrapper, this.mask); } - createToolbar(): ItemPreviewToolbar { - return new ItemPreviewToolbar(); + createToolbar(): ItemPreviewToolbar { + return new ItemPreviewToolbar(); } public showMask() { diff --git a/src/main/resources/assets/admin/common/js/app/view/ItemPreviewToolbar.ts b/src/main/resources/assets/admin/common/js/app/view/ItemPreviewToolbar.ts index 5fea1e179..0b6d2112b 100644 --- a/src/main/resources/assets/admin/common/js/app/view/ItemPreviewToolbar.ts +++ b/src/main/resources/assets/admin/common/js/app/view/ItemPreviewToolbar.ts @@ -1,13 +1,15 @@ import {Equitable} from '../../Equitable'; -import {Toolbar} from '../../ui/toolbar/Toolbar'; +import {Toolbar, ToolbarConfig} from '../../ui/toolbar/Toolbar'; -export class ItemPreviewToolbar - extends Toolbar { +export class ItemPreviewToolbar + extends Toolbar { private item: M; - constructor(className?: string) { - super('item-preview-toolbar' + (className ? ' ' + className : '')); + constructor(config?: C) { + super(config); + + this.addClass('item-preview-toolbar'); } setItem(item: M) { diff --git a/src/main/resources/assets/admin/common/js/app/view/ItemViewPanel.ts b/src/main/resources/assets/admin/common/js/app/view/ItemViewPanel.ts index 334e35fb5..0c288e314 100644 --- a/src/main/resources/assets/admin/common/js/app/view/ItemViewPanel.ts +++ b/src/main/resources/assets/admin/common/js/app/view/ItemViewPanel.ts @@ -1,6 +1,6 @@ import {Panel} from '../../ui/panel/Panel'; import {Closeable} from '../../ui/Closeable'; -import {Toolbar} from '../../ui/toolbar/Toolbar'; +import {Toolbar, ToolbarConfig} from '../../ui/toolbar/Toolbar'; import {Action} from '../../ui/Action'; import {ItemViewClosedEvent} from './ItemViewClosedEvent'; import {ViewItem} from './ViewItem'; @@ -9,7 +9,7 @@ export class ItemViewPanel extends Panel implements Closeable { - private toolbar: Toolbar; + private toolbar: Toolbar; private panel: Panel; @@ -21,7 +21,7 @@ export class ItemViewPanel super('item-view-panel'); } - setToolbar(toolbar: Toolbar) { + setToolbar(toolbar: Toolbar) { this.toolbar = toolbar; this.appendChild(this.toolbar); } diff --git a/src/main/resources/assets/admin/common/js/app/wizard/WizardPanel.ts b/src/main/resources/assets/admin/common/js/app/wizard/WizardPanel.ts index 09dc354ac..c2ccd7bc7 100644 --- a/src/main/resources/assets/admin/common/js/app/wizard/WizardPanel.ts +++ b/src/main/resources/assets/admin/common/js/app/wizard/WizardPanel.ts @@ -1,5 +1,5 @@ import * as Q from 'q'; -import {Toolbar} from '../../ui/toolbar/Toolbar'; +import {Toolbar, ToolbarConfig} from '../../ui/toolbar/Toolbar'; import {ResponsiveManager} from '../../ui/responsive/ResponsiveManager'; import {ResponsiveItem} from '../../ui/responsive/ResponsiveItem'; import {Panel} from '../../ui/panel/Panel'; @@ -47,7 +47,7 @@ export class WizardPanel protected params: WizardPanelParams; protected wizardActions: WizardActions; protected wizardHeader: WizardHeader; - protected mainToolbar: Toolbar; + protected mainToolbar: Toolbar; protected formIcon: Element; protected formMask: LoadMask; protected liveMask: LoadMask; @@ -249,7 +249,7 @@ export class WizardPanel }); } - public getMainToolbar(): Toolbar { + public getMainToolbar(): Toolbar { return this.mainToolbar; } @@ -620,7 +620,7 @@ export class WizardPanel return this.dataLoaded; } - protected createMainToolbar(): Toolbar { + protected createMainToolbar(): Toolbar { return null; } diff --git a/src/main/resources/assets/admin/common/js/app/wizard/WizardStepNavigatorAndToolbar.ts b/src/main/resources/assets/admin/common/js/app/wizard/WizardStepNavigatorAndToolbar.ts index 943e893cd..805fd65f8 100644 --- a/src/main/resources/assets/admin/common/js/app/wizard/WizardStepNavigatorAndToolbar.ts +++ b/src/main/resources/assets/admin/common/js/app/wizard/WizardStepNavigatorAndToolbar.ts @@ -1,5 +1,5 @@ import * as Q from 'q'; -import {Toolbar} from '../../ui/toolbar/Toolbar'; +import {Toolbar, ToolbarConfig} from '../../ui/toolbar/Toolbar'; import {TabBarItem} from '../../ui/tab/TabBarItem'; import {ActivatedEvent} from '../../ui/ActivatedEvent'; import {DivEl} from '../../dom/DivEl'; @@ -15,13 +15,13 @@ export class WizardStepNavigatorAndToolbar private foldButton: FoldButton; - private stepToolbar: Toolbar; + private stepToolbar: Toolbar; private stepNavigator: WizardStepNavigator; private helpTextToggleButton: DivEl; - constructor(stepNavigator: WizardStepNavigator, stepToolbar?: Toolbar) { + constructor(stepNavigator: WizardStepNavigator, stepToolbar?: Toolbar) { super('wizard-step-navigator-and-toolbar'); this.stepNavigator = stepNavigator; this.stepToolbar = stepToolbar; diff --git a/src/main/resources/assets/admin/common/js/dom/Body.ts b/src/main/resources/assets/admin/common/js/dom/Body.ts index 77796bb25..c1979c8cd 100644 --- a/src/main/resources/assets/admin/common/js/dom/Body.ts +++ b/src/main/resources/assets/admin/common/js/dom/Body.ts @@ -11,6 +11,8 @@ export class Body private childrenLoaded: boolean; + private focusedElement: Element; + constructor(loadExistingChildren: boolean = false, body?: HTMLElement) { if (!body) { body = document.body; @@ -41,6 +43,15 @@ export class Body } } + reapplyFocus() { + this.focusedElement?.giveFocus(); + this.focusedElement = null; + } + + setFocusedElement(element: Element) { + this.focusedElement = element; + } + static get(): Body { let instance: Body = Store.instance().get(BODY_KEY); diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index 35c0aacd4..5b365c152 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -718,10 +718,14 @@ export class Element { return this.insertChildElement(this, existing.getParentElement(), existingIndex); } - hasChild(child: Element) { + hasChild(child: Element): boolean { return this.children.indexOf(child) > -1; } + hasParent(): boolean { + return !!this.parentElement; + } + removeChild(child: Element): Element { assertNotNull(child, 'Child element to remove cannot be null'); diff --git a/src/main/resources/assets/admin/common/js/ui/Action.ts b/src/main/resources/assets/admin/common/js/ui/Action.ts index f375d9588..5baaad1d4 100644 --- a/src/main/resources/assets/admin/common/js/ui/Action.ts +++ b/src/main/resources/assets/admin/common/js/ui/Action.ts @@ -24,6 +24,8 @@ export class Action { private visible: boolean = true; + private foldable: boolean = true; + private executionListeners: ExecutionListener[] = []; private propertyChangedListeners: ((action: Action) => void)[] = []; @@ -197,6 +199,15 @@ export class Action { return this.mnemonic; } + isFoldable(): boolean { + return this.foldable; + } + + setFoldable(value: boolean): Action { + this.foldable = value; + return this; + } + execute(forceExecute: boolean = false): void { if (this.enabled) { this.notifyBeforeExecute(); diff --git a/src/main/resources/assets/admin/common/js/ui/WCAG.ts b/src/main/resources/assets/admin/common/js/ui/WCAG.ts index 7b3090638..6c04ca685 100644 --- a/src/main/resources/assets/admin/common/js/ui/WCAG.ts +++ b/src/main/resources/assets/admin/common/js/ui/WCAG.ts @@ -12,7 +12,8 @@ export enum AriaRole { NONE = 'presentation', BANNER = 'banner', BUTTON = 'button', - TOOLBAR = 'toolbar' + TOOLBAR = 'toolbar', + MENU = 'menu', } export enum AriaHasPopup { diff --git a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts index 677e706df..dd968e1c5 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts @@ -4,6 +4,9 @@ import {Action} from '../Action'; import {Tooltip} from '../Tooltip'; import {BrowserHelper} from '../../BrowserHelper'; import {KeyBindingAction} from '../KeyBinding'; +import {IWCAG} from '../WCAG'; +import {KeyHelper} from '../KeyHelper'; +import {Body} from '../../dom/Body'; export class ActionButton extends Button { @@ -14,7 +17,7 @@ export class ActionButton private iconClass: string; - constructor(action: Action, showTooltip: boolean = true) { + constructor(action: Action, wcag?: IWCAG) { super(); this.action = action; @@ -24,12 +27,17 @@ export class ActionButton this.addClass(action.getClass()); } + if (wcag) { + wcag.role && this.setRole(wcag.role); + wcag.ariaHasPopup && this.setAriaHasPopup(wcag.ariaHasPopup); + } + this.setEnabled(this.action.isEnabled()); this.setVisible(this.action.isVisible()); this.updateIconClass(this.action.getIconClass()); - if (this.action.hasShortcut() && showTooltip) { + if (this.action.hasShortcut()) { let combination = this.action.getShortcut().getCombination(); if (combination) { combination = combination.replace(/mod\+/i, BrowserHelper.isOSX() || BrowserHelper.isIOS() ? 'cmd+' : 'ctrl+'); @@ -46,16 +54,22 @@ export class ActionButton }); } + this.onKeyDown((event: KeyboardEvent) => KeyHelper.isEnterKey(event) && this.action.execute()); this.onClicked(() => this.action.execute()); + this.action.onExecuted(() => { + Body.get().setFocusedElement(this); + }); + this.action.onPropertyChanged((changedAction: Action) => { const toggledEnabled = this.isEnabled() !== changedAction.isEnabled(); - const becameHidden = !changedAction.isVisible() && this.isVisible(); + const toggledVisible = this.isVisible() !== changedAction.isVisible(); + const becameHidden = toggledVisible && !changedAction.isVisible(); if (this.tooltip && (toggledEnabled || becameHidden)) { this.tooltip.hide(); } - this.setEnabled(changedAction.isEnabled()); - this.setVisible(changedAction.isVisible()); + toggledEnabled && this.setEnabled(changedAction.isEnabled()); + toggledVisible && this.setVisible(changedAction.isVisible()); this.setLabel(this.createLabel(changedAction), false); this.updateIconClass(changedAction.getIconClass()); }); diff --git a/src/main/resources/assets/admin/common/js/ui/button/DropdownHandle.ts b/src/main/resources/assets/admin/common/js/ui/button/DropdownHandle.ts index 74be7cd5e..94a9153e2 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/DropdownHandle.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/DropdownHandle.ts @@ -1,7 +1,11 @@ import {ButtonEl} from '../../dom/ButtonEl'; +import {AriaRole, IWCAG as WCAG} from '../WCAG'; export class DropdownHandle extends ButtonEl { + [WCAG]: boolean = true; + role: AriaRole = AriaRole.BUTTON; + tabbable: boolean = true; constructor() { super('dropdown-handle'); diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index b76df3026..5ab20b5fb 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -7,6 +7,7 @@ import {ActionButton} from './ActionButton'; import {Body} from '../../dom/Body'; import {Element} from '../../dom/Element'; import * as Q from 'q'; +import {AriaRole, IWCAG as WCAG} from '../WCAG'; export enum MenuButtonDropdownPos { LEFT, RIGHT @@ -15,6 +16,10 @@ export enum MenuButtonDropdownPos { export class MenuButton extends DivEl { + [WCAG]: boolean = true; + role: AriaRole = AriaRole.NONE; + tabbable: boolean = true; + protected readonly mainAction: Action; protected readonly menuActions: Action[]; @@ -54,6 +59,10 @@ export class MenuButton this.initActions(this.menuActions); } + protected getActiveActionButton(): ActionButton { + return this.getActionButton(); + } + getActionButton(): ActionButton { return this.actionButton; } @@ -113,6 +122,7 @@ export class MenuButton } setDropdownHandleEnabled(enabled: boolean = true): void { + //this.setRole(enabled ? AriaRole.MENU : AriaRole.BUTTON); this.dropdownHandle.setEnabled(enabled); if (!enabled) { this.collapseMenu(); @@ -180,6 +190,7 @@ export class MenuButton private initActionButton(action: Action): void { this.actionButton = new ActionButton(action); + this.actionButton.setAriaHasPopup(); } private initActions(actions: Action[]): void { @@ -235,6 +246,17 @@ export class MenuButton }); this.menu.onClicked(() => this.dropdownHandle.giveFocus()); + + this.onFocus(() => { + const activeButton = this.getActiveActionButton(); + if (activeButton) { + console.log('Giving focus to ' + activeButton); + console.log(activeButton.giveFocus()); + } else { + this.dropdownHandle.isEnabled() && console.log('Giving focus to ' + this.dropdownHandle); + this.dropdownHandle.isEnabled() && this.dropdownHandle.giveFocus(); + } + }); } doRender(): Q.Promise { diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/DialogButton.ts b/src/main/resources/assets/admin/common/js/ui/dialog/DialogButton.ts index 42b394319..47e4ebeab 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/DialogButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/DialogButton.ts @@ -5,7 +5,7 @@ export class DialogButton extends ActionButton { constructor(action: Action) { - super(action, false); + super(action); this.addClass('dialog-button'); } } diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts b/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts index f9d63245f..2254d1808 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts @@ -382,6 +382,7 @@ export abstract class ModalDialog DialogManagerInner.get().handleClosedDialog(this); this.notifyClosed(); + Body.get().reapplyFocus(); } hide() { diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts index c353f6eac..5b9ec9ab7 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts @@ -5,9 +5,17 @@ import {Element} from '../../dom/Element'; import {StyleHelper} from '../../StyleHelper'; import {Body} from '../../dom/Body'; import {BrowserHelper} from '../../BrowserHelper'; +import {AriaHasPopup, AriaRole, IWCAG as WCAG} from '../WCAG'; export class FoldButton - extends DivEl { + extends DivEl + implements WCAG { + + [WCAG]: boolean = true; + ariaLabel: string = i18n('wcag.toolbar.foldButton'); + role: AriaRole = AriaRole.BUTTON; + ariaHasPopup: AriaHasPopup.MENU; + tabbable: boolean = true; private static expandedCls: string = 'expanded'; private span: SpanEl; diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 083ff168b..53243cc1e 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -5,11 +5,22 @@ import {ActionContainer} from '../ActionContainer'; import {Action} from '../Action'; import {ResponsiveManager} from '../responsive/ResponsiveManager'; import {Element} from '../../dom/Element'; -import {ObjectHelper} from '../../ObjectHelper'; import {FoldButton} from './FoldButton'; import {IWCAG as WCAG, AriaRole} from '../WCAG'; +import {KeyHelper} from '../KeyHelper'; +import {Body} from '../../dom/Body'; -export class Toolbar +interface ActionElement { + element: Element; + action?: Action; + folded?: boolean; +} + +export interface ToolbarConfig { + className?: string; +} + +export class Toolbar extends DivEl implements ActionContainer, WCAG { @@ -18,156 +29,386 @@ export class Toolbar role: AriaRole = AriaRole.TOOLBAR; tabbable: boolean = true; + protected config: C; protected foldButton: FoldButton; - protected actions: Action[] = []; + protected actionElements: ActionElement[] = []; private locked: boolean; private hasGreedySpacer: boolean; + private lastFocusedActionIndex = -1; + private lastActionIndex= -1; + + private visibleButtonsWidth = 0; + + private mouseClickListener: (event: MouseEvent) => void; + + constructor(config?: C) { + super(config?.className ?? undefined); + + this.addClass('toolbar'); - constructor(className?: string) { - super(!className ? 'toolbar' : className + ' toolbar'); + this.config = config; + this.initElements(); + this.initListeners(); + } - this.foldButton = new FoldButton(); - this.foldButton.hide(); - this.appendChild(this.foldButton); + protected initElements(): void { + // + } + protected initListeners(): void { // Hack: Update after styles are applied to evaluate the sizes correctly ResponsiveManager.onAvailableSizeChanged(this, () => window.setTimeout(this.foldOrExpand.bind(this))); - this.onShown(() => this.foldOrExpand()); + this.onFocus(() => { + this.focusToolbar(); + this.focusActionElement(); + }); } - addAction(action: Action): ActionButton { - this.actions.push(action); + private getFocusedActionElement(): Element { + if (this.lastFocusedActionIndex === -1) { + return null; + } - const actionButton: ActionButton = new ActionButton(action); + const lastFocusedActionElement = this.actionElements[this.lastFocusedActionIndex]; + if (lastFocusedActionElement.folded) { + return null; + } - action.onPropertyChanged(() => this.foldOrExpand()); - this.addElement(actionButton); + return lastFocusedActionElement.element; + } - return actionButton; + private focusToolbar() { + if (this.isFocused()) { + return; + } + + this.mouseClickListener = (event: MouseEvent) => { + if (!this.getEl().getHTMLElement().contains(event.target as Node)) { + this.removeFocus(); + } + }; + + this.addClassEx('focused'); + Body.get().onMouseDown(this.mouseClickListener); } - addActions(actions: Action[]) { - actions.forEach((action) => { - this.addAction(action); - }); + private removeFocus() { + this.getFocusedActionElement()?.giveBlur(); + this.removeClassEx('focused'); + Body.get().unMouseDown(this.mouseClickListener); + this.mouseClickListener = null; } - removeActions() { - this.actions.forEach((action: Action) => { - this.removeAction(action); + private isActionFocusable(actionElement: ActionElement): boolean { + return actionElement.action ? actionElement.action.isEnabled() && !actionElement.folded : actionElement.element.isVisible(); + } + + private getNextFocusableActionIndex(): number { + let focusIndex = this.lastFocusedActionIndex; + const currentFocusIndex = focusIndex; + const limit = this.actionElements.length; + + do { + focusIndex++; + if (focusIndex === limit) { + focusIndex = 0; + } + } while (focusIndex !== currentFocusIndex && !this.isActionFocusable(this.actionElements[focusIndex])); + + if (focusIndex === currentFocusIndex) { + return -1; + } + + return focusIndex; + } + + private getPreviousFocusableActionIndex(): number { + let focusIndex = this.lastFocusedActionIndex; + const currentFocusIndex = focusIndex; + const lastIndex = this.actionElements.length - 1; + + do { + focusIndex--; + if (focusIndex === -1) { + focusIndex = lastIndex; + } + } while (focusIndex !== currentFocusIndex && !this.isActionFocusable(this.actionElements[focusIndex])); + + return focusIndex; + } + + private focusActionElement(): void { + let lastFocusedActionElement = this.getFocusedActionElement(); + if (!lastFocusedActionElement) { + const focusIndex = this.getNextFocusableActionIndex(); + this.lastFocusedActionIndex = focusIndex; + lastFocusedActionElement = this.actionElements[focusIndex].element; + } + + lastFocusedActionElement.giveFocus(); + } + + private createActionButton(action: Action): ActionButton { + action.isFoldable() && action.onPropertyChanged(() => this.foldOrExpand()); + return new ActionButton(action); + } + + private initElementListeners(element: Element) { + let eventHandled = false; + element.onKeyDown((event: KeyboardEvent) => { + if (KeyHelper.isTabKey(event) && !KeyHelper.isShiftKey(event)) { + eventHandled = true; + element.giveBlur(); + this.removeFocus(); + } else if (KeyHelper.isArrowRightKey(event)) { + eventHandled = true; + this.focusNextAction(); + } else if (KeyHelper.isArrowLeftKey(event)) { + eventHandled = true; + this.focusPreviousAction(); + } + + if (eventHandled) { + event.stopImmediatePropagation(); + event.preventDefault(); + return; + } + }); + + const onFocus = () => { + this.focusToolbar(); + this.lastFocusedActionIndex = this.getActionIndexByElement(element); + }; + + const onBlur = (event: FocusEvent) => { + // If newly focused element is not a part of the toolbar, remove focus from the toolbar + if (!this.getEl().getHTMLElement().contains(event.relatedTarget as Node)) { + this.removeFocus(); + } + }; + + element.onFocus(() => onFocus()); + element.onBlur((event: FocusEvent) => onBlur(event)); + element.whenRendered(() => { + element.getChildren().forEach((child: Element) => { + child.onFocus(() => onFocus()); + child.onBlur((event: FocusEvent) => onBlur(event)); + }); }); - this.actions = []; } - removeAction(action: Action): void { - this.getChildren().concat(this.foldButton.getDropdown().getChildren()).forEach((element: Element) => { - if (ObjectHelper.iFrameSafeInstanceOf(element, ActionButton)) { - if (action.getLabel() === (element as ActionButton).getLabel()) { - element.remove(); - this.actions = this.actions.filter((a: Action) => a !== action); - } + private isFocused(): boolean { + return this.hasClassEx('focused'); + } + + private focusAction(getActionIndex: () => number) { + const focusIndex = getActionIndex(); + if (focusIndex !== -1) { + if (!this.isFocused()) { + this.giveFocus(); } + this.lastFocusedActionIndex = focusIndex; + this.actionElements[focusIndex].element.giveFocus(); + } + } + + private focusNextAction() { + this.focusAction(() => this.getNextFocusableActionIndex()); + } + + private focusPreviousAction() { + this.focusAction(() => this.getPreviousFocusableActionIndex()); + } + + addActionElement(element: Element): Element { + if (this.foldButton) { + this.addGreedySpacer(); + } + this.addElement(element); + this.actionElements.push({ + element: element }); + return element; } - getActions(): Action[] { - return this.actions; + private addFoldButton() { + const foldButton = new FoldButton(); + foldButton.hide(); + + this.addActionElement(foldButton); + + this.foldButton = foldButton; + } + + addGreedySpacer() { + this.hasGreedySpacer = true; + } + + private getActionIndexByElement(element: Element): number { + return this.actionElements.findIndex((actionElement: ActionElement) => actionElement.element === element); } - addElement(element: Element): Element { + protected addElement(element: Element): Element { + this.initElementListeners(element); + if (this.hasGreedySpacer) { element.addClass('pull-right'); - element.insertAfterEl(this.foldButton); + this.appendChild(element); } else { - element.insertBeforeEl(this.foldButton); + if (this.foldButton?.hasParent()) { + element.insertBeforeEl(this.foldButton); + } else { + this.appendChild(element); + } } return element; } - addGreedySpacer() { - this.hasGreedySpacer = true; + prependAction(action: Action): ActionButton { + return this.addAction(action, true); + } + + addAction(action: Action, prepend: boolean = false): ActionButton { + if (!this.foldButton) { + this.addFoldButton(); + } + + const actionButton = this.createActionButton(action); + + this.actionElements.splice(prepend ? 0 : this.lastActionIndex + 1,0,{ + element: actionButton, + action: action + }); + this.addElement(actionButton); + this.lastActionIndex++; + + return actionButton; + } + + addActions(actions: Action[]) { + actions.forEach((action) => this.addAction(action)); + } + + removeActions() { + this.actionElements.forEach((actionElement: ActionElement) => !!actionElement.action && this.removeAction(actionElement.action)); + } + + removeAction(action: Action): void { + const indexToRemove = this.actionElements.findIndex((a: ActionElement) => a.action === action); + if (indexToRemove === -1) { + return; + } + this.actionElements[indexToRemove].element.remove(); + this.actionElements.slice(indexToRemove, 1); + } + + getActions(): Action[] { + return this.actionElements + .filter((actionElement: ActionElement) => actionElement.action ?? false) + .map((actionElement: ActionElement) => actionElement.action); } removeGreedySpacer() { this.hasGreedySpacer = false; } + private updateVisibleButtonsWidth() { + this.visibleButtonsWidth = this.getVisibleButtonsWidth(); + } + protected foldOrExpand() { if (!this.isRendered() || !this.isVisible() || this.locked) { return; } - if (this.getToolbarWidth() <= this.getVisibleButtonsWidth()) { + let foldChanged = false; + + if (!this.visibleButtonsWidth) { + this.updateVisibleButtonsWidth(); + } + + if (this.getToolbarWidth() <= this.visibleButtonsWidth) { this.fold(); - } else { - this.expand(); + foldChanged = true; + } else if (!this.foldButton.isEmpty()) { + this.expand(); + foldChanged = true; } - this.updateFoldButtonLabel(); + foldChanged && this.updateFoldButtonLabel(); } fold(force: boolean = false): void { const toolbarWidth: number = this.getToolbarWidth(); let nextFoldableButton: Element = this.getNextFoldableButton(); + let visibleButtonsWidth = this.visibleButtonsWidth; - while (nextFoldableButton && (force || toolbarWidth <= this.getVisibleButtonsWidth())) { - const buttonWidth: number = nextFoldableButton.getEl().getWidthWithMargin(); + while (nextFoldableButton && (force || toolbarWidth <= visibleButtonsWidth)) { + const buttonWidth: number = nextFoldableButton.getEl().getWidthWithBorder(); this.removeChild(nextFoldableButton); this.foldButton.push(nextFoldableButton, buttonWidth); - - if (!this.foldButton.isVisible()) { - this.foldButton.show(); - } + this.actionElements[this.lastActionIndex].folded = true; + visibleButtonsWidth -= buttonWidth; nextFoldableButton = this.getNextFoldableButton(); + this.lastActionIndex--; } - } - protected getToolbarWidth(): number { - return this.getEl().getWidthWithoutPadding(); + if (!this.foldButton.isEmpty() && !this.foldButton.isVisible()) { + this.foldButton.show(); + } + + this.updateVisibleButtonsWidth(); } expand(): void { const toolbarWidth: number = this.getToolbarWidth(); + const foldButtonWidth: number = this.foldButton.getButtonsCount() > 1 ? 0 : this.foldButton.getEl().getWidthWithBorder(); + let visibleButtonsWidth = this.visibleButtonsWidth; // if fold has 1 child left then subtract fold button width because it will be hidden - while (!this.foldButton.isEmpty() && - (this.getVisibleButtonsWidth(this.foldButton.getButtonsCount() > 1) + this.foldButton.getNextButtonWidth() < toolbarWidth)) { + while (!this.foldButton.isEmpty() && visibleButtonsWidth + this.foldButton.getNextButtonWidth() < toolbarWidth) { - let buttonToShow = this.foldButton.pop(); + const buttonToShow = this.foldButton.pop(); buttonToShow.insertBeforeEl(this.foldButton); + visibleButtonsWidth += buttonToShow.getEl().getWidthWithBorder(); + this.actionElements[this.lastActionIndex + 1].folded = false; - if (this.foldButton.isEmpty()) { - this.foldButton.hide(); + this.lastActionIndex++; + if (this.foldButton.getButtonsCount() === 1) { + visibleButtonsWidth -= foldButtonWidth; } } - } - private getVisibleButtonsWidth(includeFold: boolean = true): number { - return this.getChildren().reduce((totalWidth: number, element: Element) => { - return totalWidth + (element.isVisible() && (includeFold || element !== this.foldButton) ? - element.getEl().getWidthWithBorder() : 0); - }, 0); + if (this.foldButton.isEmpty() && this.foldButton.isVisible()) { + this.foldButton.hide(); + } + + this.updateVisibleButtonsWidth(); } - private getNextFoldableButton(): Element { - let button: Element = this.foldButton.getPreviousElement(); + protected getToolbarWidth(): number { + return this.getEl().getWidthWithoutPadding(); + } - while (button) { - if (this.isItemAllowedToFold(button)) { - return this.getChildren().filter((child) => child.getId() === button.getId())[0]; - } + private getVisibleButtonsWidth(): number { + return this.getChildren().reduce((totalWidth: number, element: Element) => + totalWidth + (element.isVisible() ? element.getEl().getWidthWithBorder() : 0), 0); + } - const prevEl: Element = button.getPreviousElement(); + private getNextFoldableButton(): Element { + let index = this.lastActionIndex; - if (prevEl && button.getParentElement() !== prevEl.getParentElement()) { - return null; + while (index >= 0) { + const button: Element = this.actionElements[index].element; + if (this.actionElements[index].action?.isFoldable() && this.isItemAllowedToFold(button)) { + return button; } - button = button.getPreviousElement(); + index--; } return null; @@ -178,7 +419,7 @@ export class Toolbar } private areAllActionsFolded(): boolean { - return this.actions.length === this.foldButton.getButtonsCount(); + return this.getActions().length === this.foldButton.getButtonsCount(); } setLocked(value: boolean): void { @@ -190,7 +431,7 @@ export class Toolbar } updateFoldButtonLabel(): void { - this.foldButton.setLabel(this.areAllActionsFolded() ? i18n('action.actions') : i18n('action.more')); + this.setFoldButtonLabel(this.areAllActionsFolded() ? i18n('action.actions') : i18n('action.more')); } setFoldButtonLabel(value: string): void { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/button/button.less b/src/main/resources/assets/admin/common/styles/api/ui/button/button.less index 0d3c3d67e..709fef97f 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/button/button.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/button/button.less @@ -1,7 +1,9 @@ -.menu-action-button() { - .button(@admin-font-gray2, transparent, @admin-font-gray3, transparent, false); +.menu-action-button(@padding: 5px 14px) { + .button(@admin-font-gray2, transparent, @admin-font-gray3, transparent, @padding); - height: 100%; + span { + .ellipsis(); + } &.pull-right { float: right; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less b/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less index 47bfe6d16..140d7576b 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less @@ -3,7 +3,6 @@ top: 0; right: 0; width: 37px; - height: 100%; border: 0; cursor: pointer; background-color: transparent; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less b/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less index 7ee3b0eb1..df32e8400 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less @@ -1,7 +1,6 @@ .menu-button { display: flex; position: relative; - height: 40px; &.hidden-dropdown { .@{_COMMON_PREFIX}dropdown-handle, .menu { @@ -61,8 +60,7 @@ .action-button { position: relative; - min-width: 126px; - text-align: center; + flex-wrap: nowrap; } &.transparent { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/fold-button.less b/src/main/resources/assets/admin/common/styles/api/ui/fold-button.less index 672bb40b4..f7da1ca3b 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/fold-button.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/fold-button.less @@ -1,7 +1,6 @@ .fold-button { - .button(@admin-font-gray2, transparent, @admin-font-gray3, transparent, false); + .button(@admin-font-gray2, transparent, @admin-font-gray3, transparent, 7px 25px 7px 14px); - padding: 0 20px 0 17px; height: 100%; text-align: center; overflow: visible; @@ -9,11 +8,14 @@ align-items: center; position: relative; // for the dropdown to position itself + .fold-label{ + line-height: 18px; + } + &::after { content: ""; position: absolute; - top: 42%; - right: 0; + right: 5px; width: 0; height: 0; border-left: 5px solid transparent; @@ -33,10 +35,9 @@ .material-layer-shadow(); .@{_COMMON_PREFIX}button { - .menu-action-button(); + .menu-action-button(5px); width: 100%; - padding: 5px; } & > * { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-picker.less b/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-picker.less index efd078e02..cdaa1074a 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-picker.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-picker.less @@ -11,15 +11,21 @@ text-align: center; .picker-buttons { + padding: 0 20px; display: flex; - justify-content: space-between; - - .picker-dialog-button(10px 20px 9px auto); + flex-direction: row; + flex-wrap: nowrap; + align-items: end; .ok-button { - display: inline-block; + .picker-dialog-button(); + } + + .default-button { + .picker-dialog-button(@form-button-bg, 0); } + /* .default-button { background-color: @form-button-bg; padding: 4px 19px 4px 19px; @@ -27,6 +33,7 @@ display: inline-block; margin: 10px 10px 9px 20px; } + */ } .notSelectable(); diff --git a/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-range-picker.less b/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-range-picker.less index fabae0545..e2f5a70f8 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-range-picker.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/time/date-time-range-picker.less @@ -7,7 +7,10 @@ .date-time-picker { .@{_COMMON_PREFIX}wrapper { .date-time-dialog { - .picker-dialog-button(10px 0 9px 20px, inline); + /* + .ok-button { + .picker-dialog-button(); + }*/ .time-picker-dialog, .time-picker-dialog > li:first-child { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/toggle-slide.less b/src/main/resources/assets/admin/common/styles/api/ui/toggle-slide.less deleted file mode 100644 index 9ff4929ca..000000000 --- a/src/main/resources/assets/admin/common/styles/api/ui/toggle-slide.less +++ /dev/null @@ -1,64 +0,0 @@ -.toggle-slide { - @toggle-height: 17px; - @toggle-side-padding: 6px; - @toggle-border-radius: 4px; - - position: relative; - display: inline-block; - white-space: nowrap; - cursor: pointer; - .notSelectable(); - - &.disabled { - opacity: 0.5; - cursor: default; - } - - .slider { - position: absolute; - top: 0; - height: @toggle-height; - background: linear-gradient(to top, #fff, #e5e5e5); - border-radius: @toggle-border-radius; - border: 1px solid #999; - } - - .holder { - height: @toggle-height; - overflow: hidden; - border-radius: @toggle-border-radius; - - .on, - .off { - display: inline-block; - overflow: hidden; - height: @toggle-height; - width: auto; - line-height: @toggle-height; - font-size: 12px; - font-weight: bold; - font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; - text-transform: uppercase; - vertical-align: top; - white-space: nowrap; - box-shadow: inset -2px 2px 10px -5px #000; - } - - .on { - padding-left: @toggle-side-padding; - padding-right: @toggle-border-radius + @toggle-side-padding; - color: #f7fafd; - text-shadow: 0 0 2px #364a5d; - background: #5ea2da; - } - - .off { - padding-left: @toggle-border-radius + @toggle-side-padding; - padding-right: @toggle-side-padding; - text-align: right; - color: #929496; - text-shadow: 1px 0 1px #fff; - background: #d5d6d6; - } - } -} diff --git a/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less b/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less index 583c30e3e..0a3b433b9 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less @@ -1,21 +1,19 @@ -.toolbar-button() { - .menu-action-button(); +.toolbar-button(@height: @toolbar-height) { + .menu-action-button(7px 14px); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100%; - line-height: normal; - padding: 0 14px; + flex-shrink: 0; - &.pull-right { - float: right; + span { + line-height: @height - 22px; } } .toolbar { display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + align-content: center; background-color: @admin-white; height: @toolbar-height; line-height: @toolbar-height; @@ -25,30 +23,18 @@ min-width: 200px; .fold-button { - border-right: 18px solid transparent; - .@{_COMMON_PREFIX}dropdown { top: @toolbar-height + 1; } } - > .@{_COMMON_PREFIX}button { - .toolbar-button(); + > .@{_COMMON_PREFIX}button.icon-search { + padding: 8px 10px; + margin-left: 5px; } - .toggle-slide { - margin: 11px 7px; - } - - .menu-button { - height: 100%; - } - - &.content-browse-toolbar, - &.user-browse-toolbar { - button { - vertical-align: top; - } + .@{_COMMON_PREFIX}button { + .toolbar-button(); } .progress-bar { diff --git a/src/main/resources/assets/admin/common/styles/global.less b/src/main/resources/assets/admin/common/styles/global.less index c323fc5ca..0fff7866c 100644 --- a/src/main/resources/assets/admin/common/styles/global.less +++ b/src/main/resources/assets/admin/common/styles/global.less @@ -39,3 +39,15 @@ textarea, select { border-radius: 0; } + +&.@{_CLS_PREFIX}focused, +:focus-visible { + outline: -webkit-focus-ring-color auto 1px; + outline: focus-ring auto 1px; + outline-offset: -1px; +} + +:focus { + outline-color: -webkit-focus-ring-color; + outline-color: focus-ring; +} diff --git a/src/main/resources/assets/admin/common/styles/main.less b/src/main/resources/assets/admin/common/styles/main.less index 2bf851a86..700f05415 100644 --- a/src/main/resources/assets/admin/common/styles/main.less +++ b/src/main/resources/assets/admin/common/styles/main.less @@ -23,7 +23,6 @@ @import "api/ui/fold-button"; @import "api/ui/tooltip"; @import "api/ui/progress-bar"; -@import "api/ui/toggle-slide"; @import "api/ui/text/text-input"; @import "api/ui/text/password-input"; @import "api/ui/text/email-input"; diff --git a/src/main/resources/assets/admin/common/styles/mixins.less b/src/main/resources/assets/admin/common/styles/mixins.less index 9f5843893..e0a01fba9 100644 --- a/src/main/resources/assets/admin/common/styles/mixins.less +++ b/src/main/resources/assets/admin/common/styles/mixins.less @@ -16,14 +16,12 @@ } } -.picker-dialog-button(@margin, @display: block) { - .ok-button { - background: @admin-button-blue2; - padding: 4px 19px 4px 19px; - position: relative; - display: @display; - margin: @margin; - } +.picker-dialog-button(@color: @admin-button-blue2, @margin-left: auto) { + background-color: @color; + //position: relative; + margin-left: @margin-left; + //display: @display; + //margin: @margin; } .date-picker-mixin(@width: 100%, @padding: 10px) { diff --git a/src/main/resources/i18n/common.properties b/src/main/resources/i18n/common.properties index 85d5764cc..32ce802f0 100644 --- a/src/main/resources/i18n/common.properties +++ b/src/main/resources/i18n/common.properties @@ -220,3 +220,4 @@ warning.optionsview.truncated = The list is truncated # wcag.appbar.label=Header wcag.toolbar.label=Main menu bar +wcag.toolbar.foldButton=Collapsed menu items From e08b220f5059051773f47602a4d5d93e1bc3b9fc Mon Sep 17 00:00:00 2001 From: alansemenov Date: Mon, 12 Aug 2024 15:37:52 +0200 Subject: [PATCH 10/23] Fixed misc issues --- .../assets/admin/common/styles/api/input-common.less | 9 ++++++++- .../common/styles/api/ui/selector/combobox/combobox.less | 8 ++++---- .../admin/common/styles/api/ui/uploader/uploader-el.less | 1 + .../resources/assets/admin/common/styles/mixins.less | 3 --- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/resources/assets/admin/common/styles/api/input-common.less b/src/main/resources/assets/admin/common/styles/api/input-common.less index bf2566db6..f11283be8 100644 --- a/src/main/resources/assets/admin/common/styles/api/input-common.less +++ b/src/main/resources/assets/admin/common/styles/api/input-common.less @@ -68,6 +68,13 @@ } } + .form-input { + + + .uploader-el .upload-button { + border-left: none; + } + } + .validation-block { display: flex; align-items: center; @@ -187,7 +194,7 @@ + .help-text { padding-right: 175px; } - + &.show-counter { .separator { display: inline; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less b/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less index d8bd2e723..35da0cf8f 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less @@ -8,6 +8,10 @@ margin-bottom: 10px; } + > .@{_COMMON_PREFIX}dropdown-handle { + height: 37px; + } + .input-icon { position: absolute; top: 5px; @@ -52,10 +56,6 @@ height: 100% !important; } - .grid { - width: 100% !important; - } - .options-container { .slick-header { height: 0; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/uploader/uploader-el.less b/src/main/resources/assets/admin/common/styles/api/ui/uploader/uploader-el.less index d091a144d..fdd1bb783 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/uploader/uploader-el.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/uploader/uploader-el.less @@ -17,6 +17,7 @@ cursor: pointer; box-sizing: border-box; border: 1px solid @admin-medium-gray-border; + padding: 0; &:hover { box-shadow: 0 0 5px @admin-input-blue; diff --git a/src/main/resources/assets/admin/common/styles/mixins.less b/src/main/resources/assets/admin/common/styles/mixins.less index e0a01fba9..84a162fde 100644 --- a/src/main/resources/assets/admin/common/styles/mixins.less +++ b/src/main/resources/assets/admin/common/styles/mixins.less @@ -18,10 +18,7 @@ .picker-dialog-button(@color: @admin-button-blue2, @margin-left: auto) { background-color: @color; - //position: relative; margin-left: @margin-left; - //display: @display; - //margin: @margin; } .date-picker-mixin(@width: 100%, @padding: 10px) { From f1c74ec27c69259b9ba5eea0887af64bc3ee7524 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Mon, 12 Aug 2024 21:36:07 +0200 Subject: [PATCH 11/23] Fixed Checkbox not getting checked --- src/main/resources/assets/admin/common/js/ui/Checkbox.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/assets/admin/common/js/ui/Checkbox.ts b/src/main/resources/assets/admin/common/js/ui/Checkbox.ts index cb00c0221..25e9cad91 100644 --- a/src/main/resources/assets/admin/common/js/ui/Checkbox.ts +++ b/src/main/resources/assets/admin/common/js/ui/Checkbox.ts @@ -24,6 +24,7 @@ export class Checkbox this.appendChild(this.checkbox); this.appendChild(this.label); + this.setChecked(checked || false, true); } static create(): CheckboxBuilder { From 6e802dad5a20436af6031d30294a5cd6f76b699f Mon Sep 17 00:00:00 2001 From: alansemenov Date: Mon, 12 Aug 2024 23:13:53 +0200 Subject: [PATCH 12/23] Fixed focus on modal dialog buttons --- .../assets/admin/common/js/ui/button/MenuButton.ts | 6 ++---- .../assets/admin/common/js/ui/dialog/DropdownButtonRow.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index 5ab20b5fb..7b7c8fa45 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -247,13 +247,11 @@ export class MenuButton this.menu.onClicked(() => this.dropdownHandle.giveFocus()); - this.onFocus(() => { + this.onFocus((event) => { const activeButton = this.getActiveActionButton(); if (activeButton) { - console.log('Giving focus to ' + activeButton); - console.log(activeButton.giveFocus()); + activeButton.giveFocus(); } else { - this.dropdownHandle.isEnabled() && console.log('Giving focus to ' + this.dropdownHandle); this.dropdownHandle.isEnabled() && this.dropdownHandle.giveFocus(); } }); diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts b/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts index 910e27bce..09579e37d 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts @@ -17,7 +17,7 @@ export class DropdownButtonRow this.actionMenu = new MenuButton(mainAction, menuActions); if (useDefault) { - this.setDefaultElement(this.actionMenu); + this.setDefaultElement(this.actionMenu.getActionButton()); } this.actionMenu.addClass('dropdown-dialog-menu'); From 200751eb0e245bdbc894da8a4063263e91619092 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Thu, 15 Aug 2024 16:58:19 +0200 Subject: [PATCH 13/23] Accessibility: Toolbar navigation #3642 --- .../admin/common/js/app/browse/BrowsePanel.ts | 10 ++- .../js/app/browse/filter/BrowseFilterPanel.ts | 7 ++ .../assets/admin/common/js/dom/Body.ts | 6 +- .../assets/admin/common/js/dom/Element.ts | 59 ++++++++++--- .../assets/admin/common/js/ui/Action.ts | 21 +++++ .../assets/admin/common/js/ui/WCAG.ts | 6 +- .../admin/common/js/ui/button/ActionButton.ts | 7 +- .../admin/common/js/ui/button/MenuButton.ts | 17 ++++ .../admin/common/js/ui/dialog/ModalDialog.ts | 2 + .../admin/common/js/ui/toolbar/Toolbar.ts | 87 +++++++++++++++---- .../api/app/browse/browse-filter-panel.less | 8 +- .../styles/api/app/names-and-icon-view.less | 12 +-- .../admin/common/styles/api/ui/toolbar.less | 1 - .../assets/admin/common/styles/reset.less | 1 + .../resources/i18n/common_wcag.properties | 3 + 15 files changed, 201 insertions(+), 46 deletions(-) create mode 100644 src/main/resources/i18n/common_wcag.properties diff --git a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts index 14ebdba56..c8455fff1 100644 --- a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts +++ b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts @@ -244,6 +244,7 @@ export class BrowsePanel } protected showFilterPanel() { + this.browseToolbar.giveBlur(); this.filterPanelForcedShown = true; this.filterPanelForcedHidden = false; @@ -267,7 +268,7 @@ export class BrowsePanel if (this.filterPanel.hasFilterSet()) { this.toggleFilterPanelButton.addClass('filtered'); } - + this.browseToolbar.giveFocus(); } private toggleSelectionMode(isActive: boolean) { @@ -298,7 +299,9 @@ export class BrowsePanel .setAnimationDelay(100) // filter panel animation time .build(); - this.filterPanel.onHideFilterPanelButtonClicked(this.toggleFilterPanel.bind(this)); + this.filterPanel.onHideFilterPanelButtonClicked(() => { + this.toggleFilterPanel(); + }); this.filterPanel.onShowResultsButtonClicked(this.toggleFilterPanel.bind(this)); this.addToggleFilterPanelButtonInToolbar(); @@ -307,6 +310,9 @@ export class BrowsePanel private addToggleFilterPanelButtonInToolbar() { this.toggleFilterPanelAction = new ToggleFilterPanelAction(this).setFoldable(false); + this.toggleFilterPanelAction.setWcagAttributes({ + ariaLabel: i18n('tooltip.filterPanel.show') + }); this.toggleFilterPanelButton = this.browseToolbar.prependAction(this.toggleFilterPanelAction); this.toggleFilterPanelButton.setTitle(i18n('tooltip.filterPanel.show')); this.toggleFilterPanelAction.setVisible(false); diff --git a/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts b/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts index be21d573e..54188be80 100644 --- a/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts +++ b/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts @@ -17,6 +17,7 @@ import {LabelEl} from '../../../dom/LabelEl'; import {ActionButton} from '../../../ui/button/ActionButton'; import {Action} from '../../../ui/Action'; import {DefaultErrorHandler} from '../../../DefaultErrorHandler'; +import {AriaRole} from '../../../ui/WCAG'; export class BrowseFilterPanel extends Panel { @@ -40,8 +41,14 @@ export class BrowseFilterPanel this.addClass('filter-panel'); this.hideFilterPanelButton = new SpanEl('hide-filter-panel-button icon-search'); + this.hideFilterPanelButton.applyWCAGAttributes({ + ariaLabel: i18n('tooltip.filterPanel.hide'), + role: AriaRole.BUTTON, + tabbable: true + }); this.hideFilterPanelButton.setTitle(i18n('tooltip.filterPanel.hide')); this.hideFilterPanelButton.onClicked(() => this.notifyHidePanelButtonPressed()); + this.hideFilterPanelButton.onEnterPressed(() => this.notifyHidePanelButtonPressed()); let showResultsButtonWrapper = new DivEl('show-filter-results'); this.showResultsButton = new SpanEl('show-filter-results-button'); diff --git a/src/main/resources/assets/admin/common/js/dom/Body.ts b/src/main/resources/assets/admin/common/js/dom/Body.ts index c1979c8cd..5a14d80c0 100644 --- a/src/main/resources/assets/admin/common/js/dom/Body.ts +++ b/src/main/resources/assets/admin/common/js/dom/Body.ts @@ -44,8 +44,10 @@ export class Body } reapplyFocus() { - this.focusedElement?.giveFocus(); - this.focusedElement = null; + setTimeout(() => { + this.focusedElement?.giveFocus(); + this.focusedElement = null; + }, 100); } setFocusedElement(element: Element) { diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index 5b365c152..1bc19bff7 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -15,7 +15,8 @@ import {ElementRegistry} from './ElementRegistry'; import {assert, assertNotNull, assertState} from '../util/Assert'; import {ElementEvent} from './ElementEvent'; import * as DOMPurify from 'dompurify'; -import {IWCAG as WCAG, AriaRole, AriaHasPopup} from '../ui/WCAG'; +import {IWCAG as WCAG, AriaRole, AriaHasPopup, IWCAG} from '../ui/WCAG'; +import {KeyHelper} from '../ui/KeyHelper'; export interface PurifyConfig { addTags?: string[]; @@ -236,15 +237,31 @@ export class Element { } } - applyWCAGAttributes(): void { - if (!this.implementsWCAG()) { + applyWCAGAttributes(attr?: IWCAG): void { + if (!ObjectHelper.isDefined(attr) && !this.implementsWCAG()) { return; } - this['tabbable'] && this.makeTabbable(); - this['role'] && this.setRole(this['role']); - this['ariaLabel'] && this.setAriaLabel(this['ariaLabel']); - this['ariaHasPopup'] && this.setAriaHasPopup(this['ariaHasPopup']); + if (ObjectHelper.isDefined(attr)) { + this[WCAG] = true; + if (ObjectHelper.isDefined(attr.tabbable)) { + this['tabbable'] = attr['tabbable']; + } + if (ObjectHelper.isDefined(attr.role)) { + this['role'] = attr['role']; + } + if (ObjectHelper.isDefined(attr.ariaLabel)) { + this['ariaLabel'] = attr['ariaLabel']; + } + if (ObjectHelper.isDefined(attr.ariaHasPopup)) { + this['ariaHasPopup'] = attr['ariaHasPopup']; + } + } + + ObjectHelper.isDefined(this['tabbable']) && this['tabbable'] ? this.makeTabbable() : this.removeTabbable(); + ObjectHelper.isDefined(this['role']) && this.setRole(this['role']); + ObjectHelper.isDefined(this['ariaLabel']) && this.setAriaLabel(this['ariaLabel']); + ObjectHelper.isDefined(this['ariaHasPopup']) && this.setAriaHasPopup(this['ariaHasPopup']); } private implementsWCAG(): boolean { @@ -579,6 +596,7 @@ export class Element { setAriaLabel(label: string): Element { if (StringHelper.isBlank(label)) { + this.removeAriaAttribute('label'); return this; } return this.setAriaAttribute('label', label); @@ -604,16 +622,27 @@ export class Element { if (this.isAriaRole(value)) { role = value; } - this.getEl().setAttribute('role', role.toLowerCase()); + StringHelper.isBlank(role.toLowerCase()) ? + this.getEl().removeAttribute('role') : + this.getEl().setAttribute('role', role.toLowerCase()); + return this; } setAriaHasPopup(value?: string | AriaHasPopup): Element { - let hasPopup = AriaHasPopup.TRUE; + let hasPopup = value ?? AriaHasPopup.TRUE; if (this.isAriaHasPopup(value)) { hasPopup = value; } - this.setAriaAttribute('haspopup', hasPopup.toLowerCase()); + StringHelper.isBlank(hasPopup.toLowerCase()) ? + this.getEl().removeAttribute('haspopup') : + this.setAriaAttribute('haspopup', hasPopup.toLowerCase()); + + return this; + } + + removeAriaHasPopup() { + this.removeAriaAttribute('haspopup'); return this; } @@ -1088,6 +1117,16 @@ export class Element { this.getEl().removeEventListener('DOMMouseScroll', listener); } + onEnterPressed(callback: () => void) { + this.onKeyDown((event: KeyboardEvent) => { + if (KeyHelper.isEnterKey(event)) { + callback(); + event.stopPropagation(); + event.preventDefault(); + } + }); + } + onClicked(listener: (event: MouseEvent) => void) { this.getEl().addEventListener('click', listener); } diff --git a/src/main/resources/assets/admin/common/js/ui/Action.ts b/src/main/resources/assets/admin/common/js/ui/Action.ts index 5baaad1d4..a83fee966 100644 --- a/src/main/resources/assets/admin/common/js/ui/Action.ts +++ b/src/main/resources/assets/admin/common/js/ui/Action.ts @@ -1,6 +1,8 @@ import * as Q from 'q'; import {KeyBinding} from './KeyBinding'; import {Mnemonic} from './Mnemonic'; +import {IWCAG} from './WCAG'; +import {ObjectHelper} from '../ObjectHelper'; type ExecutionListener = (action: Action) => Q.Promise | void; @@ -34,6 +36,8 @@ export class Action { private parentAction: Action; + private wcag?: IWCAG; + private sortOrder: number = 10; private beforeExecuteListeners: ((action: Action) => void)[] = []; @@ -141,6 +145,10 @@ export class Action { return this.visible; } + isFocusable(): boolean { + return this.isVisible() && this.isEnabled(); + } + setVisible(value: boolean): Action { if (value !== this.visible) { this.visible = value; @@ -149,6 +157,19 @@ export class Action { return this; } + setWcagAttributes(wcag: IWCAG): Action { + this.wcag = wcag; + return this; + } + + hasWcagAttributes(): boolean { + return ObjectHelper.isDefined(this.wcag); + } + + getWcagAttributes(): IWCAG { + return this.wcag; + } + getIconClass(): string { return this.iconClass; } diff --git a/src/main/resources/assets/admin/common/js/ui/WCAG.ts b/src/main/resources/assets/admin/common/js/ui/WCAG.ts index 6c04ca685..63d9e7ae7 100644 --- a/src/main/resources/assets/admin/common/js/ui/WCAG.ts +++ b/src/main/resources/assets/admin/common/js/ui/WCAG.ts @@ -1,11 +1,11 @@ export const IWCAG = Symbol('IWCAG'); export interface IWCAG { - [IWCAG]: boolean; + [IWCAG]?: boolean; tabbable?: boolean; - role?: AriaRole; + role?: AriaRole | ''; ariaLabel?: string; - ariaHasPopup?: AriaHasPopup; + ariaHasPopup?: AriaHasPopup | ''; } export enum AriaRole { diff --git a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts index dd968e1c5..9f3884226 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts @@ -27,9 +27,8 @@ export class ActionButton this.addClass(action.getClass()); } - if (wcag) { - wcag.role && this.setRole(wcag.role); - wcag.ariaHasPopup && this.setAriaHasPopup(wcag.ariaHasPopup); + if (action.hasWcagAttributes()) { + this.applyWCAGAttributes(action.getWcagAttributes()); } this.setEnabled(this.action.isEnabled()); @@ -54,8 +53,8 @@ export class ActionButton }); } - this.onKeyDown((event: KeyboardEvent) => KeyHelper.isEnterKey(event) && this.action.execute()); this.onClicked(() => this.action.execute()); + this.onEnterPressed(() => this.action.execute()); this.action.onExecuted(() => { Body.get().setFocusedElement(this); diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index 7b7c8fa45..97a5fcef7 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -8,6 +8,8 @@ import {Body} from '../../dom/Body'; import {Element} from '../../dom/Element'; import * as Q from 'q'; import {AriaRole, IWCAG as WCAG} from '../WCAG'; +import {KeyHelper} from '../KeyHelper'; +import * as $ from 'jquery'; export enum MenuButtonDropdownPos { LEFT, RIGHT @@ -247,6 +249,19 @@ export class MenuButton this.menu.onClicked(() => this.dropdownHandle.giveFocus()); + this.onKeyDown((event) => { + const activeButton = this.getActiveActionButton(); + if (KeyHelper.isEnterKey(event)) { + if (activeButton?.isEnabled()) { + //activeButton.getAction().execute(); + $(activeButton.getHTMLElement()).simulate('click'); + } else if (this.dropdownHandle.isEnabled()) { + $(activeButton.getHTMLElement()).simulate('click'); + this.dropdownHandle.giveFocus(); + } + } + }); + this.onFocus((event) => { const activeButton = this.getActiveActionButton(); if (activeButton) { @@ -254,6 +269,8 @@ export class MenuButton } else { this.dropdownHandle.isEnabled() && this.dropdownHandle.giveFocus(); } + event.stopImmediatePropagation(); + event.preventDefault(); }); } diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts b/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts index 2254d1808..1c5c43d76 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts @@ -169,6 +169,8 @@ export abstract class ModalDialog } } this.handleClickOutside(); + event.stopPropagation(); + event.preventDefault(); } }; diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 53243cc1e..402733af3 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -56,12 +56,28 @@ export class Toolbar } protected initListeners(): void { + const onToolbarFocused = () => { + this.focusToolbar(); + this.focusActionElement(); + }; + Body.get().onClicked((event) => { + console.log(this.getEl(), event); + const clickInsideToolbar = this.getEl().getHTMLElement().contains(event.target as Node); + if (clickInsideToolbar) { + if (this.isFocused()) { + return; + } + console.log('Toolbar.onClicked, focusing toolbar'); + onToolbarFocused(); + } + }); + // Hack: Update after styles are applied to evaluate the sizes correctly ResponsiveManager.onAvailableSizeChanged(this, () => window.setTimeout(this.foldOrExpand.bind(this))); this.onFocus(() => { - this.focusToolbar(); - this.focusActionElement(); + console.log('Toolbar.onFocus'); + onToolbarFocused(); }); } @@ -90,18 +106,21 @@ export class Toolbar }; this.addClassEx('focused'); - Body.get().onMouseDown(this.mouseClickListener); + Body.get().onMouseUp(this.mouseClickListener); } private removeFocus() { + if (!this.isFocused()) { + return; + } this.getFocusedActionElement()?.giveBlur(); this.removeClassEx('focused'); - Body.get().unMouseDown(this.mouseClickListener); + Body.get().onMouseUp(this.mouseClickListener); this.mouseClickListener = null; } private isActionFocusable(actionElement: ActionElement): boolean { - return actionElement.action ? actionElement.action.isEnabled() && !actionElement.folded : actionElement.element.isVisible(); + return actionElement.action ? actionElement.action.isFocusable() && !actionElement.folded : actionElement.element.isVisible(); } private getNextFocusableActionIndex(): number { @@ -156,10 +175,15 @@ export class Toolbar private initElementListeners(element: Element) { let eventHandled = false; + let focusOnClick = false; element.onKeyDown((event: KeyboardEvent) => { if (KeyHelper.isTabKey(event) && !KeyHelper.isShiftKey(event)) { eventHandled = true; element.giveBlur(); + + element.getChildren().forEach((child: Element) => { + child.giveBlur(); + }); this.removeFocus(); } else if (KeyHelper.isArrowRightKey(event)) { eventHandled = true; @@ -176,24 +200,41 @@ export class Toolbar } }); - const onFocus = () => { + const onFocus = (event: FocusEvent) => { + if (focusOnClick) { + focusOnClick = false; + + event.stopImmediatePropagation(); + event.preventDefault(); + return; + } this.focusToolbar(); - this.lastFocusedActionIndex = this.getActionIndexByElement(element); + const focusFromOutsideToolbar = !this.getEl().getHTMLElement().contains(event.relatedTarget as Node); + if (focusFromOutsideToolbar) { + this.focusActionElement(); + } }; const onBlur = (event: FocusEvent) => { // If newly focused element is not a part of the toolbar, remove focus from the toolbar if (!this.getEl().getHTMLElement().contains(event.relatedTarget as Node)) { + element.getChildren().forEach((child: Element) => { + child.giveBlur(); + }); this.removeFocus(); } }; - element.onFocus(() => onFocus()); + element.onFocus((event: FocusEvent) => onFocus(event)); + element.onMouseDown((event) => { + focusOnClick = true; + this.lastFocusedActionIndex = this.getActionIndexByElement(element); + }); element.onBlur((event: FocusEvent) => onBlur(event)); + element.whenRendered(() => { element.getChildren().forEach((child: Element) => { - child.onFocus(() => onFocus()); - child.onBlur((event: FocusEvent) => onBlur(event)); + child.onFocus((event: FocusEvent) => onFocus(event)); }); }); } @@ -221,14 +262,30 @@ export class Toolbar this.focusAction(() => this.getPreviousFocusableActionIndex()); } - addActionElement(element: Element): Element { + prependActionElement(element: Element, isTabbable: boolean = true): Element { + this.initElementListeners(element); + this.prependChild(element); + + if (isTabbable) { + this.actionElements.splice(0, 0, { + element + }); + } + + return element; + } + + addActionElement(element: Element, isTabbable: boolean = true): Element { if (this.foldButton) { this.addGreedySpacer(); } this.addElement(element); - this.actionElements.push({ - element: element - }); + if (isTabbable) { + this.actionElements.push({ + element: element + }); + } + return element; } @@ -277,7 +334,7 @@ export class Toolbar const actionButton = this.createActionButton(action); - this.actionElements.splice(prepend ? 0 : this.lastActionIndex + 1,0,{ + this.actionElements.splice(prepend ? 0 : this.lastActionIndex + 1, 0, { element: actionButton, action: action }); diff --git a/src/main/resources/assets/admin/common/styles/api/app/browse/browse-filter-panel.less b/src/main/resources/assets/admin/common/styles/api/app/browse/browse-filter-panel.less index 765f5bc97..d3c77a716 100644 --- a/src/main/resources/assets/admin/common/styles/api/app/browse/browse-filter-panel.less +++ b/src/main/resources/assets/admin/common/styles/api/app/browse/browse-filter-panel.less @@ -41,7 +41,7 @@ font-size: 14px; min-width: 0; flex: 1; - padding: 0 0 8px 1px; + padding: 0 0 8px 5px; line-height: 24px; border: 0; outline: none; @@ -77,12 +77,12 @@ .hide-filter-panel-button { position: absolute; - left: 14px; + top: 5px; + left: 7px; + padding: 6px 8px; font-size: 16px; - line-height: 24px; cursor: pointer; color: @admin-button-blue1; - top: 8px; &:hover { color: @admin-button-blue2; diff --git a/src/main/resources/assets/admin/common/styles/api/app/names-and-icon-view.less b/src/main/resources/assets/admin/common/styles/api/app/names-and-icon-view.less index 61c5e9edc..0d3bba78a 100644 --- a/src/main/resources/assets/admin/common/styles/api/app/names-and-icon-view.less +++ b/src/main/resources/assets/admin/common/styles/api/app/names-and-icon-view.less @@ -22,7 +22,7 @@ .names-and-icon-view-styles(64px); } - .names-and-icon-view-styles(@size, @icon-names-gap: 15px) { + .names-and-icon-view-styles(@size, @icon-names-gap: 10px) { position: relative; overflow: hidden; @@ -56,10 +56,12 @@ margin: auto; } - img, - div.font-icon-default { - width: 100%; - height: 100%; + img, div { + &.font-icon-default { + width: 100%; + height: 100%; + display: flex; + } } img { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less b/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less index 0a3b433b9..b792f7a07 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/toolbar.less @@ -16,7 +16,6 @@ align-content: center; background-color: @admin-white; height: @toolbar-height; - line-height: @toolbar-height; overflow: visible; // for the button glow box-sizing: border-box; border-bottom: 1px solid @admin-bg-light-gray; diff --git a/src/main/resources/assets/admin/common/styles/reset.less b/src/main/resources/assets/admin/common/styles/reset.less index 8a2124f1e..5ecf52a9b 100644 --- a/src/main/resources/assets/admin/common/styles/reset.less +++ b/src/main/resources/assets/admin/common/styles/reset.less @@ -103,6 +103,7 @@ button::-moz-focus-inner { padding: 0; } +div, input, textarea, select, diff --git a/src/main/resources/i18n/common_wcag.properties b/src/main/resources/i18n/common_wcag.properties new file mode 100644 index 000000000..3d354ab2a --- /dev/null +++ b/src/main/resources/i18n/common_wcag.properties @@ -0,0 +1,3 @@ +# +# Accessibility +# From 90380e7667bed36dcabe83fc1e7d35fa17ae4036 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Thu, 22 Aug 2024 15:33:39 +0200 Subject: [PATCH 14/23] Refactor ActionButton and MenuButton #3677 --- .../admin/common/js/app/browse/BrowsePanel.ts | 2 +- .../assets/admin/common/js/dom/Body.ts | 4 + .../assets/admin/common/js/ui/Action.ts | 14 +- .../assets/admin/common/js/ui/KeyBindings.ts | 2 +- .../admin/common/js/ui/button/ActionButton.ts | 122 +++++++++++------- .../admin/common/js/ui/button/MenuButton.ts | 82 +++++++----- .../common/js/ui/dialog/DropdownButtonRow.ts | 6 +- .../admin/common/js/ui/dialog/ModalDialog.ts | 1 + .../admin/common/js/ui/toolbar/Toolbar.ts | 20 +-- 9 files changed, 155 insertions(+), 98 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts index c8455fff1..f61e359fb 100644 --- a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts +++ b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts @@ -208,6 +208,7 @@ export class BrowsePanel this.showFilterPanel(); } else { this.hideFilterPanel(); + this.browseToolbar.giveFocus(); } } @@ -268,7 +269,6 @@ export class BrowsePanel if (this.filterPanel.hasFilterSet()) { this.toggleFilterPanelButton.addClass('filtered'); } - this.browseToolbar.giveFocus(); } private toggleSelectionMode(isActive: boolean) { diff --git a/src/main/resources/assets/admin/common/js/dom/Body.ts b/src/main/resources/assets/admin/common/js/dom/Body.ts index 5a14d80c0..f8bcb019e 100644 --- a/src/main/resources/assets/admin/common/js/dom/Body.ts +++ b/src/main/resources/assets/admin/common/js/dom/Body.ts @@ -54,6 +54,10 @@ export class Body this.focusedElement = element; } + getFocusedElement(): Element { + return this.focusedElement; + } + static get(): Body { let instance: Body = Store.instance().get(BODY_KEY); diff --git a/src/main/resources/assets/admin/common/js/ui/Action.ts b/src/main/resources/assets/admin/common/js/ui/Action.ts index a83fee966..119c57301 100644 --- a/src/main/resources/assets/admin/common/js/ui/Action.ts +++ b/src/main/resources/assets/admin/common/js/ui/Action.ts @@ -14,7 +14,7 @@ export class Action { private title: string; - private clazz: string; + private cls: string; private iconClass: string; @@ -192,14 +192,18 @@ export class Action { } getClass(): string { - return this.clazz; + return this.cls; } setClass(value: string): Action { - this.clazz = value; + this.cls = `${value}-action`; return this; } + hasClass(): boolean { + return this.cls != null; + } + hasShortcut(): boolean { return this.shortcut != null; } @@ -260,8 +264,8 @@ export class Action { this.propertyChangedListeners.push(listener); } - unPropertyChanged(listener: () => void) { - this.propertyChangedListeners = this.propertyChangedListeners.filter((currentListener: () => void) => { + unPropertyChanged(listener: (action: Action) => void) { + this.propertyChangedListeners = this.propertyChangedListeners.filter((currentListener: (action: Action) => void) => { return listener !== currentListener; }); } diff --git a/src/main/resources/assets/admin/common/js/ui/KeyBindings.ts b/src/main/resources/assets/admin/common/js/ui/KeyBindings.ts index 612ab42cb..6eb1d0136 100644 --- a/src/main/resources/assets/admin/common/js/ui/KeyBindings.ts +++ b/src/main/resources/assets/admin/common/js/ui/KeyBindings.ts @@ -188,7 +188,7 @@ export class KeyBindings { this.helpKeyPressedListeners.push(listener); } - unHelpKeyPressed(listener: () => void) { + unHelpKeyPressed(listener: (event: Mousetrap.ExtendedKeyboardEvent) => void) { this.helpKeyPressedListeners = this.helpKeyPressedListeners.filter((currentListener: (event: Mousetrap.ExtendedKeyboardEvent) => void) => { return listener !== currentListener; diff --git a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts index 9f3884226..8baf18289 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts @@ -20,58 +20,40 @@ export class ActionButton constructor(action: Action, wcag?: IWCAG) { super(); - this.action = action; - this.setLabel(this.createLabel(action), false); this.addClass('action-button'); - if (action.getClass()) { - this.addClass(action.getClass()); - } + this.initListeners(); - if (action.hasWcagAttributes()) { - this.applyWCAGAttributes(action.getWcagAttributes()); - } - - this.setEnabled(this.action.isEnabled()); - this.setVisible(this.action.isVisible()); - - this.updateIconClass(this.action.getIconClass()); + this.setAction(action); + } - if (this.action.hasShortcut()) { - let combination = this.action.getShortcut().getCombination(); - if (combination) { - combination = combination.replace(/mod\+/i, BrowserHelper.isOSX() || BrowserHelper.isIOS() ? 'cmd+' : 'ctrl+'); + private onHelpKeyPressed(e: KeyboardEvent) { + const action = this.getAction(); + if (!action.hasShortcut()) { + return; + } + const tooltip = this.getTooltip(); + if (action.isEnabled() && KeyBindings.get().isActive(action.getShortcut())) { + if (KeyBindingAction[KeyBindingAction.KEYDOWN].toLowerCase() === e.type) { + tooltip.show(); + return; } - this.tooltip = new Tooltip(this, combination, 1000); - KeyBindings.get().onHelpKeyPressed((e) => { - if (this.action.isEnabled() && KeyBindings.get().isActive(this.action.getShortcut())) { - if (KeyBindingAction[KeyBindingAction.KEYDOWN].toLowerCase() === e.type) { - this.tooltip.show(); - return; - } - } - this.tooltip.hide(); - }); } + tooltip.hide(); + } - this.onClicked(() => this.action.execute()); - this.onEnterPressed(() => this.action.execute()); - - this.action.onExecuted(() => { + protected initListeners() { + const executeAction = () => { Body.get().setFocusedElement(this); - }); - - this.action.onPropertyChanged((changedAction: Action) => { - const toggledEnabled = this.isEnabled() !== changedAction.isEnabled(); - const toggledVisible = this.isVisible() !== changedAction.isVisible(); - const becameHidden = toggledVisible && !changedAction.isVisible(); - if (this.tooltip && (toggledEnabled || becameHidden)) { - this.tooltip.hide(); - } - toggledEnabled && this.setEnabled(changedAction.isEnabled()); - toggledVisible && this.setVisible(changedAction.isVisible()); - this.setLabel(this.createLabel(changedAction), false); - this.updateIconClass(changedAction.getIconClass()); - }); + this.getAction().execute(); + }; + + this.onClicked(() => executeAction()); + this.onEnterPressed(() => executeAction()); + + this.onHelpKeyPressed = this.onHelpKeyPressed.bind(this); + this.syncButtonWithAction = this.syncButtonWithAction.bind(this); + + KeyBindings.get().onHelpKeyPressed(this.onHelpKeyPressed); } private updateIconClass(newIconClass: string) { @@ -91,10 +73,60 @@ export class ActionButton return this.action; } + setAction(action: Action) { + if (this.action === action) { + return; + } + + if (this.action) { + if (this.action.hasClass()) { + this.removeClass(this.action.getClass()); + } + this.action.unPropertyChanged(this.syncButtonWithAction); + if (!action.hasShortcut()) { + KeyBindings.get().unHelpKeyPressed(this.onHelpKeyPressed); + } + } + this.doSetAction(action); + } + getTooltip(): Tooltip { return this.tooltip; } + private syncButtonWithAction() { + const action = this.getAction(); + + const toggledEnabled = this.isEnabled() !== action.isEnabled(); + const toggledVisible = this.isVisible() !== action.isVisible(); + const becameHidden = toggledVisible && !action.isVisible(); + const tooltip = this.getTooltip(); + if (tooltip && (toggledEnabled || becameHidden)) { + tooltip.hide(); + } + toggledEnabled && this.setEnabled(action.isEnabled()); + toggledVisible && this.setVisible(action.isVisible()); + this.setLabel(this.createLabel(action), false); + this.updateIconClass(action.getIconClass()); + + const actionClass = action.getClass(); + if (actionClass && !this.hasClass(actionClass)) { + this.addClass(actionClass); + } + + if (action.hasWcagAttributes()) { + this.applyWCAGAttributes(action.getWcagAttributes()); + } + } + + private doSetAction(action: Action) { + action.onPropertyChanged(this.syncButtonWithAction); + + this.action = action; + + this.syncButtonWithAction(); + } + protected createLabel(action: Action): string { let label: string; if (action.hasMnemonic()) { diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index 97a5fcef7..2a0cad6a1 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -15,6 +15,14 @@ export enum MenuButtonDropdownPos { LEFT, RIGHT } +interface MenuButtonConfigObject { + defaultAction: Action; + menuActions?: Action[]; + dropdownPosition?: MenuButtonDropdownPos +} + +export type MenuButtonConfig = Action | MenuButtonConfigObject; + export class MenuButton extends DivEl { @@ -22,9 +30,11 @@ export class MenuButton role: AriaRole = AriaRole.NONE; tabbable: boolean = true; - protected readonly mainAction: Action; + protected readonly defaultAction: Action; - protected readonly menuActions: Action[]; + protected menuActions: Action[] = []; + + protected readonly dropdownPosition: MenuButtonDropdownPos; protected dropdownHandle: DropdownHandle; @@ -38,11 +48,16 @@ export class MenuButton private actionPropertyListener: () => void; - constructor(mainAction: Action, menuActions: Action[] = []) { + constructor(config: Action | MenuButtonConfig) { super('menu-button'); - this.mainAction = mainAction; - this.menuActions = menuActions; + if ('defaultAction' in config) { + this.defaultAction = config.defaultAction; + this.menuActions = config.menuActions || []; + this.dropdownPosition = config.dropdownPosition || MenuButtonDropdownPos.LEFT; + } else { + this.defaultAction = config; + } this.initElements(); this.initListeners(); @@ -50,19 +65,20 @@ export class MenuButton protected initElements(): void { this.onBodyClicked = (e) => this.hideMenuOnOutsideClick(e); - // Body.get().onClicked(this.onBodyClicked); this.actionPropertyListener = this.updateActionEnabled.bind(this); this.menu = new Menu(this.menuActions); this.menu.hide(); this.initDropdownHandle(); - this.initActionButton(this.mainAction); - this.initActions(this.menuActions); + this.initActionButton(); + if (this.menuActions?.length > 0) { + this.initActions(this.menuActions); + } } - protected getActiveActionButton(): ActionButton { - return this.getActionButton(); + protected getDefaultAction(): Action { + return this.defaultAction; } getActionButton(): ActionButton { @@ -84,11 +100,14 @@ export class MenuButton addMenuActions(actions: Action[]) { this.menu.addActions(actions); this.initActions(actions); + this.menuActions = this.menu.getMenuItems().map(item => item.getAction()); } removeMenuActions(actions: Action[]) { this.menu.removeActions(actions); this.releaseActions(actions); + + this.menuActions = this.menu.getMenuItems().map(item => item.getAction()); } addMenuSeparator(): void { @@ -169,10 +188,6 @@ export class MenuButton this.toggleMenuOnAction = value; } - protected getDropdownPosition(): MenuButtonDropdownPos { - return MenuButtonDropdownPos.LEFT; - } - private hideMenuOnOutsideClick(e: Event): void { if (!this.dropdownHandle.hasClass('down')) { return; @@ -190,9 +205,16 @@ export class MenuButton this.dropdownHandle = new DropdownHandle(); } - private initActionButton(action: Action): void { - this.actionButton = new ActionButton(action); - this.actionButton.setAriaHasPopup(); + private initActionButton(): void { + this.actionButton = new ActionButton(this.defaultAction); + } + + protected setButtonAction(action: Action): void { + this.actionButton.setAction(action); + } + + protected setDefaultButtonAction(): void { + this.setButtonAction(this.defaultAction); } private initActions(actions: Action[]): void { @@ -200,9 +222,7 @@ export class MenuButton this.updateActionEnabled(); - actions.forEach((action) => { - action.onPropertyChanged(this.actionPropertyListener); - }); + actions.forEach((action) => action.onPropertyChanged(this.actionPropertyListener)); } private releaseActions(actions: Action[]): void { @@ -239,29 +259,23 @@ export class MenuButton } }); - this.actionButton.onClicked((e) => { - if (this.toggleMenuOnAction) { - this.toggleMenu(); - } else { - this.collapseMenu(); - } - }); + this.actionButton.onClicked(() => this.toggleMenuOnAction ? this.toggleMenu() : this.collapseMenu()); this.menu.onClicked(() => this.dropdownHandle.giveFocus()); this.onKeyDown((event) => { - const activeButton = this.getActiveActionButton(); if (KeyHelper.isEnterKey(event)) { - if (activeButton?.isEnabled()) { + const actionButton = this.getActionButton(); + if (actionButton?.isEnabled()) { //activeButton.getAction().execute(); - $(activeButton.getHTMLElement()).simulate('click'); + $(actionButton.getHTMLElement()).simulate('click'); } else if (this.dropdownHandle.isEnabled()) { - $(activeButton.getHTMLElement()).simulate('click'); + $(this.dropdownHandle.getHTMLElement()).simulate('click'); this.dropdownHandle.giveFocus(); } } }); - +/* this.onFocus((event) => { const activeButton = this.getActiveActionButton(); if (activeButton) { @@ -271,14 +285,14 @@ export class MenuButton } event.stopImmediatePropagation(); event.preventDefault(); - }); + });*/ } doRender(): Q.Promise { return super.doRender().then((rendered: boolean) => { const children: Element[] = []; - if (this.getDropdownPosition() === MenuButtonDropdownPos.RIGHT) { + if (this.dropdownPosition === MenuButtonDropdownPos.RIGHT) { children.push(this.actionButton, this.dropdownHandle); } else { children.push(this.dropdownHandle, this.actionButton); diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts b/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts index 09579e37d..65643bf63 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts @@ -1,4 +1,4 @@ -import {MenuButton} from '../button/MenuButton'; +import {MenuButton, MenuButtonConfig} from '../button/MenuButton'; import {ButtonRow} from './ModalDialog'; import {Action} from '../Action'; @@ -12,9 +12,9 @@ export class DropdownButtonRow this.addClass('dropdown-button-row'); } - makeActionMenu(mainAction: Action, menuActions: Action[], useDefault: boolean = true): MenuButton { + makeActionMenu(menuButtonConfig: MenuButtonConfig, useDefault: boolean = true): MenuButton { if (!this.actionMenu) { - this.actionMenu = new MenuButton(mainAction, menuActions); + this.actionMenu = new MenuButton(menuButtonConfig); if (useDefault) { this.setDefaultElement(this.actionMenu.getActionButton()); diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts b/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts index 1c5c43d76..f2803113c 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/ModalDialog.ts @@ -511,6 +511,7 @@ export abstract class ModalDialog } open() { + Body.get().getFocusedElement()?.giveBlur(); BodyMask.get().show(); KeyBindings.get().shelveBindings(); diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 402733af3..76c8abaf5 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -59,9 +59,8 @@ export class Toolbar const onToolbarFocused = () => { this.focusToolbar(); this.focusActionElement(); - }; + };/* Body.get().onClicked((event) => { - console.log(this.getEl(), event); const clickInsideToolbar = this.getEl().getHTMLElement().contains(event.target as Node); if (clickInsideToolbar) { if (this.isFocused()) { @@ -70,15 +69,12 @@ export class Toolbar console.log('Toolbar.onClicked, focusing toolbar'); onToolbarFocused(); } - }); + });*/ // Hack: Update after styles are applied to evaluate the sizes correctly ResponsiveManager.onAvailableSizeChanged(this, () => window.setTimeout(this.foldOrExpand.bind(this))); - this.onFocus(() => { - console.log('Toolbar.onFocus'); - onToolbarFocused(); - }); + this.onFocus(() => onToolbarFocused()); } private getFocusedActionElement(): Element { @@ -95,6 +91,7 @@ export class Toolbar } private focusToolbar() { + //console.log('Focusing toolbar'); if (this.isFocused()) { return; } @@ -201,7 +198,9 @@ export class Toolbar }); const onFocus = (event: FocusEvent) => { + //console.log('Focusing ', element, event); if (focusOnClick) { + //console.log('Focus on click, exiting'); focusOnClick = false; event.stopImmediatePropagation(); @@ -216,11 +215,14 @@ export class Toolbar }; const onBlur = (event: FocusEvent) => { + focusOnClick = false; // If newly focused element is not a part of the toolbar, remove focus from the toolbar + //console.log('Blurring ', element, event); if (!this.getEl().getHTMLElement().contains(event.relatedTarget as Node)) { element.getChildren().forEach((child: Element) => { child.giveBlur(); }); + //console.log('Removing focus from the toolbar'); this.removeFocus(); } }; @@ -231,12 +233,12 @@ export class Toolbar this.lastFocusedActionIndex = this.getActionIndexByElement(element); }); element.onBlur((event: FocusEvent) => onBlur(event)); - +/* element.whenRendered(() => { element.getChildren().forEach((child: Element) => { child.onFocus((event: FocusEvent) => onFocus(event)); }); - }); + });*/ } private isFocused(): boolean { From fcc089702f9f5c669bc49957446bb3d8531a4a06 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Mon, 26 Aug 2024 16:29:42 +0200 Subject: [PATCH 15/23] Refactor ActionButton and MenuButton #3677 --- .../admin/common/js/ui/button/ActionButton.ts | 82 +++++++++++-------- .../admin/common/js/ui/button/MenuButton.ts | 7 +- .../common/js/ui/dialog/DropdownButtonRow.ts | 2 +- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts index 8baf18289..2bc5b93dc 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts @@ -26,21 +26,6 @@ export class ActionButton this.setAction(action); } - private onHelpKeyPressed(e: KeyboardEvent) { - const action = this.getAction(); - if (!action.hasShortcut()) { - return; - } - const tooltip = this.getTooltip(); - if (action.isEnabled() && KeyBindings.get().isActive(action.getShortcut())) { - if (KeyBindingAction[KeyBindingAction.KEYDOWN].toLowerCase() === e.type) { - tooltip.show(); - return; - } - } - tooltip.hide(); - } - protected initListeners() { const executeAction = () => { Body.get().setFocusedElement(this); @@ -56,19 +41,6 @@ export class ActionButton KeyBindings.get().onHelpKeyPressed(this.onHelpKeyPressed); } - private updateIconClass(newIconClass: string) { - if (newIconClass === this.iconClass) { - return; - } - if (this.iconClass) { - this.removeClass(this.iconClass); - } - this.iconClass = newIconClass; - if (this.iconClass) { - this.addClass(this.iconClass); - } - } - getAction(): Action { return this.action; } @@ -94,6 +66,16 @@ export class ActionButton return this.tooltip; } + protected createLabel(action: Action): string { + let label: string; + if (action.hasMnemonic()) { + label = action.getMnemonic().underlineMnemonic(action.getLabel()); + } else { + label = action.getLabel(); + } + return label; + } + private syncButtonWithAction() { const action = this.getAction(); @@ -119,22 +101,52 @@ export class ActionButton } } + private createTooltip() { + if (!this.action.hasShortcut()) { + return; + } + + let combination = this.action.getShortcut().getCombination(); + if (combination) { + combination = combination.replace(/mod\+/i, BrowserHelper.isOSX() || BrowserHelper.isIOS() ? 'cmd+' : 'ctrl+'); + } + this.tooltip = new Tooltip(this, combination, 1000); + } + private doSetAction(action: Action) { action.onPropertyChanged(this.syncButtonWithAction); this.action = action; + this.createTooltip(); this.syncButtonWithAction(); } - protected createLabel(action: Action): string { - let label: string; - if (action.hasMnemonic()) { - label = action.getMnemonic().underlineMnemonic(action.getLabel()); - } else { - label = action.getLabel(); + private onHelpKeyPressed(e: KeyboardEvent) { + const action = this.getAction(); + if (!action.hasShortcut()) { + return; } - return label; + const tooltip = this.getTooltip(); + if (action.isEnabled() && KeyBindings.get().isActive(action.getShortcut())) { + if (KeyBindingAction[KeyBindingAction.KEYDOWN].toLowerCase() === e.type) { + tooltip.show(); + return; + } + } + tooltip.hide(); } + private updateIconClass(newIconClass: string) { + if (newIconClass === this.iconClass) { + return; + } + if (this.iconClass) { + this.removeClass(this.iconClass); + } + this.iconClass = newIconClass; + if (this.iconClass) { + this.addClass(this.iconClass); + } + } } diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index 2a0cad6a1..cfa921550 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -15,14 +15,12 @@ export enum MenuButtonDropdownPos { LEFT, RIGHT } -interface MenuButtonConfigObject { +export interface MenuButtonConfig { defaultAction: Action; menuActions?: Action[]; dropdownPosition?: MenuButtonDropdownPos } -export type MenuButtonConfig = Action | MenuButtonConfigObject; - export class MenuButton extends DivEl { @@ -48,10 +46,13 @@ export class MenuButton private actionPropertyListener: () => void; + protected readonly config: MenuButtonConfig; + constructor(config: Action | MenuButtonConfig) { super('menu-button'); if ('defaultAction' in config) { + this.config = config; this.defaultAction = config.defaultAction; this.menuActions = config.menuActions || []; this.dropdownPosition = config.dropdownPosition || MenuButtonDropdownPos.LEFT; diff --git a/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts b/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts index 65643bf63..add17fd6f 100644 --- a/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts +++ b/src/main/resources/assets/admin/common/js/ui/dialog/DropdownButtonRow.ts @@ -12,7 +12,7 @@ export class DropdownButtonRow this.addClass('dropdown-button-row'); } - makeActionMenu(menuButtonConfig: MenuButtonConfig, useDefault: boolean = true): MenuButton { + makeActionMenu(menuButtonConfig: Action | MenuButtonConfig, useDefault: boolean = true): MenuButton { if (!this.actionMenu) { this.actionMenu = new MenuButton(menuButtonConfig); From 6032c45929699815989e7042502729f41803ef5e Mon Sep 17 00:00:00 2001 From: alansemenov Date: Wed, 28 Aug 2024 16:16:00 +0200 Subject: [PATCH 16/23] Accessibility: Input highlighting #3687 --- .../admin/common/js/ui/button/MenuButton.ts | 24 ----------- .../set/optionset/form-option-set-view.less | 6 ++- .../common/styles/api/ui/button/button.less | 4 +- .../styles/api/ui/button/dropdown-handle.less | 6 +-- .../styles/api/ui/button/menu-button.less | 43 ------------------- .../api/ui/dialog/dropdown-button-row.less | 1 + .../api/ui/selector/combobox/combobox.less | 16 ++++--- .../ui/selector/combobox/rich-combobox.less | 4 -- .../api/ui/selector/dropdown/dropdown.less | 18 +++++--- .../api/ui/selector/option-filter-input.less | 8 +--- .../assets/admin/common/styles/main.less | 1 + .../assets/admin/common/styles/variables.less | 2 + 12 files changed, 37 insertions(+), 96 deletions(-) create mode 100644 src/main/resources/assets/admin/common/styles/variables.less diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index cfa921550..b8078becd 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -263,30 +263,6 @@ export class MenuButton this.actionButton.onClicked(() => this.toggleMenuOnAction ? this.toggleMenu() : this.collapseMenu()); this.menu.onClicked(() => this.dropdownHandle.giveFocus()); - - this.onKeyDown((event) => { - if (KeyHelper.isEnterKey(event)) { - const actionButton = this.getActionButton(); - if (actionButton?.isEnabled()) { - //activeButton.getAction().execute(); - $(actionButton.getHTMLElement()).simulate('click'); - } else if (this.dropdownHandle.isEnabled()) { - $(this.dropdownHandle.getHTMLElement()).simulate('click'); - this.dropdownHandle.giveFocus(); - } - } - }); -/* - this.onFocus((event) => { - const activeButton = this.getActiveActionButton(); - if (activeButton) { - activeButton.giveFocus(); - } else { - this.dropdownHandle.isEnabled() && this.dropdownHandle.giveFocus(); - } - event.stopImmediatePropagation(); - event.preventDefault(); - });*/ } doRender(): Q.Promise { diff --git a/src/main/resources/assets/admin/common/styles/api/form/set/optionset/form-option-set-view.less b/src/main/resources/assets/admin/common/styles/api/form/set/optionset/form-option-set-view.less index fbb0a14da..0007c9e23 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/set/optionset/form-option-set-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/set/optionset/form-option-set-view.less @@ -76,9 +76,13 @@ flex: 1; max-width: initial; - input { + > input { height: 47px; // match the option to prevent height bounce } + + > .@{_COMMON_PREFIX}dropdown-handle { + padding: 9px 9px 10px 9px; + } } &.selected { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/button/button.less b/src/main/resources/assets/admin/common/styles/api/ui/button/button.less index 709fef97f..29505801e 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/button/button.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/button/button.less @@ -23,7 +23,7 @@ .button(@form-button-font, @form-button-bg); &.small { - padding: 0 15px 0 15px; + padding: 3px 15px; .label { font-size: 12px; @@ -31,7 +31,7 @@ } &.large { - padding: 5px 19px 5px 19px; + padding: 8px 19px; .label { font-size: 16px; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less b/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less index 140d7576b..e475a1a2a 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/button/dropdown-handle.less @@ -1,8 +1,6 @@ .@{_COMMON_PREFIX}dropdown-handle { - position: absolute; - top: 0; - right: 0; - width: 37px; + height: inherit; + width: @input-height-big; border: 0; cursor: pointer; background-color: transparent; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less b/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less index df32e8400..cbcc8fb19 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/button/menu-button.less @@ -54,15 +54,6 @@ } } - .@{_COMMON_PREFIX}dropdown-handle { - position: relative; - } - - .action-button { - position: relative; - flex-wrap: nowrap; - } - &.transparent { .@{_COMMON_PREFIX}dropdown-handle { &::after { @@ -111,38 +102,4 @@ } } - &:not(.transparent) { - .@{_COMMON_PREFIX}dropdown-handle { - &::after { - border-top: 10px solid @admin-white; - } - - &:hover { - span { - color: @admin-white; - } - } - - &[disabled], - &[disabled]:hover { - background-color: @form-button-bg-disabled; - } - } - - .action-button { - span { - color: @admin-white; - } - - &[disabled] { - background-color: @form-button-bg-disabled; - } - - &:not([disabled]):hover { - span { - color: @admin-white; - } - } - } - } } diff --git a/src/main/resources/assets/admin/common/styles/api/ui/dialog/dropdown-button-row.less b/src/main/resources/assets/admin/common/styles/api/ui/dialog/dropdown-button-row.less index 0b8cfb049..a916eb5ee 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/dialog/dropdown-button-row.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/dialog/dropdown-button-row.less @@ -4,6 +4,7 @@ .@{_COMMON_PREFIX}dropdown-handle { margin-right: 1px !important; + color: @form-button-font; } } } diff --git a/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less b/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less index 35da0cf8f..e38ff91f2 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/combobox.less @@ -1,15 +1,21 @@ +@import "../../../../variables"; + .@{_COMMON_PREFIX}combobox { .reset(true); + .input-border(); position: relative; - display: block; + display: flex; + flex-direction: row; + flex-wrap: wrap; // to make sure dropdown is shown in the right place + height: @input-height-big; // 2px is the top+bottom border height - &.followed-by-options { - margin-bottom: 10px; + > input { + flex: 1; } - > .@{_COMMON_PREFIX}dropdown-handle { - height: 37px; + &.followed-by-options { + margin-bottom: 10px; } .input-icon { diff --git a/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/rich-combobox.less b/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/rich-combobox.less index d25a2df8a..41148e876 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/rich-combobox.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/selector/combobox/rich-combobox.less @@ -1,9 +1,5 @@ .rich-combobox { .@{_COMMON_PREFIX}combobox { - input { - width: 100%; - height: 37px; - } .option-filter-input { margin: initial; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/selector/dropdown/dropdown.less b/src/main/resources/assets/admin/common/styles/api/ui/selector/dropdown/dropdown.less index a4c69ffeb..e0360943a 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/selector/dropdown/dropdown.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/selector/dropdown/dropdown.less @@ -1,14 +1,19 @@ .@{_COMMON_PREFIX}dropdown { .reset(true); + .input-border(); position: relative; - display: inline-block; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; width: 100%; max-width: 540px; text-align: left; - input { + > input { .input-glow(); + flex: 1; } .input-icon { @@ -75,15 +80,14 @@ } .selected-option { - position: relative; - .input-border(); - .input-glow(); + //.input-border(); + //.input-glow(); + flex: 1; + position: relative; width: 100%; - min-height: 35px; padding: 0; margin: 0; - text-align: left; background: white; box-sizing: border-box; diff --git a/src/main/resources/assets/admin/common/styles/api/ui/selector/option-filter-input.less b/src/main/resources/assets/admin/common/styles/api/ui/selector/option-filter-input.less index da26f59f0..e845f4343 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/selector/option-filter-input.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/selector/option-filter-input.less @@ -1,13 +1,9 @@ .option-filter-input { box-sizing: border-box; - .input-border(); - .input-font(); + height: inherit; background-color: @admin-white; - width: 100%; - height: 37px; - padding: 4px 37px 4px 10px; - line-height: 16px; + padding: 4px 4px 4px 10px; &:focus { border-color: @admin-black; diff --git a/src/main/resources/assets/admin/common/styles/main.less b/src/main/resources/assets/admin/common/styles/main.less index 700f05415..d86535b51 100644 --- a/src/main/resources/assets/admin/common/styles/main.less +++ b/src/main/resources/assets/admin/common/styles/main.less @@ -8,6 +8,7 @@ @import "../icons/icons"; // :) +@import "variables"; @import "reset"; @import "mixins"; @import "font"; diff --git a/src/main/resources/assets/admin/common/styles/variables.less b/src/main/resources/assets/admin/common/styles/variables.less new file mode 100644 index 000000000..580cd84c6 --- /dev/null +++ b/src/main/resources/assets/admin/common/styles/variables.less @@ -0,0 +1,2 @@ +@input-height-small: 23px; +@input-height-big: 35px; //2px will be added for top and bottom border From 45655b9f0eb7dc76ae6ffcd353b48a7baa28f08c Mon Sep 17 00:00:00 2001 From: alansemenov Date: Thu, 29 Aug 2024 13:14:35 +0200 Subject: [PATCH 17/23] Use display: flex for TabMenu --- .../assets/admin/common/styles/api/ui/tab/tab-menu.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/assets/admin/common/styles/api/ui/tab/tab-menu.less b/src/main/resources/assets/admin/common/styles/api/ui/tab/tab-menu.less index c57c2afa9..c62a8f17d 100644 --- a/src/main/resources/assets/admin/common/styles/api/ui/tab/tab-menu.less +++ b/src/main/resources/assets/admin/common/styles/api/ui/tab/tab-menu.less @@ -1,4 +1,5 @@ .tab-menu { + display: flex; min-width: 140px; font-size: 14px; text-align: left; From 93d857823e032779fd14f5acec8f5b1a77249520 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Thu, 29 Aug 2024 13:48:10 +0200 Subject: [PATCH 18/23] Don't try to reapply focus to the last focused element if there was none --- src/main/resources/assets/admin/common/js/dom/Body.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/assets/admin/common/js/dom/Body.ts b/src/main/resources/assets/admin/common/js/dom/Body.ts index f8bcb019e..0f64cf6b2 100644 --- a/src/main/resources/assets/admin/common/js/dom/Body.ts +++ b/src/main/resources/assets/admin/common/js/dom/Body.ts @@ -44,6 +44,9 @@ export class Body } reapplyFocus() { + if (!this.focusedElement) { + return; + } setTimeout(() => { this.focusedElement?.giveFocus(); this.focusedElement = null; From a4499299b53e2fb8dc5c9a1a5060da7d915d790e Mon Sep 17 00:00:00 2001 From: alansemenov Date: Fri, 30 Aug 2024 08:31:31 +0200 Subject: [PATCH 19/23] Refactor ActionButton and MenuButton #3677 --- .../admin/common/js/app/browse/BrowsePanel.ts | 2 +- .../assets/admin/common/js/dom/Element.ts | 14 ++ .../assets/admin/common/js/ui/Action.ts | 4 - .../admin/common/js/ui/button/MenuButton.ts | 5 +- .../admin/common/js/ui/toolbar/FoldButton.ts | 20 +- .../admin/common/js/ui/toolbar/Toolbar.ts | 221 ++++++++---------- 6 files changed, 127 insertions(+), 139 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts index f61e359fb..de66749a8 100644 --- a/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts +++ b/src/main/resources/assets/admin/common/js/app/browse/BrowsePanel.ts @@ -313,7 +313,7 @@ export class BrowsePanel this.toggleFilterPanelAction.setWcagAttributes({ ariaLabel: i18n('tooltip.filterPanel.show') }); - this.toggleFilterPanelButton = this.browseToolbar.prependAction(this.toggleFilterPanelAction); + this.toggleFilterPanelButton = this.browseToolbar.addAction(this.toggleFilterPanelAction); this.toggleFilterPanelButton.setTitle(i18n('tooltip.filterPanel.show')); this.toggleFilterPanelAction.setVisible(false); } diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index 1bc19bff7..920c154a6 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -441,6 +441,10 @@ export class Element { return this.el.isVisible(); } + isFocusable(): boolean { + return this.isVisible() && !this.el.isDisabled(); + } + setTitle(title: string): Element { if (title.trim()) { this.el.setTitle(title.trim()); @@ -1127,6 +1131,16 @@ export class Element { }); } + onEscPressed(callback: () => void) { + this.onKeyDown((event: KeyboardEvent) => { + if (KeyHelper.isEscKey(event)) { + callback(); + event.stopPropagation(); + event.preventDefault(); + } + }); + } + onClicked(listener: (event: MouseEvent) => void) { this.getEl().addEventListener('click', listener); } diff --git a/src/main/resources/assets/admin/common/js/ui/Action.ts b/src/main/resources/assets/admin/common/js/ui/Action.ts index 119c57301..337207e1f 100644 --- a/src/main/resources/assets/admin/common/js/ui/Action.ts +++ b/src/main/resources/assets/admin/common/js/ui/Action.ts @@ -145,10 +145,6 @@ export class Action { return this.visible; } - isFocusable(): boolean { - return this.isVisible() && this.isEnabled(); - } - setVisible(value: boolean): Action { if (value !== this.visible) { this.visible = value; diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index b8078becd..a4ebbc987 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -204,6 +204,7 @@ export class MenuButton private initDropdownHandle(): void { this.dropdownHandle = new DropdownHandle(); + this.dropdownHandle.onEnterPressed(() => this.toggleMenu()); } private initActionButton(): void { @@ -214,10 +215,6 @@ export class MenuButton this.actionButton.setAction(action); } - protected setDefaultButtonAction(): void { - this.setButtonAction(this.defaultAction); - } - private initActions(actions: Action[]): void { this.setDropdownHandleEnabled(this.getMenuActions().length > 0); diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts index 5b9ec9ab7..60c2e870b 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts @@ -39,8 +39,14 @@ export class FoldButton this.hostElement = hostElement; } - (hostElement || this).onClicked(this.onButtonClicked.bind(this)); + this.initListeners(); + } + + private initListeners() { + (this.hostElement || this).onClicked(this.toggleMenu.bind(this)); this.dropdown.onClicked(this.onMenuClicked.bind(this)); + this.onEnterPressed(this.toggleMenu.bind(this)); + this.onEscPressed(this.collapse.bind(this)); } public collapse() { @@ -89,7 +95,7 @@ export class FoldButton } } - private onButtonClicked(e: MouseEvent) { + private toggleMenu(event?: MouseEvent) { this.toggle(); if (this.hasClass(FoldButton.expandedCls)) { @@ -104,17 +110,13 @@ export class FoldButton Body.get().onClicked(onBodyClicked); } - if (!BrowserHelper.isIE()) { - e.stopPropagation(); - } + event?.stopPropagation(); } - private onMenuClicked(e: MouseEvent) { + private onMenuClicked(event: MouseEvent) { this.collapse(); - if (!BrowserHelper.isIE()) { - e.stopPropagation(); - } + event.stopPropagation(); } } diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 76c8abaf5..8e0a2cb05 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -9,10 +9,10 @@ import {FoldButton} from './FoldButton'; import {IWCAG as WCAG, AriaRole} from '../WCAG'; import {KeyHelper} from '../KeyHelper'; import {Body} from '../../dom/Body'; +import {ObjectHelper} from '../../ObjectHelper'; -interface ActionElement { - element: Element; - action?: Action; +interface ToolbarElement { + el: Element | ActionButton; folded?: boolean; } @@ -31,10 +31,10 @@ export class Toolbar protected config: C; protected foldButton: FoldButton; - protected actionElements: ActionElement[] = []; + protected toolbarElements: ToolbarElement[] = []; private locked: boolean; private hasGreedySpacer: boolean; - private lastFocusedActionIndex = -1; + private lastFocusedElementIndex = -1; private lastActionIndex= -1; private visibleButtonsWidth = 0; @@ -58,18 +58,8 @@ export class Toolbar protected initListeners(): void { const onToolbarFocused = () => { this.focusToolbar(); - this.focusActionElement(); - };/* - Body.get().onClicked((event) => { - const clickInsideToolbar = this.getEl().getHTMLElement().contains(event.target as Node); - if (clickInsideToolbar) { - if (this.isFocused()) { - return; - } - console.log('Toolbar.onClicked, focusing toolbar'); - onToolbarFocused(); - } - });*/ + this.focusElement(); + }; // Hack: Update after styles are applied to evaluate the sizes correctly ResponsiveManager.onAvailableSizeChanged(this, () => window.setTimeout(this.foldOrExpand.bind(this))); @@ -77,21 +67,20 @@ export class Toolbar this.onFocus(() => onToolbarFocused()); } - private getFocusedActionElement(): Element { - if (this.lastFocusedActionIndex === -1) { + private getFocusedElement(): Element { + if (this.lastFocusedElementIndex === -1) { return null; } - const lastFocusedActionElement = this.actionElements[this.lastFocusedActionIndex]; - if (lastFocusedActionElement.folded) { + const lastFocusedElement = this.toolbarElements[this.lastFocusedElementIndex]; + if (lastFocusedElement.folded || !lastFocusedElement.el.isFocusable()) { return null; } - return lastFocusedActionElement.element; + return lastFocusedElement.el; } private focusToolbar() { - //console.log('Focusing toolbar'); if (this.isFocused()) { return; } @@ -110,27 +99,37 @@ export class Toolbar if (!this.isFocused()) { return; } - this.getFocusedActionElement()?.giveBlur(); + if (this.getFocusedElement()) { + this.getFocusedElement()?.giveBlur(); + } this.removeClassEx('focused'); Body.get().onMouseUp(this.mouseClickListener); this.mouseClickListener = null; } - private isActionFocusable(actionElement: ActionElement): boolean { - return actionElement.action ? actionElement.action.isFocusable() && !actionElement.folded : actionElement.element.isVisible(); + private isActionButton(el: Element | ActionButton): el is ActionButton { + return (el as ActionButton).getAction !== undefined; + } + + private isElementFocusable(element: ToolbarElement): boolean { + if (this.isActionButton(element.el)) { + return !element.folded && element.el.isFocusable(); + } + + return element.el.isVisible(); } - private getNextFocusableActionIndex(): number { - let focusIndex = this.lastFocusedActionIndex; + private getNextFocusableElementIndex(): number { + let focusIndex = this.lastFocusedElementIndex; const currentFocusIndex = focusIndex; - const limit = this.actionElements.length; + const limit = this.toolbarElements.length; do { focusIndex++; if (focusIndex === limit) { focusIndex = 0; } - } while (focusIndex !== currentFocusIndex && !this.isActionFocusable(this.actionElements[focusIndex])); + } while (focusIndex !== currentFocusIndex && !this.isElementFocusable(this.toolbarElements[focusIndex])); if (focusIndex === currentFocusIndex) { return -1; @@ -139,30 +138,30 @@ export class Toolbar return focusIndex; } - private getPreviousFocusableActionIndex(): number { - let focusIndex = this.lastFocusedActionIndex; + private getPreviousFocusableElementIndex(): number { + let focusIndex = this.lastFocusedElementIndex; const currentFocusIndex = focusIndex; - const lastIndex = this.actionElements.length - 1; + const lastIndex = this.toolbarElements.length - 1; do { focusIndex--; if (focusIndex === -1) { focusIndex = lastIndex; } - } while (focusIndex !== currentFocusIndex && !this.isActionFocusable(this.actionElements[focusIndex])); + } while (focusIndex !== currentFocusIndex && !this.isElementFocusable(this.toolbarElements[focusIndex])); return focusIndex; } - private focusActionElement(): void { - let lastFocusedActionElement = this.getFocusedActionElement(); - if (!lastFocusedActionElement) { - const focusIndex = this.getNextFocusableActionIndex(); - this.lastFocusedActionIndex = focusIndex; - lastFocusedActionElement = this.actionElements[focusIndex].element; + private focusElement(): void { + let lastFocusedElement = this.getFocusedElement(); + if (!lastFocusedElement) { + const focusIndex = this.getNextFocusableElementIndex(); + this.lastFocusedElementIndex = focusIndex; + lastFocusedElement = this.toolbarElements[focusIndex].el; } - lastFocusedActionElement.giveFocus(); + lastFocusedElement.giveFocus(); } private createActionButton(action: Action): ActionButton { @@ -177,17 +176,13 @@ export class Toolbar if (KeyHelper.isTabKey(event) && !KeyHelper.isShiftKey(event)) { eventHandled = true; element.giveBlur(); - - element.getChildren().forEach((child: Element) => { - child.giveBlur(); - }); this.removeFocus(); } else if (KeyHelper.isArrowRightKey(event)) { eventHandled = true; - this.focusNextAction(); + this.focusNextElement(); } else if (KeyHelper.isArrowLeftKey(event)) { eventHandled = true; - this.focusPreviousAction(); + this.focusPreviousElement(); } if (eventHandled) { @@ -198,9 +193,7 @@ export class Toolbar }); const onFocus = (event: FocusEvent) => { - //console.log('Focusing ', element, event); if (focusOnClick) { - //console.log('Focus on click, exiting'); focusOnClick = false; event.stopImmediatePropagation(); @@ -210,19 +203,14 @@ export class Toolbar this.focusToolbar(); const focusFromOutsideToolbar = !this.getEl().getHTMLElement().contains(event.relatedTarget as Node); if (focusFromOutsideToolbar) { - this.focusActionElement(); + this.focusElement(); } }; const onBlur = (event: FocusEvent) => { focusOnClick = false; // If newly focused element is not a part of the toolbar, remove focus from the toolbar - //console.log('Blurring ', element, event); if (!this.getEl().getHTMLElement().contains(event.relatedTarget as Node)) { - element.getChildren().forEach((child: Element) => { - child.giveBlur(); - }); - //console.log('Removing focus from the toolbar'); this.removeFocus(); } }; @@ -230,72 +218,61 @@ export class Toolbar element.onFocus((event: FocusEvent) => onFocus(event)); element.onMouseDown((event) => { focusOnClick = true; - this.lastFocusedActionIndex = this.getActionIndexByElement(element); + this.lastFocusedElementIndex = this.getIndexOfToolbarElement(element); }); element.onBlur((event: FocusEvent) => onBlur(event)); -/* - element.whenRendered(() => { - element.getChildren().forEach((child: Element) => { - child.onFocus((event: FocusEvent) => onFocus(event)); - }); - });*/ } private isFocused(): boolean { return this.hasClassEx('focused'); } - private focusAction(getActionIndex: () => number) { - const focusIndex = getActionIndex(); + private focusElementByIndex(getElementIndex: () => number) { + const focusIndex = getElementIndex(); if (focusIndex !== -1) { if (!this.isFocused()) { this.giveFocus(); } - this.lastFocusedActionIndex = focusIndex; - this.actionElements[focusIndex].element.giveFocus(); + this.lastFocusedElementIndex = focusIndex; + this.toolbarElements[focusIndex].el.giveFocus(); } } - private focusNextAction() { - this.focusAction(() => this.getNextFocusableActionIndex()); + private focusNextElement() { + this.focusElementByIndex(() => this.getNextFocusableElementIndex()); } - private focusPreviousAction() { - this.focusAction(() => this.getPreviousFocusableActionIndex()); + private focusPreviousElement() { + this.focusElementByIndex(() => this.getPreviousFocusableElementIndex()); } - prependActionElement(element: Element, isTabbable: boolean = true): Element { - this.initElementListeners(element); - this.prependChild(element); - - if (isTabbable) { - this.actionElements.splice(0, 0, { - element - }); + addContainer(container: Element, elements: Element[]): Element { + if (this.foldButton) { + this.addGreedySpacer(); } - return element; + elements.forEach(element => this.addTabbable(element)); + + return this.appendChild(container); } - addActionElement(element: Element, isTabbable: boolean = true): Element { + addElement(element: Element, tabbable: boolean = true): Element { if (this.foldButton) { this.addGreedySpacer(); } - this.addElement(element); - if (isTabbable) { - this.actionElements.push({ - element: element - }); + + if (tabbable) { + this.addTabbable(element); } - return element; + return this.appendChild(element); } private addFoldButton() { const foldButton = new FoldButton(); foldButton.hide(); - this.addActionElement(foldButton); + this.addElement(foldButton); this.foldButton = foldButton; } @@ -304,44 +281,43 @@ export class Toolbar this.hasGreedySpacer = true; } - private getActionIndexByElement(element: Element): number { - return this.actionElements.findIndex((actionElement: ActionElement) => actionElement.element === element); + private getIndexOfToolbarElement(element: Element): number { + return this.toolbarElements.findIndex((toolbarElement: ToolbarElement) => toolbarElement.el === element); } - protected addElement(element: Element): Element { - this.initElementListeners(element); - + appendChild(element: Element | ActionButton): Element { if (this.hasGreedySpacer) { element.addClass('pull-right'); - this.appendChild(element); - } else { - if (this.foldButton?.hasParent()) { - element.insertBeforeEl(this.foldButton); - } else { - this.appendChild(element); - } + return super.appendChild(element); + } + + if (this.isActionButton(element) && this.foldButton?.hasParent()) { + return element.insertBeforeEl(this.foldButton); } - return element; + return super.appendChild(element); } - prependAction(action: Action): ActionButton { - return this.addAction(action, true); + protected addTabbable(element: Element, index?: number) { + this.initElementListeners(element); + if (ObjectHelper.isDefined(index)) { + this.toolbarElements.splice(index, 0, {el: element}); + } else { + this.toolbarElements.push({el: element}); + } } - addAction(action: Action, prepend: boolean = false): ActionButton { + addAction(action: Action): ActionButton { if (!this.foldButton) { this.addFoldButton(); } const actionButton = this.createActionButton(action); - this.actionElements.splice(prepend ? 0 : this.lastActionIndex + 1, 0, { - element: actionButton, - action: action - }); - this.addElement(actionButton); this.lastActionIndex++; + this.addTabbable(actionButton, this.lastActionIndex); + + this.appendChild(actionButton); return actionButton; } @@ -351,22 +327,22 @@ export class Toolbar } removeActions() { - this.actionElements.forEach((actionElement: ActionElement) => !!actionElement.action && this.removeAction(actionElement.action)); + this.toolbarElements.forEach((toolbarElement: ToolbarElement) => this.isActionButton(toolbarElement.el) && this.removeAction(toolbarElement.el.getAction())); } - removeAction(action: Action): void { - const indexToRemove = this.actionElements.findIndex((a: ActionElement) => a.action === action); + removeAction(targetAction: Action): void { + const indexToRemove = this.getActions().findIndex((action: Action) => action === targetAction); if (indexToRemove === -1) { return; } - this.actionElements[indexToRemove].element.remove(); - this.actionElements.slice(indexToRemove, 1); + this.toolbarElements[indexToRemove].el.remove(); + this.toolbarElements.slice(indexToRemove, 1); } getActions(): Action[] { - return this.actionElements - .filter((actionElement: ActionElement) => actionElement.action ?? false) - .map((actionElement: ActionElement) => actionElement.action); + return this.toolbarElements + .filter((element: ToolbarElement) => this.isActionButton(element.el)) + .map((element: ToolbarElement) => (element.el as ActionButton).getAction()); } removeGreedySpacer() { @@ -409,7 +385,7 @@ export class Toolbar this.removeChild(nextFoldableButton); this.foldButton.push(nextFoldableButton, buttonWidth); - this.actionElements[this.lastActionIndex].folded = true; + this.toolbarElements[this.lastActionIndex].folded = true; visibleButtonsWidth -= buttonWidth; nextFoldableButton = this.getNextFoldableButton(); @@ -434,7 +410,7 @@ export class Toolbar const buttonToShow = this.foldButton.pop(); buttonToShow.insertBeforeEl(this.foldButton); visibleButtonsWidth += buttonToShow.getEl().getWidthWithBorder(); - this.actionElements[this.lastActionIndex + 1].folded = false; + this.toolbarElements[this.lastActionIndex + 1].folded = false; this.lastActionIndex++; if (this.foldButton.getButtonsCount() === 1) { @@ -462,9 +438,12 @@ export class Toolbar let index = this.lastActionIndex; while (index >= 0) { - const button: Element = this.actionElements[index].element; - if (this.actionElements[index].action?.isFoldable() && this.isItemAllowedToFold(button)) { - return button; + if (this.isActionButton(this.toolbarElements[index].el)) { + const button = this.toolbarElements[index].el as ActionButton; + + if (button.getAction().isFoldable() && this.isItemAllowedToFold(button)) { + return button; + } } index--; From e7e8077bc89f1d72b81d5dcfc52da9f09bbfbb83 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Fri, 30 Aug 2024 16:57:56 +0200 Subject: [PATCH 20/23] Refactor ActionButton and MenuButton #3677 --- .../assets/admin/common/js/dom/ButtonEl.ts | 11 ++++++++-- .../admin/common/js/ui/button/MenuButton.ts | 11 +++++++--- .../admin/common/js/ui/toolbar/Toolbar.ts | 21 +++++++++++-------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts b/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts index b690bc348..4bfef25f9 100644 --- a/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts +++ b/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts @@ -1,11 +1,18 @@ import {StyleHelper} from '../StyleHelper'; import {FormItemEl} from './FormItemEl'; +import * as $ from 'jquery'; +import {Body} from './Body'; export class ButtonEl extends FormItemEl { - constructor(className?: string) { - super('button', className, StyleHelper.COMMON_PREFIX); + constructor(className?: string, stylePrefix: string = StyleHelper.COMMON_PREFIX) { + super('button', className, stylePrefix); + + this.onEnterPressed(() => { + Body.get().setFocusedElement(this); + $(this.getHTMLElement()).simulate('click'); + }); } setEnabled(value: boolean) { diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index a4ebbc987..72a1e8266 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -245,11 +245,14 @@ export class MenuButton protected initListeners(): void { this.onBodyClicked = (e) => this.hideMenuOnOutsideClick(e); - this.dropdownHandle.onClicked((e: MouseEvent) => { + const onDropdownHandleClicked = () => { if (this.dropdownHandle.isEnabled()) { this.toggleMenu(); } - }); + }; + this.dropdownHandle.onClicked(onDropdownHandleClicked); + this.dropdownHandle.onEnterPressed(onDropdownHandleClicked); + this.dropdownHandle.onEscPressed(() => this.collapseMenu()); this.menu.onItemClicked((item: MenuItem) => { if (this.menu.isHideOnItemClick() && item.isEnabled()) { @@ -257,7 +260,9 @@ export class MenuButton } }); - this.actionButton.onClicked(() => this.toggleMenuOnAction ? this.toggleMenu() : this.collapseMenu()); + const onActionButtonClicked = () => this.toggleMenuOnAction ? this.toggleMenu() : this.collapseMenu(); + this.actionButton.onClicked(onActionButtonClicked); + this.actionButton.onEnterPressed(onActionButtonClicked); this.menu.onClicked(() => this.dropdownHandle.giveFocus()); } diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 8e0a2cb05..88eeb3132 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -35,7 +35,6 @@ export class Toolbar private locked: boolean; private hasGreedySpacer: boolean; private lastFocusedElementIndex = -1; - private lastActionIndex= -1; private visibleButtonsWidth = 0; @@ -285,6 +284,10 @@ export class Toolbar return this.toolbarElements.findIndex((toolbarElement: ToolbarElement) => toolbarElement.el === element); } + private getFoldButtonIndex(): number { + return this.getIndexOfToolbarElement(this.foldButton); + } + appendChild(element: Element | ActionButton): Element { if (this.hasGreedySpacer) { element.addClass('pull-right'); @@ -314,9 +317,7 @@ export class Toolbar const actionButton = this.createActionButton(action); - this.lastActionIndex++; - this.addTabbable(actionButton, this.lastActionIndex); - + this.addTabbable(actionButton, this.getFoldButtonIndex()); this.appendChild(actionButton); return actionButton; @@ -379,17 +380,18 @@ export class Toolbar const toolbarWidth: number = this.getToolbarWidth(); let nextFoldableButton: Element = this.getNextFoldableButton(); let visibleButtonsWidth = this.visibleButtonsWidth; + let foldIndex = this.getFoldButtonIndex() - 1; while (nextFoldableButton && (force || toolbarWidth <= visibleButtonsWidth)) { const buttonWidth: number = nextFoldableButton.getEl().getWidthWithBorder(); this.removeChild(nextFoldableButton); this.foldButton.push(nextFoldableButton, buttonWidth); - this.toolbarElements[this.lastActionIndex].folded = true; + this.toolbarElements[foldIndex].folded = true; visibleButtonsWidth -= buttonWidth; nextFoldableButton = this.getNextFoldableButton(); - this.lastActionIndex--; + foldIndex--; } if (!this.foldButton.isEmpty() && !this.foldButton.isVisible()) { @@ -403,6 +405,7 @@ export class Toolbar const toolbarWidth: number = this.getToolbarWidth(); const foldButtonWidth: number = this.foldButton.getButtonsCount() > 1 ? 0 : this.foldButton.getEl().getWidthWithBorder(); let visibleButtonsWidth = this.visibleButtonsWidth; + let index = this.getFoldButtonIndex() + 1; // if fold has 1 child left then subtract fold button width because it will be hidden while (!this.foldButton.isEmpty() && visibleButtonsWidth + this.foldButton.getNextButtonWidth() < toolbarWidth) { @@ -410,9 +413,9 @@ export class Toolbar const buttonToShow = this.foldButton.pop(); buttonToShow.insertBeforeEl(this.foldButton); visibleButtonsWidth += buttonToShow.getEl().getWidthWithBorder(); - this.toolbarElements[this.lastActionIndex + 1].folded = false; + this.toolbarElements[index].folded = false; - this.lastActionIndex++; + index++; if (this.foldButton.getButtonsCount() === 1) { visibleButtonsWidth -= foldButtonWidth; } @@ -435,7 +438,7 @@ export class Toolbar } private getNextFoldableButton(): Element { - let index = this.lastActionIndex; + let index = this.getFoldButtonIndex() - 1; while (index >= 0) { if (this.isActionButton(this.toolbarElements[index].el)) { From 5856bc0c63f3d920a60fa4e03e3f0e41d2c53b09 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Tue, 3 Sep 2024 16:35:11 +0200 Subject: [PATCH 21/23] Made buttons trigger action on Space --- .../js/app/browse/filter/BrowseFilterPanel.ts | 2 +- .../assets/admin/common/js/dom/ButtonEl.ts | 5 +++-- .../assets/admin/common/js/dom/Element.ts | 17 +++++++++++++---- .../admin/common/js/ui/button/ActionButton.ts | 3 +-- .../admin/common/js/ui/button/MenuButton.ts | 7 ++++--- .../admin/common/js/ui/toolbar/FoldButton.ts | 2 +- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts b/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts index 54188be80..ad55290bb 100644 --- a/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts +++ b/src/main/resources/assets/admin/common/js/app/browse/filter/BrowseFilterPanel.ts @@ -48,7 +48,7 @@ export class BrowseFilterPanel }); this.hideFilterPanelButton.setTitle(i18n('tooltip.filterPanel.hide')); this.hideFilterPanelButton.onClicked(() => this.notifyHidePanelButtonPressed()); - this.hideFilterPanelButton.onEnterPressed(() => this.notifyHidePanelButtonPressed()); + this.hideFilterPanelButton.onApplyKeyPressed(() => this.notifyHidePanelButtonPressed()); let showResultsButtonWrapper = new DivEl('show-filter-results'); this.showResultsButton = new SpanEl('show-filter-results-button'); diff --git a/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts b/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts index 4bfef25f9..7af58e65c 100644 --- a/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts +++ b/src/main/resources/assets/admin/common/js/dom/ButtonEl.ts @@ -9,10 +9,11 @@ export class ButtonEl constructor(className?: string, stylePrefix: string = StyleHelper.COMMON_PREFIX) { super('button', className, stylePrefix); - this.onEnterPressed(() => { + const triggerAction = () => { Body.get().setFocusedElement(this); $(this.getHTMLElement()).simulate('click'); - }); + }; + this.onApplyKeyPressed(triggerAction); } setEnabled(value: boolean) { diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index 920c154a6..f07455fba 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -1121,12 +1121,21 @@ export class Element { this.getEl().removeEventListener('DOMMouseScroll', listener); } - onEnterPressed(callback: () => void) { + onApplyKeyPressed(callback: () => void) { + const callbackWrapper = (event: KeyboardEvent) => { + callback(); + event.stopPropagation(); + event.preventDefault(); + }; this.onKeyDown((event: KeyboardEvent) => { if (KeyHelper.isEnterKey(event)) { - callback(); - event.stopPropagation(); - event.preventDefault(); + callbackWrapper(event); + } + }); + + this.onKeyUp((event: KeyboardEvent) => { + if (KeyHelper.isSpace(event)) { + callbackWrapper(event); } }); } diff --git a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts index 2bc5b93dc..1d252bc92 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts @@ -32,8 +32,7 @@ export class ActionButton this.getAction().execute(); }; - this.onClicked(() => executeAction()); - this.onEnterPressed(() => executeAction()); + this.onClicked(executeAction); this.onHelpKeyPressed = this.onHelpKeyPressed.bind(this); this.syncButtonWithAction = this.syncButtonWithAction.bind(this); diff --git a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts index 72a1e8266..292104d1c 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/MenuButton.ts @@ -98,6 +98,10 @@ export class MenuButton return this.dropdownHandle; } + getChildControls(): Element[] { + return [this.actionButton, this.dropdownHandle]; + } + addMenuActions(actions: Action[]) { this.menu.addActions(actions); this.initActions(actions); @@ -204,7 +208,6 @@ export class MenuButton private initDropdownHandle(): void { this.dropdownHandle = new DropdownHandle(); - this.dropdownHandle.onEnterPressed(() => this.toggleMenu()); } private initActionButton(): void { @@ -251,7 +254,6 @@ export class MenuButton } }; this.dropdownHandle.onClicked(onDropdownHandleClicked); - this.dropdownHandle.onEnterPressed(onDropdownHandleClicked); this.dropdownHandle.onEscPressed(() => this.collapseMenu()); this.menu.onItemClicked((item: MenuItem) => { @@ -262,7 +264,6 @@ export class MenuButton const onActionButtonClicked = () => this.toggleMenuOnAction ? this.toggleMenu() : this.collapseMenu(); this.actionButton.onClicked(onActionButtonClicked); - this.actionButton.onEnterPressed(onActionButtonClicked); this.menu.onClicked(() => this.dropdownHandle.giveFocus()); } diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts index 60c2e870b..d962409af 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/FoldButton.ts @@ -45,7 +45,7 @@ export class FoldButton private initListeners() { (this.hostElement || this).onClicked(this.toggleMenu.bind(this)); this.dropdown.onClicked(this.onMenuClicked.bind(this)); - this.onEnterPressed(this.toggleMenu.bind(this)); + this.onApplyKeyPressed(this.toggleMenu.bind(this)); this.onEscPressed(this.collapse.bind(this)); } From c70e426b852f234cd221ea6e019418c41edbdd40 Mon Sep 17 00:00:00 2001 From: alansemenov Date: Wed, 11 Sep 2024 10:01:25 +0200 Subject: [PATCH 22/23] Changes to comply with ESLint 9 #3693 --- .../assets/admin/common/js/dom/Element.ts | 32 +++++++++++++------ .../admin/common/js/ui/button/ActionButton.ts | 8 +++-- .../admin/common/js/ui/toolbar/Toolbar.ts | 8 +++-- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index f07455fba..b16b92ee9 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -208,7 +208,7 @@ export class Element { if (this.parentElement && this.el.getHTMLElement().parentElement) { if (!(this.parentElement.getHTMLElement() === this.el.getHTMLElement().parentElement)) { - + throw new Error('Illegal state: HTMLElement in parent Element is not the as the HTMLElement parent to this HTMLElement'); } } @@ -258,10 +258,20 @@ export class Element { } } - ObjectHelper.isDefined(this['tabbable']) && this['tabbable'] ? this.makeTabbable() : this.removeTabbable(); - ObjectHelper.isDefined(this['role']) && this.setRole(this['role']); - ObjectHelper.isDefined(this['ariaLabel']) && this.setAriaLabel(this['ariaLabel']); - ObjectHelper.isDefined(this['ariaHasPopup']) && this.setAriaHasPopup(this['ariaHasPopup']); + if (ObjectHelper.isDefined(this['tabbable']) && this['tabbable']) { + this.makeTabbable() + } else { + this.removeTabbable(); + } + if (ObjectHelper.isDefined(this['role'])) { + this.setRole(this['role']); + } + if (ObjectHelper.isDefined(this['ariaLabel'])) { + this.setAriaLabel(this['ariaLabel']); + } + if (ObjectHelper.isDefined(this['ariaHasPopup'])) { + this.setAriaHasPopup(this['ariaHasPopup']); + } } private implementsWCAG(): boolean { @@ -626,9 +636,11 @@ export class Element { if (this.isAriaRole(value)) { role = value; } - StringHelper.isBlank(role.toLowerCase()) ? - this.getEl().removeAttribute('role') : + if (StringHelper.isBlank(role.toLowerCase())) { + this.getEl().removeAttribute('role'); + } else { this.getEl().setAttribute('role', role.toLowerCase()); + } return this; } @@ -638,9 +650,11 @@ export class Element { if (this.isAriaHasPopup(value)) { hasPopup = value; } - StringHelper.isBlank(hasPopup.toLowerCase()) ? - this.getEl().removeAttribute('haspopup') : + if (StringHelper.isBlank(hasPopup.toLowerCase())) { + this.getEl().removeAttribute('haspopup'); + } else { this.setAriaAttribute('haspopup', hasPopup.toLowerCase()); + } return this; } diff --git a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts index 1d252bc92..78d164e53 100644 --- a/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ui/button/ActionButton.ts @@ -85,8 +85,12 @@ export class ActionButton if (tooltip && (toggledEnabled || becameHidden)) { tooltip.hide(); } - toggledEnabled && this.setEnabled(action.isEnabled()); - toggledVisible && this.setVisible(action.isVisible()); + if (toggledEnabled) { + this.setEnabled(action.isEnabled()); + } + if (toggledVisible) { + this.setVisible(action.isVisible()); + } this.setLabel(this.createLabel(action), false); this.updateIconClass(action.getIconClass()); diff --git a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts index 88eeb3132..4bee791b6 100644 --- a/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts +++ b/src/main/resources/assets/admin/common/js/ui/toolbar/Toolbar.ts @@ -164,7 +164,9 @@ export class Toolbar } private createActionButton(action: Action): ActionButton { - action.isFoldable() && action.onPropertyChanged(() => this.foldOrExpand()); + if (action.isFoldable()) { + action.onPropertyChanged(() => this.foldOrExpand()); + } return new ActionButton(action); } @@ -373,7 +375,9 @@ export class Toolbar foldChanged = true; } - foldChanged && this.updateFoldButtonLabel(); + if (foldChanged) { + this.updateFoldButtonLabel(); + } } fold(force: boolean = false): void { From 987b564f32b04d97b5ee7172280bbe0dd1c0e0fa Mon Sep 17 00:00:00 2001 From: alansemenov Date: Wed, 11 Sep 2024 10:40:30 +0200 Subject: [PATCH 23/23] Revert "Publish SNAPSHOT from the epic branch" This reverts commit 863edd8c814d777ed10739449cd00e190db1e1e4. --- .github/workflows/gradle.yml | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 875d9353d..6f20a473e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest outputs: - publish: ${{ steps.publish_vars.outputs.release != 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/4.') || startsWith(github.ref, 'refs/heads/epic-')) }} + publish: ${{ steps.publish_vars.outputs.release != 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/4.')) }} repo: ${{ steps.publish_vars.outputs.repo }} steps: diff --git a/gradle.properties b/gradle.properties index eec34f2b8..61fdabf73 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.10.0-WCAG-SNAPSHOT +version=4.10.0-SNAPSHOT systemProp.org.gradle.internal.http.connectionTimeout=120000 systemProp.org.gradle.internal.http.socketTimeout=120000